@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
@@ -0,0 +1,148 @@
1
+ // /deck/exec implementation.
2
+ //
3
+ // All shell spawning sits behind this helper so api/deck.js stays a thin HTTP
4
+ // adapter. The OS abstraction is intentionally tiny: pick the "opener" command
5
+ // for the platform and pass `target` as a single arg (no shell). For
6
+ // app-launching on macOS we use `open -a <App>`.
7
+ //
8
+ // Stays in host/daemon/ because it's pure process orchestration (spawn child
9
+ // processes), not domain logic.
10
+ import { spawn } from "node:child_process";
11
+
12
+ const MAC_APPS = {
13
+ // Whitelisted mac app names. Adding here is the only way the deck can
14
+ // launch something — we never honour a free-form `app` string.
15
+ claude: "Claude",
16
+ chatgpt: "ChatGPT",
17
+ cursor: "Cursor",
18
+ vscode: "Visual Studio Code",
19
+ zen: "Zen Browser",
20
+ terminal: "Terminal",
21
+ iterm: "iTerm",
22
+ finder: "Finder",
23
+ };
24
+
25
+ function platformOpener() {
26
+ if (process.platform === "darwin") return "open";
27
+ if (process.platform === "win32") return "start";
28
+ return "xdg-open";
29
+ }
30
+
31
+ function spawnDetached(cmd, args) {
32
+ return new Promise((resolve, reject) => {
33
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
34
+ let settled = false;
35
+ const done = (err) => {
36
+ if (settled) return;
37
+ settled = true;
38
+ err ? reject(err) : resolve();
39
+ };
40
+ child.on("error", done);
41
+ // Give the process a tick to fail-fast (bad binary); otherwise detach.
42
+ setTimeout(() => {
43
+ try { child.unref(); } catch {}
44
+ done(null);
45
+ }, 250);
46
+ });
47
+ }
48
+
49
+ /** Pipe `text` into the platform clipboard (pbcopy / xclip / clip). */
50
+ export async function copyToClipboard(text) {
51
+ const platform = process.platform;
52
+ const cmd =
53
+ platform === "darwin" ? "pbcopy" :
54
+ platform === "win32" ? "clip" :
55
+ "xclip";
56
+ const args = platform === "linux" ? ["-selection", "clipboard"] : [];
57
+ await new Promise((resolve, reject) => {
58
+ const child = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
59
+ child.on("error", reject);
60
+ child.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`))));
61
+ child.stdin.end(text);
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Dispatch one /deck/exec action. `ctx.projects` is the daemon's
67
+ * ProjectManager — used to resolve numeric project ids to absolute paths.
68
+ *
69
+ * Supported kinds:
70
+ * open_app { target: "<appKey>" } — mac only
71
+ * open_path { target: "<absPath>" | "<projectId>" } — opens in Finder/default
72
+ * open_path_in { target: "<projectId>", app: "<appKey>" } — mac only
73
+ * open_url { target: "https://..." }
74
+ * copy_clipboard { text: "..." }
75
+ */
76
+ export async function runDeckExec({ kind, target, appHint, text, ctx }) {
77
+ const platform = process.platform;
78
+
79
+ // Resolve a project id (number or "<n>") into an absolute path via
80
+ // the daemon's project manager. Returns null when the id is bogus.
81
+ const projectPath = (idOrPath) => {
82
+ if (!idOrPath) return null;
83
+ const str = String(idOrPath);
84
+ if (str.startsWith("/")) return str;
85
+ if (!/^\d+$/.test(str)) return null;
86
+ const p = ctx.projects?.get?.(parseInt(str, 10));
87
+ return p?.path || null;
88
+ };
89
+
90
+ if (kind === "open_app") {
91
+ if (platform !== "darwin") throw new Error("open_app only implemented on macOS for now");
92
+ const appName = MAC_APPS[String(target || "").toLowerCase()];
93
+ if (!appName) throw new Error(`unknown app: ${target}`);
94
+ // Two-step launch:
95
+ // 1. `open -a` ensures the app is running (no-op if already up).
96
+ // 2. AppleScript `activate` brings it to the foreground across
97
+ // Spaces / Stage Manager, which `open` alone often skips when
98
+ // the app was already running in the background.
99
+ await spawnDetached("open", ["-a", appName]);
100
+ try {
101
+ await new Promise((resolve) => {
102
+ const child = spawn("osascript", [
103
+ "-e",
104
+ `tell application "${appName}" to activate`,
105
+ ], { stdio: "ignore" });
106
+ child.on("close", () => resolve());
107
+ child.on("error", () => resolve());
108
+ setTimeout(() => { try { child.kill(); } catch {} ; resolve(); }, 600);
109
+ });
110
+ } catch {
111
+ // osascript missing or refused — `open -a` already ran.
112
+ }
113
+ return { app: appName };
114
+ }
115
+
116
+ if (kind === "open_path") {
117
+ const resolved = projectPath(target);
118
+ if (!resolved) throw new Error(`open_path: invalid target ${target}`);
119
+ await spawnDetached(platformOpener(), [resolved]);
120
+ return { path: resolved };
121
+ }
122
+
123
+ if (kind === "open_path_in") {
124
+ if (platform !== "darwin") throw new Error("open_path_in only implemented on macOS for now");
125
+ const resolved = projectPath(target);
126
+ if (!resolved) throw new Error(`open_path_in: invalid target ${target}`);
127
+ const appName = MAC_APPS[String(appHint || "").toLowerCase()];
128
+ if (!appName) throw new Error(`open_path_in: unknown app ${appHint}`);
129
+ await spawnDetached("open", ["-a", appName, resolved]);
130
+ return { app: appName, path: resolved };
131
+ }
132
+
133
+ if (kind === "open_url") {
134
+ if (!target || !/^https?:\/\//i.test(String(target))) {
135
+ throw new Error("open_url: target must be http(s) URL");
136
+ }
137
+ await spawnDetached(platformOpener(), [String(target)]);
138
+ return { url: target };
139
+ }
140
+
141
+ if (kind === "copy_clipboard") {
142
+ if (typeof text !== "string") throw new Error("copy_clipboard: text required");
143
+ await copyToClipboard(text);
144
+ return { bytes: text.length };
145
+ }
146
+
147
+ throw new Error(`unknown kind: ${kind}`);
148
+ }
@@ -18,7 +18,7 @@ import {
18
18
  import { ProjectManager } from "./db.js";
19
19
  import { McpRegistry } from "#core/mcp/runner.js";
20
20
  import { PluginManager } from "./plugins/index.js";
21
- import { RoutineScheduler } from "./routines.js";
21
+ import { RoutineScheduler } from "./routines-scheduler.js";
22
22
  import { buildApi } from "./api.js";
23
23
  import { createTokenStore } from "./token-store.js";
24
24
  import { triggerWakeup } from "./wakeup.js";
@@ -218,11 +218,28 @@ async function main() {
218
218
  // store, and start the incremental RAG indexer. Best-effort — never blocks
219
219
  // boot and never throws into the daemon.
220
220
  initMemory({ config: cfg, log }).catch((e) => log(`memory: init failed: ${e?.message || e}`));
221
+ // Skill Inspector: if enabled, refresh its vector index in the background so
222
+ // any SKILL.md added/edited while the daemon was down is picked up without a
223
+ // manual `apx skills index`. Best-effort; never blocks boot.
224
+ (async () => {
225
+ try {
226
+ const { isInspectorEnabled } = await import("#core/agent/skills/inspector.js");
227
+ if (!isInspectorEnabled(cfg)) return;
228
+ const { backgroundRefreshIfStale } = await import("#core/agent/skills/index-store.js");
229
+ const r = backgroundRefreshIfStale({
230
+ embedOpts: { globalConfig: cfg },
231
+ onDone: (out) => log(`skill inspector: index refreshed (${out.embedder}, +${out.changed.added.length} -${out.changed.removed.length} ~${out.changed.refreshed.length})`),
232
+ });
233
+ if (r.started) log(`skill inspector: reindexing ${r.missing} new / ${r.stale} stale / ${r.gone} gone skills…`);
234
+ } catch (e) {
235
+ log(`skill inspector: index refresh skipped (${e?.message || e})`);
236
+ }
237
+ })();
221
238
  // Fire wake-up message after a short delay so plugins (Telegram) are ready
222
239
  setTimeout(() => triggerWakeup(cfg, log), 3000);
223
240
  // Preload whisper-server in the background so first desktop transcription is fast.
224
241
  // Adopts an existing one if already on the port; otherwise spawns fresh.
225
- import("./transcription.js").then(({ preloadWhisperServer }) => {
242
+ import("./whisper-server.js").then(({ preloadWhisperServer }) => {
226
243
  preloadWhisperServer((m) => log(m));
227
244
  }).catch(() => {});
228
245
  });
@@ -257,7 +274,7 @@ async function main() {
257
274
  stopMemory();
258
275
  registries.shutdown();
259
276
  // Best-effort shutdown of whisper-server subprocess.
260
- import("./transcription.js").then(({ shutdownWhisperServer }) => {
277
+ import("./whisper-server.js").then(({ shutdownWhisperServer }) => {
261
278
  shutdownWhisperServer().catch(() => {});
262
279
  }).catch(() => {});
263
280
  server.close(() => {
@@ -32,12 +32,12 @@ import path from "node:path";
32
32
  import { TELEGRAM_STATE_PATH, APX_HOME } from "#core/config/index.js";
33
33
  import { callEngine } from "#core/engines/index.js";
34
34
  import { runSuperAgent, isSuperAgentEnabled } from "#core/agent/super-agent.js";
35
- import { stripThinking } from "../../thinking.js";
35
+ import { stripThinking } from "#core/util/thinking.js";
36
36
  import { getRecentTelegramTurnsFromFs, appendGlobalMessage } from "#core/stores/messages.js";
37
37
  import { compactChannelIfNeeded } from "#core/memory/index.js";
38
38
  import { readAgents } from "#core/apc/parser.js";
39
39
  import { buildAgentSystem } from "#core/agent/build-agent-system.js";
40
- import { transcribe as transcribeAudioFile } from "../../transcription.js";
40
+ import { transcribe as transcribeAudioFile } from "#core/voice/transcription.js";
41
41
  import { resolveAgentName, SUPERAGENT_ACTOR_ID } from "#core/identity/index.js";
42
42
  import { registerSender, resolveAllowedTools } from "#core/identity/telegram.js";
43
43
  import { buildRelationshipBlock } from "#core/agent/index.js";
@@ -45,13 +45,13 @@ import { getConfirmationStore as getConfirmStore } from "#core/confirmation/pend
45
45
  import { CHANNELS } from "#core/constants/channels.js";
46
46
  import { tryResolveSkillCommand } from "#core/agent/skills/trigger.js";
47
47
  import { createTelegramConfirmAdapter } from "#core/confirmation/adapters/telegram.js";
48
- import * as askFlow from "./ask.js";
48
+ import * as askFlow from "#core/channels/telegram/ask.js";
49
49
 
50
- // API_BASE re-imported from ./media.js below
50
+ // API_BASE re-imported from #core/channels/telegram/media.js below
51
51
  const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
52
52
 
53
- // All non-class-bound helpers live in ./helpers.js so the file stays
54
- // focused on the poller class + dispatch wiring.
53
+ // All non-class-bound channel logic lives in core/channels/telegram/ this
54
+ // file stays focused on the poller class + plugin lifecycle wiring.
55
55
  import {
56
56
  buildTelegramMeta,
57
57
  loadState,
@@ -61,11 +61,11 @@ import {
61
61
  tokenSource,
62
62
  resolveChannels,
63
63
  sleep,
64
- } from "./helpers.js";
65
- import { handleUpdate } from "./dispatch.js";
64
+ } from "#core/channels/telegram/helpers.js";
65
+ import { handleUpdate } from "#core/channels/telegram/dispatch.js";
66
66
 
67
67
  // ---------- media sending helpers (re-exports) ------------------------------
68
- import { sendPhoto, sendVoice, sendDocument, sendAudio, downloadTelegramFile, API_BASE } from "./media.js";
68
+ import { sendPhoto, sendVoice, sendDocument, sendAudio, downloadTelegramFile, API_BASE } from "#core/channels/telegram/media.js";
69
69
  export { sendPhoto, sendVoice, sendDocument, sendAudio };
70
70
 
71
71
  // ---------- per-channel poller ----------------------------------------------
@@ -0,0 +1,64 @@
1
+ // Polling timer that fires due routines. Process-state — needs the daemon
2
+ // alive, has no business being in core/. Delegates the actual work to
3
+ // core/routines/runner.js so CLI / HTTP / web / mcp-server / scripts can
4
+ // invoke the same runner without depending on this file.
5
+ import { getDueRoutines } from "#core/stores/routines.js";
6
+ import { runRoutineNow } from "#core/routines/runner.js";
7
+ import { nowIso } from "#core/util/time.js";
8
+
9
+ const TICK_MS = 5_000;
10
+
11
+ export class RoutineScheduler {
12
+ constructor({ projects, plugins, registries, globalConfig, log }) {
13
+ this.projects = projects;
14
+ this.plugins = plugins;
15
+ this.registries = registries;
16
+ this.globalConfig = globalConfig;
17
+ this.log = log || (() => {});
18
+ this._timer = null;
19
+ this._running = false;
20
+ }
21
+
22
+ start() {
23
+ if (this._timer) return;
24
+ this._timer = setInterval(
25
+ () => this._tick().catch((e) => this.log(`routines tick error: ${e.message}`)),
26
+ TICK_MS
27
+ );
28
+ this._timer.unref?.();
29
+ }
30
+
31
+ stop() {
32
+ if (this._timer) {
33
+ clearInterval(this._timer);
34
+ this._timer = null;
35
+ }
36
+ }
37
+
38
+ async _tick() {
39
+ if (this._running) return;
40
+ this._running = true;
41
+ try {
42
+ const nowStr = nowIso();
43
+ for (const proj of this.projects.list().map((p) => this.projects.get(p.id))) {
44
+ if (!proj) continue;
45
+ const due = getDueRoutines(proj.storagePath, nowStr);
46
+ for (const r of due) {
47
+ this.log(`routine ${r.name} (${r.kind}) firing in project #${proj.id}`);
48
+ await runRoutineNow(
49
+ {
50
+ project: proj,
51
+ projects: this.projects,
52
+ plugins: this.plugins,
53
+ registries: this.registries,
54
+ globalConfig: this.globalConfig,
55
+ },
56
+ r
57
+ );
58
+ }
59
+ }
60
+ } finally {
61
+ this._running = false;
62
+ }
63
+ }
64
+ }
@@ -9,6 +9,7 @@ import fs from "node:fs";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { ProjectManager } from "./db.js";
11
11
  import { McpRegistry } from "#core/mcp/runner.js";
