Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
434aa84d40 | ||
|
|
0a368e493d | ||
|
|
011756ca3d |
No files matched your search
@@ -3,6 +3,19 @@
|
|||||||
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
|
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
|
||||||
until 1.0, minor versions may contain breaking changes.
|
until 1.0, minor versions may contain breaking changes.
|
||||||
|
|
||||||
|
## [v202611-00007] – 2026-11-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Telegram bot as a chat interface:** every user can connect their own bot (*Settings →
|
||||||
|
Account → Telegram*: token from @BotFather, then `/start <code>` pairs it with their Telegram
|
||||||
|
account – other chats are ignored). Messages, photos and files go to the current session; the
|
||||||
|
agent's steps show live, the answer comes formatted, images from tools come as photos, and
|
||||||
|
approvals and questions have buttons. Commands like the terminal client: `/sessions`,
|
||||||
|
`/session`, `/new`, `/clear`, `/mode`, `/model`, `/permissions`, `/effort`, `/stop`, `/status`,
|
||||||
|
`/costs`, `/memory`, `/jobs`, `/compact`, `/help`. The server polls Telegram – no public
|
||||||
|
webhook needed.
|
||||||
|
|
||||||
## [v202611-00006] – 2026-11-09
|
## [v202611-00006] – 2026-11-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Generated
+1
@@ -3040,6 +3040,7 @@ dependencies = [
|
|||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
|
"mime_guess",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
"quinn",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Version v202611-00006" src="https://img.shields.io/badge/version-v202611--00006-d69e2e?style=flat-square">
|
<img alt="Version v202611-00007" src="https://img.shields.io/badge/version-v202611--00007-d69e2e?style=flat-square">
|
||||||
<img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-689d6a?style=flat-square">
|
<img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-689d6a?style=flat-square">
|
||||||
<img alt="Rust backend" src="https://img.shields.io/badge/backend-Rust-b7410e?style=flat-square&logo=rust">
|
<img alt="Rust backend" src="https://img.shields.io/badge/backend-Rust-b7410e?style=flat-square&logo=rust">
|
||||||
<img alt="React PWA" src="https://img.shields.io/badge/frontend-React%20PWA-2d81ff?style=flat-square&logo=react">
|
<img alt="React PWA" src="https://img.shields.io/badge/frontend-React%20PWA-2d81ff?style=flat-square&logo=react">
|
||||||
@@ -213,6 +213,9 @@ shared secret or a Gitea/GitHub HMAC signature, and its JSON payload is handed t
|
|||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
|
- **Telegram:** connect your own bot (*Settings → Account → Telegram*) and use CodeCraft from
|
||||||
|
Telegram – sessions, modes, models, approvals with buttons, photos and files; `/help` in the
|
||||||
|
bot lists the commands.
|
||||||
- **API tokens** (*Settings → Account*) with full or read-only scope for scripts and the terminal client.
|
- **API tokens** (*Settings → Account*) with full or read-only scope for scripts and the terminal client.
|
||||||
- **Prometheus:** set `server.metrics_token` and scrape `GET /metrics` with
|
- **Prometheus:** set `server.metrics_token` and scrape `GET /metrics` with
|
||||||
`Authorization: Bearer <token>` – requests, errors, tokens, cost and time per model, provider
|
`Authorization: Bearer <token>` – requests, errors, tokens, cost and time per model, provider
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ shell-words = "1"
|
|||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
ignore = "0.4"
|
ignore = "0.4"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
reqwest = { version = "0.13", default-features = false, features = ["json", "stream", "rustls", "form"] }
|
reqwest = { version = "0.13", default-features = false, features = ["json", "stream", "rustls", "form", "multipart"] }
|
||||||
rusqlite = { version = "0.41", features = ["bundled"] }
|
rusqlite = { version = "0.41", features = ["bundled"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
+62
-16
@@ -138,6 +138,7 @@ pub fn router(app: AppState) -> Router {
|
|||||||
.route("/account/prefs", get(crate::cli_api::get_prefs).put(crate::cli_api::put_prefs))
|
.route("/account/prefs", get(crate::cli_api::get_prefs).put(crate::cli_api::put_prefs))
|
||||||
.route("/account/memory", get(crate::cli_api::get_memory).put(crate::cli_api::put_memory))
|
.route("/account/memory", get(crate::cli_api::get_memory).put(crate::cli_api::put_memory))
|
||||||
.route("/account/modes", get(get_personal_modes).put(put_personal_modes))
|
.route("/account/modes", get(get_personal_modes).put(put_personal_modes))
|
||||||
|
.route("/account/telegram", get(get_telegram).put(put_telegram).delete(delete_telegram))
|
||||||
.route("/templates", get(crate::cli_api::get_templates))
|
.route("/templates", get(crate::cli_api::get_templates))
|
||||||
.route("/templates/{scope}", put(crate::cli_api::put_templates))
|
.route("/templates/{scope}", put(crate::cli_api::put_templates))
|
||||||
.route("/cli/config", get(crate::cli_api::config))
|
.route("/cli/config", get(crate::cli_api::config))
|
||||||
@@ -327,6 +328,44 @@ async fn agents(Extension(user): Extension<User>, State(app): State<AppState>, Q
|
|||||||
Json(json!(prompts::agents_for(&app, &root, &cwd, Some(&user.id))))
|
Json(json!(prompts::agents_for(&app, &root, &cwd, Some(&user.id))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Telegram bot of the user: bot name, pairing state, errors (the token stays hidden).
|
||||||
|
async fn get_telegram(Extension(user): Extension<User>, State(app): State<AppState>) -> ApiResult {
|
||||||
|
let s = crate::telegram::load(&app, &user.id);
|
||||||
|
let (running, error) = crate::telegram::status(&user.id);
|
||||||
|
Ok(Json(json!({
|
||||||
|
"configured": !s.token.is_empty(),
|
||||||
|
"bot": s.bot,
|
||||||
|
"paired": s.chat_id.is_some(),
|
||||||
|
"pair_code": if s.chat_id.is_none() { s.pair_code } else { String::new() },
|
||||||
|
"running": running,
|
||||||
|
"error": error,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TelegramBody {
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect a bot: check the token, start polling and create a pairing code.
|
||||||
|
async fn put_telegram(Extension(user): Extension<User>, State(app): State<AppState>, Json(b): Json<TelegramBody>) -> ApiResult {
|
||||||
|
let token = crate::telegram::extract_token(&b.token)
|
||||||
|
.ok_or_else(|| ApiError(StatusCode::BAD_REQUEST, "that is not a bot token – it looks like 123456789:AAH… (paste only the token from @BotFather)".into()))?;
|
||||||
|
let bot = crate::telegram::check_token(&token).await.map_err(|e| ApiError(StatusCode::BAD_REQUEST, format!("token rejected by Telegram: {e:#}")))?;
|
||||||
|
let s = crate::telegram::Settings { token, bot, pair_code: crate::telegram::new_pair_code(), ..Default::default() };
|
||||||
|
app.store.set_user_setting(&user.id, "telegram", &s)?;
|
||||||
|
crate::telegram::restart(&app, &user.id);
|
||||||
|
app.audit(Some(&user), "telegram.connect", &s.bot, "");
|
||||||
|
get_telegram(Extension(user), State(app)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_telegram(Extension(user): Extension<User>, State(app): State<AppState>) -> ApiResult {
|
||||||
|
crate::telegram::stop(&user.id);
|
||||||
|
app.store.set_user_setting(&user.id, "telegram", &crate::telegram::Settings::default())?;
|
||||||
|
app.audit(Some(&user), "telegram.disconnect", "", "");
|
||||||
|
Ok(Json(json!({"ok": true})))
|
||||||
|
}
|
||||||
|
|
||||||
/// The user's personal modes (name -> settings).
|
/// The user's personal modes (name -> settings).
|
||||||
async fn get_personal_modes(Extension(user): Extension<User>, State(app): State<AppState>) -> ApiResult {
|
async fn get_personal_modes(Extension(user): Extension<User>, State(app): State<AppState>) -> ApiResult {
|
||||||
Ok(Json(json!(prompts::personal_modes(&app, &user.id))))
|
Ok(Json(json!(prompts::personal_modes(&app, &user.id))))
|
||||||
@@ -858,41 +897,48 @@ fn set_turn_effort(app: &App, id: &str, effort: Option<&str>) -> Result<(), ApiE
|
|||||||
|
|
||||||
async fn prompt(Extension(user): Extension<User>, State(app): State<AppState>, Path(id): Path<String>, Json(body): Json<PromptBody>) -> ApiResult {
|
async fn prompt(Extension(user): Extension<User>, State(app): State<AppState>, Path(id): Path<String>, Json(body): Json<PromptBody>) -> ApiResult {
|
||||||
let session = writable(&app, &user, &id)?;
|
let session = writable(&app, &user, &id)?;
|
||||||
let text = body.text.trim().to_string();
|
send_prompt(&app, session, body.text, body.images, body.effort.as_deref()).map(Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a user message for a session (web UI and Telegram): built-in commands,
|
||||||
|
/// slash command expansion, then start or queue the turn.
|
||||||
|
pub(crate) fn send_prompt(app: &AppState, session: crate::types::Session, text: String, images: Vec<ImageData>, effort: Option<&str>) -> Result<Value, ApiError> {
|
||||||
|
let id = session.id.clone();
|
||||||
|
let text = text.trim().to_string();
|
||||||
// Built-in commands.
|
// Built-in commands.
|
||||||
if text == "/compact" {
|
if text == "/compact" {
|
||||||
if app.is_busy(&id) {
|
if app.is_busy(&id) {
|
||||||
return Err(ApiError(StatusCode::CONFLICT, "session is busy".into()));
|
return Err(ApiError(StatusCode::CONFLICT, "session is busy".into()));
|
||||||
}
|
}
|
||||||
tokio::spawn(do_compact(app.clone(), id.clone()));
|
tokio::spawn(do_compact(app.clone(), id.clone()));
|
||||||
return Ok(Json(json!({"ok": true, "command": "compact"})));
|
return Ok(json!({"ok": true, "command": "compact"}));
|
||||||
}
|
}
|
||||||
let cwd = PathBuf::from(&session.cwd);
|
let cwd = PathBuf::from(&session.cwd);
|
||||||
let root = app.root_for(session.owner_id.as_deref(), &cwd);
|
let root = app.root_for(session.owner_id.as_deref(), &cwd);
|
||||||
let mut prompt_text = text.clone();
|
let mut prompt_text = text.clone();
|
||||||
if text.starts_with('/') {
|
if text.starts_with('/') {
|
||||||
if let Some((expanded, agent, clean_room)) = prompts::expand_command(&app, &root, &cwd, &text) {
|
if let Some((expanded, agent, clean_room)) = prompts::expand_command(app, &root, &cwd, &text) {
|
||||||
prompt_text = expanded;
|
prompt_text = expanded;
|
||||||
if agent.is_some() || clean_room.is_some() {
|
if agent.is_some() || clean_room.is_some() {
|
||||||
let mut s = session.clone();
|
app.update_session(&id, |s| {
|
||||||
if let Some(a) = agent {
|
if let Some(a) = agent {
|
||||||
s.agent = a;
|
s.agent = a;
|
||||||
}
|
}
|
||||||
if clean_room.is_some() {
|
if clean_room.is_some() {
|
||||||
s.clean_room = clean_room;
|
s.clean_room = clean_room;
|
||||||
}
|
}
|
||||||
app.save_session(s)?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set_turn_effort(&app, &id, body.effort.as_deref())?;
|
set_turn_effort(app, &id, effort)?;
|
||||||
let mut parts = vec![Part::Text { text: prompt_text }];
|
let mut parts = vec![Part::Text { text: prompt_text }];
|
||||||
for image in body.images {
|
for image in images {
|
||||||
parts.push(Part::Image { image });
|
parts.push(Part::Image { image });
|
||||||
}
|
}
|
||||||
match runner::submit(app.clone(), id, parts, "user")? {
|
match runner::submit(app.clone(), id, parts, "user")? {
|
||||||
runner::Submitted::Started => Ok(Json(json!({"ok": true}))),
|
runner::Submitted::Started => Ok(json!({"ok": true})),
|
||||||
runner::Submitted::Queued(item) => Ok(Json(json!({"ok": true, "queued": item}))),
|
runner::Submitted::Queued(item) => Ok(json!({"ok": true, "queued": item})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -916,7 +962,7 @@ async fn flush_queue(Extension(user): Extension<User>, State(app): State<AppStat
|
|||||||
Ok(Json(json!({"started": runner::flush_queue(app.clone(), &id)?})))
|
Ok(Json(json!({"started": runner::flush_queue(app.clone(), &id)?})))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn do_compact(app: AppState, id: String) {
|
pub(crate) async fn do_compact(app: AppState, id: String) {
|
||||||
let cancel = tokio_util::sync::CancellationToken::new();
|
let cancel = tokio_util::sync::CancellationToken::new();
|
||||||
app.with_runtime(&id, |r| r.cancel = Some(cancel.clone()));
|
app.with_runtime(&id, |r| r.cancel = Some(cancel.clone()));
|
||||||
app.emit(Event::SessionStatus { session_id: id.clone(), busy: true });
|
app.emit(Event::SessionStatus { session_id: id.clone(), busy: true });
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ mod runner;
|
|||||||
mod sandbox;
|
mod sandbox;
|
||||||
mod shells;
|
mod shells;
|
||||||
mod store;
|
mod store;
|
||||||
|
mod telegram;
|
||||||
mod terminal;
|
mod terminal;
|
||||||
mod tools;
|
mod tools;
|
||||||
mod types;
|
mod types;
|
||||||
@@ -73,6 +74,7 @@ async fn main() -> Result<()> {
|
|||||||
health::spawn_checker(app.clone());
|
health::spawn_checker(app.clone());
|
||||||
digest::spawn(app.clone());
|
digest::spawn(app.clone());
|
||||||
budget::spawn_alerts(app.clone());
|
budget::spawn_alerts(app.clone());
|
||||||
|
telegram::spawn(app.clone());
|
||||||
backup::spawn(app.clone());
|
backup::spawn(app.clone());
|
||||||
let router = api::router(app);
|
let router = api::router(app);
|
||||||
let listener = tokio::net::TcpListener::bind(&bind).await?;
|
let listener = tokio::net::TcpListener::bind(&bind).await?;
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -6,6 +6,7 @@ import { GitAccount } from "./GitAccount";
|
|||||||
import { ApiTokens } from "./ApiTokens";
|
import { ApiTokens } from "./ApiTokens";
|
||||||
import { CliInstall } from "./CliInstall";
|
import { CliInstall } from "./CliInstall";
|
||||||
import { Memory } from "./Memory";
|
import { Memory } from "./Memory";
|
||||||
|
import { Telegram } from "./Telegram";
|
||||||
|
|
||||||
export function Account() {
|
export function Account() {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@@ -51,6 +52,7 @@ export function Account() {
|
|||||||
<Memory />
|
<Memory />
|
||||||
<GitAccount />
|
<GitAccount />
|
||||||
<CliInstall />
|
<CliInstall />
|
||||||
|
<Telegram />
|
||||||
<ApiTokens />
|
<ApiTokens />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { del, put } from "../../api";
|
||||||
|
import { confirmDialog, toast } from "../../dialogs";
|
||||||
|
import { useFetch } from "../../hooks";
|
||||||
|
import { useT } from "../../i18n";
|
||||||
|
import { Icon } from "../Icon";
|
||||||
|
|
||||||
|
interface Status {
|
||||||
|
configured: boolean;
|
||||||
|
bot: string;
|
||||||
|
paired: boolean;
|
||||||
|
pair_code: string;
|
||||||
|
running: boolean;
|
||||||
|
error?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Connect a personal Telegram bot as a chat interface. */
|
||||||
|
export function Telegram() {
|
||||||
|
const t = useT();
|
||||||
|
const { data, reload } = useFetch<Status>("/account/telegram");
|
||||||
|
const [token, setToken] = useState("");
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
const connect = async () => {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await put("/account/telegram", { token: token.trim() });
|
||||||
|
setToken("");
|
||||||
|
reload();
|
||||||
|
} catch (e) {
|
||||||
|
toast(e);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const disconnect = async () => {
|
||||||
|
if (!(await confirmDialog({ message: t("telegram.disconnectConfirm"), danger: true, confirmLabel: t("telegram.disconnect") }))) return;
|
||||||
|
await del("/account/telegram").catch((e) => toast(e));
|
||||||
|
reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="panel">
|
||||||
|
<h3>
|
||||||
|
<Icon name="share" /> {t("telegram.title")}
|
||||||
|
<span className="spacer" />
|
||||||
|
{data?.configured && (
|
||||||
|
<button className="small ghost danger" onClick={disconnect}>
|
||||||
|
{t("telegram.disconnect")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<div className="hint" style={{ marginBottom: 10 }}>
|
||||||
|
{t("telegram.intro")}
|
||||||
|
</div>
|
||||||
|
{data && !data.configured && (
|
||||||
|
<>
|
||||||
|
<ol className="cli-steps">
|
||||||
|
<li>{t("telegram.step1")}</li>
|
||||||
|
<li>{t("telegram.step2")}</li>
|
||||||
|
</ol>
|
||||||
|
<div className="row" style={{ gap: 6 }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="telegram-bot-token"
|
||||||
|
value={token}
|
||||||
|
placeholder="123456789:AAH…"
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
className="mono"
|
||||||
|
onChange={(e) => setToken(e.target.value)}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<button className="primary" onClick={() => void connect()} disabled={busy || !token.trim()}>
|
||||||
|
{busy ? t("telegram.checking") : t("telegram.connect")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{data?.configured && (
|
||||||
|
<>
|
||||||
|
<div className="row" style={{ gap: 8, alignItems: "center", flexWrap: "wrap" }}>
|
||||||
|
<span className="chip">@{data.bot}</span>
|
||||||
|
<span className={`chip ${data.running && !data.error ? "ok" : "warn"}`}>{data.running && !data.error ? t("telegram.running") : t("telegram.stopped")}</span>
|
||||||
|
{data.paired && <span className="chip ok">{t("telegram.paired")}</span>}
|
||||||
|
</div>
|
||||||
|
{!data.paired && data.pair_code && (
|
||||||
|
<div className="notice" style={{ marginTop: 10 }}>
|
||||||
|
{t("telegram.pairHint", { bot: data.bot })}{" "}
|
||||||
|
<a href={`https://t.me/${data.bot}?start=${data.pair_code}`} target="_blank" rel="noreferrer">
|
||||||
|
t.me/{data.bot}
|
||||||
|
</a>
|
||||||
|
<div style={{ marginTop: 6 }}>
|
||||||
|
<code className="token-value">/start {data.pair_code}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data.error && <div className="notice err" style={{ marginTop: 10 }}>{data.error}</div>}
|
||||||
|
{data.paired && <div className="hint" style={{ marginTop: 10 }}>{t("telegram.commands")}</div>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -405,6 +405,19 @@ export const de: Messages = {
|
|||||||
"tokens.title": "API-Tokens",
|
"tokens.title": "API-Tokens",
|
||||||
"tokens.hint": "Für den Kommandozeilen-Client (codecraft) und Skripte: als „Authorization: Bearer <token>“ mitschicken. Nur-Lesen-Tokens können nur lesen (keine Prompts, keine Änderungen).",
|
"tokens.hint": "Für den Kommandozeilen-Client (codecraft) und Skripte: als „Authorization: Bearer <token>“ mitschicken. Nur-Lesen-Tokens können nur lesen (keine Prompts, keine Änderungen).",
|
||||||
"tokens.copyNow": "Token jetzt kopieren – er wird nur einmal angezeigt:",
|
"tokens.copyNow": "Token jetzt kopieren – er wird nur einmal angezeigt:",
|
||||||
|
"telegram.title": "Telegram",
|
||||||
|
"telegram.intro": "CodeCraft über Telegram mit deinem eigenen Bot nutzen: Nachrichten gehen an deine aktuelle Sitzung, Befehle wechseln Sitzungen, Modi und Modelle, Freigaben und Rückfragen kommen mit Knöpfen. Nur dein Telegram-Konto kann den Bot nutzen.",
|
||||||
|
"telegram.step1": "In Telegram @BotFather öffnen, /newbot senden und den Schritten folgen.",
|
||||||
|
"telegram.step2": "Das Token von BotFather hier einfügen.",
|
||||||
|
"telegram.connect": "Verbinden",
|
||||||
|
"telegram.checking": "Prüfe…",
|
||||||
|
"telegram.running": "läuft",
|
||||||
|
"telegram.stopped": "läuft nicht",
|
||||||
|
"telegram.paired": "gekoppelt",
|
||||||
|
"telegram.pairHint": "Letzter Schritt: deinen Bot öffnen und den Code senden (oder den Link antippen) – danach bedient er nur noch dein Telegram-Konto:",
|
||||||
|
"telegram.commands": "In Telegram: /help zeigt die Befehle – /sessions, /new, /mode, /model, /stop, /status, /costs …",
|
||||||
|
"telegram.disconnect": "Trennen",
|
||||||
|
"telegram.disconnectConfirm": "Telegram-Bot trennen? Er antwortet dann nicht mehr; mit seinem Token kannst du ihn wieder verbinden.",
|
||||||
"tokens.copy": "Kopieren",
|
"tokens.copy": "Kopieren",
|
||||||
"tokens.copied": "Kopiert",
|
"tokens.copied": "Kopiert",
|
||||||
"tokens.name": "Name",
|
"tokens.name": "Name",
|
||||||
|
|||||||
@@ -404,6 +404,19 @@ export const en = {
|
|||||||
"tokens.title": "API tokens",
|
"tokens.title": "API tokens",
|
||||||
"tokens.hint": "For the command line client (codecraft) and scripts: send as “Authorization: Bearer <token>”. Read-only tokens can only read (no prompts, no changes).",
|
"tokens.hint": "For the command line client (codecraft) and scripts: send as “Authorization: Bearer <token>”. Read-only tokens can only read (no prompts, no changes).",
|
||||||
"tokens.copyNow": "Copy the token now – it is shown only once:",
|
"tokens.copyNow": "Copy the token now – it is shown only once:",
|
||||||
|
"telegram.title": "Telegram",
|
||||||
|
"telegram.intro": "Use CodeCraft from Telegram with your own bot: messages go to your current session, commands switch sessions, modes and models, approvals and questions come with buttons. Only your Telegram account can use the bot.",
|
||||||
|
"telegram.step1": "In Telegram, open @BotFather, send /newbot and follow the steps.",
|
||||||
|
"telegram.step2": "Paste the token BotFather gives you here.",
|
||||||
|
"telegram.connect": "Connect",
|
||||||
|
"telegram.checking": "Checking…",
|
||||||
|
"telegram.running": "running",
|
||||||
|
"telegram.stopped": "not running",
|
||||||
|
"telegram.paired": "paired",
|
||||||
|
"telegram.pairHint": "Last step: open your bot and send the code (or tap the link) – from then on only your Telegram account is served:",
|
||||||
|
"telegram.commands": "In Telegram: /help lists the commands – /sessions, /new, /mode, /model, /stop, /status, /costs …",
|
||||||
|
"telegram.disconnect": "Disconnect",
|
||||||
|
"telegram.disconnectConfirm": "Disconnect the Telegram bot? It stops answering; you can connect it again with its token.",
|
||||||
"tokens.copy": "Copy",
|
"tokens.copy": "Copy",
|
||||||
"tokens.copied": "Copied",
|
"tokens.copied": "Copied",
|
||||||
"tokens.name": "Name",
|
"tokens.name": "Name",
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
||||||
*/
|
*/
|
||||||
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
|
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
|
||||||
|
"202611-00007": {
|
||||||
|
en: ["Telegram: connect your own bot under Settings → Account → Telegram and chat with CodeCraft from your phone"],
|
||||||
|
de: ["Telegram: eigenen Bot unter Einstellungen → Konto → Telegram verbinden und von unterwegs mit CodeCraft chatten"],
|
||||||
|
},
|
||||||
"202611-00006": {
|
"202611-00006": {
|
||||||
en: ["Docker CLI in the image – with the host's Docker socket mounted, admins' agents and terminals can use Docker"],
|
en: ["Docker CLI in the image – with the host's Docker socket mounted, admins' agents and terminals can use Docker"],
|
||||||
de: ["Docker-CLI im Image – mit eingehängtem Docker-Socket können Admin-Agents und -Terminals Docker nutzen"],
|
de: ["Docker-CLI im Image – mit eingehängtem Docker-Socket können Admin-Agents und -Terminals Docker nutzen"],
|
||||||
|
|||||||
Reference in new issue
Block a user