@agentprojectcontext/apx 1.33.1 → 1.35.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 (208) hide show
  1. package/package.json +1 -1
  2. package/skills/apx/SKILL.md +49 -61
  3. package/src/core/agent/a2a/reply.js +48 -0
  4. package/src/core/agent/build-agent-system.js +136 -59
  5. package/src/core/agent/channels/voice-context.js +98 -0
  6. package/src/core/agent/memory.js +2 -1
  7. package/src/core/agent/prompt-builder.js +178 -124
  8. package/src/core/agent/prompts/channels/code.md +12 -10
  9. package/src/core/agent/prompts/channels/desktop.md +5 -32
  10. package/src/core/agent/prompts/channels/telegram.md +4 -15
  11. package/src/core/agent/prompts/channels/web_code.md +11 -11
  12. package/src/core/agent/prompts/core/agent-base.md +24 -0
  13. package/src/core/agent/prompts/core/project-agent.md +11 -0
  14. package/src/core/agent/prompts/core/super-agent.md +21 -0
  15. package/src/core/agent/prompts/discipline/action.md +10 -0
  16. package/src/core/agent/prompts/discipline/single-segment.md +6 -0
  17. package/src/core/agent/prompts/discipline/two-segment.md +11 -0
  18. package/src/core/agent/prompts/modes/code-build.md +1 -0
  19. package/src/core/agent/prompts/modes/code-plan.md +1 -0
  20. package/src/core/agent/prompts/modes/index.js +28 -0
  21. package/src/core/agent/self-memory.js +43 -1
  22. package/src/core/agent/skills/index-store.js +307 -0
  23. package/src/core/agent/skills/index.js +15 -1
  24. package/src/core/agent/skills/inspector.js +317 -0
  25. package/src/core/agent/skills/loader.js +22 -18
  26. package/src/core/agent/stream/turn-accumulator.js +73 -0
  27. package/src/core/agent/suggestions.js +37 -0
  28. package/src/core/agent/super-agent.js +7 -1
  29. package/src/core/agent/tools/handlers/_git.js +50 -0
  30. package/src/core/agent/tools/handlers/add-project.js +5 -2
  31. package/src/core/agent/tools/handlers/call-runtime.js +3 -2
  32. package/src/core/agent/tools/handlers/git-diff.js +44 -0
  33. package/src/core/agent/tools/handlers/git-log.js +38 -0
  34. package/src/core/agent/tools/handlers/git-show.js +34 -0
  35. package/src/core/agent/tools/handlers/git-status.js +61 -0
  36. package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
  37. package/src/core/agent/tools/helpers.js +2 -2
  38. package/src/core/agent/tools/names.js +169 -0
  39. package/src/core/agent/tools/registry-bridge.js +6 -14
  40. package/src/core/agent/tools/registry.js +103 -69
  41. package/src/core/apc/context-copy.js +27 -0
  42. package/src/core/apc/notes.js +19 -0
  43. package/src/core/apc/parser.js +12 -5
  44. package/src/core/apc/paths.js +87 -0
  45. package/src/core/apc/scaffold.js +82 -76
  46. package/src/core/apc/skill-sync.js +10 -0
  47. package/src/{host/daemon/plugins → core/channels}/telegram/dispatch.js +38 -16
  48. package/src/core/config/index.js +24 -2
  49. package/src/core/config/redact.js +95 -0
  50. package/src/core/constants/channels.js +2 -0
  51. package/src/core/constants/code-modes.js +10 -0
  52. package/src/core/constants/index.js +1 -0
  53. package/src/core/deck/manifest.js +186 -0
  54. package/src/core/engines/catalog.js +83 -0
  55. package/src/core/{tools → http-tools}/browser.js +0 -1
  56. package/src/core/{tools → http-tools}/fetch.js +0 -1
  57. package/src/core/{tools → http-tools}/glob.js +0 -1
  58. package/src/core/{tools → http-tools}/grep.js +0 -1
  59. package/src/core/{tools → http-tools}/registry.js +0 -1
  60. package/src/core/{tools → http-tools}/search.js +0 -1
  61. package/src/core/i18n/en.js +9 -0
  62. package/src/core/i18n/es.js +12 -0
  63. package/src/core/i18n/index.js +54 -0
  64. package/src/core/i18n/pt.js +9 -0
  65. package/src/core/identity/telegram.js +2 -1
  66. package/src/core/mcp/runner.js +272 -14
  67. package/src/core/mcp/sources.js +3 -2
  68. package/src/core/routines/index.js +16 -0
  69. package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
  70. package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
  71. package/src/core/runtime-skills/apx/SKILL.md +83 -0
  72. package/src/core/runtime-skills/apx-agency-agents/SKILL.md +125 -0
  73. package/src/core/runtime-skills/apx-agent/SKILL.md +97 -0
  74. package/src/core/runtime-skills/apx-mcp/SKILL.md +111 -0
  75. package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +169 -0
  76. package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +20 -29
  77. package/src/core/runtime-skills/apx-routine/SKILL.md +127 -0
  78. package/src/core/runtime-skills/apx-runtime/SKILL.md +99 -0
  79. package/src/core/runtime-skills/apx-sessions/SKILL.md +232 -0
  80. package/src/core/runtime-skills/apx-skill-builder/SKILL.md +129 -0
  81. package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +18 -21
  82. package/src/core/runtime-skills/apx-telegram/SKILL.md +120 -0
  83. package/src/core/runtime-skills/apx-voice/SKILL.md +117 -0
  84. package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
  85. package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
  86. package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
  87. package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
  88. package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
  89. package/src/core/stores/code-sessions.js +50 -2
  90. package/src/core/stores/routine-memory.js +1 -1
  91. package/src/core/stores/sessions-search.js +121 -0
  92. package/src/core/stores/sessions.js +38 -0
  93. package/src/core/vars/index.js +14 -0
  94. package/src/core/vars/interpolate.js +86 -0
  95. package/src/core/vars/sources.js +151 -0
  96. package/src/core/voice/audio-decode.js +38 -0
  97. package/src/core/voice/transcription.js +225 -0
  98. package/src/host/daemon/api/admin-config.js +5 -82
  99. package/src/host/daemon/api/agents.js +5 -5
  100. package/src/host/daemon/api/code.js +17 -169
  101. package/src/host/daemon/api/config.js +3 -4
  102. package/src/host/daemon/api/conversations.js +8 -29
  103. package/src/host/daemon/api/deck.js +37 -404
  104. package/src/host/daemon/api/engines.js +1 -80
  105. package/src/host/daemon/api/exec.js +1 -1
  106. package/src/host/daemon/api/mcps.js +32 -0
  107. package/src/host/daemon/api/routines.js +1 -1
  108. package/src/host/daemon/api/runtimes.js +4 -3
  109. package/src/host/daemon/api/sessions-search.js +24 -140
  110. package/src/host/daemon/api/sessions.js +12 -30
  111. package/src/host/daemon/api/shared.js +2 -1
  112. package/src/host/daemon/api/skills.js +140 -6
  113. package/src/host/daemon/api/super-agent.js +56 -1
  114. package/src/host/daemon/api/telegram.js +1 -11
  115. package/src/host/daemon/api/tools.js +6 -6
  116. package/src/host/daemon/api/transcribe.js +2 -2
  117. package/src/host/daemon/api/vars.js +137 -0
  118. package/src/host/daemon/api/voice.js +13 -290
  119. package/src/host/daemon/api.js +2 -0
  120. package/src/host/daemon/db.js +6 -6
  121. package/src/host/daemon/deck-exec.js +148 -0
  122. package/src/host/daemon/index.js +20 -3
  123. package/src/host/daemon/plugins/telegram/index.js +9 -9
  124. package/src/host/daemon/routines-scheduler.js +64 -0
  125. package/src/host/daemon/smoke.js +3 -2
  126. package/src/host/daemon/whisper-server.js +225 -0
  127. package/src/interfaces/cli/branding.js +53 -0
  128. package/src/interfaces/cli/commands/agent.js +3 -2
  129. package/src/interfaces/cli/commands/command.js +2 -3
  130. package/src/interfaces/cli/commands/messages.js +6 -2
  131. package/src/interfaces/cli/commands/pair.js +5 -4
  132. package/src/interfaces/cli/commands/search.js +1 -1
  133. package/src/interfaces/cli/commands/sessions.js +3 -2
  134. package/src/interfaces/cli/commands/skills.js +290 -55
  135. package/src/interfaces/cli/index.js +84 -2
  136. package/src/interfaces/web/dist/assets/index-C0fm31dY.js +618 -0
  137. package/src/interfaces/web/dist/assets/index-C0fm31dY.js.map +1 -0
  138. package/src/interfaces/web/dist/assets/index-UcAqlBO6.css +1 -0
  139. package/src/interfaces/web/dist/index.html +2 -2
  140. package/src/interfaces/web/package-lock.json +182 -182
  141. package/src/interfaces/web/src/components/ModelCombobox.tsx +2 -1
  142. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
  143. package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
  144. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +37 -4
  145. package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
  146. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
  147. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
  148. package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
  149. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
  150. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
  151. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
  152. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
  153. package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
  154. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
  155. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
  156. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
  157. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
  158. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +73 -4
  159. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +222 -0
  160. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
  161. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
  162. package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
  163. package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
  164. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
  165. package/src/interfaces/web/src/constants/index.ts +1 -1
  166. package/src/interfaces/web/src/hooks/useChat.ts +19 -0
  167. package/src/interfaces/web/src/i18n/en.ts +175 -7
  168. package/src/interfaces/web/src/i18n/es.ts +180 -15
  169. package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
  170. package/src/interfaces/web/src/lib/api/skills.ts +70 -0
  171. package/src/interfaces/web/src/lib/api/vars.ts +38 -0
  172. package/src/interfaces/web/src/lib/api.ts +1 -0
  173. package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
  174. package/src/interfaces/web/src/screens/SettingsScreen.tsx +6 -2
  175. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
  176. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
  177. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
  178. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
  179. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
  180. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
  181. package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
  182. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
  183. package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
  184. package/src/interfaces/web/src/types/daemon.ts +15 -0
  185. package/skills/apx-agency-agents/SKILL.md +0 -141
  186. package/skills/apx-agent/SKILL.md +0 -100
  187. package/skills/apx-mcp-builder/SKILL.md +0 -183
  188. package/skills/apx-routine/SKILL.md +0 -140
  189. package/skills/apx-runtime/SKILL.md +0 -117
  190. package/skills/apx-sessions/SKILL.md +0 -281
  191. package/skills/apx-skill-builder/SKILL.md +0 -153
  192. package/skills/apx-telegram/SKILL.md +0 -131
  193. package/skills/apx-voice/SKILL.md +0 -137
  194. package/src/core/agent/prompts/action-discipline.md +0 -24
  195. package/src/core/agent/prompts/super-agent-base.md +0 -42
  196. package/src/host/daemon/transcription.js +0 -538
  197. package/src/host/daemon/whisper-transcribe.py +0 -73
  198. package/src/interfaces/web/dist/assets/index-Aaiw8BZN.css +0 -1
  199. package/src/interfaces/web/dist/assets/index-DPqtjDjh.js +0 -602
  200. package/src/interfaces/web/dist/assets/index-DPqtjDjh.js.map +0 -1
  201. /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
  202. /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
  203. /package/src/{host/daemon/plugins → core/channels}/telegram/helpers.js +0 -0
  204. /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
  205. /package/src/core/{tools → http-tools}/index.js +0 -0
  206. /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
  207. /package/src/{host/daemon → core/stores}/conversations.js +0 -0
  208. /package/src/{host/daemon → core/util}/thinking.js +0 -0
