@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
|
@@ -1,95 +1,16 @@
|
|
|
1
1
|
// APX Deck bootstrap surface.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
{
|
|
15
|
-
id: "apx-voice",
|
|
16
|
-
title: "Voz APX",
|
|
17
|
-
source: "apx",
|
|
18
|
-
desktop: "general",
|
|
19
|
-
kind: "voice",
|
|
20
|
-
status: "available",
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
id: "apx-agents",
|
|
24
|
-
title: "Agentes APX",
|
|
25
|
-
source: "apx",
|
|
26
|
-
desktop: "ai",
|
|
27
|
-
kind: "agents",
|
|
28
|
-
status: "available",
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
id: "apx-notes",
|
|
32
|
-
title: "Notas APX",
|
|
33
|
-
source: "apx",
|
|
34
|
-
desktop: "project",
|
|
35
|
-
kind: "capture",
|
|
36
|
-
status: "available",
|
|
37
|
-
},
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const EXTERNAL_WIDGETS = [
|
|
41
|
-
["docker", "Docker", "infra"],
|
|
42
|
-
["dokploy", "Dokploy", "infra"],
|
|
43
|
-
["factorial", "Factorial", "work"],
|
|
44
|
-
["telegram", "Telegram", "comms"],
|
|
45
|
-
["gmail", "Gmail", "comms"],
|
|
46
|
-
["outlook", "Outlook", "comms"],
|
|
47
|
-
["teams", "Teams", "comms"],
|
|
48
|
-
["whatsapp", "WhatsApp", "comms"],
|
|
49
|
-
["zen", "Zen Browser", "ai"],
|
|
50
|
-
["claude", "Claude", "ai"],
|
|
51
|
-
["chatgpt", "ChatGPT", "ai"],
|
|
52
|
-
["cursor", "Cursor", "ai"],
|
|
53
|
-
["codex", "Codex", "ai"],
|
|
54
|
-
].map(([id, title, desktop]) => ({
|
|
55
|
-
id,
|
|
56
|
-
title,
|
|
57
|
-
source: "external",
|
|
58
|
-
desktop,
|
|
59
|
-
kind: "plugin",
|
|
60
|
-
status: "not_configured",
|
|
61
|
-
}));
|
|
62
|
-
|
|
63
|
-
const DESKTOPS = [
|
|
64
|
-
{ id: "general", title: "Hoy" },
|
|
65
|
-
{ id: "project", title: "Proyecto" },
|
|
66
|
-
{ id: "ai", title: "IA" },
|
|
67
|
-
{ id: "comms", title: "Comunicaciones" },
|
|
68
|
-
{ id: "infra", title: "Infra" },
|
|
69
|
-
{ id: "work", title: "Tiempo laboral" },
|
|
70
|
-
{ id: "plugins", title: "Plugins" },
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
const SAFE_ACTIONS = [
|
|
74
|
-
{
|
|
75
|
-
id: "apx.copy_context",
|
|
76
|
-
title: "Copiar contexto APX",
|
|
77
|
-
risk: "safe",
|
|
78
|
-
endpoint: "/projects/:pid/agents",
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
id: "apx.voice_turn",
|
|
82
|
-
title: "Hablar con APX",
|
|
83
|
-
risk: "safe",
|
|
84
|
-
endpoint: "/voice/turn",
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: "apx.super_agent",
|
|
88
|
-
title: "Pedir acción a APX",
|
|
89
|
-
risk: "confirm",
|
|
90
|
-
endpoint: "/projects/:pid/super-agent/chat",
|
|
91
|
-
},
|
|
92
|
-
];
|
|
2
|
+
// Read-only context for companion clients (deck, desktop capsule) plus a
|
|
3
|
+
// safe-action runner. Domain (manifest model, project-context reader, notes
|
|
4
|
+
// appender) lives in core/; process orchestration (spawn child processes,
|
|
5
|
+
// clipboard) lives next door in host/daemon/deck-exec.js. This file is the
|
|
6
|
+
// HTTP adapter.
|
|
7
|
+
import {
|
|
8
|
+
buildDeckManifest,
|
|
9
|
+
TOGGLEABLE_WIDGETS,
|
|
10
|
+
} from "#core/deck/manifest.js";
|
|
11
|
+
import { readProjectContext } from "#core/apc/context-copy.js";
|
|
12
|
+
import { appendProjectNote } from "#core/apc/notes.js";
|
|
13
|
+
import { runDeckExec, copyToClipboard } from "../deck-exec.js";
|
|
93
14
|
|
|
94
15
|
function safePluginStatus(plugins) {
|
|
95
16
|
if (!plugins || typeof plugins.status !== "function") return {};
|
|
@@ -109,104 +30,27 @@ function safeProjects(projects) {
|
|
|
109
30
|
}
|
|
110
31
|
}
|
|
111
32
|
|
|
112
|
-
function pickActiveProject(projectList) {
|
|
113
|
-
return projectList.find((project) => Number(project.id) !== 0) || projectList[0] || null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function decorateExternalWidgets(pluginStatus, overrides) {
|
|
117
|
-
return EXTERNAL_WIDGETS.map((widget) => {
|
|
118
|
-
// Three-way status:
|
|
119
|
-
// 1. user explicitly disabled it → "disabled" (sticky, regardless
|
|
120
|
-
// of plugin auto-detect),
|
|
121
|
-
// 2. daemon has a running plugin → "available",
|
|
122
|
-
// 3. user toggled it on but no plugin backing → "configured"
|
|
123
|
-
// (Deck UI can show "no daemon support yet" hint),
|
|
124
|
-
// 4. nothing → leave the static "not_configured" default.
|
|
125
|
-
const override = overrides[widget.id];
|
|
126
|
-
const status = pluginStatus[widget.id];
|
|
127
|
-
const decorated = { ...widget };
|
|
128
|
-
if (status) {
|
|
129
|
-
decorated.daemon_status = status;
|
|
130
|
-
}
|
|
131
|
-
if (override?.enabled === false) {
|
|
132
|
-
decorated.status = "disabled";
|
|
133
|
-
} else if (status) {
|
|
134
|
-
decorated.status = status.enabled === false ? "disabled" : "available";
|
|
135
|
-
} else if (override?.enabled === true) {
|
|
136
|
-
decorated.status = "configured";
|
|
137
|
-
}
|
|
138
|
-
// Always echo the user-toggle so the app can render the switch
|
|
139
|
-
// independently of the running/available bit.
|
|
140
|
-
decorated.user_enabled = override?.enabled ?? null;
|
|
141
|
-
return decorated;
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function buildDeckManifest({ projects, plugins, version, startedAt, config }) {
|
|
146
|
-
const projectList = safeProjects(projects);
|
|
147
|
-
const pluginStatus = safePluginStatus(plugins);
|
|
148
|
-
const activeProject = pickActiveProject(projectList);
|
|
149
|
-
const overrides =
|
|
150
|
-
(config?.deck && typeof config.deck === "object" && config.deck.widget_overrides) || {};
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
status: "ok",
|
|
154
|
-
daemon: {
|
|
155
|
-
name: "apx",
|
|
156
|
-
version,
|
|
157
|
-
host: config?.host || "127.0.0.1",
|
|
158
|
-
port: config?.port || 7430,
|
|
159
|
-
uptime_s: Math.round((Date.now() - startedAt) / 1000),
|
|
160
|
-
started_at: new Date(startedAt).toISOString(),
|
|
161
|
-
},
|
|
162
|
-
deck: {
|
|
163
|
-
name: "apx-deck",
|
|
164
|
-
desktops: DESKTOPS,
|
|
165
|
-
widgets: [...CORE_WIDGETS, ...decorateExternalWidgets(pluginStatus, overrides)],
|
|
166
|
-
suggested_actions: SAFE_ACTIONS,
|
|
167
|
-
},
|
|
168
|
-
apx: {
|
|
169
|
-
active_project: activeProject,
|
|
170
|
-
projects: projectList,
|
|
171
|
-
plugins: pluginStatus,
|
|
172
|
-
endpoints: {
|
|
173
|
-
health: "/health",
|
|
174
|
-
projects: "/projects",
|
|
175
|
-
plugins: "/plugins",
|
|
176
|
-
voice_turn: "/voice/turn",
|
|
177
|
-
transcribe_chunk: "/transcribe/chunk",
|
|
178
|
-
super_agent_chat: "/projects/:pid/super-agent/chat",
|
|
179
|
-
super_agent_stream: "/projects/:pid/super-agent/chat/stream",
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
safety: {
|
|
183
|
-
direct_shell: false,
|
|
184
|
-
arbitrary_commands: false,
|
|
185
|
-
dangerous_actions_require_confirmation: true,
|
|
186
|
-
allowed_actions_only: true,
|
|
187
|
-
},
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Whitelist of widget ids the user is allowed to override. Keeps a
|
|
192
|
-
// rogue client from writing arbitrary keys into the global config.
|
|
193
|
-
const TOGGLEABLE_WIDGETS = new Set([
|
|
194
|
-
...EXTERNAL_WIDGETS.map((w) => w.id),
|
|
195
|
-
// CORE_WIDGETS are intentionally NOT in here — they're built-in APX
|
|
196
|
-
// surfaces and don't make sense to disable.
|
|
197
|
-
]);
|
|
198
|
-
|
|
199
33
|
export function register(app, ctx) {
|
|
200
34
|
app.get("/deck/manifest", (_req, res) => {
|
|
201
|
-
|
|
35
|
+
const overrides =
|
|
36
|
+
(ctx.config?.deck && typeof ctx.config.deck === "object" && ctx.config.deck.widget_overrides) || {};
|
|
37
|
+
res.json(
|
|
38
|
+
buildDeckManifest({
|
|
39
|
+
projectList: safeProjects(ctx.projects),
|
|
40
|
+
pluginStatus: safePluginStatus(ctx.plugins),
|
|
41
|
+
overrides,
|
|
42
|
+
version: ctx.version,
|
|
43
|
+
startedAt: ctx.startedAt,
|
|
44
|
+
config: ctx.config,
|
|
45
|
+
})
|
|
46
|
+
);
|
|
202
47
|
});
|
|
203
48
|
|
|
204
49
|
// PATCH /deck/widgets/:id body: { enabled: boolean }
|
|
205
50
|
//
|
|
206
|
-
// Persists the user's enable/disable choice for an external widget
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
// the deck UI can stay forward-compatible with future widgets.
|
|
51
|
+
// Persists the user's enable/disable choice for an external widget into the
|
|
52
|
+
// global config under `deck.widget_overrides[id]`. No-op for unknown widget
|
|
53
|
+
// ids so the deck UI can stay forward-compatible.
|
|
210
54
|
app.patch("/deck/widgets/:id", async (req, res) => {
|
|
211
55
|
const id = req.params.id;
|
|
212
56
|
if (!TOGGLEABLE_WIDGETS.has(id)) {
|
|
@@ -217,14 +61,9 @@ export function register(app, ctx) {
|
|
|
217
61
|
return res.status(400).json({ error: "body.enabled must be boolean" });
|
|
218
62
|
}
|
|
219
63
|
// CRITICAL: we MUST read config fresh from disk before mutating.
|
|
220
|
-
// The captured `ctx.config` is a snapshot from daemon startup —
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// (which is exactly the bug we hit when we first shipped this).
|
|
224
|
-
//
|
|
225
|
-
// The on-disk file is the source of truth for everything except
|
|
226
|
-
// the override we're about to set; we mutate ONLY the override and
|
|
227
|
-
// leave everything else intact.
|
|
64
|
+
// The captured `ctx.config` is a snapshot from daemon startup — any
|
|
65
|
+
// `apx project add` that happened since then is NOT in there. Persisting
|
|
66
|
+
// that stale snapshot wipes out user-added projects.
|
|
228
67
|
try {
|
|
229
68
|
const { readConfig, writeConfig } = await import("#core/config/index.js");
|
|
230
69
|
const fresh = readConfig();
|
|
@@ -236,9 +75,8 @@ export function register(app, ctx) {
|
|
|
236
75
|
fresh.deck.widget_overrides[id] = { enabled };
|
|
237
76
|
writeConfig(fresh);
|
|
238
77
|
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
// re-read (mergeDefaults runs once at startup).
|
|
78
|
+
// Mirror into the live ctx.config so the next /deck/manifest in this
|
|
79
|
+
// same process picks it up without a re-read.
|
|
242
80
|
if (ctx.config) {
|
|
243
81
|
ctx.config.deck = ctx.config.deck || {};
|
|
244
82
|
ctx.config.deck.widget_overrides = ctx.config.deck.widget_overrides || {};
|
|
@@ -252,10 +90,8 @@ export function register(app, ctx) {
|
|
|
252
90
|
|
|
253
91
|
// POST /projects/:pid/context/copy
|
|
254
92
|
//
|
|
255
|
-
// Reads the project's AGENTS.md + .apc/memory.md, concatenates them
|
|
256
|
-
//
|
|
257
|
-
// daemon-host clipboard via pbcopy/xclip/clip. Returns byte count so
|
|
258
|
-
// the deck can toast "X bytes copiados".
|
|
93
|
+
// Reads the project's AGENTS.md + .apc/memory.md, concatenates them with a
|
|
94
|
+
// small header per file, and ships the result to the daemon-host clipboard.
|
|
259
95
|
app.post("/projects/:pid/context/copy", async (req, res) => {
|
|
260
96
|
const project = ctx.project ? ctx.project(req, res) : null;
|
|
261
97
|
if (!project) return; // project() already 404'd
|
|
@@ -270,11 +106,6 @@ export function register(app, ctx) {
|
|
|
270
106
|
});
|
|
271
107
|
|
|
272
108
|
// POST /projects/:pid/notes body: { body: "...", title?: "..." }
|
|
273
|
-
//
|
|
274
|
-
// Appends to .apc/notes/YYYY-MM-DD.md. Each note is a markdown block
|
|
275
|
-
// with timestamp + title + body. No editing — the file is append-only
|
|
276
|
-
// by design so the daemon doesn't have to manage UIDs. The deck UI
|
|
277
|
-
// can later read this back via GET if we add it.
|
|
278
109
|
app.post("/projects/:pid/notes", async (req, res) => {
|
|
279
110
|
const project = ctx.project ? ctx.project(req, res) : null;
|
|
280
111
|
if (!project) return;
|
|
@@ -295,20 +126,9 @@ export function register(app, ctx) {
|
|
|
295
126
|
|
|
296
127
|
// POST /deck/exec
|
|
297
128
|
//
|
|
298
|
-
// Light-touch action runner for the deck buttons.
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
// whitelist of "intent" verbs and the server picks the OS command.
|
|
302
|
-
//
|
|
303
|
-
// Body shape:
|
|
304
|
-
// { kind: "open_app", target: "claude" | "cursor" | "vscode" | "terminal" }
|
|
305
|
-
// { kind: "open_path", target: "/abs/path" | "<projectId>" } // opens in Finder/default
|
|
306
|
-
// { kind: "open_path_in", target: "<projectId>", app: "vscode" | "cursor" | "terminal" }
|
|
307
|
-
// { kind: "open_url", target: "https://..." }
|
|
308
|
-
// { kind: "copy_clipboard", text: "..." }
|
|
309
|
-
//
|
|
310
|
-
// Everything routes through `open` on macOS, `xdg-open` on Linux, or
|
|
311
|
-
// `start` on Windows — no shell metacharacters, all args as array.
|
|
129
|
+
// Light-touch action runner for the deck buttons. Companion clients can't
|
|
130
|
+
// shell out arbitrary commands — body picks from a whitelist of "intent"
|
|
131
|
+
// verbs and the server picks the OS command. See deck-exec.js for the kinds.
|
|
312
132
|
app.post("/deck/exec", async (req, res) => {
|
|
313
133
|
const { kind, target, app: appHint, text } = req.body || {};
|
|
314
134
|
if (!kind || typeof kind !== "string") {
|
|
@@ -322,190 +142,3 @@ export function register(app, ctx) {
|
|
|
322
142
|
}
|
|
323
143
|
});
|
|
324
144
|
}
|
|
325
|
-
|
|
326
|
-
// ── /deck/exec implementation ───────────────────────────────────────
|
|
327
|
-
//
|
|
328
|
-
// All shell spawning sits behind this helper so it can be unit-tested
|
|
329
|
-
// in isolation. The OS abstraction is intentionally tiny: pick the
|
|
330
|
-
// "opener" command for the platform and pass `target` as a single arg
|
|
331
|
-
// (no shell). For app-launching on macOS we use `open -a <App>`.
|
|
332
|
-
|
|
333
|
-
const MAC_APPS = {
|
|
334
|
-
// Whitelisted mac app names. Adding here is the only way the deck
|
|
335
|
-
// can launch something — we never honour a free-form `app` string.
|
|
336
|
-
claude: "Claude",
|
|
337
|
-
chatgpt: "ChatGPT",
|
|
338
|
-
cursor: "Cursor",
|
|
339
|
-
vscode: "Visual Studio Code",
|
|
340
|
-
zen: "Zen Browser",
|
|
341
|
-
terminal: "Terminal",
|
|
342
|
-
iterm: "iTerm",
|
|
343
|
-
finder: "Finder",
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
async function runDeckExec({ kind, target, appHint, text, ctx }) {
|
|
347
|
-
const { spawn } = await import("node:child_process");
|
|
348
|
-
const platform = process.platform;
|
|
349
|
-
|
|
350
|
-
const spawnDetached = (cmd, args) =>
|
|
351
|
-
new Promise((resolve, reject) => {
|
|
352
|
-
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
353
|
-
let settled = false;
|
|
354
|
-
const done = (err) => {
|
|
355
|
-
if (settled) return;
|
|
356
|
-
settled = true;
|
|
357
|
-
err ? reject(err) : resolve();
|
|
358
|
-
};
|
|
359
|
-
child.on("error", done);
|
|
360
|
-
// Give the process a tick to fail-fast (bad binary); otherwise
|
|
361
|
-
// detach and assume success.
|
|
362
|
-
setTimeout(() => {
|
|
363
|
-
try { child.unref(); } catch {}
|
|
364
|
-
done(null);
|
|
365
|
-
}, 250);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
const opener = () => {
|
|
369
|
-
if (platform === "darwin") return "open";
|
|
370
|
-
if (platform === "win32") return "start";
|
|
371
|
-
return "xdg-open";
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
// Resolve a project id (number or "<n>") into an absolute path via
|
|
375
|
-
// the daemon's project manager. Returns null when the id is bogus.
|
|
376
|
-
const projectPath = (idOrPath) => {
|
|
377
|
-
if (!idOrPath) return null;
|
|
378
|
-
const str = String(idOrPath);
|
|
379
|
-
if (str.startsWith("/")) return str;
|
|
380
|
-
if (!/^\d+$/.test(str)) return null;
|
|
381
|
-
const p = ctx.projects?.get?.(parseInt(str, 10));
|
|
382
|
-
return p?.path || null;
|
|
383
|
-
};
|
|
384
|
-
|
|
385
|
-
if (kind === "open_app") {
|
|
386
|
-
if (platform !== "darwin") throw new Error("open_app only implemented on macOS for now");
|
|
387
|
-
const appName = MAC_APPS[String(target || "").toLowerCase()];
|
|
388
|
-
if (!appName) throw new Error(`unknown app: ${target}`);
|
|
389
|
-
// Two-step launch:
|
|
390
|
-
// 1. `open -a` ensures the app is running (no-op if already up).
|
|
391
|
-
// 2. AppleScript `activate` brings it to the foreground across
|
|
392
|
-
// Spaces / Stage Manager, which `open` alone often skips when
|
|
393
|
-
// the app was already running in the background.
|
|
394
|
-
// Both are best-effort; we surface success as long as the launch
|
|
395
|
-
// command exited cleanly.
|
|
396
|
-
await spawnDetached("open", ["-a", appName]);
|
|
397
|
-
try {
|
|
398
|
-
await new Promise((resolve) => {
|
|
399
|
-
const child = spawn("osascript", [
|
|
400
|
-
"-e",
|
|
401
|
-
`tell application "${appName}" to activate`,
|
|
402
|
-
], { stdio: "ignore" });
|
|
403
|
-
child.on("close", () => resolve());
|
|
404
|
-
child.on("error", () => resolve());
|
|
405
|
-
setTimeout(() => { try { child.kill(); } catch {} ; resolve(); }, 600);
|
|
406
|
-
});
|
|
407
|
-
} catch {
|
|
408
|
-
// osascript missing or refused — `open -a` already ran.
|
|
409
|
-
}
|
|
410
|
-
return { app: appName };
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (kind === "open_path") {
|
|
414
|
-
const resolved = projectPath(target);
|
|
415
|
-
if (!resolved) throw new Error(`open_path: invalid target ${target}`);
|
|
416
|
-
await spawnDetached(opener(), [resolved]);
|
|
417
|
-
return { path: resolved };
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (kind === "open_path_in") {
|
|
421
|
-
if (platform !== "darwin") throw new Error("open_path_in only implemented on macOS for now");
|
|
422
|
-
const resolved = projectPath(target);
|
|
423
|
-
if (!resolved) throw new Error(`open_path_in: invalid target ${target}`);
|
|
424
|
-
const appName = MAC_APPS[String(appHint || "").toLowerCase()];
|
|
425
|
-
if (!appName) throw new Error(`open_path_in: unknown app ${appHint}`);
|
|
426
|
-
await spawnDetached("open", ["-a", appName, resolved]);
|
|
427
|
-
return { app: appName, path: resolved };
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (kind === "open_url") {
|
|
431
|
-
if (!target || !/^https?:\/\//i.test(String(target))) {
|
|
432
|
-
throw new Error("open_url: target must be http(s) URL");
|
|
433
|
-
}
|
|
434
|
-
await spawnDetached(opener(), [String(target)]);
|
|
435
|
-
return { url: target };
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (kind === "copy_clipboard") {
|
|
439
|
-
if (typeof text !== "string") throw new Error("copy_clipboard: text required");
|
|
440
|
-
// pbcopy on mac; xclip on linux; clip on windows.
|
|
441
|
-
const cmd =
|
|
442
|
-
platform === "darwin" ? "pbcopy" :
|
|
443
|
-
platform === "win32" ? "clip" :
|
|
444
|
-
"xclip";
|
|
445
|
-
const args = platform === "linux" ? ["-selection", "clipboard"] : [];
|
|
446
|
-
await new Promise((resolve, reject) => {
|
|
447
|
-
const child = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
448
|
-
child.on("error", reject);
|
|
449
|
-
child.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`))));
|
|
450
|
-
child.stdin.end(text);
|
|
451
|
-
});
|
|
452
|
-
return { bytes: text.length };
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
throw new Error(`unknown kind: ${kind}`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// ── Context + Notes helpers ────────────────────────────────────────
|
|
459
|
-
|
|
460
|
-
async function readProjectContext(projectPath) {
|
|
461
|
-
const fs = await import("node:fs/promises");
|
|
462
|
-
const path = await import("node:path");
|
|
463
|
-
const candidates = [
|
|
464
|
-
{ rel: "AGENTS.md", label: "AGENTS.md" },
|
|
465
|
-
{ rel: ".apc/memory.md", label: ".apc/memory.md" },
|
|
466
|
-
];
|
|
467
|
-
const chunks = [];
|
|
468
|
-
for (const { rel, label } of candidates) {
|
|
469
|
-
try {
|
|
470
|
-
const abs = path.join(projectPath, rel);
|
|
471
|
-
const content = await fs.readFile(abs, "utf8");
|
|
472
|
-
if (content.trim()) {
|
|
473
|
-
chunks.push(`# ${label}\n\n${content.trim()}\n`);
|
|
474
|
-
}
|
|
475
|
-
} catch {
|
|
476
|
-
// missing file is fine; we just skip it.
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
return chunks.join("\n---\n\n");
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
async function copyToClipboard(text) {
|
|
483
|
-
const { spawn } = await import("node:child_process");
|
|
484
|
-
const platform = process.platform;
|
|
485
|
-
const cmd =
|
|
486
|
-
platform === "darwin" ? "pbcopy" :
|
|
487
|
-
platform === "win32" ? "clip" :
|
|
488
|
-
"xclip";
|
|
489
|
-
const args = platform === "linux" ? ["-selection", "clipboard"] : [];
|
|
490
|
-
await new Promise((resolve, reject) => {
|
|
491
|
-
const child = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
492
|
-
child.on("error", reject);
|
|
493
|
-
child.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`))));
|
|
494
|
-
child.stdin.end(text);
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
async function appendProjectNote(projectPath, { title, body }) {
|
|
499
|
-
const fs = await import("node:fs/promises");
|
|
500
|
-
const path = await import("node:path");
|
|
501
|
-
const notesDir = path.join(projectPath, ".apc", "notes");
|
|
502
|
-
await fs.mkdir(notesDir, { recursive: true });
|
|
503
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
504
|
-
const file = path.join(notesDir, `${today}.md`);
|
|
505
|
-
const ts = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
506
|
-
const block = title
|
|
507
|
-
? `\n## ${title}\n_${ts}_\n\n${body}\n`
|
|
508
|
-
: `\n### ${ts}\n\n${body}\n`;
|
|
509
|
-
await fs.appendFile(file, block, "utf8");
|
|
510
|
-
return file;
|
|
511
|
-
}
|
|
@@ -2,56 +2,7 @@
|
|
|
2
2
|
// POST /engines/models — live model catalog from a provider.
|
|
3
3
|
// GET /engines/models — legacy (Ollama only, no auth).
|
|
4
4
|
import { ENGINE_IDS } from "#core/engines/index.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const DEFAULT_BASE = {
|
|
8
|
-
openai: "https://api.openai.com/v1",
|
|
9
|
-
groq: "https://api.groq.com/openai/v1",
|
|
10
|
-
openrouter: "https://openrouter.ai/api/v1",
|
|
11
|
-
gemini: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
12
|
-
anthropic: "https://api.anthropic.com/v1",
|
|
13
|
-
ollama: "http://localhost:11434",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// Returns { models } or { error }. Reads the right /models endpoint per engine.
|
|
17
|
-
async function listModels(engine, baseUrl, apiKey) {
|
|
18
|
-
const base = String(baseUrl || DEFAULT_BASE[engine] || "").replace(/\/$/, "");
|
|
19
|
-
|
|
20
|
-
if (engine === "ollama") {
|
|
21
|
-
const b = base || process.env.OLLAMA_HOST || "http://localhost:11434";
|
|
22
|
-
const r = await fetchJsonWithTimeout(`${b}/api/tags`, { timeoutMs: 2500 });
|
|
23
|
-
if (!r.ok) return { error: r.reason || "no se pudo contactar Ollama" };
|
|
24
|
-
const list = Array.isArray(r.json?.models) ? r.json.models : [];
|
|
25
|
-
return { models: list.map((m) => m?.name).filter((n) => typeof n === "string" && n) };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (engine === "anthropic") {
|
|
29
|
-
if (!apiKey) return { error: "falta api_key" };
|
|
30
|
-
const b = base || DEFAULT_BASE.anthropic;
|
|
31
|
-
const r = await fetchJsonWithTimeout(`${b}/models?limit=100`, {
|
|
32
|
-
timeoutMs: 5000,
|
|
33
|
-
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
34
|
-
});
|
|
35
|
-
if (!r.ok) return { error: r.reason || `HTTP ${r.status}` };
|
|
36
|
-
const data = Array.isArray(r.json?.data) ? r.json.data : [];
|
|
37
|
-
return { models: data.map((m) => m?.id).filter(Boolean) };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// openai-compatible family: openai, groq, openrouter, gemini, azure, custom
|
|
41
|
-
if (!apiKey) return { error: "falta api_key" };
|
|
42
|
-
if (!base) return { error: "falta base_url" };
|
|
43
|
-
const r = await fetchJsonWithTimeout(`${base}/models`, {
|
|
44
|
-
timeoutMs: 5000,
|
|
45
|
-
headers: { authorization: `Bearer ${apiKey}` },
|
|
46
|
-
});
|
|
47
|
-
if (!r.ok) return { error: r.reason || `HTTP ${r.status}` };
|
|
48
|
-
const data = Array.isArray(r.json?.data)
|
|
49
|
-
? r.json.data
|
|
50
|
-
: Array.isArray(r.json?.models)
|
|
51
|
-
? r.json.models
|
|
52
|
-
: [];
|
|
53
|
-
return { models: data.map((m) => m?.id || m?.name).filter(Boolean) };
|
|
54
|
-
}
|
|
5
|
+
import { listModels } from "#core/engines/catalog.js";
|
|
55
6
|
|
|
56
7
|
export function register(app, { config }) {
|
|
57
8
|
app.get("/engines", (_req, res) => res.json({ engines: ENGINE_IDS }));
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
appendTurn,
|
|
14
14
|
readConversation,
|
|
15
15
|
setStatus,
|
|
16
|
-
} from "
|
|
16
|
+
} from "#core/stores/conversations.js";
|
|
17
17
|
|
|
18
18
|
// Pick a model for a direct agent chat: explicit override → agent's own model →
|
|
19
19
|
// super-agent default (resolved via the same router the super-agent uses, so
|
|
@@ -202,4 +202,36 @@ export function register(app, { projects, registries, project }) {
|
|
|
202
202
|
res.status(500).json({ error: e.message });
|
|
203
203
|
}
|
|
204
204
|
});
|
|
205
|
+
|
|
206
|
+
// Smoke test — calls tools/list and reports either the tool catalog or a
|
|
207
|
+
// clean error message. Used by the "Test" button in the MCP card so the
|
|
208
|
+
// user can sanity-check a freshly-saved MCP without firing a real tool.
|
|
209
|
+
app.post("/projects/:pid/mcps/:name/test", async (req, res) => {
|
|
210
|
+
const p = project(req, res);
|
|
211
|
+
if (!p) return;
|
|
212
|
+
try {
|
|
213
|
+
const result = await registries.for(p).listTools(req.params.name);
|
|
214
|
+
const tools = Array.isArray(result?.tools) ? result.tools : [];
|
|
215
|
+
res.json({
|
|
216
|
+
ok: true,
|
|
217
|
+
tool_count: tools.length,
|
|
218
|
+
tools: tools.map((t) => ({
|
|
219
|
+
name: t.name,
|
|
220
|
+
description: t.description || "",
|
|
221
|
+
})),
|
|
222
|
+
});
|
|
223
|
+
} catch (e) {
|
|
224
|
+
res.status(200).json({ ok: false, error: e.message });
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// In-memory log buffer for one MCP — stderr tail (stdio) or fetch summary
|
|
229
|
+
// (http) plus a ring of recent events.
|
|
230
|
+
app.get("/projects/:pid/mcps/:name/logs", (req, res) => {
|
|
231
|
+
const p = project(req, res);
|
|
232
|
+
if (!p) return;
|
|
233
|
+
const logs = registries.for(p).getLogs(req.params.name);
|
|
234
|
+
if (!logs) return res.status(404).json({ error: "MCP not found" });
|
|
235
|
+
res.json(logs);
|
|
236
|
+
});
|
|
205
237
|
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
deleteRoutine,
|
|
16
16
|
setEnabled as setRoutineEnabled,
|
|
17
17
|
runRoutineNow,
|
|
18
|
-
} from "
|
|
18
|
+
} from "#core/routines/index.js";
|
|
19
19
|
|
|
20
20
|
export function register(app, { projects, registries, plugins, project, config }) {
|
|
21
21
|
app.get("/projects/:pid/routines", (req, res) => {
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
import fs from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { readAgents } from "#core/apc/parser.js";
|
|
11
|
+
import { apcProjectFile, apcAgentsDir } from "#core/apc/paths.js";
|
|
11
12
|
import { readSessionFrontmatter } from "#core/stores/sessions.js";
|
|
12
13
|
import { buildAgentSystem } from "#core/agent/build-agent-system.js";
|
|
13
14
|
import { CHANNELS } from "#core/constants/channels.js";
|
|
14
15
|
import { getRuntime, RUNTIME_IDS } from "../runtimes/index.js";
|
|
15
|
-
import { detectAll } from "
|
|
16
|
+
import { detectAll } from "#core/runtimes/detect.js";
|
|
16
17
|
import { buildRuntimeBridgeHint as buildApfHint } from "#core/agent/runtime-bridge.js";
|
|
17
18
|
import {
|
|
18
19
|
createRuntimeSession,
|
|
@@ -52,7 +53,7 @@ export function register(app, { projects, registries, plugins, project, config }
|
|
|
52
53
|
let projectName = path.basename(p.path);
|
|
53
54
|
try {
|
|
54
55
|
const meta = JSON.parse(
|
|
55
|
-
fs.readFileSync(
|
|
56
|
+
fs.readFileSync(apcProjectFile(p.path), "utf8")
|
|
56
57
|
);
|
|
57
58
|
if (meta.name) projectName = meta.name;
|
|
58
59
|
} catch {}
|
|
@@ -152,7 +153,7 @@ export function register(app, { projects, registries, plugins, project, config }
|
|
|
152
153
|
|
|
153
154
|
const sessionRoots = [
|
|
154
155
|
path.join(p.storagePath || p.path, "agents"),
|
|
155
|
-
|
|
156
|
+
apcAgentsDir(p.path),
|
|
156
157
|
];
|
|
157
158
|
let sessionFile = null;
|
|
158
159
|
let agentSlug = null;
|