@agentprojectcontext/apx 1.31.0 → 1.31.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +0 -1
  2. package/package.json +1 -1
  3. package/skills/apc-context/SKILL.md +0 -1
  4. package/skills/apx-agency-agents/SKILL.md +1 -1
  5. package/skills/apx-agent/SKILL.md +6 -6
  6. package/skills/apx-project/SKILL.md +1 -2
  7. package/src/core/agent/self-memory.js +1 -1
  8. package/src/core/agent-memory.js +64 -0
  9. package/src/core/agent-system.js +3 -2
  10. package/src/core/confirmation/adapters/code.js +41 -0
  11. package/src/core/confirmation/adapters/telegram.js +134 -0
  12. package/src/core/confirmation/adapters/terminal.js +35 -0
  13. package/src/core/confirmation/adapters/web.js +53 -0
  14. package/src/core/confirmation/index.js +44 -0
  15. package/src/core/confirmation/pending-store.js +68 -0
  16. package/src/core/scaffold.js +43 -18
  17. package/src/core/tools/registry.js +7 -7
  18. package/src/host/daemon/api/agents.js +19 -21
  19. package/src/host/daemon/api/code.js +2 -0
  20. package/src/host/daemon/api/confirm.js +30 -0
  21. package/src/host/daemon/api/sessions-search.js +1 -1
  22. package/src/host/daemon/api/shared.js +5 -8
  23. package/src/host/daemon/api/super-agent.js +12 -4
  24. package/src/host/daemon/api.js +2 -0
  25. package/src/host/daemon/plugins/telegram.js +28 -0
  26. package/src/host/daemon/super-agent-tools/helpers.js +27 -6
  27. package/src/host/daemon/super-agent-tools/index.js +1 -0
  28. package/src/host/daemon/super-agent-tools/tools/add-project.js +2 -2
  29. package/src/host/daemon/super-agent-tools/tools/call-mcp.js +1 -1
  30. package/src/host/daemon/super-agent-tools/tools/call-runtime.js +1 -1
  31. package/src/host/daemon/super-agent-tools/tools/edit-file.js +2 -2
  32. package/src/host/daemon/super-agent-tools/tools/import-agent.js +4 -2
  33. package/src/host/daemon/super-agent-tools/tools/read-agent-memory.js +5 -4
  34. package/src/host/daemon/super-agent-tools/tools/run-shell.js +1 -1
  35. package/src/host/daemon/super-agent-tools/tools/search-files.js +1 -4
  36. package/src/host/daemon/super-agent-tools/tools/send-telegram.js +1 -1
  37. package/src/host/daemon/super-agent-tools/tools/set-identity.js +2 -2
  38. package/src/host/daemon/super-agent-tools/tools/set-permission-mode.js +2 -2
  39. package/src/host/daemon/super-agent-tools/tools/write-file.js +2 -2
  40. package/src/host/daemon/super-agent.js +5 -1
  41. package/src/interfaces/cli/commands/agent.js +4 -1
  42. package/src/interfaces/cli/commands/memory.js +9 -10
  43. package/src/interfaces/web/dist/assets/{index-CfWyjPBa.js → index-BV615I9p.js} +5 -5
  44. package/src/interfaces/web/dist/assets/{index-CfWyjPBa.js.map → index-BV615I9p.js.map} +1 -1
  45. package/src/interfaces/web/dist/index.html +1 -1
  46. package/src/interfaces/web/package-lock.json +3 -3
  47. package/src/interfaces/web/src/i18n/en.ts +6 -6
  48. package/src/interfaces/web/src/i18n/es.ts +6 -6
  49. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
package/README.md CHANGED
@@ -64,7 +64,6 @@ Runtime state — local machine only, never committed:
64
64
 