@@ -4,12 +4,10 @@
4
4
  // POST /projects/:pid/agents/:slug/compact
5
5
  // POST /projects/:pid/agents/:slug/conversations/:id/compact
6
6
  // POST /projects/:pid/send (agent-to-agent)
7
- import fs from "node:fs";
8
- import path from "node:path";
9
7
  import { readAgents } from "#core/apc/parser.js";
10
- import { callEngine } from "#core/engines/index.js";
11
- import { listConversations, readConversation } from "../conversations.js";
12
- import { compactConversation } from "../compact.js";
8
+ import { listConversations, readConversation } from "#core/stores/conversations.js";
9
+ import { compactConversation } from "#core/stores/conversations-compactor.js";
10
+ import { replyAsAgent } from "#core/agent/a2a/reply.js";
13
11
  import { nowIso } from "./shared.js";
14
12
 
15
13
  export function register(app, { project, config }) {
@@ -102,30 +100,11 @@ export function register(app, { project, config }) {
102
100
  let reply = null;
103
101
  if (deliver && toAgent.fields.Model) {
104
102
  try {
105
- const tf = toAgent.fields;
106
- const parts = [];
107
- if (tf.Description) parts.push(tf.Description);
108
- if (tf.Role) parts.push(`Role: ${tf.Role}`);
109
- if (tf.Language) parts.push(`Default language: ${tf.Language}`);
110
- parts.push(
111
- `You are ${toAgent.slug}. You just received a message from ${fromAgent.slug}. Reply concisely.`
112
- );
113
- const memPath = path.join(
114
- p.path,
115
- ".apc",
116
- "agents",
117
- toAgent.slug,
118
- "memory.md"
119
- );
120
- if (fs.existsSync(memPath))
121
- parts.push("## Memory\n" + fs.readFileSync(memPath, "utf8"));
122
-
123
- const result = await callEngine({
124
- modelId: toAgent.fields.Model,
125
- system: parts.join("\n\n"),
126
- messages: [
127
- { role: "user", content: `From ${fromAgent.slug}:\n\n${body}` },
128
- ],
103
+ const result = await replyAsAgent({
104
+ projectPath: p.path,
105
+ toAgent,
106
+ fromAgent,
107
+ body,
129
108
  config: p.config || config,
130
109
  });
131
110
 
@@ -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,86 +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
- // Gemini's native models endpoint returns a much richer catalog than the
17
- // OpenAI-compat shim (which only echoes back a handful). We always query the
18
- // native URL regardless of the user's configured base_url.
19
- const GEMINI_NATIVE_BASE = "https://generativelanguage.googleapis.com/v1beta";
20
-
21
- // Returns { models } or { error }. Reads the right /models endpoint per engine.
22
- async function listModels(engine, baseUrl, apiKey) {
23
- const base = String(baseUrl || DEFAULT_BASE[engine] || "").replace(/\/$/, "");
24
-
25
- if (engine === "ollama") {
26
- const b = base || process.env.OLLAMA_HOST || "http://localhost:11434";
27
- const r = await fetchJsonWithTimeout(`${b}/api/tags`, { timeoutMs: 2500 });
28
- if (!r.ok) return { error: r.reason || "no se pudo contactar Ollama" };
29
- const list = Array.isArray(r.json?.models) ? r.json.models : [];
30
- return { models: list.map((m) => m?.name).filter((n) => typeof n === "string" && n) };
31
- }
32
-
33
- if (engine === "anthropic") {
34
- if (!apiKey) return { error: "falta api_key" };
35
- const b = base || DEFAULT_BASE.anthropic;
36
- const r = await fetchJsonWithTimeout(`${b}/models?limit=100`, {
37
- timeoutMs: 5000,
38
- headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
39
- });
40
- if (!r.ok) return { error: r.reason || `HTTP ${r.status}` };
41
- const data = Array.isArray(r.json?.data) ? r.json.data : [];
42
- return { models: data.map((m) => m?.id).filter(Boolean) };
43
- }
44
-
45
- if (engine === "gemini") {
46
- if (!apiKey) return { error: "falta api_key" };
47
- // Native Gemini API: returns a `models` array with rich metadata, including
48
- // `supportedGenerationMethods` so we can drop embeddings/vision-only entries.
49
- // Names come back as "models/<id>"; strip the prefix for display.
50
- const r = await fetchJsonWithTimeout(
51
- `${GEMINI_NATIVE_BASE}/models?key=${encodeURIComponent(apiKey)}&pageSize=200`,
52
- { timeoutMs: 5000 },
53
- );
54
- if (!r.ok) return { error: r.reason || `HTTP ${r.status}` };
55
- const data = Array.isArray(r.json?.models) ? r.json.models : [];
56
- const models = data
57
- .filter((m) => {
58
- const methods = m?.supportedGenerationMethods;
59
- if (!Array.isArray(methods)) return true;
60
- return methods.includes("generateContent");
61
- })
62
- .map((m) => {
63
- const name = typeof m?.name === "string" ? m.name : "";
64
- return name.startsWith("models/") ? name.slice("models/".length) : name;
65
- })
66
- .filter(Boolean);
67
- return { models };
68
- }
69
-
70
- // openai-compatible family: openai, groq, openrouter, azure, custom
71
- if (!apiKey) return { error: "falta api_key" };
72
- if (!base) return { error: "falta base_url" };
73
- const r = await fetchJsonWithTimeout(`${base}/models`, {
74
- timeoutMs: 5000,
75
- headers: { authorization: `Bearer ${apiKey}` },
76
- });
77
- if (!r.ok) return { error: r.reason || `HTTP ${r.status}` };
78
- const data = Array.isArray(r.json?.data)
79
- ? r.json.data
80
- : Array.isArray(r.json?.models)
81
- ? r.json.models
82
- : [];
83
- return { models: data.map((m) => m?.id || m?.name).filter(Boolean) };
84
- }
5
+ import { listModels } from "#core/engines/catalog.js";
85
6
 
86
7
  export function register(app, { config }) {
87
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
  }