Files
AgapHost/ai/todoist-capture-plugin/index.js
alvis 9094d71e2f ai: migrate LLM backbone from Kimi CLI to Codex CLI
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
2026-08-01 06:13:27 +00:00

103 lines
4.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 };
}
},
});
},
});