65
65
  ```text
66
66
  ~/.apx/projects/<project-id>/
67
- ├── project.db ← regenerable SQLite cache
68
67
  ├── messages/ ← local message history
69
68
  └── agents/
70
69
  ├── <slug>/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.31.0",
3
+ "version": "1.31.2",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -81,7 +81,6 @@ Do not store:
81
81
  .apc/sessions/
82
82
  .apc/conversations/
83
83
  .apc/messages/
84
- .apc/project.db
85
84
  .apc/cache/
86
85
  .apc/tmp/
87
86
  .apc/private/
@@ -59,7 +59,7 @@ apx agent vault list --all
59
59
  # Create a new template (writes ~/.apx/agents/<slug>.md)
60
60
  apx agent vault add reviewer \
61
61
  --role "Code reviewer" \
62
- --model claude-haiku-4-5 \
62
+ --model ollama:llama3.2:3b \
63
63
  --language es \
64
64
  --skills code-review,git \
65
65
  --description "Reviews PRs and pushes back on hand-wavy diffs."
@@ -5,7 +5,7 @@ description: How to create, configure, and use project agents in APX. Load when
5
5
 
6
6
  # apx-agent
7
7
 
8
- A project agent is a named persona inside an APC project. Canonical definition: `.apc/agents/<slug>.md` (flat file). `AGENTS.md` is auto-regenerated for discovery. Per-agent runtime data: `.apc/agents/<slug>/memory.md` (and optional `sessions/` only when using external runtimes that write APC session stubs APX-native sessions live under `~/.apx/projects/<id>/`).
8
+ A project agent is a named persona inside an APC project. Canonical definition: `.apc/agents/<slug>.md` (flat file). `AGENTS.md` is auto-regenerated for discovery. Per-agent runtime data (memory, conversations, sessions) lives under `~/.apx/projects/<apx_id>/agents/<slug>/` and is never committed. APX still reads legacy `.apc/agents/<slug>/memory.md` as a migration fallback only.
9
9
 
10
10
  ## Concrete CLI calls
11
11
 
@@ -13,10 +13,10 @@ A project agent is a named persona inside an APC project. Canonical definition:
13
13
  # List agents in a project
14
14
  apx agent list --project iacrmar
15
15
 
16
- # Create a new agent (writes .apc/agents/<slug>.md + regenerates AGENTS.md)
16
+ # Create a new agent (writes .apc/agents/<slug>.md, creates runtime dir, regenerates AGENTS.md)
17
17
  apx agent add reviewer \
18
18
  --role "Code reviewer" \
19
- --model claude-haiku-4-5 \
19
+ --model ollama:llama3.2:3b \
20
20
  --language es \
21
21
  --description "Reviews PRs and pushes back on hand-wavy diffs." \
22
22
  --tools read,write,run \
@@ -45,7 +45,7 @@ apx memory <slug> --project iacrmar --replace < file.md # full replace fro
45
45
  2. Description (from AGENTS.md).
46
46
  3. Role + Language fields.
47
47
  4. Invocation context: `engine | telegram | routine | runtime` — the channel calling the agent.
48
- 5. Memory: `.apc/agents/<slug>/memory.md` if it exists.
48
+ 5. Memory: `~/.apx/projects/<apx_id>/agents/<slug>/memory.md` if it exists, with legacy `.apc/agents/<slug>/memory.md` as a migration fallback.
49
49
  6. Skills declared in the agent's `Skills:` field, each loaded from `.apc/skills/<slug>.md` or the bundled set.
50
50
  7. The `apx` meta-skill (so the agent knows how to operate APX).
51
51
  8. ACTION_DISCIPLINE_RULES (fixed footer — anti-ghost, anti-disclaimer, action-first).
@@ -54,13 +54,13 @@ That's the prompt the engine sees on every `apx exec <agent>` or `apx chat <agen
54
54
 
55
55
  ## Models per agent
56
56
 
57
- Each agent can set `Model:` in its `AGENT.md` to override the global super-agent model. Useful when a particular agent should use a cheaper / smaller / specialized model.
57
+ Each agent can set `Model:` in its `AGENT.md` to override the global super-agent model. Leave it empty when the agent should follow the project/global default.
58
58
 
59
59
  ```markdown
60
60
  # .apc/agents/reviewer.md
61
61
  ---
62
62
  Role: Code reviewer
63
- Model: claude-haiku-4-5 ← this agent always uses Haiku, independent of super_agent.model
63
+ Model: ollama:llama3.2:3b ← this agent uses this model, independent of super_agent.model
64
64
  Language: es
65
65
  ---
66
66
  ```
@@ -57,7 +57,6 @@ The CLI calls `resolveProjectId()` which does fuzzy id-or-name-or-path matching.
57
57
  └── .apc/
