@agentprojectcontext/apx 1.33.0 → 1.34.0

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 (172) hide show
  1. package/package.json +1 -1
  2. package/skills/apc-context/SKILL.md +2 -5
  3. package/skills/apx/SKILL.md +49 -61
  4. package/src/core/agent/a2a/reply.js +48 -0
  5. package/src/core/agent/build-agent-system.js +4 -3
  6. package/src/core/agent/channels/voice-context.js +98 -0
  7. package/src/core/agent/memory.js +2 -1
  8. package/src/core/agent/prompt-builder.js +2 -1
  9. package/src/core/agent/prompts/modes/code-build.md +1 -0
  10. package/src/core/agent/prompts/modes/code-plan.md +1 -0
  11. package/src/core/agent/prompts/modes/index.js +28 -0
  12. package/src/core/agent/skills/loader.js +22 -18
  13. package/src/core/agent/stream/turn-accumulator.js +73 -0
  14. package/src/core/agent/suggestions.js +37 -0
  15. package/src/core/agent/tools/handlers/add-project.js +5 -2
  16. package/src/core/agent/tools/handlers/call-runtime.js +3 -2
  17. package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
  18. package/src/core/agent/tools/helpers.js +2 -2
  19. package/src/core/agent/tools/names.js +138 -0
  20. package/src/core/agent/tools/registry-bridge.js +6 -14
  21. package/src/core/agent/tools/registry.js +68 -65
  22. package/src/core/apc/context-copy.js +27 -0
  23. package/src/core/apc/notes.js +19 -0
  24. package/src/core/apc/parser.js +13 -6
  25. package/src/core/apc/paths.js +87 -0
  26. package/src/core/apc/scaffold.js +82 -74
  27. package/src/core/apc/skill-sync.js +13 -1
  28. package/src/core/channels/telegram/dispatch.js +595 -0
  29. package/src/core/channels/telegram/helpers.js +130 -0
  30. package/src/core/config/index.js +3 -2
  31. package/src/core/config/redact.js +95 -0
  32. package/src/core/constants/channels.js +2 -0
  33. package/src/core/constants/code-modes.js +10 -0
  34. package/src/core/constants/index.js +1 -0
  35. package/src/core/deck/manifest.js +186 -0
  36. package/src/core/engines/catalog.js +83 -0
  37. package/src/core/engines/gemini.js +28 -11
  38. package/src/core/engines/index.js +11 -1
  39. package/src/core/{tools → http-tools}/browser.js +0 -1
  40. package/src/core/{tools → http-tools}/fetch.js +0 -1
  41. package/src/core/{tools → http-tools}/glob.js +0 -1
  42. package/src/core/{tools → http-tools}/grep.js +0 -1
  43. package/src/core/{tools → http-tools}/registry.js +0 -1
  44. package/src/core/{tools → http-tools}/search.js +0 -1
  45. package/src/core/i18n/en.js +9 -0
  46. package/src/core/i18n/es.js +12 -0
  47. package/src/core/i18n/index.js +54 -0
  48. package/src/core/i18n/pt.js +9 -0
  49. package/src/core/identity/telegram.js +2 -1
  50. package/src/core/mcp/runner.js +272 -14
  51. package/src/core/mcp/sources.js +3 -2
  52. package/src/core/routines/index.js +16 -0
  53. package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
  54. package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
  55. package/src/core/runtime-skills/apx/SKILL.md +95 -0
  56. package/src/core/runtime-skills/apx-mcp/SKILL.md +116 -0
  57. package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
  58. package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
  59. package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
  60. package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
  61. package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
  62. package/src/core/stores/code-sessions.js +50 -2
  63. package/src/core/stores/routine-memory.js +1 -1
  64. package/src/core/stores/sessions-search.js +121 -0
  65. package/src/core/stores/sessions.js +38 -0
  66. package/src/core/vars/index.js +14 -0
  67. package/src/core/vars/interpolate.js +86 -0
  68. package/src/core/vars/sources.js +151 -0
  69. package/src/core/voice/audio-decode.js +38 -0
  70. package/src/core/voice/transcription.js +225 -0
  71. package/src/host/daemon/api/admin-config.js +5 -82
  72. package/src/host/daemon/api/agents.js +5 -5
  73. package/src/host/daemon/api/code.js +17 -169
  74. package/src/host/daemon/api/config.js +3 -4
  75. package/src/host/daemon/api/conversations.js +8 -29
  76. package/src/host/daemon/api/deck.js +37 -404
  77. package/src/host/daemon/api/engines.js +1 -50
  78. package/src/host/daemon/api/exec.js +1 -1
  79. package/src/host/daemon/api/mcps.js +32 -0
  80. package/src/host/daemon/api/routines.js +1 -1
  81. package/src/host/daemon/api/runtimes.js +4 -3
  82. package/src/host/daemon/api/sessions-search.js +24 -140
  83. package/src/host/daemon/api/sessions.js +12 -30
  84. package/src/host/daemon/api/shared.js +2 -1
  85. package/src/host/daemon/api/telegram.js +1 -11
  86. package/src/host/daemon/api/tools.js +6 -6
  87. package/src/host/daemon/api/transcribe.js +2 -2
  88. package/src/host/daemon/api/vars.js +137 -0
  89. package/src/host/daemon/api/voice.js +13 -290
  90. package/src/host/daemon/api.js +2 -0
  91. package/src/host/daemon/db.js +6 -6
  92. package/src/host/daemon/deck-exec.js +148 -0
  93. package/src/host/daemon/index.js +3 -3
  94. package/src/host/daemon/plugins/telegram/index.js +24 -687
  95. package/src/host/daemon/routines-scheduler.js +64 -0
  96. package/src/host/daemon/smoke.js +3 -2
  97. package/src/host/daemon/whisper-server.js +225 -0
  98. package/src/interfaces/cli/commands/agent.js +3 -2
  99. package/src/interfaces/cli/commands/command.js +2 -3
  100. package/src/interfaces/cli/commands/messages.js +6 -2
  101. package/src/interfaces/cli/commands/pair.js +5 -4
  102. package/src/interfaces/cli/commands/search.js +1 -1
  103. package/src/interfaces/cli/commands/sessions.js +3 -2
  104. package/src/interfaces/cli/commands/skills.js +36 -55
  105. package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +1 -0
  106. package/src/interfaces/web/dist/assets/index-M4FspaCH.js +613 -0
  107. package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +1 -0
  108. package/src/interfaces/web/dist/index.html +2 -2
  109. package/src/interfaces/web/package-lock.json +182 -182
  110. package/src/interfaces/web/src/components/ModelCombobox.tsx +44 -8
  111. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
  112. package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
  113. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
  114. package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
  115. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
  116. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
  117. package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
  118. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
  119. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
  120. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
  121. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
  122. package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
  123. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
  124. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
  125. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
  126. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
  127. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +5 -4
  128. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
  129. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
  130. package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
  131. package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
  132. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
  133. package/src/interfaces/web/src/constants/index.ts +1 -1
  134. package/src/interfaces/web/src/i18n/en.ts +174 -7
  135. package/src/interfaces/web/src/i18n/es.ts +179 -15
  136. package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
  137. package/src/interfaces/web/src/lib/api/vars.ts +38 -0
  138. package/src/interfaces/web/src/lib/api.ts +1 -0
  139. package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
  140. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
  141. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
  142. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
  143. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
  144. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
  145. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
  146. package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
  147. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
  148. package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
  149. package/src/interfaces/web/src/types/daemon.ts +5 -0
  150. package/src/host/daemon/transcription.js +0 -538
  151. package/src/host/daemon/whisper-transcribe.py +0 -73
  152. package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +0 -1
  153. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js +0 -602
  154. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js.map +0 -1
  155. /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
  156. /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
  157. /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
  158. /package/src/core/{tools → http-tools}/index.js +0 -0
  159. /package/{skills → src/core/runtime-skills}/apx-agency-agents/SKILL.md +0 -0
  160. /package/{skills → src/core/runtime-skills}/apx-agent/SKILL.md +0 -0
  161. /package/{skills → src/core/runtime-skills}/apx-mcp-builder/SKILL.md +0 -0
  162. /package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +0 -0
  163. /package/{skills → src/core/runtime-skills}/apx-routine/SKILL.md +0 -0
  164. /package/{skills → src/core/runtime-skills}/apx-runtime/SKILL.md +0 -0
  165. /package/{skills → src/core/runtime-skills}/apx-sessions/SKILL.md +0 -0
  166. /package/{skills → src/core/runtime-skills}/apx-skill-builder/SKILL.md +0 -0
  167. /package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +0 -0
  168. /package/{skills → src/core/runtime-skills}/apx-telegram/SKILL.md +0 -0
  169. /package/{skills → src/core/runtime-skills}/apx-voice/SKILL.md +0 -0
  170. /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
  171. /package/src/{host/daemon → core/stores}/conversations.js +0 -0
  172. /package/src/{host/daemon → core/util}/thinking.js +0 -0
