@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
@@ -1,95 +1,16 @@
1
1
  // APX Deck bootstrap surface.
2
- // It exposes read-only context for companion clients without making external
3
- // services first-class APX daemon dependencies.
4
-
5
- const CORE_WIDGETS = [
6
- {
7
- id: "apx-current-project",
8
- title: "Proyecto actual",
9
- source: "apx",
10
- desktop: "project",
11
- kind: "context",
12
- status: "available",
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
- res.json(buildDeckManifest(ctx));
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
- // into the global config under `deck.widget_overrides[id]`. The next
208
- // /deck/manifest call reflects it. No-op for unknown widget ids so
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
- // any `apx project add` that happened since then is NOT in there.
222
- // Persisting that stale snapshot wipes out user-added projects
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
- // Also mirror the override into the live ctx.config so the next
240
- // /deck/manifest in this same process picks it up without a
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
- // with a small header per file, and ships the result to the
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. We don't want the
299
- // companion clients to shell out arbitrary commands — that's the
300
- // safety promise in /deck/manifest so the body picks from a small
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 { fetchJsonWithTimeout } from "#core/engines/_health.js";
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 "../conversations.js";
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 "../routines.js";
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 "../env-detect.js";
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(path.join(p.path, ".apc", "project.json"), "utf8")
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
- path.join(p.path, ".apc", "agents"),
156
+ apcAgentsDir(p.path),
156
157
  ];
157
158
  let sessionFile = null;
158
159
  let agentSlug = null;