58
58
  ├── project.json ← { apxId, name, ... }
59
59
  ├── agents/<slug>.md
60
- ├── agents/<slug>/memory.md
61
60
  ├── skills/<slug>.md or <slug>/SKILL.md
62
61
  ├── mcps.json ← shared MCPs (committed)
63
62
  ├── commands/ ← custom slash-commands
@@ -65,7 +64,7 @@ The CLI calls `resolveProjectId()` which does fuzzy id-or-name-or-path matching.
65
64
 
66
65
  ~/.apx/projects/<apxId>/ ← runtime state (never committed)
67
66
  ├── messages/YYYY-MM-DD.jsonl
68
- ├── agents/<slug>/{sessions/, conversations/}
67
+ ├── agents/<slug>/{memory.md, sessions/, conversations/}
69
68
  ├── routines.json
70
69
  ├── tasks/YYYY-MM.jsonl
71
70
  ├── artifacts/
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // This is distinct from:
4
4
  // - identity.json → who Roby is (name, personality, owner)
5
- // - project agents' .apc/agents/<slug>/memory.md → per-agent, per-project
5
+ // - project agents' ~/.apx/projects/<apx_id>/agents/<slug>/memory.md → per-agent, per-project
6
6
  // - sessions → raw transcripts of past work (search_sessions)
7
7
  //
8
8
  // It is a single free-form markdown file at ~/.apx/memory.md that Roby keeps
@@ -0,0 +1,64 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { projectStorageRoot } from "./config.js";
4
+ import { getOrCreateApxId } from "./scaffold.js";
5
+
6
+ const EMPTY_MEMORY = (slug) =>
7
+ `# Memory — ${slug}\n\n` +
8
+ `## Identity\n- \n\n` +
9
+ `## Long-term facts\n- \n\n` +
10
+ `## Recent context\n- \n`;
11
+
12
+ export function agentRuntimeDir(projectOrRoot, slug) {
13
+ const storagePath =
14
+ typeof projectOrRoot === "object" && projectOrRoot?.storagePath
15
+ ? projectOrRoot.storagePath
16
+ : null;
17
+ const root =
18
+ typeof projectOrRoot === "string"
19
+ ? projectOrRoot
20
+ : projectOrRoot?.path;
21
+ const base = storagePath || projectStorageRoot(getOrCreateApxId(root));
22
+ return path.join(base, "agents", slug);
23
+ }
24
+
25
+ export function agentMemoryPath(projectOrRoot, slug) {
26
+ return path.join(agentRuntimeDir(projectOrRoot, slug), "memory.md");
27
+ }
28
+
29
+ export function legacyAgentMemoryPath(projectRoot, slug) {
30
+ return path.join(projectRoot, ".apc", "agents", slug, "memory.md");
31
+ }
32
+
33
+ export function ensureAgentRuntimeDir(projectOrRoot, slug, { createMemory = false } = {}) {
34
+ const dir = agentRuntimeDir(projectOrRoot, slug);
35
+ fs.mkdirSync(dir, { recursive: true });
36
+ if (createMemory) {
37
+ const memory = path.join(dir, "memory.md");
38
+ if (!fs.existsSync(memory)) fs.writeFileSync(memory, EMPTY_MEMORY(slug));
39
+ }
40
+ return dir;
41
+ }
42
+
43
+ export function readAgentMemory(projectOrRoot, slug) {
44
+ const primary = agentMemoryPath(projectOrRoot, slug);
45
+ if (fs.existsSync(primary)) return fs.readFileSync(primary, "utf8");
46
+
47
+ const root =
48
+ typeof projectOrRoot === "string"
49
+ ? projectOrRoot
50
+ : projectOrRoot?.path;
51
+ if (root) {
52
+ const legacy = legacyAgentMemoryPath(root, slug);
53
+ if (fs.existsSync(legacy)) return fs.readFileSync(legacy, "utf8");
54
+ }
55
+
56
+ return "";
57
+ }
58
+
59
+ export function writeAgentMemory(projectOrRoot, slug, body) {
60
+ ensureAgentRuntimeDir(projectOrRoot, slug);
61
+ const memory = agentMemoryPath(projectOrRoot, slug);
62
+ fs.writeFileSync(memory, body);
63
+ return memory;
64
+ }
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { readAgentMemory } from "./agent-memory.js";
3
4
 
