@agentprojectcontext/apx 1.33.1 → 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 (169) 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 +4 -3
  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 +2 -1
  8. package/src/core/agent/prompts/modes/code-build.md +1 -0
  9. package/src/core/agent/prompts/modes/code-plan.md +1 -0
  10. package/src/core/agent/prompts/modes/index.js +28 -0
  11. package/src/core/agent/skills/loader.js +22 -18
  12. package/src/core/agent/stream/turn-accumulator.js +73 -0
  13. package/src/core/agent/suggestions.js +37 -0
  14. package/src/core/agent/tools/handlers/add-project.js +5 -2
  15. package/src/core/agent/tools/handlers/call-runtime.js +3 -2
  16. package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
  17. package/src/core/agent/tools/helpers.js +2 -2
  18. package/src/core/agent/tools/names.js +138 -0
  19. package/src/core/agent/tools/registry-bridge.js +6 -14
  20. package/src/core/agent/tools/registry.js +68 -65
  21. package/src/core/apc/context-copy.js +27 -0
  22. package/src/core/apc/notes.js +19 -0
  23. package/src/core/apc/parser.js +12 -5
  24. package/src/core/apc/paths.js +87 -0
  25. package/src/core/apc/scaffold.js +82 -76
  26. package/src/core/apc/skill-sync.js +10 -0
  27. package/src/{host/daemon/plugins → core/channels}/telegram/dispatch.js +38 -16
  28. package/src/core/config/index.js +3 -2
  29. package/src/core/config/redact.js +95 -0
  30. package/src/core/constants/channels.js +2 -0
  31. package/src/core/constants/code-modes.js +10 -0
  32. package/src/core/constants/index.js +1 -0
  33. package/src/core/deck/manifest.js +186 -0
  34. package/src/core/engines/catalog.js +83 -0
  35. package/src/core/{tools → http-tools}/browser.js +0 -1
  36. package/src/core/{tools → http-tools}/fetch.js +0 -1
  37. package/src/core/{tools → http-tools}/glob.js +0 -1
  38. package/src/core/{tools → http-tools}/grep.js +0 -1
  39. package/src/core/{tools → http-tools}/registry.js +0 -1
  40. package/src/core/{tools → http-tools}/search.js +0 -1
  41. package/src/core/i18n/en.js +9 -0
  42. package/src/core/i18n/es.js +12 -0
  43. package/src/core/i18n/index.js +54 -0
  44. package/src/core/i18n/pt.js +9 -0
  45. package/src/core/identity/telegram.js +2 -1
  46. package/src/core/mcp/runner.js +272 -14
  47. package/src/core/mcp/sources.js +3 -2
  48. package/src/core/routines/index.js +16 -0
  49. package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
  50. package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
  51. package/src/core/runtime-skills/apx/SKILL.md +95 -0
  52. package/src/core/runtime-skills/apx-mcp/SKILL.md +116 -0
  53. package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
  54. package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
  55. package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
  56. package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
  57. package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
  58. package/src/core/stores/code-sessions.js +50 -2
  59. package/src/core/stores/routine-memory.js +1 -1
  60. package/src/core/stores/sessions-search.js +121 -0
  61. package/src/core/stores/sessions.js +38 -0
  62. package/src/core/vars/index.js +14 -0
  63. package/src/core/vars/interpolate.js +86 -0
  64. package/src/core/vars/sources.js +151 -0
  65. package/src/core/voice/audio-decode.js +38 -0
  66. package/src/core/voice/transcription.js +225 -0
  67. package/src/host/daemon/api/admin-config.js +5 -82
  68. package/src/host/daemon/api/agents.js +5 -5
  69. package/src/host/daemon/api/code.js +17 -169
  70. package/src/host/daemon/api/config.js +3 -4
  71. package/src/host/daemon/api/conversations.js +8 -29
  72. package/src/host/daemon/api/deck.js +37 -404
  73. package/src/host/daemon/api/engines.js +1 -80
  74. package/src/host/daemon/api/exec.js +1 -1
  75. package/src/host/daemon/api/mcps.js +32 -0
  76. package/src/host/daemon/api/routines.js +1 -1
  77. package/src/host/daemon/api/runtimes.js +4 -3
  78. package/src/host/daemon/api/sessions-search.js +24 -140
  79. package/src/host/daemon/api/sessions.js +12 -30
  80. package/src/host/daemon/api/shared.js +2 -1
  81. package/src/host/daemon/api/telegram.js +1 -11
  82. package/src/host/daemon/api/tools.js +6 -6
  83. package/src/host/daemon/api/transcribe.js +2 -2
  84. package/src/host/daemon/api/vars.js +137 -0
  85. package/src/host/daemon/api/voice.js +13 -290
  86. package/src/host/daemon/api.js +2 -0
  87. package/src/host/daemon/db.js +6 -6
  88. package/src/host/daemon/deck-exec.js +148 -0
  89. package/src/host/daemon/index.js +3 -3
  90. package/src/host/daemon/plugins/telegram/index.js +9 -9
  91. package/src/host/daemon/routines-scheduler.js +64 -0
  92. package/src/host/daemon/smoke.js +3 -2
  93. package/src/host/daemon/whisper-server.js +225 -0
  94. package/src/interfaces/cli/commands/agent.js +3 -2
  95. package/src/interfaces/cli/commands/command.js +2 -3
  96. package/src/interfaces/cli/commands/messages.js +6 -2
  97. package/src/interfaces/cli/commands/pair.js +5 -4
  98. package/src/interfaces/cli/commands/search.js +1 -1
  99. package/src/interfaces/cli/commands/sessions.js +3 -2
  100. package/src/interfaces/cli/commands/skills.js +36 -55
  101. package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +1 -0
  102. package/src/interfaces/web/dist/assets/index-M4FspaCH.js +613 -0
  103. package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +1 -0
  104. package/src/interfaces/web/dist/index.html +2 -2
  105. package/src/interfaces/web/package-lock.json +182 -182
  106. package/src/interfaces/web/src/components/ModelCombobox.tsx +2 -1
  107. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
  108. package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
  109. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
  110. package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
  111. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
  112. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
  113. package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
  114. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
  115. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
  116. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
  117. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
  118. package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
  119. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
  120. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
  121. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
  122. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
  123. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +5 -4
  124. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
  125. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
  126. package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
  127. package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
  128. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
  129. package/src/interfaces/web/src/constants/index.ts +1 -1
  130. package/src/interfaces/web/src/i18n/en.ts +174 -7
  131. package/src/interfaces/web/src/i18n/es.ts +179 -15
  132. package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
  133. package/src/interfaces/web/src/lib/api/vars.ts +38 -0
  134. package/src/interfaces/web/src/lib/api.ts +1 -0
  135. package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
  136. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
  137. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
  138. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
  139. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
  140. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
  141. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
  142. package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
  143. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
  144. package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
  145. package/src/interfaces/web/src/types/daemon.ts +5 -0
  146. package/src/host/daemon/transcription.js +0 -538
  147. package/src/host/daemon/whisper-transcribe.py +0 -73
  148. package/src/interfaces/web/dist/assets/index-Aaiw8BZN.css +0 -1
  149. package/src/interfaces/web/dist/assets/index-DPqtjDjh.js +0 -602
  150. package/src/interfaces/web/dist/assets/index-DPqtjDjh.js.map +0 -1
  151. /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
  152. /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
  153. /package/src/{host/daemon/plugins → core/channels}/telegram/helpers.js +0 -0
  154. /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
  155. /package/src/core/{tools → http-tools}/index.js +0 -0
  156. /package/{skills → src/core/runtime-skills}/apx-agency-agents/SKILL.md +0 -0
  157. /package/{skills → src/core/runtime-skills}/apx-agent/SKILL.md +0 -0
  158. /package/{skills → src/core/runtime-skills}/apx-mcp-builder/SKILL.md +0 -0
  159. /package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +0 -0
  160. /package/{skills → src/core/runtime-skills}/apx-routine/SKILL.md +0 -0
  161. /package/{skills → src/core/runtime-skills}/apx-runtime/SKILL.md +0 -0
  162. /package/{skills → src/core/runtime-skills}/apx-sessions/SKILL.md +0 -0
  163. /package/{skills → src/core/runtime-skills}/apx-skill-builder/SKILL.md +0 -0
  164. /package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +0 -0
  165. /package/{skills → src/core/runtime-skills}/apx-telegram/SKILL.md +0 -0
  166. /package/{skills → src/core/runtime-skills}/apx-voice/SKILL.md +0 -0
  167. /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
  168. /package/src/{host/daemon → core/stores}/conversations.js +0 -0
  169. /package/src/{host/daemon → core/util}/thinking.js +0 -0
