@agentprojectcontext/apx 1.0.3 → 1.2.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.
package/README.md CHANGED
@@ -16,7 +16,7 @@ APX is a daemon + CLI that brings the APC convention to life:
16
16
  - **Plugins** — Telegram bot integration out of the box
17
17
  - **MCP support** — each agent can expose or consume MCP servers
18
18
 
19
- APX is opinionated about storage: the filesystem is the source of truth. No database required to read agent state. Project definitions live in the repo; runtime state (memory, sessions) lives in `~/.apx/` and is never committed.
19
+ APX is opinionated about storage: the filesystem is the source of truth. Project definitions and curated memory live in the repo. Runtime state such as sessions, conversations, messages, and caches lives in `~/.apx/` and is never committed.
20
20
 
21
21
  ## Quick start
22
22
 
@@ -65,13 +65,12 @@ Runtime state — local machine only, never committed:
65
65
  ```text
66
66
  ~/.apx/projects/<project-id>/
67
67
  ├── project.db ← regenerable SQLite cache
68
+ ├── messages/ ← local message history
68
69
  └── agents/
69
70
  ├── <slug>/
70
- │ ├── memory.md ← durable memory, updated by the agent
71
71
  │ ├── sessions/ ← one .md per runtime invocation
72
72
  │ └── conversations/ ← LLM conversation threads
73
73
  └── default/ ← fallback when no agent role is active
74
- ├── memory.md
75
74
  └── sessions/
76
75
  ```
77
76
 
@@ -94,7 +93,8 @@ apx messages tail --channel runtime # only agent invocations
94
93
 
95
94
  ## Message channels
96
95
 
97
- All activity is logged to `.apc/messages/YYYY-MM-DD.jsonl`:
96
+ Activity belongs to APX runtime state, not `.apc/`. Message storage is local to APX, under
97
+ `~/.apx/`:
98
98
 
99
99
  | Channel | What it captures |
100
100
  |---------|-----------------|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -33,7 +33,10 @@
33
33
  "node-fetch": "^3.3.2"
34
34
  },
35
35
  "devDependencies": {
36
- "better-sqlite3": "^11.3.0"
36
+ "@semantic-release/changelog": "^6.0.3",
37
+ "@semantic-release/git": "^10.0.1",
38
+ "better-sqlite3": "^11.3.0",
39
+ "conventional-changelog-conventionalcommits": "^9.3.1"
37
40
  },
