Files
alvis f67a5bee67 openai: OpenClaw plugins, memory migration tooling, backup and GPU scripts
Plugins for the Adolf gateway:
  - hindsight-openclaw-plugin: expanded memory recall/retain surface for the
    Cognee -> Hindsight migration
  - todoist-capture-plugin: posts captured ideas to agap-mcp's /capture-idea,
    sending the kb#180 bearer token when AGAP_MCP_TOKEN is present
  - feedback-loop-openclaw-plugin, kimi-quota-footer-plugin, cognee-mcp,
    cognee-openclaw-plugin

Plus migrate-adolf-memory-banks.mjs for the memory-bank split,
backup-hindsight-adolf.sh / backup-llm-dbs.sh (the Hindsight and adolf-state
backups that were previously missing), and gpu_preload_check.sh for the
GTX 1070 residency checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 04:41:53 +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 openai/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 };
}
},
});
},
});