@agentprojectcontext/apx 1.0.3

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/package.json +52 -0
  4. package/skills/apx/SKILL.md +77 -0
  5. package/src/cli/commands/a2a.js +66 -0
  6. package/src/cli/commands/agent.js +181 -0
  7. package/src/cli/commands/chat.js +84 -0
  8. package/src/cli/commands/command.js +42 -0
  9. package/src/cli/commands/config.js +56 -0
  10. package/src/cli/commands/daemon.js +148 -0
  11. package/src/cli/commands/exec.js +56 -0
  12. package/src/cli/commands/identity.js +146 -0
  13. package/src/cli/commands/init.js +23 -0
  14. package/src/cli/commands/mcp.js +147 -0
  15. package/src/cli/commands/memory.js +69 -0
  16. package/src/cli/commands/messages.js +61 -0
  17. package/src/cli/commands/plugins.js +23 -0
  18. package/src/cli/commands/project.js +124 -0
  19. package/src/cli/commands/routine.js +99 -0
  20. package/src/cli/commands/runtime.js +64 -0
  21. package/src/cli/commands/session.js +387 -0
  22. package/src/cli/commands/skills.js +153 -0
  23. package/src/cli/commands/telegram.js +48 -0
  24. package/src/cli/http.js +102 -0
  25. package/src/cli/index.js +481 -0
  26. package/src/cli/postinstall.js +25 -0
  27. package/src/core/apc-context-skill.md +150 -0
  28. package/src/core/apx-skill.md +78 -0
  29. package/src/core/config.js +129 -0
  30. package/src/core/identity.js +23 -0
  31. package/src/core/messages-store.js +421 -0
  32. package/src/core/parser.js +217 -0
  33. package/src/core/routines-store.js +144 -0
  34. package/src/core/scaffold.js +417 -0
  35. package/src/core/session-store.js +36 -0
  36. package/src/daemon/apc-runtime-context.js +123 -0
  37. package/src/daemon/api.js +946 -0
  38. package/src/daemon/compact.js +140 -0
  39. package/src/daemon/conversations.js +108 -0
  40. package/src/daemon/db.js +81 -0
  41. package/src/daemon/engines/anthropic.js +58 -0
  42. package/src/daemon/engines/gemini.js +55 -0
  43. package/src/daemon/engines/index.js +65 -0
  44. package/src/daemon/engines/mock.js +18 -0
  45. package/src/daemon/engines/ollama.js +66 -0
  46. package/src/daemon/engines/openai.js +58 -0
  47. package/src/daemon/env-detect.js +69 -0
  48. package/src/daemon/index.js +156 -0
  49. package/src/daemon/mcp-runner.js +218 -0
  50. package/src/daemon/mcp-sources.js +114 -0
  51. package/src/daemon/plugins/index.js +91 -0
  52. package/src/daemon/plugins/telegram.js +549 -0
  53. package/src/daemon/project-config.js +98 -0
  54. package/src/daemon/routines.js +211 -0
  55. package/src/daemon/runtimes/_spawn.js +44 -0
  56. package/src/daemon/runtimes/aider.js +32 -0
  57. package/src/daemon/runtimes/claude-code.js +60 -0
  58. package/src/daemon/runtimes/codex.js +30 -0
  59. package/src/daemon/runtimes/index.js +39 -0
  60. package/src/daemon/runtimes/opencode.js +28 -0
  61. package/src/daemon/smoke.js +54 -0
  62. package/src/daemon/super-agent-tools.js +539 -0
  63. package/src/daemon/super-agent.js +188 -0
  64. package/src/daemon/thinking.js +45 -0
  65. package/src/daemon/tool-call-parser.js +116 -0
  66. package/src/daemon/wakeup.js +92 -0
  67. package/src/mcp/index.js +220 -0
