@agentprojectcontext/apx 1.33.0 → 1.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/package.json +1 -1
  2. package/skills/apc-context/SKILL.md +2 -5
  3. package/skills/apx/SKILL.md +49 -61
  4. package/src/core/agent/a2a/reply.js +48 -0
  5. package/src/core/agent/build-agent-system.js +4 -3
  6. package/src/core/agent/channels/voice-context.js +98 -0
  7. package/src/core/agent/memory.js +2 -1
  8. package/src/core/agent/prompt-builder.js +2 -1
  9. package/src/core/agent/prompts/modes/code-build.md +1 -0
  10. package/src/core/agent/prompts/modes/code-plan.md +1 -0
  11. package/src/core/agent/prompts/modes/index.js +28 -0
  12. package/src/core/agent/skills/loader.js +22 -18
  13. package/src/core/agent/stream/turn-accumulator.js +73 -0
  14. package/src/core/agent/suggestions.js +37 -0
  15. package/src/core/agent/tools/handlers/add-project.js +5 -2
  16. package/src/core/agent/tools/handlers/call-runtime.js +3 -2
  17. package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
  18. package/src/core/agent/tools/helpers.js +2 -2
  19. package/src/core/agent/tools/names.js +138 -0
  20. package/src/core/agent/tools/registry-bridge.js +6 -14
  21. package/src/core/agent/tools/registry.js +68 -65
  22. package/src/core/apc/context-copy.js +27 -0
  23. package/src/core/apc/notes.js +19 -0
  24. package/src/core/apc/parser.js +13 -6
  25. package/src/core/apc/paths.js +87 -0
  26. package/src/core/apc/scaffold.js +82 -74
  27. package/src/core/apc/skill-sync.js +13 -1
  28. package/src/core/channels/telegram/dispatch.js +595 -0
  29. package/src/core/channels/telegram/helpers.js +130 -0
  30. package/src/core/config/index.js +3 -2
  31. package/src/core/config/redact.js +95 -0
  32. package/src/core/constants/channels.js +2 -0
  33. package/src/core/constants/code-modes.js +10 -0
  34. package/src/core/constants/index.js +1 -0
  35. package/src/core/deck/manifest.js +186 -0
  36. package/src/core/engines/catalog.js +83 -0
  37. package/src/core/engines/gemini.js +28 -11
  38. package/src/core/engines/index.js +11 -1
  39. package/src/core/{tools → http-tools}/browser.js +0 -1
  40. package/src/core/{tools → http-tools}/fetch.js +0 -1
  41. package/src/core/{tools → http-tools}/glob.js +0 -1
  42. package/src/core/{tools → http-tools}/grep.js +0 -1
  43. package/src/core/{tools → http-tools}/registry.js +0 -1
  44. package/src/core/{tools → http-tools}/search.js +0 -1
  45. package/src/core/i18n/en.js +9 -0
  46. package/src/core/i18n/es.js +12 -0
  47. package/src/core/i18n/index.js +54 -0
  48. package/src/core/i18n/pt.js +9 -0
  49. package/src/core/identity/telegram.js +2 -1
  50. package/src/core/mcp/runner.js +272 -14
  51. package/src/core/mcp/sources.js +3 -2
  52. package/src/core/routines/index.js +16 -0
  53. package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
  54. package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
  55. package/src/core/runtime-skills/apx/SKILL.md +95 -0
  56. package/src/core/runtime-skills/apx-mcp/SKILL.md +116 -0
  57. package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
  58. package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
  59. package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
  60. package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
  61. package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
  62. package/src/core/stores/code-sessions.js +50 -2
  63. package/src/core/stores/routine-memory.js +1 -1
  64. package/src/core/stores/sessions-search.js +121 -0
  65. package/src/core/stores/sessions.js +38 -0
  66. package/src/core/vars/index.js +14 -0
  67. package/src/core/vars/interpolate.js +86 -0
  68. package/src/core/vars/sources.js +151 -0
  69. package/src/core/voice/audio-decode.js +38 -0
  70. package/src/core/voice/transcription.js +225 -0
  71. package/src/host/daemon/api/admin-config.js +5 -82
  72. package/src/host/daemon/api/agents.js +5 -5
  73. package/src/host/daemon/api/code.js +17 -169
  74. package/src/host/daemon/api/config.js +3 -4
  75. package/src/host/daemon/api/conversations.js +8 -29
  76. package/src/host/daemon/api/deck.js +37 -404
  77. package/src/host/daemon/api/engines.js +1 -50
  78. package/src/host/daemon/api/exec.js +1 -1
  79. package/src/host/daemon/api/mcps.js +32 -0
  80. package/src/host/daemon/api/routines.js +1 -1
  81. package/src/host/daemon/api/runtimes.js +4 -3
  82. package/src/host/daemon/api/sessions-search.js +24 -140
  83. package/src/host/daemon/api/sessions.js +12 -30
  84. package/src/host/daemon/api/shared.js +2 -1
  85. package/src/host/daemon/api/telegram.js +1 -11
  86. package/src/host/daemon/api/tools.js +6 -6
  87. package/src/host/daemon/api/transcribe.js +2 -2
  88. package/src/host/daemon/api/vars.js +137 -0
  89. package/src/host/daemon/api/voice.js +13 -290
  90. package/src/host/daemon/api.js +2 -0
  91. package/src/host/daemon/db.js +6 -6
  92. package/src/host/daemon/deck-exec.js +148 -0
  93. package/src/host/daemon/index.js +3 -3
  94. package/src/host/daemon/plugins/telegram/index.js +24 -687
  95. package/src/host/daemon/routines-scheduler.js +64 -0
  96. package/src/host/daemon/smoke.js +3 -2
  97. package/src/host/daemon/whisper-server.js +225 -0
  98. package/src/interfaces/cli/commands/agent.js +3 -2
  99. package/src/interfaces/cli/commands/command.js +2 -3
  100. package/src/interfaces/cli/commands/messages.js +6 -2
  101. package/src/interfaces/cli/commands/pair.js +5 -4
  102. package/src/interfaces/cli/commands/search.js +1 -1
  103. package/src/interfaces/cli/commands/sessions.js +3 -2
  104. package/src/interfaces/cli/commands/skills.js +36 -55
  105. package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +1 -0
  106. package/src/interfaces/web/dist/assets/index-M4FspaCH.js +613 -0
  107. package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +1 -0
  108. package/src/interfaces/web/dist/index.html +2 -2
  109. package/src/interfaces/web/package-lock.json +182 -182
  110. package/src/interfaces/web/src/components/ModelCombobox.tsx +44 -8
  111. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
  112. package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
  113. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
  114. package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
  115. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
  116. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
  117. package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
  118. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
  119. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
  120. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
  121. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
  122. package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
  123. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
  124. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
  125. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
  126. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
  127. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +5 -4
  128. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
  129. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
  130. package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
  131. package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
  132. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
  133. package/src/interfaces/web/src/constants/index.ts +1 -1
  134. package/src/interfaces/web/src/i18n/en.ts +174 -7
  135. package/src/interfaces/web/src/i18n/es.ts +179 -15
  136. package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
  137. package/src/interfaces/web/src/lib/api/vars.ts +38 -0
  138. package/src/interfaces/web/src/lib/api.ts +1 -0
  139. package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
  140. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
  141. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
  142. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
  143. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
  144. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
  145. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
  146. package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
  147. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
  148. package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
  149. package/src/interfaces/web/src/types/daemon.ts +5 -0
  150. package/src/host/daemon/transcription.js +0 -538
  151. package/src/host/daemon/whisper-transcribe.py +0 -73
  152. package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +0 -1
  153. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js +0 -602
  154. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js.map +0 -1
  155. /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
  156. /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
  157. /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
  158. /package/src/core/{tools → http-tools}/index.js +0 -0
  159. /package/{skills → src/core/runtime-skills}/apx-agency-agents/SKILL.md +0 -0
  160. /package/{skills → src/core/runtime-skills}/apx-agent/SKILL.md +0 -0
  161. /package/{skills → src/core/runtime-skills}/apx-mcp-builder/SKILL.md +0 -0
  162. /package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +0 -0
  163. /package/{skills → src/core/runtime-skills}/apx-routine/SKILL.md +0 -0
  164. /package/{skills → src/core/runtime-skills}/apx-runtime/SKILL.md +0 -0
  165. /package/{skills → src/core/runtime-skills}/apx-sessions/SKILL.md +0 -0
  166. /package/{skills → src/core/runtime-skills}/apx-skill-builder/SKILL.md +0 -0
  167. /package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +0 -0
  168. /package/{skills → src/core/runtime-skills}/apx-telegram/SKILL.md +0 -0
  169. /package/{skills → src/core/runtime-skills}/apx-voice/SKILL.md +0 -0
  170. /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
  171. /package/src/{host/daemon → core/stores}/conversations.js +0 -0
  172. /package/src/{host/daemon → core/util}/thinking.js +0 -0