@@ -0,0 +1,12 @@
1
+ // Backend strings — Spanish (es). Keep this file flat dot-paths only; the
2
+ // web admin has its own i18n tree.
3
+ export default {
4
+ // Telegram channel
5
+ "telegram.heads_up": "Dale, estoy con eso… 🛠️",
6
+ "telegram.reset_ack": "Listo, contexto borrado. Arranco un hilo nuevo, ¿qué necesitás?",
7
+ "telegram.error_generic": "Algo se rompió de mi lado — ya lo registré.",
8
+ "telegram.fallback_listo": "Listo.",
9
+
10
+ // Generic helpers reused from several surfaces
11
+ "common.unknown_error": "Algo salió mal.",
12
+ };
@@ -0,0 +1,54 @@
1
+ // Backend i18n for daemon-side messages (Telegram heads-up, system replies,
2
+ // any other user-facing string emitted from the host/core layer). The web
3
+ // admin has its own dict tree under src/interfaces/web/src/i18n/ — that one
4
+ // stays separate, this is for what the daemon sends back.
5
+ //
6
+ // Usage:
7
+ // import { t, resolveLang } from "#core/i18n/index.js";
8
+ // const lang = resolveLang(globalConfig);
9
+ // await sendTelegram(t("telegram.heads_up", { lang }));
10
+ //
11
+ // Adding a key: pick a clear dotted path, add it to every locale dict, and
12
+ // the unit test in tests/i18n.test.js will assert parity (no missing
13
+ // translations). Values can include {var} placeholders that t() will fill.
14
+ import en from "./en.js";
15
+ import es from "./es.js";
16
+ import pt from "./pt.js";
17
+
18
+ const DICTS = Object.freeze({ en, es, pt });
19
+ const DEFAULT_LANG = "es";
20
+
21
+ /**
22
+ * Pull the user's preferred language code from a globalConfig snapshot.
23
+ * Falls back to DEFAULT_LANG when nothing is set. The 2-char slice keeps
24
+ * "es-AR" / "en-US" / "pt-BR" working without per-region dicts.
25
+ */
26
+ export function resolveLang(globalConfig) {
27
+ const raw = globalConfig?.user?.language;
28
+ return String(raw || DEFAULT_LANG).slice(0, 2).toLowerCase();
29
+ }
30
+
31
+ function format(s, vars) {
32
+ if (!vars) return s;
33
+ return s.replace(/\{(\w+)\}/g, (_m, k) => (k in vars ? String(vars[k]) : `{${k}}`));
34
+ }
35
+
36
+ /**
37
+ * Translate a key into the active locale. Missing keys fall back through:
38
+ * requested lang → DEFAULT_LANG → the key itself (as a last-resort
39
+ * placeholder so the caller can spot the gap).
40
+ */
41
+ export function t(key, { lang = DEFAULT_LANG, vars } = {}) {
42
+ const code = String(lang || DEFAULT_LANG).slice(0, 2).toLowerCase();
43
+ const dict = DICTS[code] || DICTS[DEFAULT_LANG];
44
+ const value = dict?.[key] ?? DICTS[DEFAULT_LANG]?.[key] ?? key;
45
+ return format(value, vars);
46
+ }
47
+
48
+ /** Lower-level: get the active dict, e.g. for bulk lookups in a loop. */
49
+ export function getDict(lang) {
50
+ const code = String(lang || DEFAULT_LANG).slice(0, 2).toLowerCase();
51
+ return DICTS[code] || DICTS[DEFAULT_LANG];
52
+ }
53
+
54
+ export { DICTS, DEFAULT_LANG };
@@ -0,0 +1,9 @@
1
+ // Backend strings — Portuguese (pt).
2
+ export default {
3
+ "telegram.heads_up": "Já estou nisso… 🛠️",
4
+ "telegram.reset_ack": "Pronto, contexto limpo. Começando do zero — do que você precisa?",
5
+ "telegram.error_generic": "Algo quebrou do meu lado — já registrei.",
6
+ "telegram.fallback_listo": "Pronto.",
7
+
8
+ "common.unknown_error": "Algo deu errado.",
9
+ };
@@ -17,6 +17,7 @@ import {
17
17
  upsertContact,
18
18
  upsertTelegramChannel,
19
19
  } from "../config/index.js";
