Retires the Moonshot/Kimi subscription in favour of the already-paid ChatGPT plan. Both CLI wrappers now run `codex exec`; the kimi-agent container is gone. adolf-llm + hindsight-llm: - runKimi -> runCodex (`codex exec --json --skip-git-repo-check`), resume via `codex exec resume <thread_id>`. - MCP moves from a per-session .mcp.json (a workaround for Kimi having no --mcp-config-file flag) to a $CODEX_HOME/config.toml generated once at startup from shared-mcp.json. Field translation is load-bearing: bearerTokenEnvVar -> bearer_token_env_var, enabledTools -> enabled_tools. - approval_policy="never" + sandbox_mode required, or unattended turns block on an approval prompt nobody can answer. kimi-agent removed. It was the ONLY large-tier deployment behind LiteLLM, so deleting it outright would have silently degraded every large-tier request to the local 4B model via the existing fallbacks. tier-large, the auto_router complex-reasoning route and their fallbacks now point at the codex-backed adolf-llm wrapper (model_name: codex-agent). Three environment blockers fixed along the way: - OpenAI geo-blocks this host (403 unsupported_country_region_territory). Both containers now egress via the host xray proxy, with NO_PROXY keeping MCP and *.alogins.net traffic off the tunnel. - node:22-slim ships no system CA store; the Rust codex binary validates TLS against it, so every HTTPS call failed with a generic transport error while Node's own fetch worked. ca-certificates added to both images. - `codex exec resume` rejects -C/--cd (plain `codex exec` accepts it), which broke follow-up turns while first turns succeeded. Known regression: Kimi's managed-usage API has no Codex equivalent, so the /usage route returns 501 and there is no quota probe for the codex model. The two quota plugins degrade quietly to no output. Also: stop tracking cognee.env (live LLM + JWT secrets) and gitignore it. The secrets remain in earlier history and should be rotated. Verified live: plain turn, SSE streaming, session resume, MCP tool call, bearer-token MCP call, and completions through both LiteLLM routes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Y5QPagv4iun1ghpwM96Ff
103 lines
4.6 KiB
JavaScript
103 lines
4.6 KiB
JavaScript
/**
|
||
* Todoist Idea Capture (kb#170 component 1) — registers `/idea <text>` on
|
||
* Adolf's Matrix channel.
|
||
*
|
||
* Same reasoning as quota-command-openclaw-plugin (kb#62): OpenClaw's
|
||
* native-command dispatch (`api.registerCommand`) runs BEFORE the agent
|
||
* turn, so this never spends a Kimi turn. That property is not incidental
|
||
* here — it's the whole point of kb#170's "encoder-only, not an LLM call"
|
||
* design: classification runs on bge-m3 (agap-mcp/src/classifier.js), and
|
||
* routing the capture through a native command means the ENTIRE
|
||
* capture -> classify -> Todoist path costs zero model tokens, not just the
|
||
* classification step.
|
||
*
|
||
* This plugin does no classification itself — it POSTs the raw text to
|
||
* agap-mcp's /capture-idea endpoint (same container agap-mcp already
|
||
* exposes at :3100 for the MCP tool surface; this is a second, plain-REST
|
||
* entry point to the same todoistCaptureIdea() function, added because a
|
||
* native command handler is simplest calling plain JSON over HTTP rather
|
||
* than speaking MCP JSON-RPC to invoke its own tool). See agap-mcp/src/
|
||
* capture.js for the classify+create logic and agap-mcp/src/server.js for
|
||
* the /capture-idea route.
|
||
*
|
||
* Gating: requireAuth: true (the registerCommand default) restricts the
|
||
* command to the same Matrix DM allowlist (channels.matrix.dm.allowFrom in
|
||
* openclaw.json) that already gates every other interaction with Adolf —
|
||
* no separate tier needed, this creates a task in the operator's own
|
||
* Todoist inbox, not a privileged/destructive action.
|
||
*/
|
||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||
|
||
// agap-mcp is a sibling reached via host.docker.internal, same mapping
|
||
// openclaw.json's mcp.servers.agap.url already uses for this container.
|
||
const CAPTURE_URL = "http://host.docker.internal:3100/capture-idea";
|
||
const FETCH_TIMEOUT_MS = 15000; // bge-m3 embed + Todoist create can take a few seconds
|
||
|
||
// kb#180: agap-mcp's :3100 listener is authenticated now — /capture-idea is
|
||
// no longer an open REST endpoint (it never should have been: it reaches
|
||
// Todoist writes from any LAN peer). This plugin runs inside the adolf
|
||
// container, so it presents Adolf's own agap-mcp bearer token, injected as
|
||
// AGAP_MCP_TOKEN by ai/docker-compose.yml from .env (never inlined
|
||
// here). If the var is unset the request goes out unauthenticated and
|
||
// agap-mcp answers 401 — a visible failure of /idea, not a silent one.
|
||
const AGAP_MCP_TOKEN = process.env.AGAP_MCP_TOKEN || "";
|
||
|
||
async function captureIdea(text) {
|
||
const controller = new AbortController();
|
||
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||
try {
|
||
const res = await fetch(CAPTURE_URL, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
...(AGAP_MCP_TOKEN ? { Authorization: `Bearer ${AGAP_MCP_TOKEN}` } : {}),
|
||
},
|
||
body: JSON.stringify({ text }),
|
||
signal: controller.signal,
|
||
});
|
||
const body = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new Error(body?.error || `agap-mcp /capture-idea HTTP ${res.status}`);
|
||
return body;
|
||
} finally {
|
||
clearTimeout(timer);
|
||
}
|
||
}
|
||
|
||
function formatReply({ task, classification }) {
|
||
const bits = [
|
||
`area=${classification.area.label}`,
|
||
`urgency=${classification.urgency.label}`,
|
||
];
|
||
if (classification.decompose.label === "needs-decomposition") bits.push("требует декомпозиции в Kanboard");
|
||
if (classification.area.ambiguous) bits.push("область — неточно, уточни при ревью");
|
||
return `Записал в Todoist: «${task.content}» (${bits.join(", ")}).`;
|
||
}
|
||
|
||
export default definePluginEntry({
|
||
id: "todoist-capture",
|
||
name: "Todoist Idea Capture",
|
||
description:
|
||
"LLM-free /idea command: classifies free text via agap-mcp (local bge-m3, no Kimi call) and creates a labelled Todoist task.",
|
||
register(api) {
|
||
api.registerCommand({
|
||
name: "idea",
|
||
description: "Capture an idea/quick task -> classified (area/urgency/decompose) and filed in Todoist. No Kimi call.",
|
||
acceptsArgs: true,
|
||
requireAuth: true,
|
||
handler: async (ctx) => {
|
||
const text = (ctx.args || "").trim();
|
||
if (!text) {
|
||
return { text: "Использование: /idea <текст идеи>", suppressReply: true };
|
||
}
|
||
try {
|
||
const result = await captureIdea(text);
|
||
return { text: formatReply(result), suppressReply: true };
|
||
} catch (e) {
|
||
api.logger?.warn?.(`todoist-capture: capture failed (${e?.message || e})`);
|
||
return { text: `Не удалось захватить идею: ${e?.message || e}`, suppressReply: true };
|
||
}
|
||
},
|
||
});
|
||
},
|
||
});
|