12
+ import { agentsMdFile, apcProjectFile } from "#core/apc/paths.js";
12
13
  import { readAgents } from "#core/apc/parser.js";
13
14
 
14
15
  const __filename = fileURLToPath(import.meta.url);
@@ -23,8 +24,8 @@ const EXAMPLE_CANDIDATES = [
23
24
  path.resolve(__dirname, "..", "..", "..", "..", "apc", "examples", "my-first-project"),
24
25
  ];
25
26
  const EXAMPLE = EXAMPLE_CANDIDATES.find((p) =>
26
- fs.existsSync(path.join(p, "AGENTS.md")) &&
27
- fs.existsSync(path.join(p, ".apc", "project.json"))
27
+ fs.existsSync(agentsMdFile(p)) &&
28
+ fs.existsSync(apcProjectFile(p))
28
29
  );
29
30
 
30
31
  function assert(cond, msg) {
@@ -0,0 +1,225 @@
1
+ // Subprocess lifecycle for the persistent whisper-server.py.
2
+ //
3
+ // Owns:
4
+ // - the Python child process (spawn, health-watch, kill on shutdown)
5
+ // - port collision recovery (kill an orphan listener and retry)
6
+ // - daemon-boot preload + warmup + graceful teardown
7
+ //
8
+ // Does NOT do the actual transcription — that's an HTTP call to localhost
9
+ // and lives in core/voice/transcription.js. The port number is the single
10
+ // piece of shared state and is exported from core; this file imports it.
11
+ import { spawn, exec } from "node:child_process";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import {
15
+ WHISPER_LOCAL_PORT,
16
+ DEFAULT_LOCAL,
17
+ getConfig,
18
+ } from "#core/voice/transcription.js";
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = path.dirname(__filename);
22
+ const WHISPER_SERVER = path.join(__dirname, "whisper-server.py");
23
+
24
+ let _serverProcess = null;
25
+ let _serverModel = null;
26
+
27
+ function _sleep(ms) {
28
+ return new Promise((r) => setTimeout(r, ms));
29
+ }
30
+
31
+ async function _isServerHealthy() {
32
+ try {
33
+ const res = await fetch(`http://127.0.0.1:${WHISPER_LOCAL_PORT}/health`, {
34
+ signal: AbortSignal.timeout(800),
35
+ });
36
+ return res.ok;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ async function _serverModelName() {
43
+ try {
44
+ const res = await fetch(`http://127.0.0.1:${WHISPER_LOCAL_PORT}/health`, {
45
+ signal: AbortSignal.timeout(800),
46
+ });
47
+ if (!res.ok) return null;
48
+ const j = await res.json();
49
+ return j?.model || null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ async function _findListenerPid() {
56
+ return new Promise((resolve) => {
57
+ exec(`lsof -ti tcp:${WHISPER_LOCAL_PORT} -sTCP:LISTEN`, (err, stdout) => {
58
+ if (err || !stdout) return resolve(null);
59
+ const candidates = stdout.trim().split("\n")
60
+ .map(s => parseInt(s, 10))
61
+ .filter(n => Number.isFinite(n) && n !== process.pid);
62
+ resolve(candidates[0] || null);
63
+ });
64
+ });
65
+ }
66
+
67
+ async function _killOrphanWhisper() {
68
+ try {
69
+ await fetch(`http://127.0.0.1:${WHISPER_LOCAL_PORT}/shutdown`, {
70
+ method: "POST", signal: AbortSignal.timeout(1000),
71
+ });
72
+ await _sleep(600);
73
+ } catch {}
74
+ const pid = await _findListenerPid();
75
+ if (pid && pid !== process.pid) {
76
+ try { process.kill(pid, "SIGTERM"); } catch {}
77
+ await _sleep(400);
78
+ try { process.kill(pid, 0); try { process.kill(pid, "SIGKILL"); } catch {} } catch {}
79
+ await _sleep(300);
80
+ }
81
+ }
82
+
83
+ export async function ensureWhisperServer(opts) {
84
+ const model = opts.model || DEFAULT_LOCAL.model;
85
+
86
+ if (_serverProcess && _serverModel === model) {
87
+ if (await _isServerHealthy()) return;
88
+ _serverProcess = null;
89
+ _serverModel = null;
90
+ }
91
+
92
+ if (!_serverProcess) {
93
+ const existing = await _serverModelName();
94
+ if (existing === model) {
95
+ _serverModel = model;
96
+ return;
97
+ }
98
+ if (existing) {
99
+ await _killOrphanWhisper();
100
+ }
101
+ }
102
+
103
+ if (_serverProcess) {
104
+ try { _serverProcess.kill(); } catch {}
105
+ _serverProcess = null;
106
+ _serverModel = null;
107
+ await _sleep(300);
108
+ }
109
+
110
+ await _spawnWhisper(opts, model, /* retried */ false);
111
+ }
112
+
113
+ async function _spawnWhisper(opts, model, retried) {
114
+ const args = [
115
+ WHISPER_SERVER,
116
+ "--port", String(WHISPER_LOCAL_PORT),
117
+ "--model", model,
118
+ "--device", String(opts.device || DEFAULT_LOCAL.device),
119
+ "--compute-type", String(opts.compute_type || DEFAULT_LOCAL.compute_type),
120
+ "--idle-minutes", String(opts.idle_minutes ?? DEFAULT_LOCAL.idle_minutes),
121
+ ];
122
+
123
+ const proc = spawn("python3", args, {
124
+ stdio: ["ignore", "pipe", "inherit"],
125
+ detached: false,
126
+ });
127
+
128
+ _serverProcess = proc;
129
+ _serverModel = model;
130
+
131
+ proc.on("exit", () => {
132
+ if (_serverProcess === proc) {
133
+ _serverProcess = null;
134
+ _serverModel = null;
135
+ }
136
+ });
137
+
138
+ try {
139
+ await new Promise((resolve, reject) => {
140
+ const timeout = setTimeout(
141
+ () => reject(new Error("whisper-server startup timed out (15s)")),
142
+ 15_000
143
+ );
144
+ let buf = "";
145
+ proc.stdout.on("data", (chunk) => {
146
+ buf += chunk.toString();
147
+ const nl = buf.indexOf("\n");
148
+ if (nl === -1) return;
149
+ const line = buf.slice(0, nl).trim();
150
+ buf = buf.slice(nl + 1);
151
+ clearTimeout(timeout);
152
+ try {
153
+ const msg = JSON.parse(line);
154
+ if (msg.status === "error") return reject(new Error(msg.error || "whisper-server error"));
155
+ resolve();
156
+ } catch {
157
+ resolve();
158
+ }
159
+ });
160
+ proc.on("exit", (code) => {
161
+ clearTimeout(timeout);
162
+ reject(new Error(`whisper-server exited (code ${code}) before becoming ready`));
163
+ });
164
+ });
165
+ } catch (e) {
166
+ const msg = e.message || "";
167
+ if (!retried && /address already in use|errno 48|eaddrinuse/i.test(msg)) {
168
+ _serverProcess = null;
169
+ _serverModel = null;
170
+ await _killOrphanWhisper();
171
+ return _spawnWhisper(opts, model, /* retried */ true);
172
+ }
173
+ throw e;
174
+ }
175
+ }
176
+
177
+ export async function preloadWhisperServer(log = console.log) {
178
+ try {
179
+ const cfg = await getConfig();
180
+ if (cfg.provider === "openai") return;
181
+ log(`whisper: preloading model "${cfg.local.model}" on port ${WHISPER_LOCAL_PORT}…`);
182
+ await ensureWhisperServer(cfg.local);
183
+ log(`whisper: ready on port ${WHISPER_LOCAL_PORT} (model: ${_serverModel})`);
184
+ } catch (e) {
185
+ log(`whisper: preload failed — ${e.message} (will retry lazily on first request)`);
186
+ }
187
+ }
188
+
189
+ export async function warmupWhisper() {
190
+ try {
191
+ const cfg = await getConfig();
192
+ if (cfg.provider === "openai") return { ok: true, provider: "openai", loaded: false };
193
+ await ensureWhisperServer(cfg.local);
194
+ let loaded = false;
195
+ try {
196
+ const r = await fetch(`http://127.0.0.1:${WHISPER_LOCAL_PORT}/warmup`, {
197
+ signal: AbortSignal.timeout(40_000),
198
+ });
199
+ const j = await r.json().catch(() => ({}));
200
+ loaded = !!j.loaded;
201
+ } catch {}
202
+ return { ok: true, provider: "local", model: _serverModel, loaded };
203
+ } catch (e) {
204
+ return { ok: false, error: e.message };
205
+ }
206
+ }
207
+
208
+ export async function shutdownWhisperServer() {
209
+ if (_serverProcess) {
210
+ try { _serverProcess.kill(); } catch {}
211
+ _serverProcess = null;
212
+ _serverModel = null;
213
+ } else {
214
+ try {
215
+ await fetch(`http://127.0.0.1:${WHISPER_LOCAL_PORT}/shutdown`, {
216
+ method: "POST", signal: AbortSignal.timeout(500),
217
+ });
218
+ } catch {}
219
+ }
220
+ }
221
+
222
+ export const WHISPER_PATHS = {
223
+ whisper_server: WHISPER_SERVER,
224
+ port: WHISPER_LOCAL_PORT,
225
+ };
@@ -0,0 +1,53 @@
1
+ // APX CLI branding — a consistent "you're running APX vX" mark on every command.
2
+ //
3
+ // Two shapes:
4
+ // apxBanner(version, subtitle) big ASCII wordmark for branding-heavy moments
5
+ // (onboarding, top-level entry). Loud on purpose.
6
+ // apxHeader(version, subtitle) one-line "▸ APX CLI · vX · <subtitle>" for the
7
+ // everyday commands. Quiet, never in the way.
8
+ //
9
+ // Both write to STDERR so they never pollute piped stdout (`apx exec … | jq`,
10
+ // `apx config show > file`). Like mascot.js, they always print (so the mark is
11
+ // truly on every run), and self-suppress only when APX_QUIET / APX_NO_BANNER is
12
+ // set — the escape hatch for scripts and CI.
13
+ //
14
+ // Color: reuses raw ANSI like mascot.js. Honors NO_COLOR.
15
+
16
+ const NO_COLOR = !!process.env.NO_COLOR;
17
+ const c = (code) => (s) => (NO_COLOR ? s : `\x1b[${code}m${s}\x1b[0m`);
18
+ const B = c("1");
19
+ const DI = c("2");
20
+ const GR = c("32");
21
+ const CY = c("36");
22
+ const WH = c("97");
23
+
24
+ function suppressed() {
25
+ return !!(process.env.APX_NO_BANNER || process.env.APX_QUIET);
26
+ }
27
+
28
+ // Compact, single-line header. The default for everyday subcommands.
29
+ // ▸ APX CLI · v1.34.0 · skills inspector
30
+ export function apxHeader(version, subtitle = "") {
31
+ if (suppressed()) return;
32
+ const tag = `${GR("▸")} ${B(WH("APX"))} ${DI("CLI")}`;
33
+ const ver = DI(`v${version}`);
34
+ const sub = subtitle ? ` ${DI("·")} ${CY(subtitle)}` : "";
35
+ process.stderr.write(`\n${tag} ${DI("·")} ${ver}${sub}\n\n`);
36
+ }
37
+
38
+ // Big ASCII wordmark for branding-heavy commands.
39
+ export function apxBanner(version, subtitle = "") {
40
+ if (suppressed()) return;
41
+ const g = (s) => GR(s);
42
+ const lines = [
43
+ "",
44
+ ` ${g("█████╗ ██████╗ ██╗ ██╗")}`,
45
+ ` ${g("██╔══██╗██╔══██╗╚██╗██╔╝")}`,
46
+ ` ${g("███████║██████╔╝ ╚███╔╝ ")} ${B(WH("Agent Project Context"))}`,
47
+ ` ${g("██╔══██║██╔═══╝ ██╔██╗ ")} ${DI(`v${version}`)}`,
48
+ ` ${g("██║ ██║██║ ██╔╝ ██╗")}${subtitle ? ` ${CY(subtitle)}` : ""}`,
49
+ ` ${g("╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝")}`,
50
+ "",
51
+ ];
52
+ process.stderr.write(lines.join("\n") + "\n");
53
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { findApfRoot, readAgents, readVaultAgents, readVaultAgent, VAULT_DIR, SLUG_RE } from "#core/apc/parser.js";
4
+ import { apcAgentFile } from "#core/apc/paths.js";
4
5
  import { writeAgentFile, writeVaultAgentFile, removeVaultAgent, restoreVaultAgent, addImportedAgent, ensureAgentDir } from "#core/apc/scaffold.js";
5
6
  import { ensureAgentRuntimeDir, agentMemoryPath } from "#core/agent/memory.js";
6
7
  import { http } from "../http.js";
@@ -194,7 +195,7 @@ export async function cmdAgentImport(args) {
194
195
  throw new Error(`"${slug}" not found in vault. Available: ${available}`);
195
196
  }
196
197
 
197
- const alreadyLocal = fs.existsSync(path.join(root, ".apc", "agents", `${slug}.md`));
198
+ const alreadyLocal = fs.existsSync(apcAgentFile(root, slug));
198
199
  if (alreadyLocal && !args.flags.force) {
199
200
  console.log(dim(` "${slug}" already has a local definition. Use --force to overwrite.`));
200
201
  return;
@@ -202,7 +203,7 @@ export async function cmdAgentImport(args) {
202
203
 
203
204
  if (args.flags.copy) {
204
205
  // Copy .md into project so user can edit locally
205
- fs.copyFileSync(vaultPath, path.join(root, ".apc", "agents", `${slug}.md`));
206
+ fs.copyFileSync(vaultPath, apcAgentFile(root, slug));
206
207
  console.log(`\n ${bold(slug)} copied from vault to project (now local)\n`);
207
208
  } else {
208
209
  // Just register as imported — reads from vault at runtime
@@ -2,12 +2,11 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { findApfRoot } from "#core/apc/parser.js";
5
+ import { apcCommandsDir } from "#core/apc/paths.js";
5
6
  import { http } from "../http.js";
6
7
  import { resolveProjectId } from "./project.js";
7
8
 
8
- function commandsDir(root) {
9
- return path.join(root, ".apc", "commands");
10
- }
9
+ const commandsDir = apcCommandsDir;
11
10
 
12
11
  function listCommandFiles(root) {
13
12
  const dir = commandsDir(root);
@@ -1,9 +1,13 @@
1
1
  import { http } from "../http.js";
2
2
  import { resolveProjectId } from "./project.js";
3
+ import { CHANNELS } from "#core/constants/channels.js";
3
4
 
4
5
  // Channels that live in ~/.apx/messages/<channel>/ (global, cross-project).
5
6
  // Everything else is project-scoped.
6
- const GLOBAL_CHANNELS = new Set(["telegram", "direct", "whatsapp"]);
7
+ // DIRECT and WHATSAPP are still placeholders (no plugin lives behind them
8
+ // yet) but they're real channel ids — when those plugins land, message
9
+ // lookup already routes them as project-less / global.
10
+ const GLOBAL_CHANNELS = new Set([CHANNELS.TELEGRAM, CHANNELS.DIRECT, CHANNELS.WHATSAPP]);
7
11
 
8
12
  function isGlobalChannel(channel) {
9
13
  return channel && GLOBAL_CHANNELS.has(channel);
@@ -71,7 +75,7 @@ function printChatRows(rows) {
71
75
  }
72
76
 
73
77
  export async function cmdMessagesChat(args) {
74
- const channel = args.flags.channel && args.flags.channel !== true ? args.flags.channel : "telegram";
78
+ const channel = args.flags.channel && args.flags.channel !== true ? args.flags.channel : CHANNELS.TELEGRAM;
75
79
  const n = args.flags.n || args.flags.last || "50";
76
80
  const isGlobal = args.flags.global || isGlobalChannel(channel);
77
81