20
+ import { SENDER_ROLES } from "../constants/roles.js";
20
21
 
21
22
  function telegramDisplayName(from) {
22
23
  const full = [from?.first_name, from?.last_name].filter(Boolean).join(" ").trim();
@@ -62,7 +63,7 @@ export function resolveAllowedTools(cfg, sender) {
62
63
  if (sender?.isOwner) return "*";
63
64
  const def = cfg?.telegram?.roles?.[sender?.role];
64
65
  if (def && def.tools !== undefined) return def.tools;
65
- if (sender?.role === "guest") return [];
66
+ if (sender?.role === SENDER_ROLES.GUEST) return [];
66
67
  return "*";
67
68
  }
68
69
 
@@ -1,9 +1,22 @@
1
- // MCP runner: spawn child MCP processes and proxy JSON-RPC tools/call.
2
- // Speaks the stdio transport: newline-delimited JSON-RPC 2.0 messages.
1
+ // MCP runner: spawn child MCP processes (stdio) or talk to remote MCP
2
+ // servers (HTTP). Speaks JSON-RPC 2.0 either way.
3
+ //
4
+ // Variables referenced as `${var.NAME}` in args/env/url/headers are resolved
5
+ // at process/client construction time against project + global vars. Missing
6
+ // references surface as a MissingVarError with the full list so the UI can
7
+ // report "missing TOKEN_A, TOKEN_B" instead of one-at-a-time.
3
8
  import { spawn } from "node:child_process";
4
9
  import { loadAll } from "./sources.js";
10
+ import { interpolate, MissingVarError } from "#core/vars/interpolate.js";
11
+ import { loadAllVars } from "#core/vars/sources.js";
5
12
 
6
13
  const DEFAULT_TIMEOUT_MS = 30_000;
14
+ const LOG_CAP = 64; // entries per MCP we keep in memory
15
+ const STDERR_BUF_CAP = 4096; // bytes of stderr tail we hand back
16
+
17
+ function nowIso() {
18
+ return new Date().toISOString();
19
+ }
7
20
 
8
21
  class McpProcess {
9
22
  constructor({ name, command, args = [], env = {} }) {
@@ -11,6 +24,7 @@ class McpProcess {
11
24
  this.command = command;
12
25
  this.args = args;
13
26
  this.env = env;
27
+ this.transport = "stdio";
14
28
  this.proc = null;
15
29
  this.buffer = "";
16
30
  this.pending = new Map(); // id -> { resolve, reject, timer }
@@ -18,14 +32,24 @@ class McpProcess {
18
32
  this._initPromise = null;
19
33
  this._initialized = false;
20
34
  this._stderrBuf = "";
35
+ this.logs = []; // { ts, level, msg }
36
+ this.startedAt = null;
37
+ this.lastExitCode = null;
38
+ }
39
+
40
+ _log(level, msg) {
41
+ this.logs.push({ ts: nowIso(), level, msg });
42
+ if (this.logs.length > LOG_CAP) this.logs.shift();
21
43
  }
22
44
 
23
45
  start() {
24
46
  if (this.proc) return;
47
+ this._log("info", `spawn ${this.command} ${(this.args || []).join(" ")}`);
25
48
  this.proc = spawn(this.command, this.args, {
26
49
  env: { ...process.env, ...this.env },
27
50
  stdio: ["pipe", "pipe", "pipe"],
28
51
  });
52
+ this.startedAt = nowIso();
29
53
 
30
54
  this.proc.stdout.setEncoding("utf8");
31
55
  this.proc.stderr.setEncoding("utf8");
@@ -33,12 +57,16 @@ class McpProcess {
33
57
  this.proc.stdout.on("data", (chunk) => this._onStdout(chunk));
34
58
  this.proc.stderr.on("data", (chunk) => {
35
59
  this._stderrBuf += chunk;
36
- if (this._stderrBuf.length > 4096) {
37
- this._stderrBuf = this._stderrBuf.slice(-4096);
60
+ if (this._stderrBuf.length > STDERR_BUF_CAP) {
61
+ this._stderrBuf = this._stderrBuf.slice(-STDERR_BUF_CAP);
38
62
  }
63
+ const trimmed = chunk.trim();
64
+ if (trimmed) this._log("stderr", trimmed.slice(-512));
39
65
  });
40
66
 
41
67
  this.proc.on("exit", (code) => {
68
+ this.lastExitCode = code;
69
+ this._log("info", `exit code=${code}`);
42
70
  const err = new Error(
43
71
  `MCP "${this.name}" exited with code ${code}. stderr: ${this._stderrBuf.trim()}`
44
72
  );
@@ -133,6 +161,19 @@ class McpProcess {
133
161
  return this._send("tools/call", { name, arguments: args || {} });
134
162
  }
135
163
 
164
+ getLogs() {
165
+ return {
166
+ transport: "stdio",
167
+ command: this.command,
168
+ args: this.args,
169
+ started_at: this.startedAt,
170
+ running: !!this.proc,
171
+ last_exit_code: this.lastExitCode,
172
+ stderr_tail: this._stderrBuf,
173
+ events: this.logs.slice(),
174
+ };
175
+ }
176
+
136
177
  stop() {
137
178
  if (this.proc) {
138
179
  try {
@@ -143,6 +184,190 @@ class McpProcess {
143
184
  }
144
185
  }
145
186
 
187
+ // HTTP MCP client. Posts JSON-RPC 2.0 to the configured URL with the
188
+ // configured headers. Each call is a fresh fetch — we do not maintain a
189
+ // long-lived SSE stream. This works for servers that implement the simple
190
+ // JSON-RPC response style (which is most third-party MCP HTTP servers,
191
+ // including Asana's mcp.asana.com endpoint).
192
+ // Header values must be Latin1 — fetch throws "Cannot convert argument to a
193
+ // ByteString" on any code point above 255. We also normalize whitespace that
194
+ // the web editor's contentEditable injects: zero-width chars + non-breaking
195
+ // spaces (U+00A0 — the silent space substitute that makes Asana reject
196
+ // `Bearer\xA0token` with "Authorization header must be in format Bearer
197
+ // <token>"). One spot of sanitization covers every header value the runner
198
+ // sends, regardless of where the poisoned char originated.
199
+ function sanitizeHeaderValue(v) {
200
+ return String(v)
201
+ .replace(/[\u200B-\u200F\u202A-\u202E\u2060\uFEFF]/g, "")
202
+ .replace(/\u00A0/g, " ");
203
+ }
204
+
205
+ function sanitizeHeaders(h) {
206
+ if (!h) return {};
207
+ const out = {};
208
+ for (const [k, v] of Object.entries(h)) {
209
+ out[sanitizeHeaderValue(k)] = sanitizeHeaderValue(v);
210
+ }
211
+ return out;
212
+ }
213
+
214
+ class HttpMcpClient {
215
+ constructor({ name, url, headers = {} }) {
216
+ this.name = name;
217
+ this.url = sanitizeHeaderValue(url);
218
+ this.headers = sanitizeHeaders(headers);
219
+ this.transport = "http";
220
+ this._nextId = 1;
221
+ this._initialized = false;
222
+ this._initPromise = null;
223
+ this.logs = [];
224
+ this.startedAt = null;
225
+ this.lastError = null;
226
+ }
227
+
228
+ _log(level, msg) {
229
+ this.logs.push({ ts: nowIso(), level, msg });
230
+ if (this.logs.length > LOG_CAP) this.logs.shift();
231
+ }
232
+
233
+ async _rpc(method, params, timeoutMs = DEFAULT_TIMEOUT_MS) {
234
+ if (!this.startedAt) this.startedAt = nowIso();
235
+ const id = this._nextId++;
236
+ const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
237
+ const ctrl = new AbortController();
238
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
239
+ this._log("info", `POST ${method}`);
240
+ let res;
241
+ try {
242
+ res = await fetch(this.url, {
243
+ method: "POST",
244
+ headers: {
245
+ "Content-Type": "application/json",
246
+ Accept: "application/json, text/event-stream",
247
+ "MCP-Protocol-Version": "2024-11-05",
248
+ ...this.headers,
249
+ },
250
+ body,
251
+ signal: ctrl.signal,
252
+ });
253
+ } catch (e) {
254
+ this.lastError = e.message;
255
+ this._log("error", `fetch failed: ${e.message}`);
256
+ throw new Error(`MCP "${this.name}" HTTP error: ${e.message}`);
257
+ } finally {
258
+ clearTimeout(timer);
259
+ }
260
+ const contentType = res.headers.get("content-type") || "";
261
+ const text = await res.text();
262
+ if (!res.ok) {
263
+ this.lastError = `HTTP ${res.status}`;
264
+ this._log("error", `HTTP ${res.status} ${text.slice(0, 200)}`);
265
+ throw new Error(
266
+ `MCP "${this.name}" HTTP ${res.status}: ${text.slice(0, 300)}`
267
+ );
268
+ }
269
+ // text/event-stream — pluck the first JSON-RPC payload from the SSE frames.
270
+ let payload;
271
+ if (contentType.includes("text/event-stream")) {
272
+ payload = parseFirstSseJson(text);
273
+ if (!payload) {
274
+ this.lastError = "no JSON in SSE stream";
275
+ throw new Error(`MCP "${this.name}" returned empty SSE stream`);
276
+ }
277
+ } else {
278
+ try {
279
+ payload = JSON.parse(text);
280
+ } catch (e) {
281
+ this.lastError = `non-JSON response: ${e.message}`;
282
+ throw new Error(
283
+ `MCP "${this.name}" non-JSON response: ${text.slice(0, 300)}`
284
+ );
285
+ }
286
+ }
287
+ if (payload.error) {
288
+ this.lastError = payload.error.message || "rpc error";
289
+ throw new Error(payload.error.message || "MCP error");
290
+ }
291
+ return payload.result;
292
+ }
293
+
294
+ async _ensureInitialized() {
295
+ if (this._initialized) return;
296
+ if (!this._initPromise) {
297
+ this._initPromise = (async () => {
298
+ await this._rpc(
299
+ "initialize",
300
+ {
301
+ protocolVersion: "2024-11-05",
302
+ capabilities: {},
303
+ clientInfo: { name: "apx-daemon", version: "0.1.0" },
304
+ },
305
+ 10_000
306
+ );
307
+ // Best-effort notification — many servers ignore this for HTTP.
308
+ try {
309
+ await fetch(this.url, {
310
+ method: "POST",
311
+ headers: {
312
+ "Content-Type": "application/json",
313
+ Accept: "application/json",
314
+ "MCP-Protocol-Version": "2024-11-05",
315
+ ...this.headers,
316
+ },
317
+ body: JSON.stringify({
318
+ jsonrpc: "2.0",
319
+ method: "notifications/initialized",
320
+ }),
321
+ });
322
+ } catch {}
323
+ this._initialized = true;
324
+ })();
325
+ }
326
+ return this._initPromise;
327
+ }
328
+
329
+ async listTools() {
330
+ await this._ensureInitialized();
331
+ return this._rpc("tools/list", {});
332
+ }
333
+
334
+ async callTool(name, args) {
335
+ await this._ensureInitialized();
336
+ return this._rpc("tools/call", { name, arguments: args || {} });
337
+ }
338
+
339
+ getLogs() {
340
+ return {
341
+ transport: "http",
342
+ url: this.url,
343
+ started_at: this.startedAt,
344
+ last_error: this.lastError,
345
+ events: this.logs.slice(),
346
+ };
347
+ }
348
+
349
+ stop() {
350
+ this._initialized = false;
351
+ this._initPromise = null;
352
+ }
353
+ }
354
+
355
+ function parseFirstSseJson(raw) {
356
+ for (const block of raw.split(/\r?\n\r?\n/)) {
357
+ const dataLines = [];
358
+ for (const line of block.split(/\r?\n/)) {
359
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
360
+ }
361
+ if (!dataLines.length) continue;
362
+ try {
363
+ return JSON.parse(dataLines.join("\n"));
364
+ } catch {
365
+ continue;
366
+ }
367
+ }
368
+ return null;
369
+ }
370
+
146
371
  function entryToMeta(e) {
147
372
  return {
148
373
  name: e.name,
@@ -158,8 +383,6 @@ function entryToMeta(e) {
158
383
  }
159
384
 
160
385
  export class McpRegistry {
161
- // Accepts either the project path string (back-compat) or an object
162
- // { projectPath, storagePath } so the runtime scope can be aggregated.
163
386
  constructor(arg) {
164
387
  if (typeof arg === "string" || arg == null) {
165
388
  this.projectPath = arg || null;
@@ -168,7 +391,7 @@ export class McpRegistry {
168
391
  this.projectPath = arg.projectPath || null;
169
392
  this.storagePath = arg.storagePath || null;
170
393
  }
171
- this.processes = new Map(); // mcp name -> McpProcess
394
+ this.processes = new Map(); // mcp name -> McpProcess | HttpMcpClient
172
395
  }
173
396
 
174
397
  _load() {
@@ -196,19 +419,40 @@ export class McpRegistry {
196
419
  return e ? entryToMeta(e) : null;
197
420
  }
198
421
 
422
+ _resolveVars() {
423
+ return loadAllVars({ storagePath: this.storagePath }).effective;
424
+ }
425
+
426
+ _resolveMeta(meta) {
427
+ try {
428
+ return interpolate(meta, this._resolveVars());
429
+ } catch (e) {
430
+ if (e instanceof MissingVarError) {
431
+ const list = e.missing.map((n) => `\${var.${n}}`).join(", ");
432
+ throw new Error(
433
+ `MCP "${meta.name}" has undefined variable${e.missing.length > 1 ? "s" : ""}: ${list}. Define them at /p/<id>/vars (or globally at /p/0/vars).`
434
+ );
435
+ }
436
+ throw e;
437
+ }
438
+ }
439
+
199
440
  _ensureProcess(name) {
200
441
  let proc = this.processes.get(name);
201
- if (proc && proc.proc) return proc;
442
+ if (proc) {
443
+ if (proc.transport === "stdio" && proc.proc) return proc;
444
+ if (proc.transport === "http") return proc;
445
+ }
202
446
  const meta = this.getByName(name);
203
447
  if (!meta) throw new Error(`MCP "${name}" not registered`);
204
448
  if (!meta.enabled) throw new Error(`MCP "${name}" is disabled`);
205
- if (meta.transport === "http" || meta.url) {
206
- throw new Error(
207
- `MCP "${name}" uses HTTP transport (url=${meta.url}); HTTP/SSE transport arrives in v0.2. Use a stdio MCP for now.`
208
- );
449
+ const resolved = this._resolveMeta(meta);
450
+ if (resolved.transport === "http" || resolved.url) {
451
+ proc = new HttpMcpClient(resolved);
452
+ } else {
453
+ if (!resolved.command) throw new Error(`MCP "${name}" has no command — invalid registration`);
454
+ proc = new McpProcess(resolved);
209
455
  }
210
- if (!meta.command) throw new Error(`MCP "${name}" has no command — invalid registration`);
211
- proc = new McpProcess(meta);
212
456
  this.processes.set(name, proc);
213
457
  return proc;
214
458
  }
@@ -223,6 +467,20 @@ export class McpRegistry {
223
467
  return proc.listTools();
224
468
  }
225
469
 
470
+ getLogs(name) {
471
+ const proc = this.processes.get(name);
472
+ if (proc) return proc.getLogs();
473
+ const meta = this.getByName(name);
474
+ if (!meta) return null;
475
+ return {
476
+ transport: meta.transport || "stdio",
477
+ running: false,
478
+ started_at: null,
479
+ events: [],
480
+ note: "MCP not started yet — open the Test or Call panel to spawn it.",
481
+ };
482
+ }
483
+
226
484
  shutdown() {
227
485
  for (const p of this.processes.values()) p.stop();
228
486
  this.processes.clear();
@@ -36,6 +36,7 @@
36
36
  import fs from "node:fs";
37
37
  import os from "node:os";
38
38
  import path from "node:path";
39
+ import { apcMcpsFile } from "#core/apc/paths.js";
39
40
 
40
41
  const APX_HOME = path.join(os.homedir(), ".apx");
41
42
  const GLOBAL_MCPS_FILE = path.join(APX_HOME, "mcps.json");
@@ -175,7 +176,7 @@ function normalize(name, server, sourceId) {
175
176
  // ---------------------------------------------------------------------------
176
177
 
177
178
  export function readApfMcps(projectRoot) {
178
- const p = path.join(projectRoot, ".apc", "mcps.json");
179
+ const p = apcMcpsFile(projectRoot);
179
180
  if (!fs.existsSync(p)) return { mcpServers: {} };
180
181
  try {
181
182
  const json = JSON.parse(fs.readFileSync(p, "utf8"));
@@ -187,7 +188,7 @@ export function readApfMcps(projectRoot) {
187
188
  }
188
189
 
189
190
  export function writeApfMcps(projectRoot, json) {
190
- const p = path.join(projectRoot, ".apc", "mcps.json");
191
+ const p = apcMcpsFile(projectRoot);
191
192
  fs.mkdirSync(path.dirname(p), { recursive: true });
192
193
  fs.writeFileSync(p, JSON.stringify(json, null, 2) + "\n");
193
194
  }
@@ -0,0 +1,16 @@
1
+ // Public entry point for routines. Re-exports the CRUD helpers from
2
+ // core/stores/routines.js plus the runner — so callers (CLI, HTTP, scheduler,
3
+ // MCP server) import everything from one place.
4
+ export {
5
+ listRoutines,
6
+ getRoutine,
7
+ upsertRoutine,
8
+ deleteRoutine,
9
+ setEnabled,
10
+ updateRunState,
11
+ getDueRoutines,
12
+ parseSchedule,
13
+ computeNextRun,
14
+ } from "#core/stores/routines.js";
15
+
16
+ export { runRoutineNow } from "./runner.js";