38
41
  "keywords": [
39
42
  "apc",
@@ -4,11 +4,13 @@ description: "APX CLI skill. Activate ONLY when the user asks about running agen
4
4
  homepage: https://github.com/agentprojectcontext/apx
5
5
  ---
6
6
 
7
- # APX — Agent Project Framework
7
+ # APX — Agent Project Context Runtime
8
8
 
9
9
  This project uses **APX**. The daemon runs on `127.0.0.1:7430` and auto-starts on first `apx` call.
10
10
  Your current session, project, and agent are already injected above this block — refer to them.
11
11
 
12
+ APX runtime state belongs outside `.apc/`, under `~/.apx/projects/<project-id>/`.
13
+
12
14
  ---
13
15
 
14
16
  ## Discover the project
@@ -32,7 +34,7 @@ apx exec <slug> "<prompt>"
32
34
  The output of `apx run` / `apx exec` is the agent's full stdout.
33
35
  If the agent printed `APC_RESULT: <value>`, that value is also captured as structured output.
34
36
 
35
- ## Memory — durable, persists between sessions
37
+ ## Memory — durable, safe facts
36
38
 
37
39
  ```bash
38
40
  apx memory <slug> # read agent's memory.md
@@ -40,7 +42,7 @@ apx memory <slug> --append "<fact>" # append a durable note (non-destructive
40
42
  apx memory <slug> --replace < file.md # replace entire memory from stdin
41
43
  ```
42
44
 
43
- Write to memory when you discover something the agent should know on every future run.
45
+ Write to memory only when you discover safe project context the agent should know on future runs.
44
46
 
45
47
  ## Observe activity
46
48
 
@@ -74,4 +76,5 @@ Print this on the last meaningful line of your output:
74
76
  APC_RESULT: <one-line summary or value>
75
77
  ```
76
78
  The invoker (`apx run`, super-agent, Telegram bot) captures it as structured output.
77
- Keep it factual and short it becomes the session result stored in `.apc/agents/<slug>/sessions/`.
79
+ Keep it factual and short. It becomes the session result stored in APX local runtime state, not
80
+ inside `.apc/`.
@@ -109,11 +109,8 @@ export async function resolveProjectId(target) {
109
109
  // No override: walk up from cwd
110
110
  const root = findApfRoot();
111
111
  if (!root) {
112
- const registered = await http.get("/projects").catch(() => []);
113
- const hint = registered.length
114
- ? `registered projects: ${registered.map((p) => `"${p.name}" (id ${p.id})`).join(", ")}`
115
- : "no projects registered yet — run `apx project add <path>` first";
116
- throw new Error(`not inside an APC project. Use --project <name|id|path>. ${hint}`);
112
+ // Fall back to the default project (id=0) always available, no .apc/ required.
113
+ return 0;
117
114
  }
118
115
  const projects = await http.get("/projects");
119
116
  const found = projects.find((p) => p.path === root);
@@ -1,8 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { findApfRoot, readAgents } from "../../core/parser.js";
4
- import { ensureAgentDir } from "../../core/scaffold.js";
4
+ import { getOrCreateApxId } from "../../core/scaffold.js";
5
5
  import { generateSessionId } from "../../core/session-store.js";
6
+ import { ensureProjectStorage } from "../../core/config.js";
6
7
  import { http } from "../http.js";
7
8
  import { resolveProjectId } from "./project.js";
8
9
 
@@ -14,6 +15,12 @@ function requireRoot() {
14
15
  return root;
15
16
  }
16
17
 
18
+ function requireStorageRoot(root) {
19
+ const apxId = getOrCreateApxId(root);
20
+ if (!apxId) throw new Error("could not resolve APX project storage id");
21
+ return ensureProjectStorage(apxId);
22
+ }
23
+
17
24
  const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
18
25
 
19
26
  function readStdinSync() {
@@ -61,7 +68,7 @@ function setFrontmatterField(text, field, value) {
61
68
  }
62
69
 
63
70
  function listAllSessions(root) {
64
- const agentsDir = path.join(root, ".apc", "agents");
71
+ const agentsDir = path.join(requireStorageRoot(root), "agents");
65
72
  if (!fs.existsSync(agentsDir)) return [];
66
73
  const out = [];
67
74
  for (const slug of fs.readdirSync(agentsDir)) {
@@ -124,10 +131,11 @@ export function cmdSessionNew(args) {
124
131
  throw new Error(`agent "${slug}" not found in AGENTS.md`);
125
132
  }
126
133
 
127
- ensureAgentDir(root, slug);
128
- const id = generateSessionId(root, slug);
134
+ const storageRoot = requireStorageRoot(root);
135
+ const id = generateSessionId(storageRoot, slug);
129
136
  const filename = `${id}.md`;
130
- const filepath = path.join(root, ".apc", "agents", slug, "sessions", filename);
137
+ const filepath = path.join(storageRoot, "agents", slug, "sessions", filename);
138
+ fs.mkdirSync(path.dirname(filepath), { recursive: true });
131
139
 
132
140
  const taskRef = args.flags["task-ref"] === true ? "" : (args.flags["task-ref"] || "");
133
141
  let body = "";
@@ -0,0 +1,336 @@
1
+ // apx setup — interactive first-run wizard.
2
+ // Guides the user through provider, model, channels, and language.
3
+ // Starts the daemon and sends a fun wake-up message when done.
4
+
5
+ import fs from "node:fs";
6
+ import https from "node:https";
7
+ import http from "node:http";
8
+ import readline from "node:readline";
9
+ import { spawnSync, spawn } from "node:child_process";
10
+ import { readConfig, writeConfig, APX_HOME } from "../../core/config.js";
11
+
12
+ // ── ANSI helpers ──────────────────────────────────────────────────────────────
13
+ const c = {
14
+ reset: "\x1b[0m",
15
+ bold: "\x1b[1m",
16
+ dim: "\x1b[2m",
17
+ cyan: "\x1b[36m",
18
+ green: "\x1b[32m",
19
+ yellow:"\x1b[33m",
20
+ red: "\x1b[31m",
21
+ gray: "\x1b[90m",
22
+ };
23
+ const b = (s) => `${c.bold}${s}${c.reset}`;
24
+ const cy = (s) => `${c.cyan}${s}${c.reset}`;
25
+ const gr = (s) => `${c.green}${s}${c.reset}`;
26
+ const di = (s) => `${c.dim}${s}${c.reset}`;
27
+
28
+ // ── readline helpers ─────────────────────────────────────────────────────────
29
+ let rl;
30
+ function initRl() {
31
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
32
+ }
33
+ function ask(prompt) {
34
+ return new Promise((resolve) => rl.question(prompt, (a) => resolve(a.trim())));
35
+ }
36
+ function close() { rl.close(); }
37
+
38
+ // ── Fetch helpers ─────────────────────────────────────────────────────────────
39
+ function fetchJson(url, timeout = 4000) {
40
+ return new Promise((resolve) => {
41
+ const mod = url.startsWith("https") ? https : http;
42
+ const req = mod.get(url, { timeout }, (res) => {
43
+ let body = "";
44
+ res.on("data", (c) => (body += c));
45
+ res.on("end", () => { try { resolve(JSON.parse(body)); } catch { resolve(null); } });
46
+ });
47
+ req.on("error", () => resolve(null));
48
+ req.on("timeout", () => { req.destroy(); resolve(null); });
49
+ });
50
+ }
51
+
52
+ async function fetchOllamaModels(baseUrl) {
53
+ const data = await fetchJson(`${baseUrl.replace(/\/$/, "")}/api/tags`);
54
+ if (!data?.models) return [];
55
+ return data.models.map((m) => m.name).filter(Boolean);
56
+ }
57
+
58
+ // ── Provider definitions ──────────────────────────────────────────────────────
59
+ const PROVIDERS = [
60
+ {
61
+ id: "anthropic",
62
+ label: "Anthropic (Claude)",
63
+ needsKey: true,
64
+ keyLabel: "Anthropic API key",
65
+ keyHint: "sk-ant-...",
66
+ models: ["claude-sonnet-4-5", "claude-haiku-4-5", "claude-opus-4-5"],
67
+ },
68
+ {
69
+ id: "openai",
70
+ label: "OpenAI (GPT)",
71
+ needsKey: true,
72
+ keyLabel: "OpenAI API key",
73
+ keyHint: "sk-...",
74
+ models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"],
75
+ },
76
+ {
77
+ id: "ollama",
78
+ label: "Ollama (local / self-hosted)",
79
+ needsKey: false,
80
+ models: [], // fetched dynamically
81
+ },
82
+ {
83
+ id: "gemini",
84
+ label: "Gemini (Google)",
85
+ needsKey: true,
86
+ keyLabel: "Gemini API key",
87
+ keyHint: "AIza...",
88
+ models: ["gemini-2.0-flash", "gemini-1.5-pro"],
89
+ },
90
+ ];
91
+
92
+ // ── Main wizard ───────────────────────────────────────────────────────────────
93
+ export async function cmdSetup() {
94
+ initRl();
95
+
96
+ console.log();
97
+ console.log(b(cy(" ╔═══════════════════════════════════╗")));
98
+ console.log(b(cy(" ║ APX Setup Wizard ║")));
99
+ console.log(b(cy(" ╚═══════════════════════════════════╝")));
100
+ console.log();
101
+ console.log(di(" This will configure the APX daemon and super-agent."));
102
+ console.log(di(" You can re-run `apx setup` at any time to change settings."));
103
+ console.log();
104
+
105
+ // ── Super-agent? ────────────────────────────────────────────────────────────
106
+ const wantAgent = await ask(` Enable super-agent? ${di("[Y/n]")} `);
107
+ if (/^n/i.test(wantAgent)) {
108
+ console.log(`\n ${gr("✓")} Skipping super-agent. Run ${cy("apx daemon start")} to start the daemon.\n`);
109
+ close();
110
+ return;
111
+ }
112
+
113
+ // ── Provider ────────────────────────────────────────────────────────────────
114
+ console.log();
115
+ console.log(b(" AI Provider:"));
116
+ PROVIDERS.forEach((p, i) => console.log(` ${cy(String(i + 1))}. ${p.label}`));
117
+ console.log();
118
+ let providerIdx = -1;
119
+ while (providerIdx < 0) {
120
+ const ans = await ask(` Choose [1-${PROVIDERS.length}]: `);
121
+ const n = parseInt(ans, 10);
122
+ if (n >= 1 && n <= PROVIDERS.length) providerIdx = n - 1;
123
+ else console.log(` ${c.yellow}Please enter a number between 1 and ${PROVIDERS.length}.${c.reset}`);
124
+ }
125
+ const provider = PROVIDERS[providerIdx];
126
+ let apiKey = "";
127
+ let ollamaUrl = "http://localhost:11434";
128
+
129
+ if (provider.id === "ollama") {
130
+ const urlAns = await ask(` Ollama URL ${di("[http://localhost:11434]")}: `);
131
+ ollamaUrl = urlAns || "http://localhost:11434";
132
+ } else if (provider.needsKey) {
133
+ apiKey = await ask(` ${provider.keyLabel} ${di(`(${provider.keyHint})`)}: `);
134
+ }
135
+
136
+ // ── Model ───────────────────────────────────────────────────────────────────
137
+ console.log();
138
+ let models = [...provider.models];
139
+
140
+ if (provider.id === "ollama") {
141
+ process.stdout.write(` Fetching models from Ollama... `);
142
+ const fetched = await fetchOllamaModels(ollamaUrl);
143
+ if (fetched.length) {
144
+ models = fetched;
145
+ console.log(gr(`${fetched.length} found`));
146
+ } else {
147
+ console.log(di("(couldn't reach Ollama, enter manually)"));
148
+ }
149
+ }
150
+
151
+ let chosenModel = "";
152
+ if (models.length) {
153
+ console.log(b(" Model:"));
154
+ models.forEach((m, i) => console.log(` ${cy(String(i + 1))}. ${m}`));
155
+ console.log(` ${cy(String(models.length + 1))}. ${di("Enter manually")}`);
156
+ console.log();
157
+ let modelIdx = -1;
158
+ while (modelIdx < 0) {
159
+ const ans = await ask(` Choose [1-${models.length + 1}]: `);
160
+ const n = parseInt(ans, 10);
161
+ if (n >= 1 && n <= models.length) { modelIdx = n - 1; chosenModel = models[modelIdx]; }
162
+ else if (n === models.length + 1) {
163
+ chosenModel = await ask(" Model name: ");
164
+ modelIdx = 0;
165
+ } else {
166
+ console.log(` ${c.yellow}Invalid choice.${c.reset}`);
167
+ }
168
+ }
169
+ } else {
170
+ chosenModel = await ask(" Model name (e.g. qwen2.5:14b): ");
171
+ }
172
+ chosenModel = `${provider.id}:${chosenModel}`;
173
+
174
+ // ── Channels ────────────────────────────────────────────────────────────────
175
+ console.log();
176
+ console.log(b(" Channels:"));
177
+ console.log(` ${cy("1")}. Web (local API — always on)`);
178
+ console.log(` ${cy("2")}. Telegram`);
179
+ console.log();
180
+ const chAns = await ask(` Enable Telegram? ${di("[Y/n]")} `);
181
+ const wantTelegram = !/^n/i.test(chAns);
182
+
183
+ let botToken = "";
184
+ let chatId = "";
185
+
186
+ if (wantTelegram) {
187
+ console.log();
188
+ console.log(di(" Create a bot at https://t.me/BotFather → get the token."));
189
+ console.log(di(" Then message your bot and visit:"));
190
+ console.log(di(" https://api.telegram.org/bot<TOKEN>/getUpdates to find your chat_id."));
191
+ console.log();
192
+ botToken = await ask(" Bot token: ");
193
+ chatId = await ask(" Your chat ID: ");
194
+ }
195
+
196
+ // ── Language ────────────────────────────────────────────────────────────────
197
+ console.log();
198
+ console.log(b(" Language:"));
199
+ console.log(di(" The super-agent will always respond in your language."));
200
+ console.log();
201
+ const language = await ask(" Your language (e.g. English, Español, Português): ") || "English";
202
+
203
+ // ── Summary ─────────────────────────────────────────────────────────────────
204
+ console.log();
205
+ console.log(b(" ─── Summary ───────────────────────────────────────────"));
206
+ console.log(` Provider: ${cy(provider.label)}`);
207
+ console.log(` Model: ${cy(chosenModel)}`);
208
+ if (provider.id === "ollama") console.log(` Ollama URL: ${cy(ollamaUrl)}`);
209
+ console.log(` Telegram: ${wantTelegram ? gr("enabled") : di("disabled")}`);
210
+ console.log(` Language: ${cy(language)}`);
211
+ console.log(b(" ────────────────────────────────────────────────────────"));
212
+ console.log();
213
+
214
+ const confirm = await ask(` Start the daemon with these settings? ${di("[Y/n]")} `);
215
+ if (/^n/i.test(confirm)) {
216
+ console.log("\n Cancelled. Run `apx setup` again to configure.\n");
217
+ close();
218
+ return;
219
+ }
220
+
221
+ close(); // done with prompts
222
+
223
+ // ── Write config ─────────────────────────────────────────────────────────────
224
+ const cfg = readConfig();
225
+
226
+ cfg.super_agent.enabled = true;
227
+ cfg.super_agent.model = chosenModel;
228
+ // System prompt: language instruction only, no wizard references
229
+ cfg.super_agent.system = `Always respond in the user's language: ${language}.`;
230
+
231
+ if (provider.id === "ollama") {
232
+ cfg.engines.ollama.base_url = ollamaUrl;
233
+ } else if (provider.needsKey && apiKey) {
234
+ cfg.engines[provider.id].api_key = apiKey;
235
+ }
236
+
237
+ if (wantTelegram && botToken && chatId) {
238
+ cfg.telegram.enabled = true;
239
+ cfg.telegram.bot_token = botToken;
240
+ cfg.telegram.chat_id = chatId;
241
+ }
242
+
243
+ writeConfig(cfg);
244
+ console.log(`\n ${gr("✓")} Config saved to ${di("~/.apx/config.json")}`);
245
+
246
+ // ── Start daemon ─────────────────────────────────────────────────────────────
247
+ console.log();
248
+ process.stdout.write(` Starting daemon... `);
249
+
250
+ const start = spawnSync("apx", ["daemon", "start"], { encoding: "utf8" });
251
+ if (start.status !== 0) {
252
+ console.log(c.red + "failed" + c.reset);
253
+ console.log(start.stderr || start.stdout);
254
+ process.exit(1);
255
+ }
256
+ console.log(gr("running ✓"));
257
+
258
+ // Give daemon a moment to come up
259
+ await new Promise((r) => setTimeout(r, 2000));
260
+
261
+ // ── Wake-up Telegram message ─────────────────────────────────────────────────
262
+ if (wantTelegram && botToken && chatId) {
263
+ console.log();
264
+ process.stdout.write(` Sending wake-up message... `);
265
+ try {
266
+ const resp = await sendTelegramWakeup({ botToken, chatId, language, model: chosenModel });
267
+ if (resp) console.log(gr("sent ✓"));
268
+ else console.log(di("(couldn't reach Telegram)"));
269
+ } catch {
270
+ console.log(di("(couldn't reach Telegram)"));
271
+ }
272
+ }
273
+
274
+ console.log();
275
+ console.log(gr(b(" ✅ APX is ready!")));
276
+ console.log();
277
+ console.log(` Daemon: ${cy("http://127.0.0.1:7430")}`);
278
+ if (wantTelegram) console.log(` Telegram: ${cy("active — message your bot")}`);
279
+ console.log();
280
+ console.log(di(" Tip: run `apx daemon status` anytime to check health."));
281
+ console.log();
282
+ }
283
+
284
+ // Send a fun wake-up message via Telegram using super-agent.
285
+ // The prompt is in English so the model knows to reply in the user's language.
286
+ async function sendTelegramWakeup({ botToken, chatId, language, model }) {
287
+ const prompt =
288
+ `You are APX, an AI agent assistant that just came online. ` +
289
+ `Send a short, fun, enthusiastic wake-up message to the user. ` +
290
+ `Be playful and creative — like a friendly AI that just woke up. ` +
291
+ `Keep it under 3 sentences. ` +
292
+ `IMPORTANT: respond in ${language}. ` +
293
+ `Do not mention that you were configured or set up.`;
294
+
295
+ // Ask the daemon's super-agent (give it a second attempt window)
296
+ let text;
297
+ try {
298
+ const res = await fetchJson("http://127.0.0.1:7430/super-agent/ask", 8000);
299
+ text = res?.text;
300
+ } catch {}
301
+
302
+ // Fallback: generate a simple message without daemon
303
+ if (!text) {
304
+ text = languageFallback(language);
305
+ }
306
+
307
+ // Send via Telegram bot API
308
+ return new Promise((resolve) => {
309
+ const body = JSON.stringify({ chat_id: chatId, text, parse_mode: "Markdown" });
310
+ const req = https.request({
311
+ hostname: "api.telegram.org",
312
+ path: `/bot${botToken}/sendMessage`,
313
+ method: "POST",
314
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
315
+ timeout: 6000,
316
+ }, (res) => {
317
+ let d = "";
318
+ res.on("data", (c) => (d += c));
319
+ res.on("end", () => { try { resolve(JSON.parse(d)); } catch { resolve(null); } });
320
+ });
321
+ req.on("error", () => resolve(null));
322
+ req.write(body);
323
+ req.end();
324
+ });
325
+ }
326
+
327
+ // Minimal fallback messages per common language (used only if daemon can't respond)
328
+ function languageFallback(lang) {
329
+ const l = lang.toLowerCase();
330
+ if (/espa[ñn]|spanish|arg|lat/i.test(l)) return "⚡ ¡Despierto y listo para trabajar! APX online.";
331
+ if (/portugu|brasil/i.test(l)) return "⚡ Acordei e pronto para trabalhar! APX online.";
332
+ if (/franc|french/i.test(l)) return "⚡ Réveillé et prêt à travailler ! APX en ligne.";
333
+ if (/deutsch|german/i.test(l)) return "⚡ Aufgewacht und bereit! APX ist online.";
334
+ if (/ital/i.test(l)) return "⚡ Sveglio e pronto a lavorare! APX online.";
335
+ return "⚡ I'm awake and ready to go! APX is online.";
336
+ }
@@ -0,0 +1,75 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import readline from "node:readline";
3
+ import { getLatestVersion } from "../../core/update-check.js";
4
+
5
+ const PACKAGE_NAME = "@agentprojectcontext/apx";
6
+
7
+ export async function cmdUpdate(args) {
8
+ const force = args.flags.force || args.flags.yes || args.flags.y;
9
+
10
+ console.log("Checking for updates...");
11
+ const latest = await getLatestVersion();
12
+
13
+ if (!latest) {
14
+ console.error("Could not reach npm registry. Check your connection.");
15
+ process.exit(1);
16
+ }
17
+
18
+ // Read current version from the package that owns this file.
19
+ const { createRequire } = await import("node:module");
20
+ const { fileURLToPath } = await import("node:url");
21
+ const require = createRequire(import.meta.url);
22
+ const pkg = require("../../package.json");
23
+ const current = pkg.version;
24
+
25
+ function isNewer(cur, lat) {
26
+ const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
27
+ const [ma, mi, pa] = parse(cur);
28
+ const [mb, mib, pb] = parse(lat);
29
+ if (mb > ma) return true;
30
+ if (mb === ma && mib > mi) return true;
31
+ if (mb === ma && mib === mi && pb > pa) return true;
32
+ return false;
33
+ }
34
+
35
+ if (!isNewer(current, latest)) {
36
+ console.log(`✅ Already up to date (${current})`);
37
+ return;
38
+ }
39
+
40
+ console.log(`\n Current: ${current}`);
41
+ console.log(` Latest: ${latest}`);
42
+
43
+ if (!force) {
44
+ const confirmed = await confirm(`\nUpdate to ${latest}? [y/N] `);
45
+ if (!confirmed) {
46
+ console.log("Cancelled.");
47
+ return;
48
+ }
49
+ }
50
+
51
+ console.log(`\nRunning: npm install -g ${PACKAGE_NAME}@${latest}\n`);
52
+ const result = spawnSync(
53
+ "npm",
54
+ ["install", "-g", `${PACKAGE_NAME}@${latest}`],
55
+ { stdio: "inherit" }
56
+ );
57
+
58
+ if (result.status !== 0) {
59
+ console.error(`\n❌ Update failed (exit ${result.status})`);
60
+ process.exit(result.status || 1);
61
+ }
62
+
63
+ console.log(`\n✅ Updated to ${latest}. Restart any running apx daemon:`);
64
+ console.log(` apx daemon stop && apx daemon start`);
65
+ }
66
+
67
+ function confirm(prompt) {
68
+ return new Promise((resolve) => {
69
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
70
+ rl.question(prompt, (answer) => {
71
+ rl.close();
72
+ resolve(/^y(es)?$/i.test(answer.trim()));
73
+ });
74
+ });
75
+ }
package/src/cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // apx — unified CLI for APC (Agent Project Framework).
2
+ // apx — unified CLI for APC (Agent Project Context).
3
3
  // ESM, Node >= 18.
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
@@ -71,6 +71,9 @@ import { cmdPluginsList, cmdPluginStatus } from "./commands/plugins.js";
71
71
  import { cmdSkillsAdd, cmdSkillsList, cmdSkillsStatus } from "./commands/skills.js";