@@ -0,0 +1,387 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { findApfRoot, readAgents } from "../../core/parser.js";
4
+ import { ensureAgentDir } from "../../core/scaffold.js";
5
+ import { generateSessionId } from "../../core/session-store.js";
6
+ import { http } from "../http.js";
7
+ import { resolveProjectId } from "./project.js";
8
+
9
+ const STALE_HOURS = 1;
10
+
11
+ function requireRoot() {
12
+ const root = findApfRoot();
13
+ if (!root) throw new Error("not inside an APC project (run `apx init` first)");
14
+ return root;
15
+ }
16
+
17
+ const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
18
+
19
+ function readStdinSync() {
20
+ const chunks = [];
21
+ const buf = Buffer.alloc(65536);
22
+ try {
23
+ while (true) {
24
+ const bytes = fs.readSync(0, buf, 0, buf.length);
25
+ if (!bytes) break;
26
+ chunks.push(buf.slice(0, bytes).toString("utf8"));
27
+ }
28
+ } catch {}
29
+ return chunks.join("");
30
+ }
31
+
32
+ function parseFrontmatter(text) {
33
+ if (!text.startsWith("---\n")) return { fm: {}, bodyStart: 0 };
34
+ const end = text.indexOf("\n---", 4);
35
+ if (end === -1) return { fm: {}, bodyStart: 0 };
36
+ const fmText = text.slice(4, end);
37
+ const fm = {};
38
+ for (const line of fmText.split("\n")) {
39
+ const m = line.match(/^([a-zA-Z_]+):\s*(.*)$/);
40
+ if (m) fm[m[1]] = m[2].trim();
41
+ }
42
+ return { fm, bodyStart: end + 4 };
43
+ }
44
+
45
+ function setFrontmatterField(text, field, value) {
46
+ if (!text.startsWith("---\n")) return text;
47
+ const end = text.indexOf("\n---", 4);
48
+ if (end === -1) return text;
49
+ const fmText = text.slice(4, end);
50
+ const lines = fmText.split("\n");
51
+ let found = false;
52
+ const out = lines.map((line) => {
53
+ if (line.match(new RegExp(`^${field}:`))) {
54
+ found = true;
55
+ return `${field}: ${value}`;
56
+ }
57
+ return line;
58
+ });
59
+ if (!found) out.push(`${field}: ${value}`);
60
+ return `---\n${out.join("\n")}\n---${text.slice(end + 4)}`;
61
+ }
62
+
63
+ function listAllSessions(root) {
64
+ const agentsDir = path.join(root, ".apc", "agents");
65
+ if (!fs.existsSync(agentsDir)) return [];
66
+ const out = [];
67
+ for (const slug of fs.readdirSync(agentsDir)) {
68
+ const dir = path.join(agentsDir, slug, "sessions");
69
+ if (!fs.existsSync(dir)) continue;
70
+ for (const f of fs.readdirSync(dir)) {
71
+ if (!f.endsWith(".md")) continue;
72
+ const filepath = path.join(dir, f);
73
+ const body = fs.readFileSync(filepath, "utf8");
74
+ const { fm } = parseFrontmatter(body);
75
+ out.push({
76
+ agent: slug,
77
+ filename: f,
78
+ path: filepath,
79
+ id: fm.id || f.replace(/\.md$/, ""),
80
+ title: fm.title || "(no title)",
81
+ status: fm.status || "",
82
+ started: fm.started || "",
83
+ completed: fm.completed || "",
84
+ result: fm.result || "",
85
+ task_ref: fm.task_ref || "",
86
+ });
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+
92
+ function findSessionById(root, id) {
93
+ for (const s of listAllSessions(root)) {
94
+ if (s.id === id || s.filename.replace(/\.md$/, "") === id) return s;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ function statusEmoji(status) {
100
+ if (/Completada|complete/i.test(status)) return "✅";
101
+ if (/En progreso|in.progress/i.test(status)) return "🔄";
102
+ if (/stale|Cerrada/i.test(status)) return "⚠️";
103
+ return "❓";
104
+ }
105
+
106
+ function hoursSince(iso) {
107
+ if (!iso) return Infinity;
108
+ const t = Date.parse(iso);
109
+ if (isNaN(t)) return Infinity;
110
+ return (Date.now() - t) / 3600_000;
111
+ }
112
+
113
+ // ---------------------------------------------------------------------
114
+
115
+ export function cmdSessionNew(args) {
116
+ const slug = args._[0];
117
+ if (!slug) throw new Error("apx session new: missing <agent-slug>");
118
+ const title = args.flags.title === true ? null : args.flags.title;
119
+ if (!title) throw new Error("apx session new: --title required");
120
+
121
+ const root = requireRoot();
122
+ const agents = readAgents(root);
123
+ if (!agents.find((a) => a.slug === slug)) {
124
+ throw new Error(`agent "${slug}" not found in AGENTS.md`);
125
+ }
126
+
127
+ ensureAgentDir(root, slug);
128
+ const id = generateSessionId(root, slug);
129
+ const filename = `${id}.md`;
130
+ const filepath = path.join(root, ".apc", "agents", slug, "sessions", filename);
131
+
132
+ const taskRef = args.flags["task-ref"] === true ? "" : (args.flags["task-ref"] || "");
133
+ let body = "";
134
+ if (args.flags.body === "-") body = readStdinSync();
135
+ else if (args.flags.body && args.flags.body !== true) body = String(args.flags.body);
136
+
137
+ const started = nowIso();
138
+ const content =
139
+ `---\n` +
140
+ `id: ${id}\n` +
141
+ `agent: ${slug}\n` +
142
+ `title: ${title}\n` +
143
+ `description: \n` +
144
+ `task_ref: ${taskRef}\n` +
145
+ `status: open\n` +
146
+ `date: ${started.slice(0, 10)}\n` +
147
+ `started: ${started}\n` +
148
+ `completed: \n` +
149
+ `---\n\n` +
150
+ `# ${title}\n\n${body}\n`;
151
+
152
+ fs.writeFileSync(filepath, content);
153
+ console.log(`✅ Session created: ${id}`);
154
+ console.log(` Agent: ${slug}`);
155
+ console.log(` Title: ${title}`);
156
+ console.log(` File: ${path.relative(process.cwd(), filepath)}`);
157
+ }
158
+
159
+ export function cmdSessionList(args) {
160
+ const root = requireRoot();
161
+ const slug = args._[0];
162
+ const limit = args.flags.last ? parseInt(args.flags.last, 10) : null;
163
+
164
+ let sessions = listAllSessions(root);
165
+ if (slug) sessions = sessions.filter((s) => s.agent === slug);
166
+ sessions.sort((a, b) => b.filename.localeCompare(a.filename));
167
+ if (limit) sessions = sessions.slice(0, limit);
168
+
169
+ if (sessions.length === 0) {
170
+ console.log("(no sessions)");
171
+ return;
172
+ }
173
+
174
+ console.log(
175
+ `${"ID".padEnd(16)} ${"S".padEnd(2)} ${"AGENT".padEnd(12)} TITLE`
176
+ );
177
+ console.log(
178
+ `${"─".repeat(14).padEnd(16)} ${"─".repeat(1).padEnd(2)} ${"─".repeat(10).padEnd(12)} ${"─".repeat(40)}`
179
+ );
180
+ for (const s of sessions) {
181
+ console.log(
182
+ `${s.id.padEnd(16)} ${statusEmoji(s.status).padEnd(2)} ${s.agent.padEnd(12)} ${s.title.slice(0, 60)}`
183
+ );
184
+ }
185
+ }
186
+
187
+ export function cmdSessionGet(args) {
188
+ const id = args._[0];
189
+ if (!id) throw new Error("apx session get: missing <id>");
190
+ const root = requireRoot();
191
+ const s = findSessionById(root, id);
192
+ if (!s) throw new Error(`session "${id}" not found`);
193
+ if (args.flags.body) {
194
+ process.stdout.write(fs.readFileSync(s.path, "utf8"));
195
+ return;
196
+ }
197
+ console.log(`id: ${s.id}`);
198
+ console.log(`agent: ${s.agent}`);
199
+ console.log(`title: ${s.title}`);
200
+ console.log(`status: ${s.status}`);
201
+ console.log(`started: ${s.started}`);
202
+ console.log(`completed: ${s.completed}`);
203
+ console.log(`task_ref: ${s.task_ref}`);
204
+ console.log(`result: ${s.result}`);
205
+ console.log(`file: ${path.relative(process.cwd(), s.path)}`);
206
+ }
207
+
208
+ export function cmdSessionUpdate(args) {
209
+ const id = args._[0];
210
+ if (!id) throw new Error("apx session update: missing <id>");
211
+ const root = requireRoot();
212
+ const s = findSessionById(root, id);
213
+ if (!s) throw new Error(`session "${id}" not found`);
214
+
215
+ let text = fs.readFileSync(s.path, "utf8");
216
+ const fields = ["status", "result", "title", "task_ref", "completed"];
217
+ let touched = [];
218
+ for (const k of fields) {
219
+ if (args.flags[k] !== undefined && args.flags[k] !== true) {
220
+ text = setFrontmatterField(text, k, args.flags[k]);
221
+ touched.push(k);
222
+ }
223
+ }
224
+ if (touched.length === 0) throw new Error("apx session update: no fields provided");
225
+ fs.writeFileSync(s.path, text);
226
+ console.log(`updated ${s.id}: ${touched.join(", ")}`);
227
+ }
228
+
229
+ export function cmdSessionClose(args) {
230
+ const id = args._[0];
231
+ if (!id) throw new Error("apx session close: missing <id>");
232
+ const root = requireRoot();
233
+ const s = findSessionById(root, id);
234
+ if (!s) throw new Error(`session "${id}" not found`);
235
+
236
+ let text = fs.readFileSync(s.path, "utf8");
237
+ text = setFrontmatterField(text, "status", "✅ Completada");
238
+ text = setFrontmatterField(text, "completed", nowIso());
239
+ if (args.flags.result && args.flags.result !== true) {
240
+ text = setFrontmatterField(text, "result", String(args.flags.result));
241
+ }
242
+ fs.writeFileSync(s.path, text);
243
+ console.log(`✅ Session ${s.id} closed`);
244
+ }
245
+
246
+ export function cmdSessionCheck() {
247
+ const root = requireRoot();
248
+ const sessions = listAllSessions(root).filter((s) =>
249
+ /En progreso/i.test(s.status)
250
+ );
251
+
252
+ if (sessions.length === 0) {
253
+ console.log("✅ No active sessions. Safe to proceed.");
254
+ process.exit(0);
255
+ }
256
+
257
+ let active = 0;
258
+ let stale = [];
259
+ for (const s of sessions) {
260
+ const h = hoursSince(s.started);
261
+ if (h >= STALE_HOURS) {
262
+ stale.push({ ...s, hours: h });
263
+ } else {
264
+ console.log(`🔄 ACTIVE: ${s.id} (${s.hours = h.toFixed(1)}h ago)`);
265
+ console.log(` Agent: ${s.agent}`);
266
+ console.log(` Title: ${s.title}`);
267
+ active++;
268
+ }
269
+ }
270
+ if (stale.length) {
271
+ console.log(`\nStale sessions (>${STALE_HOURS}h, can be auto-closed):`);
272
+ for (const s of stale) {
273
+ console.log(`⚠️ ${s.id} (${s.hours.toFixed(1)}h) — ${s.agent} — ${s.title}`);
274
+ }
275
+ console.log(` → Run: apx session close-stale`);
276
+ }
277
+ if (active === 0) {
278
+ console.log("✅ Safe to proceed (only stale sessions).");
279
+ process.exit(0);
280
+ }
281
+ console.log("\n⛔ Another agent is working. Wait or coordinate.");
282
+ process.exit(1);
283
+ }
284
+
285
+ export async function cmdSessionResume(args) {
286
+ const id = args._[0];
287
+ if (!id) throw new Error("apx session resume: missing <session-id>");
288
+ const pid = await resolveProjectId(args?.flags?.project);
289
+ const summarize = args.flags.summary || args.flags.summarize ? "true" : "false";
290
+ const result = await http.get(
291
+ `/projects/${pid}/sessions/${id}/resume?summarize=${summarize}`
292
+ );
293
+
294
+ console.log(`# session ${id} (agent: ${result.agent})`);
295
+ console.log(`path: ${result.session_path}`);
296
+ console.log("");
297
+ console.log("## frontmatter");
298
+ for (const [k, v] of Object.entries(result.frontmatter || {})) {
299
+ if (v) console.log(`${k}: ${v}`);
300
+ }
301
+ console.log("");
302
+ if (result.external_transcript) {
303
+ const t = result.external_transcript;
304
+ console.log(`## external transcript`);
305
+ console.log(`path: ${t.path}`);
306
+ console.log(`size: ${t.size} bytes`);
307
+ if (args.flags.full) {
308
+ console.log("");
309
+ console.log("--- tail ---");
310
+ process.stdout.write(t.tail);
311
+ } else {
312
+ console.log(`(use --full to print the last ${t.tail.length} chars)`);
313
+ }
314
+ } else if (result.frontmatter?.external_session_path) {
315
+ console.log(`## external transcript: ${result.frontmatter.external_session_path}`);
316
+ console.log("(file no longer exists on disk)");
317
+ } else {
318
+ console.log("## external transcript: (none — runtime didn't report one)");
319
+ }
320
+ if (result.summary) {
321
+ console.log("");
322
+ console.log("## summary (super-agent)");
323
+ console.log(result.summary);
324
+ } else if (summarize === "true") {
325
+ console.log("");
326
+ console.log("## summary: (failed — super-agent not configured?)");
327
+ }
328
+ }
329
+
330
+ export function cmdSessionCloseStale() {
331
+ const root = requireRoot();
332
+ const sessions = listAllSessions(root).filter((s) =>
333
+ /En progreso/i.test(s.status)
334
+ );
335
+ let closed = 0;
336
+ for (const s of sessions) {
337
+ const h = hoursSince(s.started);
338
+ if (h < STALE_HOURS) continue;
339
+ let text = fs.readFileSync(s.path, "utf8");
340
+ text = setFrontmatterField(
341
+ text,
342
+ "status",
343
+ `⚠️ Cerrada automáticamente (stale >${STALE_HOURS}h)`
344
+ );
345
+ text = setFrontmatterField(text, "completed", nowIso());
346
+ text = setFrontmatterField(
347
+ text,
348
+ "result",
349
+ `Auto-closed by apx (stale >${h.toFixed(1)}h without completion)`
350
+ );
351
+ fs.writeFileSync(s.path, text);
352
+ console.log(`⚠️ Closed stale: ${s.id} (${h.toFixed(1)}h old)`);
353
+ closed++;
354
+ }
355
+ if (closed === 0) console.log("No stale sessions found.");
356
+ else console.log(`Closed ${closed} stale session(s).`);
357
+ }
358
+
359
+ export async function cmdSessionCompact(args) {
360
+ const slug = args._[0];
361
+ if (!slug) throw new Error("apx session compact: missing <agent-slug>");
362
+ const convId = args.flags.conversation === true ? null : (args.flags.conversation || null);
363
+ const model = args.flags.model === true ? null : (args.flags.model || null);
364
+
365
+ const pid = await resolveProjectId(args?.flags?.project);
366
+
367
+ const url = convId
368
+ ? `/projects/${pid}/agents/${slug}/conversations/${convId}/compact`
369
+ : `/projects/${pid}/agents/${slug}/compact`;
370
+
371
+ const body = model ? { model } : {};
372
+
373
+ console.log(`Compacting conversation for ${slug}${convId ? ` (${convId})` : " (latest)"}...`);
374
+ const result = await http.post(url, body);
375
+
376
+ console.log(`✅ Compacted ${result.compacted_turns} turns → ${result.filename}`);
377
+ console.log(` Kept last ${result.kept_turns} turns verbatim`);
378
+ console.log(` Model: ${result.model}`);
379
+ if (result.usage) {
380
+ const u = result.usage;
381
+ console.log(` Tokens: ${u.input_tokens ?? u.prompt_tokens ?? "?"} in / ${u.output_tokens ?? u.completion_tokens ?? "?"} out`);
382
+ }
383
+ console.log("");
384
+ console.log("Summary:");
385
+ console.log("─".repeat(60));
386
+ console.log(result.summary);
387
+ }
@@ -0,0 +1,153 @@
1
+ // apx skills — install the APX skill into AI IDE rule files.
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import readline from "node:readline";
6
+ import { findApfRoot } from "../../core/parser.js";
7
+ import { IDE_TARGETS, installIdeSkills, installGlobalSkills } from "../../core/scaffold.js";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Prompt helper
11
+ // ---------------------------------------------------------------------------
12
+
13
+ function ask(question) {
14
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
15
+ return new Promise((resolve) => {
16
+ rl.question(question, (answer) => {
17
+ rl.close();
18
+ resolve(answer.trim().toLowerCase());
19
+ });
20
+ });
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // apx skills add [<target>...] [--global] [--project]
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export async function cmdSkillsAdd(args) {
28
+ const forceGlobal = !!args.flags.global;
29
+ const forceProject = !!args.flags.project;
30
+ const hasTargets = args._.length > 0;
31
+
32
+ // If neither flag given and no specific targets, ask the user.
33
+ let scope;
34
+ if (forceGlobal) {
35
+ scope = "global";
36
+ } else if (forceProject || hasTargets) {
37
+ scope = "project";
38
+ } else {
39
+ console.log("Where do you want to install the APX skill?\n");
40
+ console.log(" [g] Global — ~/.claude/skills/, ~/.cursor/skills/, ~/.agents/skills/");
41
+ console.log(" Works across all your projects (recommended for first install).");
42
+ console.log(" [p] Project — .claude/skills/apx/, .cursor/rules/apx.mdc, .windsurf/rules/apx.md, etc.");
43
+ console.log(" Scoped to this project only.\n");
44
+ const answer = await ask("Choice [g/p] (default: g): ");
45
+ scope = answer === "p" || answer === "project" ? "project" : "global";
46
+ }
47
+
48
+ if (scope === "global") {
49
+ const results = installGlobalSkills();
50
+ const home = os.homedir();
51
+ console.log("");
52
+ for (const r of results) {
53
+ const short = r.file.replace(home, "~");
54
+ console.log(` ${r.status.padEnd(10)} ${short}`);
55
+ }
56
+ console.log("\n Loaded by: Claude Code, Cursor, Codex (OpenAI), Antigravity, and skills.sh-compatible tools.");
57
+ console.log(" Activates automatically when working in a project with AGENTS.md or .apc/");
58
+ return;
59
+ }
60
+
61
+ // Project scope
62
+ const root = findApfRoot();
63
+ if (!root) throw new Error("not inside an APC project (run `apx init` first)");
64
+
65
+ const requested = hasTargets ? args._.map((s) => s.toLowerCase()) : null;
66
+ if (requested) {
67
+ const unknown = requested.filter((id) => !IDE_TARGETS.some((t) => t.id === id));
68
+ if (unknown.length) {
69
+ throw new Error(
70
+ `unknown target(s): ${unknown.join(", ")}\nAvailable: ${IDE_TARGETS.map((t) => t.id).join(", ")}`
71
+ );
72
+ }
73
+ }
74
+
75
+ const results = installIdeSkills(root, requested);
76
+ console.log("");
77
+ const width = Math.max(...results.map((r) => r.label.length));
78
+ for (const r of results) {
79
+ console.log(` ${r.label.padEnd(width)} ${r.status.padEnd(16)} ${r.file}`);
80
+ }
81
+ const notes = IDE_TARGETS.filter((t) => t.note && (!requested || requested.includes(t.id)));
82
+ if (notes.length) {
83
+ console.log("");
84
+ for (const t of notes) console.log(` note: ${t.note}`);
85
+ }
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // apx skills list
90
+ // ---------------------------------------------------------------------------
91
+
92
+ export async function cmdSkillsList() {
93
+ const root = findApfRoot();
94
+ const skillsDir = root ? path.join(root, ".apc", "skills") : null;
95
+ const files = skillsDir && fs.existsSync(skillsDir)
96
+ ? fs.readdirSync(skillsDir).filter((f) => f.endsWith(".md"))
97
+ : [];
98
+
99
+ if (files.length === 0) {
100
+ console.log("(no skills installed in .apc/skills/)");
101
+ return;
102
+ }
103
+ for (const f of files) console.log(f.replace(/\.md$/, ""));
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // apx skills status
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export async function cmdSkillsStatus() {
111
+ const root = findApfRoot();
112
+
113
+ // Global
114
+ const SKILL_SLUGS = ["apx", "apc-context"];
115
+ console.log("Global skills:");
116
+ const GLOBAL_DIRS = [
117
+ { label: "Claude Code / Cursor compat", dir: path.join(os.homedir(), ".claude", "skills") },
118
+ { label: "Cursor (primary)", dir: path.join(os.homedir(), ".cursor", "skills") },
119
+ { label: "Codex", dir: path.join(os.homedir(), ".codex", "skills") },
120
+ { label: "Antigravity / others", dir: path.join(os.homedir(), ".agents", "skills") },
121
+ ];
122
+ const gw = Math.max(...GLOBAL_DIRS.map((d) => d.label.length));
123
+ const sw = Math.max(...SKILL_SLUGS.map((s) => s.length));
124
+ for (const { label, dir } of GLOBAL_DIRS) {
125
+ for (const slug of SKILL_SLUGS) {
126
+ const dest = path.join(dir, slug, "SKILL.md");
127
+ console.log(` ${label.padEnd(gw)} ${slug.padEnd(sw)} ${fs.existsSync(dest) ? "installed" : "not installed"}`);
128
+ }
129
+ }
130
+
131
+ // Project-scoped
132
+ if (root) {
133
+ console.log("\nProject skills (this project only):");
134
+ const width = Math.max(...IDE_TARGETS.map((t) => t.label.length));
135
+ for (const t of IDE_TARGETS) {
136
+ const dest = path.join(root, t.file);
137
+ let status;
138
+ if (!fs.existsSync(dest)) {
139
+ status = "not installed";
140
+ } else if (t.guard) {
141
+ const txt = fs.readFileSync(dest, "utf8");
142
+ status = txt.includes(t.guard) ? "installed" : "file exists (no apx block)";
143
+ } else {
144
+ status = "installed";
145
+ }
146
+ console.log(` ${t.label.padEnd(width)} ${status}`);
147
+ }
148
+ console.log(` ${"Codex / Antigravity".padEnd(width)} reads AGENTS.md (automatic)`);
149
+ }
150
+
151
+ console.log("\n Tip: run `apx skills add` for an interactive install.");
152
+ console.log(" Claude Desktop has no project-file support (use apx-mcp instead).");
153
+ }
@@ -0,0 +1,48 @@
1
+ import { http } from "../http.js";
2
+
3
+ export async function cmdTelegramSend(args) {
4
+ const text = args._[0];
5
+ if (!text) throw new Error("apx telegram send: missing <text>");
6
+ const chat_id = args.flags.chat === true ? undefined : args.flags.chat;
7
+ const result = await http.post("/telegram/send", { chat_id, text });
8
+ console.log(`✅ sent (message_id=${result.message_id})`);
9
+ }
10
+
11
+ export async function cmdTelegramStatus() {
12
+ const s = await http.get("/telegram/status");
13
+ console.log(`enabled: ${s.enabled}`);
14
+ if (!s.channels || s.channels.length === 0) {
15
+ console.log("(no channels configured)");
16
+ return;
17
+ }
18
+ for (const c of s.channels) {
19
+ console.log("");
20
+ console.log(`channel: ${c.name}`);
21
+ console.log(` polling: ${c.polling}`);
22
+ console.log(` bot_token: ${c.bot_token_present ? "✓" : "✗"} (source: ${c.bot_token_source || "—"})`);
23
+ console.log(` chat_id: ${c.chat_id || "(unset)"}`);
24
+ console.log(` project: ${c.project || "(first registered)"}`);
25
+ console.log(` route_to_agent: ${c.route_to_agent || "(none → super-agent fallback)"}`);
26
+ console.log(` respond_w/engine: ${c.respond_with_engine}`);
27
+ console.log(` offset: ${c.offset}`);
28
+ console.log(` last_update_at: ${c.last_update_at || "(never)"}`);
29
+ console.log(` last_error: ${c.last_error || "(none)"}`);
30
+ }
31
+ }
32
+
33
+ export function cmdTelegramSetup() {
34
+ console.log(`Edit ~/.apx/config.json — telegram section:
35
+
36
+ "telegram": {
37
+ "enabled": true,
38
+ "bot_token": "<your bot token from @BotFather>",
39
+ "chat_id": "<numeric chat id where outbound goes>",
40
+ "poll_interval_ms": 1500,
41
+ "route_to_agent": "<slug>", // optional: who auto-replies
42
+ "respond_with_engine": true // false → only log inbound
43
+ }
44
+
45
+ Then restart the daemon (apx daemon stop && any apx command will auto-start it).
46
+ You can verify with: apx telegram status
47
+ `);
48
+ }
@@ -0,0 +1,102 @@
1
+ // Tiny HTTP client to talk to the APX daemon. Auto-starts the daemon if down.
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawn } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ const DEFAULT_PORT = parseInt(process.env.APX_PORT || "7430", 10);
12
+ const DEFAULT_HOST = process.env.APX_HOST || "127.0.0.1";
13
+
14
+ function baseUrl() {
15
+ return `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;
16
+ }
17
+
18
+ async function ping(timeoutMs = 400) {
19
+ try {
20
+ const ctrl = new AbortController();
21
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
22
+ const res = await fetch(`${baseUrl()}/health`, { signal: ctrl.signal });
23
+ clearTimeout(t);
24
+ return res.ok;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ function findDaemonEntry() {
31
+ const candidates = [
32
+ path.resolve(__dirname, "..", "daemon", "index.js"),
33
+ path.resolve(__dirname, "..", "node_modules", "apx-daemon", "src", "index.js"),
34
+ ];
35
+ for (const c of candidates) if (fs.existsSync(c)) return c;
36
+ return null;
37
+ }
38
+
39
+ async function autoStart({ silent = false } = {}) {
40
+ const entry = findDaemonEntry();
41
+ if (!entry) {
42
+ throw new Error(
43
+ "apx daemon not installed and not found at ../daemon/src/index.js. Install with `npm i -g apx-daemon` or run from the apc monorepo."
44
+ );
45
+ }
46
+ const logPath = path.join(os.homedir(), ".apx", "daemon.log");
47
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
48
+ const out = fs.openSync(logPath, "a");
49
+ const child = spawn(process.execPath, [entry], {
50
+ detached: true,
51
+ stdio: ["ignore", out, out],
52
+ env: { ...process.env },
53
+ });
54
+ child.unref();
55
+ if (!silent) process.stderr.write("apx: starting daemon...\n");
56
+ // Wait up to 4s for /health
57
+ for (let i = 0; i < 20; i++) {
58
+ await new Promise((r) => setTimeout(r, 200));
59
+ if (await ping(200)) return true;
60
+ }
61
+ throw new Error("apx daemon failed to start within 4s — check ~/.apx/daemon.log");
62
+ }
63
+
64
+ export async function ensureDaemon(opts = {}) {
65
+ if (await ping()) return;
66
+ await autoStart(opts);
67
+ }
68
+
69
+ async function request(method, path, body, opts = {}) {
70
+ if (opts.autoStart !== false) await ensureDaemon();
71
+ else if (!(await ping())) {
72
+ throw new Error(`apx daemon not running (no response on ${baseUrl()})`);
73
+ }
74
+ const res = await fetch(`${baseUrl()}${path}`, {
75
+ method,
76
+ headers: body ? { "content-type": "application/json" } : {},
77
+ body: body ? JSON.stringify(body) : undefined,
78
+ });
79
+ const text = await res.text();
80
+ let json;
81
+ try {
82
+ json = text ? JSON.parse(text) : null;
83
+ } catch {
84
+ if (!res.ok) throw new Error(`${method} ${path} → ${res.status}: ${text}`);
85
+ return text;
86
+ }
87
+ if (!res.ok) {
88
+ const msg = json?.error || `${method} ${path} → ${res.status}`;
89
+ throw new Error(msg);
90
+ }
91
+ return json;
92
+ }
93
+
94
+ export const http = {
95
+ get: (p, opts) => request("GET", p, undefined, opts),
96
+ post: (p, body, opts) => request("POST", p, body, opts),
97
+ put: (p, body, opts) => request("PUT", p, body, opts),
98
+ patch: (p, body, opts) => request("PATCH", p, body, opts),
99
+ delete: (p, opts) => request("DELETE", p, undefined, opts),
100
+ baseUrl,
101
+ ping,
102
+ };