@@ -0,0 +1,130 @@
1
+ // Stateless helpers for the Telegram plugin. Extracted from index.js so the
2
+ // big poller class stays focused on lifecycle + message dispatch. Each
3
+ // function is pure (no `this`) — instances import them and call as needed.
4
+ import fs from "node:fs";
5
+ import { TELEGRAM_STATE_PATH } from "#core/config/index.js";
6
+
7
+ const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
8
+
9
+ /**
10
+ * Build the channelMeta block the super-agent loop receives for a Telegram
11
+ * turn. The prompt template at src/core/agent/prompts/channels/telegram.md
12
+ * interpolates `{{projectBlock}}` and `{{routeBlock}}` verbatim, so we
13
+ * pre-render them as plain text (the template engine doesn't do conditionals).
14
+ */
15
+ export function buildTelegramMeta({ channelName, author, chatId, target, routeToAgent }) {
16
+ const projectBlock = target
17
+ ? `\nProject pin: **${target.name || "(unnamed)"}** (\`${target.path || "?"}\`).\n` +
18
+ "This Telegram channel belongs to that project. Default any " +
19
+ "project-scoped tool call (list_agents, list_tasks, list_mcps, " +
20
+ "list_skills, create_task, list_routines, …) to " +
21
+ `\`${target.name || target.path}\` without asking the user "which ` +
22
+ 'project?". Only ask when they explicitly reference another project ' +
23
+ "by name."
24
+ : "";
25
+ const routeBlock = routeToAgent
26
+ ? `\nMaster agent for this channel: **${routeToAgent}**. Prefer ` +
27
+ `delegating substantive work to that agent via call_agent({ project: ` +
28
+ `"${target?.name || target?.path || ""}", agent: "${routeToAgent}", ` +
29
+ "prompt: <user message> }) rather than answering yourself, unless " +
30
+ "the message is small-talk or a quick factual reply."
31
+ : "";
32
+ return {
33
+ channelName,
34
+ author,
35
+ chatId,
36
+ projectBlock,
37
+ routeBlock,
38
+ ...(target ? {
39
+ projectId: String(target.id),
40
+ projectName: target.name || "",
41
+ projectPath: target.path || "",
42
+ } : {}),
43
+ ...(routeToAgent ? { routeToAgent } : {}),
44
+ };
45
+ }
46
+
47
+ /** Load the cross-channel offset state from ~/.apx/telegram-state.json. */
48
+ export function loadState() {
49
+ if (!fs.existsSync(TELEGRAM_STATE_PATH)) return { channels: {} };
50
+ try {
51
+ const raw = JSON.parse(fs.readFileSync(TELEGRAM_STATE_PATH, "utf8"));
52
+ return { channels: raw.channels || {}, _legacy_offset: raw.offset || 0 };
53
+ } catch {
54
+ return { channels: {} };
55
+ }
56
+ }
57
+
58
+ /** Write the cross-channel offset state. Adds an `updated_at` timestamp. */
59
+ export function saveState(state) {
60
+ fs.writeFileSync(
61
+ TELEGRAM_STATE_PATH,
62
+ JSON.stringify({ ...state, updated_at: nowIso() }, null, 2) + "\n"
63
+ );
64
+ }
65
+
66
+ export function resolveBotToken(channel) {
67
+ return (
68
+ channel.bot_token ||
69
+ process.env.BOT_TELEGRAM_TOKEN ||
70
+ process.env.TELEGRAM_BOT_TOKEN ||
71
+ ""
72
+ );
73
+ }
74
+
75
+ export function resolveChatId(channel) {
76
+ return (
77
+ channel.chat_id ||
78
+ process.env.TELEGRAM_CHAT_ID ||
79
+ process.env.BOT_TELEGRAM_CHAT_ID ||
80
+ ""
81
+ );
82
+ }
83
+
84
+ export function tokenSource(channel) {
85
+ if (channel.bot_token) return "config";
86
+ if (process.env.BOT_TELEGRAM_TOKEN) return "env:BOT_TELEGRAM_TOKEN";
87
+ if (process.env.TELEGRAM_BOT_TOKEN) return "env:TELEGRAM_BOT_TOKEN";
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * Resolve the list of telegram channels to poll, honouring both the
93
+ * canonical telegram.channels[] and the legacy single-channel mode.
94
+ */
95
+ export function resolveChannels(globalConfig) {
96
+ const tg = globalConfig.telegram || {};
97
+ if (Array.isArray(tg.channels) && tg.channels.length > 0) {
98
+ return tg.channels.map((c, i) => ({
99
+ name: c.name || `channel-${i + 1}`,
100
+ bot_token: c.bot_token || "",
101
+ chat_id: c.chat_id || "",
102
+ route_to_agent: c.route_to_agent || "",
103
+ project: c.project || null,
104
+ respond_with_engine:
105
+ c.respond_with_engine !== undefined
106
+ ? c.respond_with_engine
107
+ : tg.respond_with_engine !== false,
108
+ poll_interval_ms: c.poll_interval_ms || tg.poll_interval_ms || 1500,
109
+ }));
110
+ }
111
+ // Legacy single-channel mode
112
+ if (!tg.bot_token && !process.env.BOT_TELEGRAM_TOKEN && !process.env.TELEGRAM_BOT_TOKEN) {
113
+ return [];
114
+ }
115
+ return [
116
+ {
117
+ name: "default",
118
+ bot_token: tg.bot_token || "",
119
+ chat_id: tg.chat_id || "",
120
+ route_to_agent: tg.route_to_agent || "",
121
+ project: null,
122
+ respond_with_engine: tg.respond_with_engine !== false,
123
+ poll_interval_ms: tg.poll_interval_ms || 1500,
124
+ },
125
+ ];
126
+ }
127
+
128
+ export function sleep(ms) {
129
+ return new Promise((resolve) => setTimeout(resolve, ms));
130
+ }
@@ -6,6 +6,7 @@ import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { APX_HOME, CONFIG_PATH } from "./paths.js";
8
8
  import { PERMISSION_MODES, DEFAULT_PERMISSION_MODE } from "../constants/permissions.js";
9
+ import { agentsMdFile, apcProjectFile } from "../apc/paths.js";
9
10
 
10
11
  export {
11
12
  APX_HOME,
@@ -395,10 +396,10 @@ export function effectiveHost(cfg) {
395
396
 
396
397
  export function addProject(cfg, projectPath) {
397
398
  const abs = path.resolve(projectPath);
398
- if (!fs.existsSync(path.join(abs, "AGENTS.md"))) {
399
+ if (!fs.existsSync(agentsMdFile(abs))) {
399
400
  throw new Error(`not an APC project: ${abs} (no AGENTS.md)`);
400
401
  }
401
- if (!fs.existsSync(path.join(abs, ".apc", "project.json"))) {
402
+ if (!fs.existsSync(apcProjectFile(abs))) {
402
403
  throw new Error(`not an APC project: ${abs} (no .apc/project.json)`);
403
404
  }
404
405
  const exists = cfg.projects.find((p) => path.resolve(p.path) === abs);
@@ -0,0 +1,95 @@
1
+ // Secret redaction for the global config. Wraps any string secret with a
2
+ // `*** set *** (...<suffix>)` marker so the web admin can show "value is set"
3
+ // without leaking it, AND so PATCH callers can echo back the marker to mean
4
+ // "don't touch this one" — see isSecretMarker / mergeRedactedChannels below.
5
+ //
6
+ // The dotted paths in SECRET_PATHS are the single source of truth for "which
7
+ // keys are secrets". Anything new (a new engine api_key, a new TTS provider
8
+ // key, etc.) goes here and every redaction path picks it up.
9
+
10
+ const SECRET_MARKER_PREFIX = "*** set ***";
11
+
12
+ export const SECRET_PATHS = [
13
+ "engines.anthropic.api_key",
14
+ "engines.openai.api_key",
15
+ "engines.groq.api_key",
16
+ "engines.openrouter.api_key",
17
+ "engines.gemini.api_key",
18
+ "voice.tts.elevenlabs.api_key",
19
+ "voice.tts.openai.api_key",
20
+ "voice.tts.gemini.api_key",
21
+ "memory.embeddings.openai.api_key",
22
+ "memory.embeddings.gemini.api_key",
23
+ // Telegram bot tokens live inside an array — handled separately in redact()
24
+ // because dotted paths can't address array entries.
25
+ "telegram.channels.*.bot_token",
26
+ ];
27
+
28
+ /** Replace a secret string with the visible marker, preserving the last 5 chars. */
29
+ export function secretMarker(value) {
30
+ if (typeof value !== "string" || !value.length) return value;
31
+ const suffix = value.slice(-5);
32
+ return `${SECRET_MARKER_PREFIX} (...${suffix})`;
33
+ }
34
+
35
+ /** True when a value is the placeholder a redacted view sends back unchanged. */
36
+ export function isSecretMarker(value) {
37
+ return typeof value === "string" && value.startsWith(SECRET_MARKER_PREFIX);
38
+ }
39
+
40
+ /** Deep-copy of `cfg` with every secret string replaced by its marker. */
41
+ export function redactConfig(cfg) {
42
+ const out = JSON.parse(JSON.stringify(cfg || {}));
43
+ const mark = (val) => (typeof val === "string" && val.length ? secretMarker(val) : val);
44
+
45
+ for (const dotted of SECRET_PATHS) {
46
+ if (dotted.includes("*")) continue;
47
+ const parts = dotted.split(".");
48
+ let cur = out;
49
+ for (let i = 0; i < parts.length - 1; i++) {
50
+ if (!cur[parts[i]] || typeof cur[parts[i]] !== "object") { cur = null; break; }
51
+ cur = cur[parts[i]];
52
+ }
53
+ if (cur && cur[parts[parts.length - 1]]) {
54
+ cur[parts[parts.length - 1]] = mark(cur[parts[parts.length - 1]]);
55
+ }
56
+ }
57
+ const channels = out?.telegram?.channels;
58
+ if (Array.isArray(channels)) {
59
+ for (const ch of channels) {
60
+ if (ch && typeof ch.bot_token === "string" && ch.bot_token.length) {
61
+ ch.bot_token = mark(ch.bot_token);
62
+ }
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /** Redact a single Telegram channel record. */
69
+ export function redactChannel(channel) {
70
+ if (!channel?.bot_token) return channel;
71
+ return { ...channel, bot_token: secretMarker(channel.bot_token) };
72
+ }
73
+
74
+ /**
75
+ * Merge a PATCH-shape `nextChannels` against the prior on-disk list. Any
76
+ * incoming channel whose bot_token is missing or a marker takes the prior
77
+ * token verbatim — so a UI that echoes the redacted view back doesn't wipe
78
+ * the real secret.
79
+ */
80
+ export function mergeRedactedChannels(nextChannels, priorChannels) {
81
+ if (!Array.isArray(nextChannels)) return nextChannels;
82
+ const priorByName = new Map(
83
+ (Array.isArray(priorChannels) ? priorChannels : [])
84
+ .filter((c) => c && typeof c.name === "string")
85
+ .map((c) => [c.name, c])
86
+ );
87
+ return nextChannels.map((channel) => {
88
+ if (!channel || typeof channel !== "object") return channel;
89
+ const prior = priorByName.get(channel.name);
90
+ if (prior?.bot_token && (channel.bot_token === undefined || isSecretMarker(channel.bot_token))) {
91
+ return { ...channel, bot_token: prior.bot_token };
92
+ }
93
+ return channel;
94
+ });
95
+ }
@@ -16,4 +16,6 @@ export const CHANNELS = Object.freeze({
16
16
  DECK: "deck", // Mobile cockpit dashboard
17
17
  DESKTOP: "desktop", // Electron capsule (always voice mode)
18
18
  CODE: "code", // `apx code` — terminal coding session
19
+ DIRECT: "direct", // Planned: 1:1 channel that isn't a chat platform
20
+ WHATSAPP: "whatsapp", // Planned: WhatsApp bot integration
19
21
  });
@@ -0,0 +1,10 @@
1
+ // Code session modes. PLAN = read-only exploration (the agent proposes
2
+ // changes but never mutates); BUILD = unrestricted execution. The value
3
+ // lives in code-sessions.json (session.mode) and is what api/code.js,
4
+ // stores/code-sessions.js, and agent/prompts/modes/ all branch on.
5
+ export const CODE_MODES = Object.freeze({
6
+ PLAN: "plan",
7
+ BUILD: "build",
8
+ });
9
+
10
+ export const DEFAULT_CODE_MODE = CODE_MODES.BUILD;
@@ -3,3 +3,4 @@ export * from "./permissions.js";
3
3
  export * from "./channels.js";
4
4
  export * from "./roles.js";
5
5
  export * from "./actors.js";
6
+ export * from "./code-modes.js";
@@ -0,0 +1,186 @@
1
+ // APX Deck manifest — the data model the companion clients (deck, desktop
2
+ // capsule) read on boot. Pure data + decoration; no HTTP or filesystem.
3
+ // host/daemon/api/deck.js wraps this for the /deck/manifest endpoint.
4
+
5
+ export 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
+ export 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
+ export 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
+ export 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
+ ];
93
+
94
+ // Widget ids the user is allowed to override. Keeps a rogue client from
95
+ // writing arbitrary keys into the global config under deck.widget_overrides.
96
+ // CORE_WIDGETS are intentionally NOT in here — they're built-in APX surfaces
97
+ // and don't make sense to disable.
98
+ export const TOGGLEABLE_WIDGETS = new Set(EXTERNAL_WIDGETS.map((w) => w.id));
99
+
100
+ function pickActiveProject(projectList) {
101
+ return projectList.find((project) => Number(project.id) !== 0) || projectList[0] || null;
102
+ }
103
+
104
+ /**
105
+ * Apply runtime status + user overrides to the static EXTERNAL_WIDGETS list.
106
+ *
107
+ * 1. user explicitly disabled it → "disabled" (sticky, regardless of plugin
108
+ * auto-detect)
109
+ * 2. daemon has a running plugin → "available"
110
+ * 3. user toggled it on but no plugin backing → "configured"
111
+ * 4. nothing → leave the static "not_configured" default
112
+ */
113
+ export function decorateExternalWidgets(pluginStatus = {}, overrides = {}) {
114
+ return EXTERNAL_WIDGETS.map((widget) => {
115
+ const override = overrides[widget.id];
116
+ const status = pluginStatus[widget.id];
117
+ const decorated = { ...widget };
118
+ if (status) decorated.daemon_status = status;
119
+ if (override?.enabled === false) {
120
+ decorated.status = "disabled";
121
+ } else if (status) {
122
+ decorated.status = status.enabled === false ? "disabled" : "available";
123
+ } else if (override?.enabled === true) {
124
+ decorated.status = "configured";
125
+ }
126
+ // Always echo the user-toggle so the app can render the switch
127
+ // independently of the running/available bit.
128
+ decorated.user_enabled = override?.enabled ?? null;
129
+ return decorated;
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Build the full /deck/manifest response body.
135
+ *
136
+ * Inputs are *resolved* runtime values, not the live managers — caller is
137
+ * responsible for catching errors in projects.list()/plugins.status() and
138
+ * passing the resulting arrays/maps in (or empty defaults).
139
+ */
140
+ export function buildDeckManifest({
141
+ projectList = [],
142
+ pluginStatus = {},
143
+ overrides = {},
144
+ version,
145
+ startedAt,
146
+ config,
147
+ }) {
148
+ const activeProject = pickActiveProject(projectList);
149
+ return {
150
+ status: "ok",
151
+ daemon: {
152
+ name: "apx",
153
+ version,
154
+ host: config?.host || "127.0.0.1",
155
+ port: config?.port || 7430,
156
+ uptime_s: Math.round((Date.now() - startedAt) / 1000),
157
+ started_at: new Date(startedAt).toISOString(),
158
+ },
159
+ deck: {
160
+ name: "apx-deck",
161
+ desktops: DESKTOPS,
162
+ widgets: [...CORE_WIDGETS, ...decorateExternalWidgets(pluginStatus, overrides)],
163
+ suggested_actions: SAFE_ACTIONS,
164
+ },
165
+ apx: {
166
+ active_project: activeProject,
167
+ projects: projectList,
168
+ plugins: pluginStatus,
169
+ endpoints: {
170
+ health: "/health",
171
+ projects: "/projects",
172
+ plugins: "/plugins",
173
+ voice_turn: "/voice/turn",
174
+ transcribe_chunk: "/transcribe/chunk",
175
+ super_agent_chat: "/projects/:pid/super-agent/chat",
176
+ super_agent_stream: "/projects/:pid/super-agent/chat/stream",
177
+ },
178
+ },
179
+ safety: {
180
+ direct_shell: false,
181
+ arbitrary_commands: false,
182
+ dangerous_actions_require_confirmation: true,
183
+ allowed_actions_only: true,
184
+ },
185
+ };
186
+ }
@@ -0,0 +1,83 @@
1
+ // Live model catalogs per engine. Wraps each provider's "list models" endpoint
2
+ // behind one signature: listModels(engine, baseUrl?, apiKey?) → { models } or
3
+ // { error }. Pure transport — no daemon dependencies. Both the daemon HTTP
4
+ // adapter and CLI commands can reuse this.
5
+ import { fetchJsonWithTimeout } from "./_health.js";
6
+
7
+ export 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
+ export async function listModels(engine, baseUrl, apiKey) {
22
+ const base = String(baseUrl || DEFAULT_BASE[engine] || "").replace(/\/$/, "");
23
+
24
+ if (engine === "ollama") {
25
+ const b = base || process.env.OLLAMA_HOST || "http://localhost:11434";
26
+ const r = await fetchJsonWithTimeout(`${b}/api/tags`, { timeoutMs: 2500 });
27
+ if (!r.ok) return { error: r.reason || "no se pudo contactar Ollama" };
28
+ const list = Array.isArray(r.json?.models) ? r.json.models : [];
29
+ return { models: list.map((m) => m?.name).filter((n) => typeof n === "string" && n) };
30
+ }
31
+
32
+ if (engine === "anthropic") {
33
+ if (!apiKey) return { error: "falta api_key" };
34
+ const b = base || DEFAULT_BASE.anthropic;
35
+ const r = await fetchJsonWithTimeout(`${b}/models?limit=100`, {
36
+ timeoutMs: 5000,
37
+ headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
38
+ });
39
+ if (!r.ok) return { error: r.reason || `HTTP ${r.status}` };
40
+ const data = Array.isArray(r.json?.data) ? r.json.data : [];
41
+ return { models: data.map((m) => m?.id).filter(Boolean) };
42
+ }
43
+
44
+ if (engine === "gemini") {
45
+ if (!apiKey) return { error: "falta api_key" };
46
+ // Native Gemini API returns rich metadata, including supportedGenerationMethods
47
+ // so we can drop embeddings/vision-only entries. Names come back as
48
+ // "models/<id>"; strip the prefix.
49
+ const r = await fetchJsonWithTimeout(
50
+ `${GEMINI_NATIVE_BASE}/models?key=${encodeURIComponent(apiKey)}&pageSize=200`,
51
+ { timeoutMs: 5000 },
52
+ );
53
+ if (!r.ok) return { error: r.reason || `HTTP ${r.status}` };
54
+ const data = Array.isArray(r.json?.models) ? r.json.models : [];
55
+ const models = data
56
+ .filter((m) => {
57
+ const methods = m?.supportedGenerationMethods;
58
+ if (!Array.isArray(methods)) return true;
59
+ return methods.includes("generateContent");
60
+ })
61
+ .map((m) => {
62
+ const name = typeof m?.name === "string" ? m.name : "";
63
+ return name.startsWith("models/") ? name.slice("models/".length) : name;
64
+ })
65
+ .filter(Boolean);
66
+ return { models };
67
+ }
68
+
69
+ // openai-compatible family: openai, groq, openrouter, azure, custom
70
+ if (!apiKey) return { error: "falta api_key" };
71
+ if (!base) return { error: "falta base_url" };
72
+ const r = await fetchJsonWithTimeout(`${base}/models`, {
73
+ timeoutMs: 5000,
74
+ headers: { authorization: `Bearer ${apiKey}` },
75
+ });
76
+ if (!r.ok) return { error: r.reason || `HTTP ${r.status}` };
77
+ const data = Array.isArray(r.json?.data)
78
+ ? r.json.data
79
+ : Array.isArray(r.json?.models)
80
+ ? r.json.models
81
+ : [];
82
+ return { models: data.map((m) => m?.id || m?.name).filter(Boolean) };
83
+ }
@@ -51,15 +51,24 @@ function toGeminiContents(messages) {
51
51
  if (m.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
52
52
  out.push({
53
53
  role: "model",
54
- parts: m.tool_calls.map((tc) => ({
55
- functionCall: {
56
- name: tc.function?.name || tc.name,
57
- args:
58
- typeof tc.function?.arguments === "string"
59
- ? safeParseJson(tc.function.arguments)
60
- : tc.function?.arguments || tc.arguments || {},
61
- },
62
- })),
54
+ parts: m.tool_calls.map((tc) => {
55
+ const part = {
56
+ functionCall: {
57
+ name: tc.function?.name || tc.name,
58
+ args:
59
+ typeof tc.function?.arguments === "string"
60
+ ? safeParseJson(tc.function.arguments)
61
+ : tc.function?.arguments || tc.arguments || {},
62
+ },
63
+ };
64
+ // Gemini 3.x thinking models require us to echo back the
65
+ // thoughtSignature that came attached to the original functionCall
66
+ // part, or the API rejects the next turn with 400. We captured it
67
+ // in the response parser; replay it verbatim when present.
68
+ const sig = tc._thoughtSignature || tc.thought_signature;
69
+ if (sig) part.thoughtSignature = sig;
70
+ return part;
71
+ }),
63
72
  });
64
73
  continue;
65
74
  }
@@ -143,14 +152,22 @@ export default {
143
152
  for (const p of parts) {
144
153
  const fc = p.functionCall || p.function_call;
145
154
  if (fc?.name) {
146
- toolCalls.push({
155
+ const tc = {
147
156
  id: `gemini_${randomUUID().slice(0, 8)}`,
148
157
  type: "function",
149
158
  function: {
150
159
  name: fc.name,
151
160
  arguments: typeof fc.args === "string" ? fc.args : JSON.stringify(fc.args || {}),
152
161
  },
153
- });
162
+ };
163
+ // Thinking models (Gemini 3.x) attach a thoughtSignature to the part
164
+ // alongside the functionCall. We must replay it on the next request
165
+ // or the API 400s. Carry it on the tool_call so the next call to
166
+ // toGeminiContents() can put it back. Underscore prefix marks it as
167
+ // adapter-private metadata other engines should ignore.
168
+ const sig = p.thoughtSignature || p.thought_signature;
169
+ if (sig) tc._thoughtSignature = sig;
170
+ toolCalls.push(tc);
154
171
  }
155
172
  }
156
173
 
@@ -52,12 +52,22 @@ export async function callEngine({ modelId, system, messages, config, temperatur
52
52
  const { provider, model } = resolveProvider(modelId);
53
53
  const adapter = getAdapter(provider);
54
54
  const providerCfg = (config && config.engines && config.engines[provider]) || {};
55
+ // The per-provider `default_max_tokens` set in the web admin (Provider modal
56
+ // slider) acts as a floor: callers may ask for more, but never less. This
57
+ // matters for "thinking" models (e.g. Gemini 3.x) whose internal reasoning
58
+ // tokens count against maxOutputTokens — too low a cap and the visible reply
59
+ // gets truncated mid-sentence. Fallback chain:
60
+ // caller value → provider cfg → 2048 (safe baseline that survives thinking
61
+ // models without truncating; non-thinking models just don't fill it).
62
+ const providerCap = Number(providerCfg.default_max_tokens) || 0;
63
+ const callerCap = Number(maxTokens) || 0;
64
+ const effectiveMaxTokens = Math.max(callerCap, providerCap) || 2048;
55
65
  return adapter.chat({
56
66
  system,
57
67
  messages,
58
68
  model,
59
69
  temperature,
60
- maxTokens,
70
+ maxTokens: effectiveMaxTokens,
61
71
  tools,
62
72
  toolChoice,
63
73
  config: providerCfg,
@@ -1,4 +1,3 @@
1
- // daemon/tools/browser.js
2
1
  // Puppeteer-backed browser automation tools for APX.
3
2
  //
4
3
  // Logic adapted from the puppeteer-server MCP server
@@ -1,4 +1,3 @@
1
- // daemon/tools/fetch.js
2
1
  // Lightweight HTTP fetch tools — no Puppeteer, no Chromium. Starts in
3
2
  // milliseconds. Use this when you only need to hit an HTTP endpoint
4
3
  // (REST API, raw page HTML, JSON). For JS-rendered pages, real clicks,
@@ -1,4 +1,3 @@
1
- // daemon/tools/glob.js
2
1
  // Glob tool for APX — lists files matching a glob pattern.
3
2
  // Backends, in order of preference:
4
3
  // 1. fast-glob (npm) — full glob spec, brace expansion, negation patterns
@@ -1,4 +1,3 @@
1
- // daemon/tools/grep.js
2
1
  // Native Grep tool for APX — searches file contents by regex pattern.
3
2
  // Tries ripgrep (rg) first for speed, falls back to pure Node.js walk.
4
3
  //
@@ -1,4 +1,3 @@
1
- // daemon/tools/registry.js
2
1
  // Tool Registry on-demand for APX.
3
2
  //
4
3
  // Endpoints registered by api.js:
@@ -1,4 +1,3 @@
1
- // daemon/tools/search.js
2
1
  // WebSearch tool for APX — 3 modes:
3
2
  // 1. DuckDuckGo HTML scraping (no API key, uses node-fetch)
4
3
  // 2. Brave Search API (requires BRAVE_API_KEY env)
@@ -0,0 +1,9 @@
1
+ // Backend strings — English (en).
2
+ export default {
3
+ "telegram.heads_up": "On it — working on that… 🛠️",
4
+ "telegram.reset_ack": "Done, context cleared. Starting fresh. What do you need?",
5
+ "telegram.error_generic": "Something broke on my side — already logged.",
6
+ "telegram.fallback_listo": "Done.",
7
+
8
+ "common.unknown_error": "Something went wrong.",
9
+ };