72
72
  import { cmdIdentity } from "./commands/identity.js";
73
73
  import { cmdCommandList, cmdCommandShow } from "./commands/command.js";
74
+ import { cmdUpdate } from "./commands/update.js";
75
+ import { cmdSetup } from "./commands/setup.js";
76
+ import { checkForUpdate } from "../core/update-check.js";
74
77
  import {
75
78
  cmdRoutineList,
76
79
  cmdRoutineGet,
@@ -87,17 +90,18 @@ const VERSION = JSON.parse(
87
90
  fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8")
88
91
  ).version;
89
92
 
90
- const HELP = `apx — Agent Project Framework
93
+ const HELP = `apx — Agent Project Context
91
94
 
92
95
  Usage:
93
96
  apx <command> [<subcommand>] [args] [--flags]
94
97
 
95
98
  Bootstrap:
96
99
  apx init [path] [--name "<name>"] initialize an APC project
97
- apx project add <path> register a project with the daemon
100
+ apx project add [path] register a project with the daemon
98
101
  apx project list
99
102
  apx project remove <path|id>
100
- apx project rebuild [<path|id>] rebuild SQLite cache from filesystem
103
+ apx project rebuild [<path|id>] rebuild project index from filesystem
104
+ apx add project [path] alias for: apx project add
101
105
 
102
106
  Agents:
103
107
  apx agent add <slug> [--role R] [--model M] [--skills a,b] [--language es-AR] [--description D]
@@ -144,7 +148,7 @@ Telegram:
144
148
  Messages:
145
149
  apx messages tail [--agent <slug>] [--channel <ch>] [-n 50] [--global]
146
150
  global channels (telegram, direct, whatsapp) → ~/.apx/messages/<ch>/
147
- project channels (runtime, a2a, exec) → <project>/.apc/messages/
151
+ project channels (runtime, a2a, exec) → ~/.apx/projects/<id>/messages/
148
152
  apx messages search "<query>"
149
153
 
150
154
  LLM engines (v0.2):
@@ -200,6 +204,8 @@ Skills (IDE integration):
200
204
  apx skills status show which IDE targets are installed (project + global)
201
205
 
202
206
  Other:
207
+ apx setup interactive setup wizard (alias: apx install)
208
+ apx update check for updates and upgrade (alias: apx upgrade)
203
209
  apx --help
204
210
  apx --version
205
211
 
@@ -466,6 +472,24 @@ async function dispatch(cmd, rest) {
466
472
  await cmdIdentity(parseArgs(rest));
467
473
  break;
468
474
 
475
+ case "add": {
476
+ // apx add <domain> [...args] — consistent alternative to apx <domain> add
477
+ const sub = rest[0];
478
+ if (sub === "project") await cmdProjectAdd(parseArgs(rest.slice(1)));
479
+ else die(`unknown 'add' subcommand: ${sub || "(none)"} — try: project`);
480
+ break;
481
+ }
482
+
483
+ case "setup":
484
+ case "install":
485
+ await cmdSetup();
486
+ return;
487
+
488
+ case "update":
489
+ case "upgrade":
490
+ await cmdUpdate(parseArgs(rest));
491
+ return; // skip checkForUpdate after an update
492
+
469
493
  default:
470
494
  die(`unknown command: ${cmd}\nRun \`apx --help\` for usage.`);
471
495
  }
@@ -475,6 +499,7 @@ const [topCmd, ...topRest] = argv;
475
499
  (async () => {
476
500
  try {
477
501
  await dispatch(topCmd, topRest);
502
+ checkForUpdate(VERSION);
478
503
  } catch (err) {
479
504
  die(err && err.message ? err.message : String(err));
480
505
  }