@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
@@ -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
+ };
@@ -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
 
@@ -8,6 +8,7 @@
8
8
  import os from "node:os";
9
9
  import qrcode from "qrcode-terminal";
10
10
  import { http } from "../http.js";
11
+ import { CHANNELS } from "#core/constants/channels.js";
11
12
 
12
13
  const c = {
13
14
  reset: "\x1b[0m",
@@ -88,10 +89,10 @@ async function runPairing(args, channel) {
88
89
  // 2. QR payload differs per channel — no mixing.
89
90
  // deck: JSON {v,url,pid,fp} that the Deck app parses.
90
91
  // web : the scan-to-login URL the browser understands.
91
- const qrData = channel === "web"
92
+ const qrData = channel === CHANNELS.WEB
92
93
  ? webLink
93
94
  : JSON.stringify({ v: 1, url: baseUrl, pid: init.pairing_id, fp: init.fingerprint });
94
- const title = channel === "web" ? "APX pairing (web)" : "APX pairing (deck)";
95
+ const title = channel === CHANNELS.WEB ? "APX pairing (web)" : "APX pairing (deck)";
95
96
 
96
97
  console.log("");
97
98
  console.log(` ${fmt.bold(title)} ${fmt.gray("·")} ${fmt.dim(`expires in ${ttlSec}s`)}`);
@@ -100,7 +101,7 @@ async function runPairing(args, channel) {
100
101
  console.log(qr);
101
102
  });
102
103
 
103
- if (channel === "web") {
104
+ if (channel === CHANNELS.WEB) {
104
105
  console.log(` ${fmt.gray("scan with the phone camera — opens the web already linked")}`);
105
106
  console.log(` ${fmt.gray("link:")} ${fmt.cyan(webLink)}`);
106
107
  console.log(` ${fmt.gray("code:")} ${fmt.bold(init.pairing_id)} ${fmt.dim("(or paste it in the web pairing screen)")}`);
@@ -111,7 +112,7 @@ async function runPairing(args, channel) {
111
112
  console.log("");
112
113
 
113
114
  // 3. Poll /pair/status until confirmed or expired.
114
- const reRun = channel === "web" ? "apx pair web" : "apx pair";
115
+ const reRun = channel === CHANNELS.WEB ? "apx pair web" : "apx pair";
115
116
  const deadline = Date.now() + (init.ttl_ms || 90_000) + 5_000;
116
117
  while (Date.now() < deadline) {
117
118
  await sleep(1500);
@@ -3,7 +3,7 @@
3
3
  // Uses the daemon's tools/search.js module directly (no HTTP roundtrip,
4
4
  // no need to have `apx daemon start` running).
5
5
 
6
- import { webSearch } from "#core/tools/search.js";
6
+ import { webSearch } from "#core/http-tools/search.js";
7
7
 
8
8
  const DIM = "\x1b[2m";
9
9
  const BOLD = "\x1b[1m";
@@ -5,6 +5,7 @@
5
5
  import fs from "node:fs";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
8
+ import { apcProjectFile } from "#core/apc/paths.js";
8
9
 
9
10
  // ── shared helpers ───────────────────────────────────────────────────────────
10
11
 
@@ -124,7 +125,7 @@ function readApxProjects(opts) {
124
125
  const proj = { path: path.resolve(e.path), name: null, apxId: null };
125
126
  try {
126
127
  const pj = JSON.parse(
127
- fs.readFileSync(path.join(proj.path, ".apc", "project.json"), "utf8")
128
+ fs.readFileSync(apcProjectFile(proj.path), "utf8")
128
129
  );
129
130
  if (pj.name) proj.name = pj.name;
130
131
  if (pj.apx_id) proj.apxId = pj.apx_id;
@@ -608,7 +609,7 @@ const apxEngine = {
608
609
  let apxId = null;
609
610
  try {
610
611
  const pj = JSON.parse(
611
- fs.readFileSync(path.join(dir, ".apc", "project.json"), "utf8")
612
+ fs.readFileSync(apcProjectFile(dir), "utf8")
612
613
  );
613
614
  apxId = pj.apx_id || null;
614
615
  } catch {}
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import os from "node:os";
5
5
  import readline from "node:readline";
6
6
  import { findApfRoot } from "#core/apc/parser.js";
7
+ import { apcSkillsDir } from "#core/apc/paths.js";
7
8
  import { http } from "../http.js";
8
9
  import {
9
10
  IDE_TARGETS,
@@ -11,6 +12,8 @@ import {
11
12
  installGlobalSkills,
12
13
  listBundledSkillSlugs,
13
14
  listBundledSkills,
15
+ listEngineSkills,
16
+ listLegacyPruneSlugs,
14
17
  } from "#core/apc/scaffold.js";
15
18
 
16
19
  // ---------------------------------------------------------------------------
@@ -100,37 +103,27 @@ export async function cmdSkillsAdd(args) {
100
103
  // ---------------------------------------------------------------------------
101
104
 
102
105
  export async function cmdSkillsSync(args) {
103
- const includeOptional = !!args?.flags?.["include-optional"] || !!args?.flags?.optional;
104
- const includeInternal = !!args?.flags?.["include-internal"] || !!args?.flags?.internal;
105
106
  const prune = args?.flags?.["no-prune"] ? false : true;
106
107
 
107
- const results = installGlobalSkills({ includeOptional, includeInternal, prune });
108
+ const results = installGlobalSkills({ prune });
108
109
  const home = os.homedir();
109
110
 
110
- // Group by skill so the output is dense and scannable.
111
111
  const bySkill = {};
112
- const scopeOf = {};
113
112
  for (const r of results) {
114
113
  if (!bySkill[r.skill]) bySkill[r.skill] = [];
115
- bySkill[r.skill].push({ dir: r.dir.replace(home, "~"), status: r.status });
116
- scopeOf[r.skill] = r.scope;
114
+ bySkill[r.skill].push({ dir: r.dir.replace(home, "~"), status: r.status, scope: r.scope });
117
115
  }
118
116
 
119
- const slugs = Object.keys(bySkill).sort();
120
- if (slugs.length === 0) {
121
- console.log("(no bundled skills found in skills/)");
117
+ const engineSet = listEngineSkills().map((s) => s.slug);
118
+ if (engineSet.length === 0) {
119
+ console.log("(no engine skills found in skills/engines/)");
122
120
  return;
123
121
  }
124
122
 
125
- const filters = [];
126
- if (includeOptional) filters.push("+optional");
127
- if (includeInternal) filters.push("+internal");
128
- console.log(
129
- `Syncing ${slugs.length} bundled skill(s) to global skill dirs` +
130
- (filters.length ? ` [${filters.join(" ")}]` : "") + ":\n"
131
- );
123
+ console.log(`Syncing engine skill set (${engineSet.join(", ")}) to global dirs:\n`);
132
124
 
133
- const sw = Math.max(...slugs.map((s) => s.length));
125
+ const slugs = Object.keys(bySkill).sort();
126
+ const sw = Math.max(...slugs.map((s) => s.length), 8);
134
127
  const totals = { unchanged: 0, updated: 0, created: 0, pruned: 0 };
135
128
  for (const slug of slugs) {
136
129
  const entries = bySkill[slug];
@@ -141,9 +134,8 @@ export async function cmdSkillsSync(args) {
141
134
  for (const k of ["created", "updated", "unchanged", "pruned"]) {
142
135
  if (counts[k]) parts.push(`${counts[k]} ${k}`);
143
136
  }
144
- const scope = scopeOf[slug];
145
- const tag = scope === "public" ? "" : ` [${scope}]`;
146
- console.log(` ${slug.padEnd(sw)}${tag.padEnd(11)} ${parts.join(", ")}`);
137
+ const tag = entries[0]?.scope === "legacy" ? " [legacy]" : "";
138
+ console.log(` ${slug.padEnd(sw)}${tag.padEnd(10)} ${parts.join(", ")}`);
147
139
  }
148
140
  console.log("");
149
141
  console.log(`Targets: .claude/skills, .cursor/skills, .codex/skills, .agents/skills`);
@@ -153,21 +145,6 @@ export async function cmdSkillsSync(args) {
153
145
  }
154
146
  console.log(`Totals: ${totalParts.join(", ") || "(no changes)"}`);
155
147
 
156
- // Hint about non-default tiers.
157
- const skipped = listBundledSkills().filter(
158
- (s) =>
159
- (s.scope === "internal" && !includeInternal) ||
160
- (s.scope === "optional" && !includeOptional)
161
- );
162
- if (skipped.length > 0) {
163
- console.log("");
164
- console.log("Skipped (not pushed by default):");
165
- for (const s of skipped) console.log(` ${s.slug.padEnd(sw)} scope=${s.scope}`);
166
- console.log("");
167
- console.log("Re-run with --include-optional / --include-internal to push them too,");
168
- console.log("or `apx skills add <slug> --global` for one-off install.");
169
- }
170
-
171
148
  if (args?.flags?.verbose) {
172
149
  console.log("");
173
150
  for (const slug of slugs) {
@@ -184,8 +161,8 @@ export async function cmdSkillsSync(args) {
184
161
  // ---------------------------------------------------------------------------
185
162
 
186
163
  export async function cmdSkillsList(args = {}) {
187
- // --all queries the daemon, which returns project + global + bundled +
188
- // runtime-skills (same catalog the super-agent sees and the web picker uses).
164
+ // --all queries the daemon, which returns project + global + bundled
165
+ // (the same catalog the super-agent sees and the web picker uses).
189
166
  // Without --all we only list `.apc/skills/` (what the user installed in
190
167
  // THIS project), matching the historical behaviour.
191
168
  if (args?.flags?.all) {
@@ -205,7 +182,7 @@ export async function cmdSkillsList(args = {}) {
205
182
  }
206
183
 
207
184
  const root = findApfRoot();
208
- const skillsDir = root ? path.join(root, ".apc", "skills") : null;
185
+ const skillsDir = root ? apcSkillsDir(root) : null;
209
186
  const files = skillsDir && fs.existsSync(skillsDir)
210
187
  ? fs.readdirSync(skillsDir).filter((f) => f.endsWith(".md"))
211
188
  : [];
@@ -224,17 +201,15 @@ export async function cmdSkillsList(args = {}) {
224
201
  export async function cmdSkillsStatus() {
225
202
  const root = findApfRoot();
226
203
 
227
- // Global discovered list of every bundled skill with its scope.
228
- const bundled = listBundledSkills();
229
- const byScope = {
230
- public: bundled.filter((s) => s.scope === "public"),
231
- optional: bundled.filter((s) => s.scope === "optional"),
232
- internal: bundled.filter((s) => s.scope === "internal"),
233
- };
204
+ const engineSet = listEngineSkills(); // what we publish to engines
205
+ const bundled = listBundledSkills(); // what stays in-repo for the super-agent
206
+ const legacy = listLegacyPruneSlugs(); // slugs APX shipped historically — pruned on sync
207
+
234
208
  console.log(
235
- `Bundled skills: ${bundled.length} total (${byScope.public.length} public, ` +
236
- `${byScope.optional.length} optional, ${byScope.internal.length} internal)`
209
+ `Engine skill set (replicated to global dirs): ${engineSet.length} ` +
210
+ `(${engineSet.map((s) => s.slug).join(", ") || "—"})`
237
211
  );
212
+ console.log(`In-repo bundled skills (super-agent only): ${bundled.length}`);
238
213
  console.log("");
239
214
  console.log(`Global skill dirs:`);
240
215
  const GLOBAL_DIRS = [
@@ -243,17 +218,23 @@ export async function cmdSkillsStatus() {
243
218
  { label: "Codex", dir: path.join(os.homedir(), ".codex", "skills") },
244
219
  { label: "Antigravity / others", dir: path.join(os.homedir(), ".agents", "skills") },
245
220
  ];
246
- const sw = Math.max(...bundled.map((s) => s.slug.length));
221
+ const allSlugs = [...engineSet.map((s) => s.slug), ...legacy];
222
+ const sw = Math.max(...allSlugs.map((s) => s.length), 8);
247
223
  for (const { label, dir } of GLOBAL_DIRS) {
248
224
  console.log(`\n ${label} — ${dir.replace(os.homedir(), "~")}`);
249
- for (const { slug, scope } of bundled) {
225
+ for (const { slug } of engineSet) {
250
226
  const dest = path.join(dir, slug, "SKILL.md");
251
227
  const present = fs.existsSync(dest);
252
- const tag = scope === "public" ? "" : ` [${scope}]`;
253
- const state = present
254
- ? "✓ installed"
255
- : (scope === "public" ? "✗ MISSING (run `apx skills sync`)" : "—");
256
- console.log(` ${slug.padEnd(sw)}${tag.padEnd(11)} ${state}`);
228
+ const state = present ? "✓ installed" : " MISSING (run `apx skills sync`)";
229
+ console.log(` ${slug.padEnd(sw)} ${state}`);
230
+ }
231
+ const stale = legacy.filter((slug) =>
232
+ fs.existsSync(path.join(dir, slug, "SKILL.md"))
233
+ );
234
+ if (stale.length) {
235
+ for (const slug of stale) {
236
+ console.log(` ${slug.padEnd(sw)} [legacy] ⚠ stale (run \`apx skills sync\` to prune)`);
237
+ }
257
238
  }
258
239
  }
259
240