@@ -11,14 +11,22 @@ import {
11
11
  } from "./parser.js";
12
12
  import { readApcContextSkill } from "./skill-sync.js";
13
13
  import { nowIso } from "../util/time.js";
14
+ import {
15
+ apcDir,
16
+ apcProjectFile,
17
+ apcAgentsDir,
18
+ apcAgentFile,
19
+ agentsMdFile,
20
+ } from "./paths.js";
14
21
 
15
22
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
23
  // Now under src/core/apc/ — one more "../" to escape than before.
17
24
  const PACKAGE_ROOT = path.resolve(__dirname, "..", "..", "..");
25
+ // <packageRoot>/skills/<slug>/SKILL.md — the SLIM engine-side set: every skill
26
+ // here is replicated verbatim into project IDE rule files (`apx skills add`) and
27
+ // into ~/.<host>/skills/ (`apx skills sync`). The rich super-agent catalog lives
28
+ // at src/core/runtime-skills/ and is intentionally NOT copied out.
18
29
  const BUNDLED_SKILLS_DIR = path.join(PACKAGE_ROOT, "skills");
19
- // runtime-skills lives at src/core/runtime-skills/, one level up from this
20
- // file's new home in src/core/apc/ (was a sibling before the Phase 3 move).
21
- const RUNTIME_SKILLS_DIR = path.join(__dirname, "..", "runtime-skills");
22
30
 