4
5
  // ---------------------------------------------------------------------------
5
6
  // Anti-ghost-response rules injected into every agent system prompt.
@@ -62,8 +63,8 @@ export function buildAgentSystem(project, agent, {
62
63
 
63
64
  parts.push(buildInvocationContext({ invocation, runtime, channel, caller, routine }));
64
65
 
65
- const memPath = path.join(project.path, ".apc", "agents", agent.slug, "memory.md");
66
- if (fs.existsSync(memPath)) parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
66
+ const memory = readAgentMemory(project, agent.slug);
67
+ if (memory) parts.push("## Memory\n" + memory);
67
68
 
68
69
  const apxSkill = path.join(project.path, ".apc", "skills", "apx.md");
69
70
  if (fs.existsSync(apxSkill)) parts.push("## APX\n" + fs.readFileSync(apxSkill, "utf8"));
@@ -0,0 +1,41 @@
1
+ // Code-surface confirmation adapter.
2
+ //
3
+ // Same stdin readline pattern as terminal, but formatted for the code TUI:
4
+ // more structured output so it's visually distinct from agent output in the
5
+ // split-pane Build mode. Uses stderr to avoid polluting the code output stream.
6
+
7
+ import readline from "node:readline";
8
+
9
+ const SEPARATOR = "─".repeat(60);
10
+
11
+ /**
12
+ * Creates a requestConfirmation function for the code channel.
13
+ *
14
+ * @returns {(tool: string, args: object, description: string) => Promise<boolean>}
15
+ */
16
+ export function createCodeConfirmAdapter() {
17
+ return async function requestConfirmation(_tool, _args, description) {
18
+ const rl = readline.createInterface({
19
+ input: process.stdin,
20
+ output: process.stderr,
21
+ terminal: false,
22
+ });
23
+
24
+ return new Promise((resolve) => {
25
+ process.stderr.write(
26
+ `\n${SEPARATOR}\n` +
27
+ `[apx] CONFIRM ACTION\n` +
28
+ ` ${description}\n` +
29
+ ` Answer [y = yes / N = no]: `
30
+ );
31
+ rl.once("line", (answer) => {
32
+ rl.close();
33
+ const confirmed = /^(y|yes|ok)$/i.test(answer.trim());
34
+ process.stderr.write(confirmed ? "✓ Confirmed\n" : "✗ Cancelled\n");
35
+ process.stderr.write(`${SEPARATOR}\n`);
36
+ resolve(confirmed);
37
+ });
38
+ rl.once("close", () => resolve(false));
39
+ });
40
+ };
41
+ }
@@ -0,0 +1,134 @@
1
+ // Telegram confirmation adapter — async inline keyboard.
2
+ //
3
+ // Flow (why a Promise survives across two independent HTTP calls):
4
+ //
5
+ // 1. requestConfirmation() is called by the agent loop mid-tool-execution.
6
+ // It creates a pending entry in the shared store, embeds the correlationId
7
+ // in the button callback_data, sends the keyboard to Telegram, and returns
8
+ // a Promise that is NOT yet resolved.
9
+ //
10
+ // 2. The agent loop is now suspended at `await requestConfirmation(...)`.
11
+ // The Telegram bot's polling loop keeps running independently.
12
+ //
13
+ // 3. When the user taps a button, Telegram sends a callback_query update.
14
+ // The plugin's _handleUpdate() routes it to handleCallbackQuery() here.
15
+ //
16
+ // 4. handleCallbackQuery() calls pendingStore.resolve(correlationId, value),
17
+ // which finds the Promise's resolve function in the in-memory map and
18
+ // calls it. The agent loop resumes with true or false.
19
+ //
20
+ // Idempotency: once the promise resolves the entry is removed from the store.
21
+ // Any subsequent tap on the same button won't find an entry and is a no-op.
22
+ //
23
+ // Post-restart stale buttons: if the process restarted after sending the
24
+ // keyboard but before the user tapped, pendingStore.wasKnown() detects the
25
+ // SQLite row with no memory entry and we show "Expirado" instead of an error.
26
+
27
+ const API_BASE = "https://api.telegram.org";
28
+ const TIMEOUT_MS = 60_000; // 60 s — long enough for a human, short enough to not block forever
29
+
30
+ /**
31
+ * @param {{ token: string, chatId: string|number, pendingStore: ConfirmationPendingStore }} opts
32
+ * @returns {{ requestConfirmation, handleCallbackQuery }}
33
+ */
34
+ export function createTelegramConfirmAdapter({ token, chatId, pendingStore }) {
35
+ async function requestConfirmation(tool, _args, description) {
36
+ const { correlationId, promise } = pendingStore.create({ timeoutMs: TIMEOUT_MS });
37
+
38
+ await sendConfirmKeyboard(token, chatId, description, correlationId, TIMEOUT_MS);
39
+
40
+ return promise;
41
+ }
42
+
43
+ // Called by ChannelPoller._handleUpdate() when a callback_query arrives.
44
+ // Returns true if the callback matched our pattern (consumed it), false otherwise.
45
+ async function handleCallbackQuery(callbackQuery) {
46
+ const data = callbackQuery.data || "";
47
+ const match = data.match(/^apx:confirm:([a-f0-9]{16}):(yes|no)$/);
48
+ if (!match) return false;
49
+
50
+ const [, correlationId, answer] = match;
51
+ const confirmed = answer === "yes";
52
+
53
+ // ACK the callback immediately to clear the loading spinner on the button.
54
+ // Fire-and-forget — a slow ACK is annoying but not fatal.
55
+ await answerCallbackQuery(token, callbackQuery.id, confirmed ? "✅ Confirmed" : "❌ Cancelled");
56
+
57
+ const resolved = pendingStore.resolve(correlationId, confirmed);
58
+
59
+ // If not resolved, the entry timed out or the process restarted — show "Expired"
60
+ // so the user knows the button is no longer actionable.
61
+ const inlineKeyboard = resolved
62
+ ? [[{ text: confirmed ? "✅ Confirmed" : "❌ Cancelled", callback_data: "apx:noop" }]]
63
+ : [[{ text: "⏱ Expired", callback_data: "apx:noop" }]];
64
+
65
+ if (callbackQuery.message?.chat?.id && callbackQuery.message?.message_id) {
66
+ await editMessageButtons(
67
+ token,
68
+ callbackQuery.message.chat.id,
69
+ callbackQuery.message.message_id,
70
+ inlineKeyboard
71
+ );
72
+ }
73
+
74
+ return true;
75
+ }
76
+
77
+ return { requestConfirmation, handleCallbackQuery };
78
+ }
79
+
80
+ // ---------- Telegram API helpers --------------------------------------------
81
+
82
+ async function sendConfirmKeyboard(token, chatId, description, correlationId, timeoutMs) {
83
+ const timeoutSec = Math.round(timeoutMs / 1000);
84
+ await fetch(`${API_BASE}/bot${token}/sendMessage`, {
85
+ method: "POST",
86
+ headers: { "content-type": "application/json" },
87
+ body: JSON.stringify({
88
+ chat_id: chatId,
89
+ text:
90
+ `⚠️ *Confirm action*\n\n${escapeMarkdown(description)}\n\n` +
91
+ `_Expires in ${timeoutSec}s. No response → cancelled._`,
92
+ parse_mode: "Markdown",
93
+ reply_markup: {
94
+ inline_keyboard: [[
95
+ { text: "✅ Yes", callback_data: `apx:confirm:${correlationId}:yes` },
96
+ { text: "❌ No", callback_data: `apx:confirm:${correlationId}:no` },
97
+ ]],
98
+ },
99
+ }),
100
+ });
101
+ }
102
+
103
+ async function answerCallbackQuery(token, callbackQueryId, text) {
104
+ try {
105
+ await fetch(`${API_BASE}/bot${token}/answerCallbackQuery`, {
106
+ method: "POST",
107
+ headers: { "content-type": "application/json" },
108
+ body: JSON.stringify({ callback_query_id: callbackQueryId, text }),
109
+ });
110
+ } catch {
111
+ // best-effort — Telegram gives only 30s to answer; after that it's already cleared
112
+ }
113
+ }
114
+
115
+ async function editMessageButtons(token, chatId, messageId, inlineKeyboard) {
116
+ try {
117
+ await fetch(`${API_BASE}/bot${token}/editMessageReplyMarkup`, {
118
+ method: "POST",
119
+ headers: { "content-type": "application/json" },
120
+ body: JSON.stringify({
121
+ chat_id: chatId,
122
+ message_id: messageId,
123
+ reply_markup: { inline_keyboard: inlineKeyboard },
124
+ }),
125
+ });
126
+ } catch {
127
+ // best-effort
128
+ }
129
+ }
130
+
131
+ // Escape Markdown special chars so description text doesn't break Telegram markup.
132
+ function escapeMarkdown(text) {
133
+ return String(text || "").replace(/[_*[\]()~`>#+\-=|{}.!]/g, "\\$&");
134
+ }
@@ -0,0 +1,35 @@
1
+ // Terminal confirmation adapter — synchronous stdin readline.
2
+ //
3
+ // The agent loop blocks here while waiting for user input. This is fine on
4
+ // the terminal surface because the whole process is interactive and
5
+ // single-threaded from the user's perspective.
6
+
7
+ import readline from "node:readline";
8
+
9
+ /**
10
+ * Creates a requestConfirmation function for the terminal channel.
11
+ * Uses stderr so stdout remains clean (useful when output is piped).
12
+ *
13
+ * @returns {(tool: string, args: object, description: string) => Promise<boolean>}
14
+ */
15
+ export function createTerminalConfirmAdapter() {
16
+ return async function requestConfirmation(_tool, _args, description) {
17
+ const rl = readline.createInterface({
18
+ input: process.stdin,
19
+ output: process.stderr,
20
+ terminal: false,
21
+ });
22
+
23
+ return new Promise((resolve) => {
24
+ process.stderr.write(
25
+ `\n⚠ Confirmation required\n ${description}\n Continue? [y/N] `
26
+ );
27
+ rl.once("line", (answer) => {
28
+ rl.close();
29
+ resolve(/^(y|yes|ok)$/i.test(answer.trim()));
30
+ });
31
+ // Covers Ctrl+C / EOF while waiting.
32
+ rl.once("close", () => resolve(false));
33
+ });
34
+ };
35
+ }
@@ -0,0 +1,53 @@
1
+ // Web / TUI confirmation adapter — async SSE + HTTP confirm endpoint.
2
+ //
3
+ // How the Promise resolves across two separate HTTP calls:
4
+ //
5
+ // 1. The agent loop (running inside an SSE request handler) calls
6
+ // requestConfirmation(). This emits a "confirmation_required" SSE event
7
+ // containing the correlationId and description, then suspends by awaiting
8
+ // the pending store's Promise.
9
+ //
10
+ // 2. The browser / TUI receives the SSE event and renders a confirm/cancel
11
+ // dialog. The dialog is keyed by correlationId.
12
+ //
13
+ // 3. The user responds → frontend POSTs to
14
+ // POST /super-agent/confirm/:correlationId { confirmed: boolean }
15
+ //
16
+ // 4. The API handler (api/confirm.js) calls pendingStore.resolve(correlationId,
17
+ // value). This finds the in-memory resolve callback and calls it, unblocking
18
+ // the agent loop. The SSE stream receives the next event and continues.
19
+ //
20
+ // `onEvent` is the SSE emitter for the current turn. It's injected at adapter
21
+ // creation time (each turn gets its own adapter instance) so the "please show
22
+ // a dialog" event reaches the right open SSE connection.
23
+
24
+ import { getConfirmationStore } from "../pending-store.js";
25
+
26
+ const TIMEOUT_MS = 120_000; // 2 min — humans on screens are slower than keyboard
27
+
28
+ /**
29
+ * Factory — call once per SSE turn, passing the turn's `onEvent` emitter.
30
+ *
31
+ * @param {{ onEvent: (event: object) => Promise<void>|void }} opts
32
+ * @returns {(tool: string, args: object, description: string) => Promise<boolean>}
33
+ */
34
+ export function createWebConfirmAdapter({ onEvent }) {
35
+ return async function requestConfirmation(tool, _args, description) {
36
+ const store = getConfirmationStore();
37
+
38
+ const { correlationId, promise } = store.create({ timeoutMs: TIMEOUT_MS });
39
+
40
+ // Push a structured event to the open SSE stream so the frontend knows
41
+ // to render a confirmation dialog. The correlationId is the shared key
42
+ // between this pending promise and the POST /confirm/:correlationId call.
43
+ await onEvent({
44
+ type: "confirmation_required",
45
+ correlationId,
46
+ tool,
47
+ description,
48
+ timeout_ms: TIMEOUT_MS,
49
+ });
50
+
51
+ return promise;
52
+ };
53
+ }
@@ -0,0 +1,44 @@
1
+ // Human-in-the-loop confirmation system.
2
+ //
3
+ // Public surface:
4
+ // getConfirmationStore() shared SQLite-backed pending store
5
+ // buildConfirmDescription(t, a) human-readable action summary
6
+ // isConfirmationRequired(err) true when a tool threw requires_confirmation:
7
+ //
8
+ // Adapters (one per channel type) live under ./adapters/.
9
+
10
+ export { getConfirmationStore, ConfirmationPendingStore } from "./pending-store.js";
11
+
12
+ /**
13
+ * Returns true when `error` was thrown by createPermissionGuard() to signal
14
+ * that this tool call needs explicit user approval before proceeding.
15
+ */
16
+ export function isConfirmationRequired(error) {
17
+ return (
18
+ error != null &&
19
+ typeof error.message === "string" &&
20
+ error.message.startsWith("requires_confirmation:")
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Build a short, human-readable description of the action being confirmed.
26
+ * Shown in all confirmation channels (terminal prompt, Telegram message, web dialog).
27
+ */
28
+ export function buildConfirmDescription(tool, args) {
29
+ const text = (s, max = 150) => String(s || "").slice(0, max);
30
+
31
+ const builders = {
32
+ send_telegram: (a) => `Send Telegram message: "${text(a.text)}"`,
33
+ run_shell: (a) => `Run shell command: \`${text(a.command)}\``,
34
+ write_file: (a) => `Write file: ${a.path || a.file || "(no path)"}`,
35
+ edit_file: (a) => `Edit file: ${a.path || a.file || "(no path)"}`,
36
+ create_task: (a) => `Create task: "${a.title || a.name || "?"}"`,
37
+ add_project: (a) => `Add project: ${a.path || a.name || "?"}`,
38
+ set_identity: (a) => `Change agent identity to: "${a.name || "?"}"`,
39
+ call_runtime: (a) => `Call runtime: ${a.runtime || a.name || "?"}`,
40
+ };
41
+
42
+ const fn = builders[tool];
43
+ return fn ? fn(args) : `Run tool: \`${tool}\``;
44
+ }
@@ -0,0 +1,68 @@
1
+ // Pending confirmation store: in-memory map of unresolved confirmations.
2
+ //
3
+ // Each entry holds a Promise's resolve callback and a timeout timer.
4
+ // When the user responds (any channel), resolve() is called and the agent
5
+ // loop that was suspended at `await requestConfirmation(...)` resumes.
6
+ //
7
+ // No persistence needed: confirmations are ephemeral (30–120s window).
8
+ // If the daemon restarts, the agent runs that created them are also dead,
9
+ // so there is nothing to resume. Stale Telegram buttons simply won't find
10
+ // an entry and the handleCallbackQuery path shows "Expired" gracefully.
11
+
12
+ import { randomBytes } from "node:crypto";
13
+
14
+ function generateId() {
15
+ return randomBytes(8).toString("hex"); // 16 hex chars, URL-safe
16
+ }
17
+
18
+ export class ConfirmationPendingStore {
19
+ constructor() {
20
+ // correlationId -> { resolve: (boolean) => void, timer: NodeJS.Timeout }
21
+ this._pending = new Map();
22
+ }
23
+
24
+ /**
25
+ * Register a new pending confirmation.
26
+ *
27
+ * Returns { correlationId, promise } where:
28
+ * - correlationId: embed in the reply (button callback_data, SSE event…)
29
+ * - promise: resolves to true (confirmed) or false (denied / timeout)
30
+ *
31
+ * After timeoutMs with no response the promise auto-resolves to false.
32
+ */
33
+ create({ timeoutMs = 30_000 } = {}) {
34
+ const correlationId = generateId();
35
+
36
+ const promise = new Promise((resolve) => {
37
+ const timer = setTimeout(() => {
38
+ this._pending.delete(correlationId);
39
+ resolve(false);
40
+ }, timeoutMs);
41
+ this._pending.set(correlationId, { resolve, timer });
42
+ });
43
+
44
+ return { correlationId, promise };
45
+ }
46
+
47
+ /**
48
+ * Resolve a pending confirmation.
49
+ * Returns true if found and resolved, false if not found (timed out, already
50
+ * resolved, or stale button after a process restart).
51
+ */
52
+ resolve(correlationId, value) {
53
+ const entry = this._pending.get(correlationId);
54
+ if (!entry) return false;
55
+ clearTimeout(entry.timer);
56
+ this._pending.delete(correlationId);
57
+ entry.resolve(value);
58
+ return true;
59
+ }
60
+ }
61
+
62
+ // Singleton — one store per daemon process, shared by all adapters.
63
+ let _store = null;
64
+
65
+ export function getConfirmationStore() {
66
+ if (!_store) _store = new ConfirmationPendingStore();
67
+ return _store;
68
+ }
@@ -325,24 +325,60 @@ const AGENTS_MD_TEMPLATE = `# AGENTS.md
325
325
  <!-- Hard constraints: what agents must always / never do here. -->
326
326
  `;
327
327
 
328
- const APC_GITIGNORE = `# APC runtime data — never in the repository
329
- # Chat conversations and runtime sessions belong in ~/.apx/projects/<id>/
330
- agents/*/sessions/
331
- agents/*/conversations/
328
+ const APC_GITIGNORE = `# APC repository-safe context only.
329
+ # Runtime state belongs in ~/.apx/projects/<id>/, not in .apc/.
330
+
331
+ # Legacy per-agent runtime dirs (agent definitions are flat: agents/<slug>.md)
332
+ agents/*/
333
+
334
+ # Runtime sessions / conversations / messages
332
335
  sessions/
333
336
  conversations/
334
337
  messages/
335
338
  chats/
339
+ threads/
340
+ transcripts/
341
+ runs/
342
+
343
+ # Runtime memory, indexes, databases, and caches
344
+ memory.local.md
345
+ auto-memory.md
336
346
  cache/
337
347
  tmp/
338
348
  private/
339
349
  secrets/
350
+ *.db
351
+ *.db-*
352
+ *.sqlite
353
+ *.sqlite3
354
+ project.db
355
+ memory.db
356
+ memory-index.jsonl
357
+ memory-cursor.json
358
+
359
+ # Local config and secrets
360
+ .env
340
361
  *.local.json
341
362
  *.secret.json
342
363
  *.env
343
364
  *.env.*
344
- project.db
365
+ *.key
366
+ *.pem
367
+ *.p12
368
+ *.crt
369
+ credentials.json
370
+ service-account*.json
371
+ token*.json
372
+ mcps.local.json
373
+ config.local.json
374
+
375
+ # Scratch planning state
376
+ plans/scratch/
377
+ plans/*.local.md
378
+
379
+ # Migration marker and OS noise
345
380
  migrate.md
381
+ .DS_Store
346
382
  `;
347
383
 
348
384
  function nowIso() {
@@ -448,19 +484,8 @@ export function initApf(directory, { name } = {}) {
448
484
  }
449
485
 
450
486
  export function ensureAgentDir(root, slug) {
451
- const dir = path.join(root, ".apc", "agents", slug);
452
- fs.mkdirSync(dir, { recursive: true });
453
- const memory = path.join(dir, "memory.md");
454
- if (!fs.existsSync(memory)) {
455
- fs.writeFileSync(
456
- memory,
457
- `# Memory — ${slug}\n\n` +
458
- `## Identity\n- \n\n` +
459
- `## Long-term facts\n- \n\n` +
460
- `## Recent context\n- \n`
461
- );
462
- }
463
- return dir;
487
+ fs.mkdirSync(path.join(root, ".apc", "agents"), { recursive: true });
488
+ return path.join(root, ".apc", "agents");
464
489
  }
465
490
 
466
491
  // Write .apc/agents/<slug>.md — the canonical agent definition file.