@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.
- package/package.json +1 -1
- package/skills/apc-context/SKILL.md +2 -5
- package/skills/apx/SKILL.md +49 -61
- package/src/core/agent/a2a/reply.js +48 -0
- package/src/core/agent/build-agent-system.js +4 -3
- package/src/core/agent/channels/voice-context.js +98 -0
- package/src/core/agent/memory.js +2 -1
- package/src/core/agent/prompt-builder.js +2 -1
- package/src/core/agent/prompts/modes/code-build.md +1 -0
- package/src/core/agent/prompts/modes/code-plan.md +1 -0
- package/src/core/agent/prompts/modes/index.js +28 -0
- package/src/core/agent/skills/loader.js +22 -18
- package/src/core/agent/stream/turn-accumulator.js +73 -0
- package/src/core/agent/suggestions.js +37 -0
- package/src/core/agent/tools/handlers/add-project.js +5 -2
- package/src/core/agent/tools/handlers/call-runtime.js +3 -2
- package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
- package/src/core/agent/tools/helpers.js +2 -2
- package/src/core/agent/tools/names.js +138 -0
- package/src/core/agent/tools/registry-bridge.js +6 -14
- package/src/core/agent/tools/registry.js +68 -65
- package/src/core/apc/context-copy.js +27 -0
- package/src/core/apc/notes.js +19 -0
- package/src/core/apc/parser.js +13 -6
- package/src/core/apc/paths.js +87 -0
- package/src/core/apc/scaffold.js +82 -74
- package/src/core/apc/skill-sync.js +13 -1
- package/src/core/channels/telegram/dispatch.js +595 -0
- package/src/core/channels/telegram/helpers.js +130 -0
- package/src/core/config/index.js +3 -2
- package/src/core/config/redact.js +95 -0
- package/src/core/constants/channels.js +2 -0
- package/src/core/constants/code-modes.js +10 -0
- package/src/core/constants/index.js +1 -0
- package/src/core/deck/manifest.js +186 -0
- package/src/core/engines/catalog.js +83 -0
- package/src/core/engines/gemini.js +28 -11
- package/src/core/engines/index.js +11 -1
- package/src/core/{tools → http-tools}/browser.js +0 -1
- package/src/core/{tools → http-tools}/fetch.js +0 -1
- package/src/core/{tools → http-tools}/glob.js +0 -1
- package/src/core/{tools → http-tools}/grep.js +0 -1
- package/src/core/{tools → http-tools}/registry.js +0 -1
- package/src/core/{tools → http-tools}/search.js +0 -1
- package/src/core/i18n/en.js +9 -0
- package/src/core/i18n/es.js +12 -0
- package/src/core/i18n/index.js +54 -0
- package/src/core/i18n/pt.js +9 -0
- package/src/core/identity/telegram.js +2 -1
- package/src/core/mcp/runner.js +272 -14
- package/src/core/mcp/sources.js +3 -2
- package/src/core/routines/index.js +16 -0
- package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
- package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
- package/src/core/runtime-skills/apx/SKILL.md +95 -0
- package/src/core/runtime-skills/apx-mcp/SKILL.md +116 -0
- package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
- package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
- package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
- package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
- package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
- package/src/core/stores/code-sessions.js +50 -2
- package/src/core/stores/routine-memory.js +1 -1
- package/src/core/stores/sessions-search.js +121 -0
- package/src/core/stores/sessions.js +38 -0
- package/src/core/vars/index.js +14 -0
- package/src/core/vars/interpolate.js +86 -0
- package/src/core/vars/sources.js +151 -0
- package/src/core/voice/audio-decode.js +38 -0
- package/src/core/voice/transcription.js +225 -0
- package/src/host/daemon/api/admin-config.js +5 -82
- package/src/host/daemon/api/agents.js +5 -5
- package/src/host/daemon/api/code.js +17 -169
- package/src/host/daemon/api/config.js +3 -4
- package/src/host/daemon/api/conversations.js +8 -29
- package/src/host/daemon/api/deck.js +37 -404
- package/src/host/daemon/api/engines.js +1 -50
- package/src/host/daemon/api/exec.js +1 -1
- package/src/host/daemon/api/mcps.js +32 -0
- package/src/host/daemon/api/routines.js +1 -1
- package/src/host/daemon/api/runtimes.js +4 -3
- package/src/host/daemon/api/sessions-search.js +24 -140
- package/src/host/daemon/api/sessions.js +12 -30
- package/src/host/daemon/api/shared.js +2 -1
- package/src/host/daemon/api/telegram.js +1 -11
- package/src/host/daemon/api/tools.js +6 -6
- package/src/host/daemon/api/transcribe.js +2 -2
- package/src/host/daemon/api/vars.js +137 -0
- package/src/host/daemon/api/voice.js +13 -290
- package/src/host/daemon/api.js +2 -0
- package/src/host/daemon/db.js +6 -6
- package/src/host/daemon/deck-exec.js +148 -0
- package/src/host/daemon/index.js +3 -3
- package/src/host/daemon/plugins/telegram/index.js +24 -687
- package/src/host/daemon/routines-scheduler.js +64 -0
- package/src/host/daemon/smoke.js +3 -2
- package/src/host/daemon/whisper-server.js +225 -0
- package/src/interfaces/cli/commands/agent.js +3 -2
- package/src/interfaces/cli/commands/command.js +2 -3
- package/src/interfaces/cli/commands/messages.js +6 -2
- package/src/interfaces/cli/commands/pair.js +5 -4
- package/src/interfaces/cli/commands/search.js +1 -1
- package/src/interfaces/cli/commands/sessions.js +3 -2
- package/src/interfaces/cli/commands/skills.js +36 -55
- package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +1 -0
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js +613 -0
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +182 -182
- package/src/interfaces/web/src/components/ModelCombobox.tsx +44 -8
- package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
- package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
- package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
- package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
- package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
- package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
- package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
- package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
- package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
- package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
- package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
- package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
- package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
- package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +5 -4
- package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
- package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
- package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
- package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
- package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
- package/src/interfaces/web/src/constants/index.ts +1 -1
- package/src/interfaces/web/src/i18n/en.ts +174 -7
- package/src/interfaces/web/src/i18n/es.ts +179 -15
- package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
- package/src/interfaces/web/src/lib/api/vars.ts +38 -0
- package/src/interfaces/web/src/lib/api.ts +1 -0
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
- package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
- package/src/interfaces/web/src/types/daemon.ts +5 -0
- package/src/host/daemon/transcription.js +0 -538
- package/src/host/daemon/whisper-transcribe.py +0 -73
- package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +0 -1
- package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js +0 -602
- package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js.map +0 -1
- /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
- /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
- /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
- /package/src/core/{tools → http-tools}/index.js +0 -0
- /package/{skills → src/core/runtime-skills}/apx-agency-agents/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-agent/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-mcp-builder/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-routine/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-runtime/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-sessions/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-skill-builder/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-telegram/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-voice/SKILL.md +0 -0
- /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
- /package/src/{host/daemon → core/stores}/conversations.js +0 -0
- /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 ===
|
|
66
|
+
if (sender?.role === SENDER_ROLES.GUEST) return [];
|
|
66
67
|
return "*";
|
|
67
68
|
}
|
|
68
69
|
|
package/src/core/mcp/runner.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
// MCP runner: spawn child MCP processes
|
|
2
|
-
//
|
|
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 >
|
|
37
|
-
this._stderrBuf = this._stderrBuf.slice(-
|
|
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
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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();
|
package/src/core/mcp/sources.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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";
|