23
31
  export const SPEC_VERSION = "0.1.0";
24
32
 
@@ -28,12 +36,12 @@ export const SPEC_VERSION = "0.1.0";
28
36
  // install/update from the canonical APC repo (see src/interfaces/cli/postinstall.js).
29
37
  // ---------------------------------------------------------------------------
30
38
 
31
- // Bundled skills apx lives in skills/apx/. apc-context is synced from
32
- // the canonical APC repo (../apc or GitHub) never edited in APX.
39
+ // Read one slim skill from <packageRoot>/skills/<slug>/SKILL.md. `apc-context`
40
+ // is special-cased to refresh from the canonical APC repo if available.
33
41
  function readBundledSkill(slug) {
34
42
  if (slug === "apc-context") {
35
43
  const synced = readApcContextSkill();
36
- return synced?.text || null;
44
+ if (synced?.text) return synced.text;
37
45
  }
38
46
  const file = path.join(BUNDLED_SKILLS_DIR, slug, "SKILL.md");
39
47
  if (!fs.existsSync(file)) return null;
@@ -134,19 +142,11 @@ const GLOBAL_SKILL_DIRS = [
134
142
  path.join(os.homedir(), ".agents", "skills"), // Antigravity/other skills.sh adopters
135
143
  ];
136
144
 
137
- function readRuntimeSkillFiles() {
138
- if (!fs.existsSync(RUNTIME_SKILLS_DIR)) return [];
139
- return fs.readdirSync(RUNTIME_SKILLS_DIR)
140
- .filter((name) => name.endsWith(".md"))
141
- .sort()
142
- .map((name) => ({
143
- slug: path.basename(name, ".md"),
144
- md: fs.readFileSync(path.join(RUNTIME_SKILLS_DIR, name), "utf8").trim(),
145
- }));
146
- }
147
-
148
145
  // Install APX + APC context skills into IDE rule files. Returns an array of result objects.
149
146
  // targetIds: array of target ids to install; null = all project targets.
147
+ // Writes the slim engine-side skill from <packageRoot>/skills/. The rich
148
+ // super-agent set in src/core/runtime-skills/ is intentionally never written
149
+ // into project IDE files.
150
150
  export function installIdeSkills(root, targetIds = null) {
151
151
  const apxRaw = readBundledSkill("apx");
152
152
  const apcRaw = readBundledSkill("apc-context");
@@ -197,36 +197,27 @@ export function installIdeSkills(root, targetIds = null) {
197
197
  // on the user's machine after `npm install -g .` (or `npm update -g apx`)
198
198
  // without anyone having to touch this file.
199
199
  //
200
- // Excluded: directory names starting with "." (e.g. .DS_Store), and any
201
- // runtime-only CLI skill that lives under src/core/runtime-skills/ those
202
- // are loaded in-process at daemon startup and are NOT for IDE consumption.
203
- // Public: bundled skill slugs grouped by scope.
204
- // public → pushed to every global skill dir on install / sync (default).
205
- // optional → not pushed by default; user opts in with --include-optional
206
- // or `apx skills add <slug> --global` for one-off install.
207
- // internal → APX-developer skills (mcp-builder, skill-builder, etc.); never
208
- // pushed globally, only available to APX itself via the bundled
209
- // copy. Avoids cluttering other IDEs with stuff their users won't
210
- // run.
200
+ // Excluded: directory names starting with "." (e.g. .DS_Store).
201
+ // Every slug under <packageRoot>/skills/ is part of the slim engine set and
202
+ // gets pushed to global dirs on `apx skills sync`. No scope filtering — the
203
+ // dir IS the contract.
211
204
  export function listBundledSkillSlugs() {
212
205
  return discoverBundledSkills().map((s) => s.slug);
213
206
  }
214
207
 
215
208
  export function listBundledSkills() {
216
- return discoverBundledSkills().map(({ slug, scope }) => ({ slug, scope }));
209
+ return discoverBundledSkills().map(({ slug }) => ({ slug }));
210
+ }
211
+
212
+ // Backwards-compat alias — every bundled slug here IS an engine slug now.
213
+ export function listEngineSkills() {
214
+ return listBundledSkills();
217
215
  }
218
216
 
219
- // Tiny frontmatter peek we only need the `scope:` field. Avoids pulling in
220
- // a full YAML parser for one optional line.
221
- function parseFrontmatterScope(md) {
222
- if (!md.startsWith("---\n")) return "public";
223
- const end = md.indexOf("\n---", 4);
224
- if (end === -1) return "public";
225
- const m = md.slice(4, end).match(/^scope:\s*(\w+)/m);
226
- if (!m) return "public";
227
- const s = m[1].toLowerCase();
228
- if (s === "internal" || s === "optional" || s === "public") return s;
229
- return "public";
217
+ // Legacy slugs APX used to ship to global dirs but no longer does — exposed so
218
+ // the CLI can report what `installGlobalSkills` will prune.
219
+ export function listLegacyPruneSlugs() {
220
+ return [...PRUNE_LEGACY_SLUGS];
230
221
  }
231
222
 
232
223
  function discoverBundledSkills() {
@@ -239,65 +230,80 @@ function discoverBundledSkills() {
239
230
  const skillFile = path.join(root, entry.name, "SKILL.md");
240
231
  if (!fs.existsSync(skillFile)) continue;
241
232
  const md = fs.readFileSync(skillFile, "utf8");
242
- out.push({ slug: entry.name, md, scope: parseFrontmatterScope(md) });
233
+ out.push({ slug: entry.name, md });
243
234
  }
244
235
  return out.sort((a, b) => a.slug.localeCompare(b.slug));
245
236
  }
246
237
 
247
- // Install bundled skills to every global ~/.../skills/ dir so Claude Code,
248
- // Cursor, Codex, and other IDEs see them.
238
+ // Install the slim engine skill set to every global ~/.../skills/ dir
239
+ // (Claude Code, Cursor, Codex, Antigravity/skills.sh). External engines only
240
+ // need to know how to talk TO apx — not the full APX sub-skill catalog. The
241
+ // rich bundled set in skills/<slug>/ stays in-repo for the APX super-agent.
242
+ //
243
+ // The set lives at skills/engines/<slug>/SKILL.md and is currently:
244
+ // apx, apx-mcp, apc-context.
249
245
  //
250
- // By default only `scope: public` skills land globally. Pass
251
- // includeOptional / includeInternal to push the other tiers (or call
252
- // `apx skills add <slug> --global` for a single one).
246
+ // `includeOptional` / `includeInternal` are kept as no-op flags for backward
247
+ // compatibility with `apx skills sync --include-…`; the slim set has no tiers.
253
248
  //
254
- // Pruning: if a slug that was previously installed has since been demoted to
255
- // internal/optional (or marked as non-public for the current call), we remove
256
- // the stale global copy unless prune=false. Keeps IDE skill lists clean.
249
+ // Pruning: removes stale APX-shipped slugs that are no longer in the engine
250
+ // set (the catalog of slugs APX has ever published, see PRUNE_LEGACY_SLUGS).
251
+ // Skills the user installed themselves are NOT touched.
257
252
  //
258
253
  // Returns an array of { dir, skill, file, status, scope }.
259
- // status ∈ {created, updated, unchanged, pruned, skipped}
254
+ // status ∈ {created, updated, unchanged, pruned}
255
+ const PRUNE_LEGACY_SLUGS = [
256
+ "apx-agency-agents",
257
+ "apx-agent",
258
+ "apx-mcp-builder",
259
+ "apx-project",
260
+ "apx-routine",
261
+ "apx-runtime",
262
+ "apx-sessions",
263
+ "apx-skill-builder",
264
+ "apx-task",
265
+ "apx-telegram",
266
+ "apx-voice",
267
+ // Runtime CLI docs that previously leaked into global dirs — these are
268
+ // loaded in-process by the daemon and should NOT live on disk in IDE skill
269
+ // dirs.
270
+ "claude-code",
271
+ "codex-cli",
272
+ "opencode-cli",
273
+ "openrouter",
274
+ ];
275
+
260
276
  export function installGlobalSkills({
261
- includeOptional = false,
262
- includeInternal = false,
263
277
  prune = true,
278
+ // No-ops kept for CLI backward compatibility (the slim engine set has no tiers).
279
+ includeOptional: _includeOptional = false,
280
+ includeInternal: _includeInternal = false,
264
281
  } = {}) {
265
- const all = discoverBundledSkills();
266
- const wanted = all.filter((s) => {
267
- if (s.scope === "internal") return includeInternal;
268
- if (s.scope === "optional") return includeOptional;
269
- return true; // public
270
- });
282
+ const wanted = discoverBundledSkills();
271
283
  const wantedSlugs = new Set(wanted.map((s) => s.slug));
272
- const knownSlugs = new Set(all.map((s) => s.slug));
273
284
 
274
285
  const results = [];
275
286
  for (const base of GLOBAL_SKILL_DIRS) {
276
- // Push the wanted set.
277
- for (const { slug, md, scope } of wanted) {
287
+ for (const { slug, md } of wanted) {
278
288
  const dest = path.join(base, slug, "SKILL.md");
279
289
  fs.mkdirSync(path.dirname(dest), { recursive: true });
280
290
  const existed = fs.existsSync(dest);
281
291
  const previous = existed ? fs.readFileSync(dest, "utf8") : null;
282
292
  if (previous === md) {
283
- results.push({ dir: base, skill: slug, file: dest, status: "unchanged", scope });
293
+ results.push({ dir: base, skill: slug, file: dest, status: "unchanged", scope: "engine" });
284
294
  continue;
285
295
  }
286
296
  fs.writeFileSync(dest, md, "utf8");
287
- results.push({ dir: base, skill: slug, file: dest, status: existed ? "updated" : "created", scope });
297
+ results.push({ dir: base, skill: slug, file: dest, status: existed ? "updated" : "created", scope: "engine" });
288
298
  }
289
- // Prune anything WE shipped previously but should no longer be there
290
- // (slug exists in the bundle but isn't `wanted` this run).
291
299
  if (prune) {
292
- for (const { slug, scope } of all) {
300
+ for (const slug of PRUNE_LEGACY_SLUGS) {
293
301
  if (wantedSlugs.has(slug)) continue;
294
- if (!knownSlugs.has(slug)) continue;
295
302
  const dest = path.join(base, slug, "SKILL.md");
296
303
  if (!fs.existsSync(dest)) continue;
297
304
  fs.unlinkSync(dest);
298
- // Best-effort: drop the now-empty <slug>/ dir too.
299
305
  try { fs.rmdirSync(path.dirname(dest)); } catch {}
300
- results.push({ dir: base, skill: slug, file: dest, status: "pruned", scope });
306
+ results.push({ dir: base, skill: slug, file: dest, status: "pruned", scope: "legacy" });
301
307
  }
302
308
  }
303
309
  }
@@ -423,7 +429,7 @@ function writeMigrateMd(apfDir, found) {
423
429
  // Get the stable APX storage ID for a project, generating one if it doesn't exist.
424
430
  // Called by the daemon when registering a project.
425
431
  export function getOrCreateApxId(root) {
426
- const p = path.join(root, ".apc", "project.json");
432
+ const p = apcProjectFile(root);
427
433
  if (!fs.existsSync(p)) return null;
428
434
  let cfg;
429
435
  try { cfg = JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
@@ -439,7 +445,7 @@ export function initApf(directory, { name } = {}) {
439
445
  const root = path.resolve(directory);
440
446
  fs.mkdirSync(root, { recursive: true });
441
447
 
442
- const apfDir = path.join(root, ".apc");
448
+ const apfDir = apcDir(root);
443
449
  fs.mkdirSync(path.join(apfDir, "agents"), { recursive: true });
444
450
  fs.mkdirSync(path.join(apfDir, "skills"), { recursive: true });
445
451
  fs.mkdirSync(path.join(apfDir, "commands"), { recursive: true });
@@ -469,7 +475,7 @@ export function initApf(directory, { name } = {}) {
469
475
  fs.writeFileSync(gitignore, APC_GITIGNORE);
470
476
  }
471
477
 
472
- const agentsMd = path.join(root, "AGENTS.md");
478
+ const agentsMd = agentsMdFile(root);
473
479
  if (!fs.existsSync(agentsMd)) {
474
480
  fs.writeFileSync(agentsMd, AGENTS_MD_TEMPLATE);
475
481
  }
@@ -485,13 +491,13 @@ export function initApf(directory, { name } = {}) {
485
491
  }
486
492
 
487
493
  export function ensureAgentDir(root, slug) {
488
- fs.mkdirSync(path.join(root, ".apc", "agents"), { recursive: true });
489
- return path.join(root, ".apc", "agents");
494
+ fs.mkdirSync(apcAgentsDir(root), { recursive: true });
495
+ return apcAgentsDir(root);
490
496
  }
491
497
 
492
498
  // Write .apc/agents/<slug>.md — the canonical agent definition file.
493
499
  export function writeAgentFile(root, slug, fields, body = "") {
494
- const dest = path.join(root, ".apc", "agents", `${slug}.md`);
500
+ const dest = apcAgentFile(root, slug);
495
501
  const lines = ["---"];
496
502
  const order = ["role", "model", "language", "description", "skills", "tools"];
497
503
  const written = new Set();
@@ -579,7 +585,7 @@ export function restoreVaultAgent(slug) {
579
585
 
580
586
  // Add a slug to the project's agents.imported list in project.json
581
587
  export function addImportedAgent(root, slug) {
582
- const p = path.join(root, ".apc", "project.json");
588
+ const p = apcProjectFile(root);
583
589
  let cfg = {};
584
590
  try { cfg = JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
585
591
  if (!cfg.agents) cfg.agents = {};
@@ -8,7 +8,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
8
  // Repo root is three levels up, not two.
9
9
  export const PACKAGE_ROOT = path.resolve(__dirname, "..", "..", "..");
10
10
 
11
+ // Engine-side slim copy (replicated to ~/.<host>/skills/ by apx skills sync).
11
12
  export const APC_SKILL_REL = path.join("skills", "apc-context", "SKILL.md");
13
+ // Runtime-internal copy (loaded by the super-agent — never published outside).
14
+ export const APC_BUILTIN_SKILL_REL = path.join("src", "core", "runtime-skills", "apc-context", "SKILL.md");
12
15
  export const APC_SKILL_REMOTE =
13
16
  "https://raw.githubusercontent.com/agentprojectcontext/agentprojectcontext/main/skills/apc-context/SKILL.md";
14
17
 
@@ -95,5 +98,12 @@ export async function refreshApcContextSkill({ packageRoot = PACKAGE_ROOT, timeo
95
98
 
96
99
  fs.mkdirSync(path.dirname(dest), { recursive: true });
97
100
  fs.writeFileSync(dest, text, "utf8");
101
+
102
+ // Keep the runtime-internal copy (src/core/runtime-skills/apc-context/) in
103
+ // sync — that's where the super-agent loads it from.
104
+ const builtinDest = path.join(packageRoot, APC_BUILTIN_SKILL_REL);
105
+ fs.mkdirSync(path.dirname(builtinDest), { recursive: true });
106
+ fs.writeFileSync(builtinDest, text, "utf8");
107
+
98
108
  return { ok: true, source, refreshed: true };
99
109
  }
@@ -1,10 +1,38 @@
1
1
  // Inbound Telegram update dispatcher.
2
2
  //
3
- // Extracted from the ChannelPoller class so the index.js stays under ~800
4
- // lines and the routing logic for text/photo/voice/document updates lives in
5
- // its own module. The function takes the poller instance as `self`; every
6
- // `this.X` in the original method becomes `self.X` here. The poller exposes
7
- // _handleUpdate as a thin facade that delegates to this function.
3
+ // Extracted from the ChannelPoller class so index.js stays under ~800 lines
4
+ // and the routing logic for text/photo/voice/document updates lives on its
5
+ // own. Takes the poller instance as `self`; every `this.X` in the original
6
+ // method becomes `self.X` here. The poller exposes _handleUpdate as a thin
7
+ // facade that delegates to handleUpdate(this, u).
8
+ //
9
+ // IMPORTANT: this module needs the same imports the original index.js had
10
+ // in module scope, because the extracted body references identifiers like
11
+ // `appendGlobalMessage`, `CHANNELS`, `nowIso`, etc. Top-level imports here
12
+ // keep that scope intact — earlier splits forgot them and the bug only
13
+ // surfaced when a real telegram update arrived (ReferenceError at runtime).
14
+ import path from "node:path";
15
+ import { callEngine } from "#core/engines/index.js";
16
+ import { runSuperAgent, isSuperAgentEnabled } from "#core/agent/super-agent.js";
17
+ import { stripThinking } from "#core/util/thinking.js";
18
+ import { getRecentTelegramTurnsFromFs, appendGlobalMessage } from "#core/stores/messages.js";
19
+ import { compactChannelIfNeeded } from "#core/memory/index.js";
20
+ import { readAgents } from "#core/apc/parser.js";
21
+ import { buildAgentSystem } from "#core/agent/build-agent-system.js";
22
+ import { transcribe as transcribeAudioFile } from "#core/voice/transcription.js";
23
+ import { resolveAgentName, SUPERAGENT_ACTOR_ID } from "#core/identity/index.js";
24
+ import { registerSender, resolveAllowedTools } from "#core/identity/telegram.js";
25
+ import { buildRelationshipBlock } from "#core/agent/index.js";
26
+ import { getConfirmationStore as getConfirmStore } from "#core/confirmation/pending-store.js";
27
+ import { CHANNELS } from "#core/constants/channels.js";
28
+ import { tryResolveSkillCommand } from "#core/agent/skills/trigger.js";
29
+ import { createTelegramConfirmAdapter } from "#core/confirmation/adapters/telegram.js";
30
+ import * as askFlow from "./ask.js";
31
+ import { buildTelegramMeta, resolveBotToken, sleep } from "./helpers.js";
32
+ import { sendPhoto, sendVoice, sendDocument, sendAudio, downloadTelegramFile, API_BASE } from "./media.js";
33
+ import { t, resolveLang } from "#core/i18n/index.js";
34
+
35
+ const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
8
36
 
9
37
  export async function handleUpdate(self, u) {
10
38
  self.lastUpdateAt = nowIso();
@@ -259,7 +287,7 @@ export async function handleUpdate(self, u) {
259
287
  // honor it for future messages.
260
288
  if (isReset) {
261
289
  try {
262
- const ack = "Done, context cleared. Starting fresh. What do you need?";
290
+ const ack = t("telegram.reset_ack", { lang: resolveLang(self.globalConfig) });
263
291
  await self._send({ chat_id, text: ack });
264
292
  appendGlobalMessage({
265
293
  channel: CHANNELS.TELEGRAM,
@@ -343,15 +371,7 @@ export async function handleUpdate(self, u) {
343
371
  // short localized heads-up the moment real work starts (first tool_start),
344
372
  // but only if the agent didn't already write its own "on it" line.
345
373
  let sentHeadsUp = false;
346
- const headsUpPhrase = () => {
347
- const lang = (self.globalConfig?.user?.language || "es").slice(0, 2);
348
- const byLang = {
349
- es: "Dale, estoy con eso… 🛠️",
350
- en: "On it — working on that… 🛠️",
351
- pt: "Já estou nisso… 🛠️",
352
- };
353
- return byLang[lang] || byLang.es;
354
- };
374
+ const headsUpPhrase = () => t("telegram.heads_up", { lang: resolveLang(self.globalConfig) });
355
375
  if (!replyText && isSuperAgentEnabled(self.globalConfig)) {
356
376
  const onEvent = async (ev) => {
357
377
  try {
@@ -523,7 +543,9 @@ export async function handleUpdate(self, u) {
523
543
  const finalClean = replyText ? stripThinking(replyText).trim() : "";
524
544
  let toSend = "";
525
545
  if (finalClean && finalClean !== lastStreamedText) toSend = finalClean;
526
- else if (!finalClean && streamedCount === 0) toSend = "Listo.";
546
+ else if (!finalClean && streamedCount === 0) {
547
+ toSend = t("telegram.fallback_listo", { lang: resolveLang(self.globalConfig) });
548
+ }
527
549
 
528
550
  stopTyping();
529
551
  if (!toSend) return; // everything was already streamed — nothing left to send
@@ -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";