@agentprojectcontext/apx 1.21.0 → 1.22.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.21.0",
3
+ "version": "1.22.1",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -98,6 +98,29 @@ apx session list <slug> # list sessions
98
98
  apx session check # exits 1 if session already active
99
99
  ```
100
100
 
101
+ ### List sessions of other AI engines
102
+
103
+ `apx sessions list` lists sessions of external AI engines (Claude Code, Codex)
104
+ without opening their interactive pickers. It resolves the project directory
105
+ from a registered APX project (`--project`) or an explicit path (`--dir`).
106
+
107
+ ```bash
108
+ apx sessions list # APX engine projects (default)
109
+ apx sessions list --engine claude # Claude Code project folders
110
+ apx sessions list --engine claude --project iacrmar # sessions of a registered project
111
+ apx sessions list --engine claude --dir /path/to/repo # sessions of any directory
112
+ apx sessions list --engine codex --dir /path/to/repo # Codex sessions
113
+ ```
114
+
115
+ Output prints date + session id + title, newest first, plus the exact resume command.
116
+
117
+ **Resume a Claude Code session** (run from the project directory):
118
+
119
+ ```bash
120
+ claude --continue # resume the most recent session
121
+ claude -p --resume <session-id> "..." # resume a specific session, always with -p (print mode)
122
+ ```
123
+
101
124
  ## Observe activity
102
125
 
103
126
  ```bash
@@ -0,0 +1,517 @@
1
+ // apx sessions — list AI engine sessions (Claude Code, Codex, APX, ...) without
2
+ // opening an interactive picker. Each engine is a small adapter; the command
3
+ // resolves a working directory (from a registered APX project or an explicit
4
+ // --dir) and asks the adapter to list that engine's sessions for it.
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+
9
+ // ── shared helpers ───────────────────────────────────────────────────────────
10
+
11
+ const homeDir = (opts) => (opts && opts.home) || os.homedir();
12
+
13
+ // Claude Code encodes a project cwd into a folder name by replacing every
14
+ // non-alphanumeric character with "-". Mirrors encodeClaudeProjectPath in the
15
+ // claude-code runtime adapter.
16
+ function encodeClaudeProjectPath(cwd) {
17
+ return String(cwd || "").replace(/[^A-Za-z0-9]/g, "-");
18
+ }
19
+
20
+ function safeStatMtime(p) {
21
+ try {
22
+ return fs.statSync(p).mtimeMs;
23
+ } catch {
24
+ return 0;
25
+ }
26
+ }
27
+
28
+ function fmtDate(ms) {
29
+ if (!ms) return " ";
30
+ const d = new Date(ms);
31
+ const p = (n) => String(n).padStart(2, "0");
32
+ return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
33
+ }
34
+
35
+ // Read the first bytes of a file (used for one-line JSONL headers).
36
+ function readHead(file, bytes = 8192) {
37
+ let fd;
38
+ try {
39
+ fd = fs.openSync(file, "r");
40
+ const buf = Buffer.alloc(bytes);
41
+ const n = fs.readSync(fd, buf, 0, bytes, 0);
42
+ return buf.subarray(0, n).toString("utf8");
43
+ } catch {
44
+ return "";
45
+ } finally {
46
+ if (fd !== undefined) {
47
+ try {
48
+ fs.closeSync(fd);
49
+ } catch {}
50
+ }
51
+ }
52
+ }
53
+
54
+ // Registered APX projects come from ~/.apx/config.json — APX does not know
55
+ // every project on disk, so an unregistered project must be passed via --dir.
56
+ function readApxProjects(opts) {
57
+ const cfgPath = path.join(homeDir(opts), ".apx", "config.json");
58
+ let entries = [];
59
+ try {
60
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
61
+ entries = Array.isArray(cfg.projects) ? cfg.projects : [];
62
+ } catch {}
63
+ return entries
64
+ .filter((e) => e && e.path)
65
+ .map((e) => {
66
+ const proj = { path: path.resolve(e.path), name: null, apxId: null };
67
+ try {
68
+ const pj = JSON.parse(
69
+ fs.readFileSync(path.join(proj.path, ".apc", "project.json"), "utf8")
70
+ );
71
+ if (pj.name) proj.name = pj.name;
72
+ if (pj.apx_id) proj.apxId = pj.apx_id;
73
+ } catch {}
74
+ if (!proj.name) proj.name = path.basename(proj.path);
75
+ return proj;
76
+ });
77
+ }
78
+
79
+ // Resolve the working directory the user wants sessions for.
80
+ // --dir <path> → explicit path
81
+ // --project <name> → look up a registered APX project
82
+ // neither → null (caller enters discovery mode)
83
+ function resolveTargetDir(args, opts) {
84
+ const dirFlag = args.flags.dir;
85
+ if (dirFlag && dirFlag !== true) return path.resolve(String(dirFlag));
86
+
87
+ const projFlag = args.flags.project;
88
+ if (projFlag && projFlag !== true) {
89
+ const q = String(projFlag).toLowerCase();
90
+ const projects = readApxProjects(opts);
91
+ const exact =
92
+ projects.find((p) => p.name.toLowerCase() === q) ||
93
+ projects.find((p) => path.basename(p.path).toLowerCase() === q);
94
+ if (exact) return exact.path;
95
+
96
+ const fuzzy = projects.filter(
97
+ (p) =>
98
+ p.name.toLowerCase().includes(q) || p.path.toLowerCase().includes(q)
99
+ );
100
+ if (fuzzy.length === 1) return fuzzy[0].path;
101
+ if (fuzzy.length > 1) {
102
+ throw new Error(
103
+ `--project "${projFlag}" is ambiguous; matches: ${fuzzy
104
+ .map((p) => p.name)
105
+ .join(", ")}`
106
+ );
107
+ }
108
+ const known = projects.length
109
+ ? projects.map((p) => p.name).join(", ")
110
+ : "(none registered)";
111
+ throw new Error(
112
+ `--project "${projFlag}" not found in registered APX projects (${known}). ` +
113
+ `Use --dir <path> for an unregistered project.`
114
+ );
115
+ }
116
+ return null;
117
+ }
118
+
119
+ // ── claude code engine ───────────────────────────────────────────────────────
120
+
121
+ function claudeProjectsDir(opts) {
122
+ return path.join(homeDir(opts), ".claude", "projects");
123
+ }
124
+
125
+ function claudeReadTitle(file) {
126
+ let title = null;
127
+ let lastPrompt = null;
128
+ let text;
129
+ try {
130
+ text = fs.readFileSync(file, "utf8");
131
+ } catch {
132
+ return null;
133
+ }
134
+ for (const line of text.split("\n")) {
135
+ if (!line.includes('"aiTitle"') && !line.includes('"lastPrompt"')) continue;
136
+ let d;
137
+ try {
138
+ d = JSON.parse(line);
139
+ } catch {
140
+ continue;
141
+ }
142
+ if (d.type === "ai-title" && d.aiTitle) title = d.aiTitle;
143
+ else if (d.type === "last-prompt" && d.lastPrompt) lastPrompt = d.lastPrompt;
144
+ }
145
+ return title || lastPrompt || null;
146
+ }
147
+
148
+ const claudeEngine = {
149
+ id: "claude",
150
+ label: "Claude Code",
151
+ implemented: true,
152
+ detect(opts) {
153
+ const dir = claudeProjectsDir(opts);
154
+ return fs.existsSync(dir)
155
+ ? { available: true }
156
+ : { available: false, reason: `${dir} not found` };
157
+ },
158
+ listProjects(opts) {
159
+ const root = claudeProjectsDir(opts);
160
+ const known = new Map(
161
+ readApxProjects(opts).map((p) => [encodeClaudeProjectPath(p.path), p])
162
+ );
163
+ const out = [];
164
+ for (const name of fs.readdirSync(root)) {
165
+ const dir = path.join(root, name);
166
+ let files;
167
+ try {
168
+ files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
169
+ } catch {
170
+ continue;
171
+ }
172
+ if (files.length === 0) continue;
173
+ const matched = known.get(name);
174
+ out.push({
175
+ key: matched ? matched.name : name,
176
+ dir: matched ? matched.path : null,
177
+ label: matched ? matched.path : name,
178
+ count: files.length,
179
+ mtime: Math.max(...files.map((f) => safeStatMtime(path.join(dir, f)))),
180
+ });
181
+ }
182
+ return out.sort((a, b) => b.mtime - a.mtime);
183
+ },
184
+ listSessions(dir, opts) {
185
+ const folder = path.join(
186
+ claudeProjectsDir(opts),
187
+ encodeClaudeProjectPath(dir)
188
+ );
189
+ if (!fs.existsSync(folder)) return { found: false, location: folder };
190
+ const sessions = [];
191
+ for (const f of fs.readdirSync(folder)) {
192
+ if (!f.endsWith(".jsonl")) continue;
193
+ const file = path.join(folder, f);
194
+ sessions.push({
195
+ id: f.slice(0, -6),
196
+ mtime: safeStatMtime(file),
197
+ title: claudeReadTitle(file) || "(sin título)",
198
+ });
199
+ }
200
+ sessions.sort((a, b) => b.mtime - a.mtime);
201
+ return { found: true, location: folder, sessions };
202
+ },
203
+ continueHint() {
204
+ return `claude --continue (run from the project directory)`;
205
+ },
206
+ resumeHint(id) {
207
+ return `claude -p --resume ${id} "your prompt"`;
208
+ },
209
+ };
210
+
211
+ // ── codex engine ─────────────────────────────────────────────────────────────
212
+
213
+ function codexSessionsDir(opts) {
214
+ return path.join(homeDir(opts), ".codex", "sessions");
215
+ }
216
+
217
+ // Codex stores titles in ~/.codex/session_index.jsonl keyed by session id.
218
+ function codexTitleIndex(opts) {
219
+ const file = path.join(homeDir(opts), ".codex", "session_index.jsonl");
220
+ const map = new Map();
221
+ let text;
222
+ try {
223
+ text = fs.readFileSync(file, "utf8");
224
+ } catch {
225
+ return map;
226
+ }
227
+ for (const line of text.split("\n")) {
228
+ if (!line.trim()) continue;
229
+ try {
230
+ const d = JSON.parse(line);
231
+ if (d.id && d.thread_name) map.set(d.id, d.thread_name);
232
+ } catch {}
233
+ }
234
+ return map;
235
+ }
236
+
237
+ // Walk ~/.codex/sessions/YYYY/MM/DD/ collecting rollout-*.jsonl files and
238
+ // reading their session_meta header (first line) for id + cwd.
239
+ function codexScanRollouts(opts) {
240
+ const root = codexSessionsDir(opts);
241
+ const found = [];
242
+ const walk = (dir) => {
243
+ let entries;
244
+ try {
245
+ entries = fs.readdirSync(dir, { withFileTypes: true });
246
+ } catch {
247
+ return;
248
+ }
249
+ for (const e of entries) {
250
+ const full = path.join(dir, e.name);
251
+ if (e.isDirectory()) walk(full);
252
+ else if (e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")) {
253
+ const head = readHead(full);
254
+ const id = (head.match(/"id":"([^"]+)"/) || [])[1];
255
+ const cwd = (head.match(/"cwd":"([^"]+)"/) || [])[1];
256
+ if (id) found.push({ id, cwd: cwd || null, mtime: safeStatMtime(full) });
257
+ }
258
+ }
259
+ };
260
+ walk(root);
261
+ return found;
262
+ }
263
+
264
+ const codexEngine = {
265
+ id: "codex",
266
+ label: "Codex",
267
+ implemented: true,
268
+ detect(opts) {
269
+ const dir = codexSessionsDir(opts);
270
+ return fs.existsSync(dir)
271
+ ? { available: true }
272
+ : { available: false, reason: `${dir} not found` };
273
+ },
274
+ listProjects(opts) {
275
+ const byCwd = new Map();
276
+ for (const r of codexScanRollouts(opts)) {
277
+ if (!r.cwd) continue;
278
+ const cur = byCwd.get(r.cwd) || { count: 0, mtime: 0 };
279
+ cur.count++;
280
+ if (r.mtime > cur.mtime) cur.mtime = r.mtime;
281
+ byCwd.set(r.cwd, cur);
282
+ }
283
+ return [...byCwd.entries()]
284
+ .map(([cwd, v]) => ({
285
+ key: cwd,
286
+ dir: cwd,
287
+ label: cwd,
288
+ count: v.count,
289
+ mtime: v.mtime,
290
+ }))
291
+ .sort((a, b) => b.mtime - a.mtime);
292
+ },
293
+ listSessions(dir, opts) {
294
+ const titles = codexTitleIndex(opts);
295
+ const sessions = codexScanRollouts(opts)
296
+ .filter((r) => r.cwd === dir)
297
+ .map((r) => ({
298
+ id: r.id,
299
+ mtime: r.mtime,
300
+ title: titles.get(r.id) || "(sin título)",
301
+ }))
302
+ .sort((a, b) => b.mtime - a.mtime);
303
+ return { found: sessions.length > 0, location: dir, sessions };
304
+ },
305
+ continueHint() {
306
+ return `codex resume --last`;
307
+ },
308
+ resumeHint(id) {
309
+ return `codex exec resume ${id} "your prompt" (interactive: codex resume ${id})`;
310
+ },
311
+ };
312
+
313
+ // ── apx engine (default) ─────────────────────────────────────────────────────
314
+
315
+ function parseFrontmatter(text) {
316
+ if (!text.startsWith("---\n")) return {};
317
+ const end = text.indexOf("\n---", 4);
318
+ if (end === -1) return {};
319
+ const fm = {};
320
+ for (const line of text.slice(4, end).split("\n")) {
321
+ const m = line.match(/^([a-zA-Z_]+):\s*(.*)$/);
322
+ if (m) fm[m[1]] = m[2].trim();
323
+ }
324
+ return fm;
325
+ }
326
+
327
+ function apxStorageRoot(opts) {
328
+ return path.join(homeDir(opts), ".apx", "projects");
329
+ }
330
+
331
+ const apxEngine = {
332
+ id: "apx",
333
+ label: "APX",
334
+ implemented: true,
335
+ detect() {
336
+ return { available: true };
337
+ },
338
+ listProjects(opts) {
339
+ return readApxProjects(opts).map((p) => {
340
+ let count = 0;
341
+ let mtime = 0;
342
+ if (p.apxId) {
343
+ const agentsDir = path.join(apxStorageRoot(opts), p.apxId, "agents");
344
+ try {
345
+ for (const slug of fs.readdirSync(agentsDir)) {
346
+ const sdir = path.join(agentsDir, slug, "sessions");
347
+ try {
348
+ for (const f of fs.readdirSync(sdir)) {
349
+ if (!f.endsWith(".md")) continue;
350
+ count++;
351
+ mtime = Math.max(mtime, safeStatMtime(path.join(sdir, f)));
352
+ }
353
+ } catch {}
354
+ }
355
+ } catch {}
356
+ }
357
+ return { key: p.name, dir: p.path, label: p.path, count, mtime };
358
+ });
359
+ },
360
+ listSessions(dir, opts) {
361
+ let apxId = null;
362
+ try {
363
+ const pj = JSON.parse(
364
+ fs.readFileSync(path.join(dir, ".apc", "project.json"), "utf8")
365
+ );
366
+ apxId = pj.apx_id || null;
367
+ } catch {}
368
+ if (!apxId) return { found: false, location: dir };
369
+ const agentsDir = path.join(apxStorageRoot(opts), apxId, "agents");
370
+ if (!fs.existsSync(agentsDir)) return { found: false, location: agentsDir };
371
+ const sessions = [];
372
+ for (const slug of fs.readdirSync(agentsDir)) {
373
+ const sdir = path.join(agentsDir, slug, "sessions");
374
+ let files;
375
+ try {
376
+ files = fs.readdirSync(sdir);
377
+ } catch {
378
+ continue;
379
+ }
380
+ for (const f of files) {
381
+ if (!f.endsWith(".md")) continue;
382
+ const file = path.join(sdir, f);
383
+ const fm = parseFrontmatter(fs.readFileSync(file, "utf8"));
384
+ sessions.push({
385
+ id: fm.id || f.slice(0, -3),
386
+ mtime: safeStatMtime(file),
387
+ title: `[${slug}] ${fm.title || "(sin título)"}`,
388
+ });
389
+ }
390
+ }
391
+ sessions.sort((a, b) => b.mtime - a.mtime);
392
+ return { found: sessions.length > 0, location: agentsDir, sessions };
393
+ },
394
+ continueHint() {
395
+ return `apx session list (run from the project directory)`;
396
+ },
397
+ resumeHint(id) {
398
+ return `apx session resume ${id}`;
399
+ },
400
+ };
401
+
402
+ // ── antigravity engine (detected, listing not implemented) ───────────────────
403
+
404
+ const antigravityEngine = {
405
+ id: "antigravity",
406
+ label: "Antigravity",
407
+ implemented: false,
408
+ detect(opts) {
409
+ const candidates = [
410
+ path.join(homeDir(opts), ".antigravity"),
411
+ path.join(
412
+ homeDir(opts),
413
+ "Library",
414
+ "Application Support",
415
+ "Antigravity"
416
+ ),
417
+ ];
418
+ const hit = candidates.find((c) => fs.existsSync(c));
419
+ return hit
420
+ ? { available: true }
421
+ : { available: false, reason: "Antigravity not installed" };
422
+ },
423
+ };
424
+
425
+ export const ENGINES = {
426
+ apx: apxEngine,
427
+ claude: claudeEngine,
428
+ codex: codexEngine,
429
+ antigravity: antigravityEngine,
430
+ };
431
+
432
+ // ── command ──────────────────────────────────────────────────────────────────
433
+
434
+ function printSessions(engine, dir, result, limit) {
435
+ if (!result.found) {
436
+ console.log(`(no ${engine.label} sessions for ${dir})`);
437
+ if (result.location) console.log(` looked in: ${result.location}`);
438
+ return;
439
+ }
440
+ let sessions = result.sessions;
441
+ if (limit && limit > 0) sessions = sessions.slice(0, limit);
442
+
443
+ console.log(`${engine.label} sessions for ${dir}`);
444
+ console.log(` ${result.location}`);
445
+ console.log("");
446
+ console.log(`${"DATE".padEnd(12)} ${"SESSION ID".padEnd(38)} TITLE`);
447
+ console.log(`${"─".repeat(12)} ${"─".repeat(38)} ${"─".repeat(40)}`);
448
+ for (const s of sessions) {
449
+ console.log(
450
+ `${fmtDate(s.mtime).padEnd(12)} ${String(s.id).padEnd(38)} ${String(
451
+ s.title
452
+ ).slice(0, 70)}`
453
+ );
454
+ }
455
+ console.log("");
456
+ console.log("Resume:");
457
+ if (engine.continueHint) console.log(` latest: ${engine.continueHint(dir)}`);
458
+ if (engine.resumeHint && sessions[0]) {
459
+ console.log(` specific: ${engine.resumeHint(sessions[0].id)}`);
460
+ }
461
+ }
462
+
463
+ function printProjects(engine, projects) {
464
+ if (projects.length === 0) {
465
+ console.log(`(no ${engine.label} projects found)`);
466
+ return;
467
+ }
468
+ console.log(`${engine.label} projects:`);
469
+ console.log("");
470
+ console.log(`${"SESSIONS".padEnd(9)} ${"LAST".padEnd(12)} PROJECT`);
471
+ console.log(`${"─".repeat(9)} ${"─".repeat(12)} ${"─".repeat(40)}`);
472
+ for (const p of projects) {
473
+ console.log(
474
+ `${String(p.count).padEnd(9)} ${fmtDate(p.mtime).padEnd(12)} ${p.label}`
475
+ );
476
+ }
477
+ console.log("");
478
+ console.log(
479
+ `Re-run with --project <name> or --dir <path> to list sessions of one project.`
480
+ );
481
+ }
482
+
483
+ export function cmdSessionsList(args, opts = {}) {
484
+ const engineFlag = args.flags.engine;
485
+ if (engineFlag === true) {
486
+ throw new Error("--engine requires a value (apx, claude, codex, antigravity)");
487
+ }
488
+ const engineId = engineFlag ? String(engineFlag) : "apx";
489
+ const engine = ENGINES[engineId];
490
+ if (!engine) {
491
+ throw new Error(
492
+ `unknown engine "${engineId}" — valid engines: ${Object.keys(ENGINES).join(", ")}`
493
+ );
494
+ }
495
+
496
+ const detected = engine.detect(opts);
497
+ if (!detected.available) {
498
+ console.log(`engine "${engine.id}" not available: ${detected.reason}`);
499
+ return;
500
+ }
501
+ if (!engine.implemented) {
502
+ console.log(
503
+ `engine "${engine.id}" (${engine.label}) is detected but session listing is not implemented yet.`
504
+ );
505
+ return;
506
+ }
507
+
508
+ const dir = resolveTargetDir(args, opts);
509
+ const limitFlag = args.flags.limit || args.flags.last;
510
+ const limit = limitFlag && limitFlag !== true ? parseInt(limitFlag, 10) : null;
511
+
512
+ if (dir) {
513
+ printSessions(engine, dir, engine.listSessions(dir, opts), limit);
514
+ } else {
515
+ printProjects(engine, engine.listProjects(opts));
516
+ }
517
+ }
@@ -17,8 +17,11 @@ function isNewer(cur, lat) {
17
17
  function hasPnpmGlobal() {
18
18
  const r = spawnSync("pnpm", ["--version"], { encoding: "utf8", stdio: "pipe" });
19
19
  if (r.status !== 0) return false;
20
- // pnpm needs PNPM_HOME configured to manage global packages
21
- const check = spawnSync("pnpm", ["root", "-g"], { encoding: "utf8", stdio: "pipe" });
20
+ // `pnpm add -g` needs a configured global *bin* directory (PNPM_HOME / the
21
+ // result of `pnpm setup`). `pnpm root -g` succeeds even without it, so probe
22
+ // `pnpm bin -g` instead — it fails with ERR_PNPM_NO_GLOBAL_BIN_DIR when the
23
+ // global bin directory is missing.
24
+ const check = spawnSync("pnpm", ["bin", "-g"], { encoding: "utf8", stdio: "pipe" });
22
25
  return check.status === 0 && !!check.stdout?.trim();
23
26
  }
24
27
 
@@ -62,15 +65,25 @@ export async function cmdUpdate(args, currentVersion) {
62
65
  console.log("stopped.");
63
66
  }
64
67
 
65
- // Prefer pnpm global if configured, fall back to npm.
66
- const usePnpm = hasPnpmGlobal();
67
- const pm = usePnpm ? "pnpm" : "npm";
68
- const installArgs = usePnpm
69
- ? ["add", "-g", `${PACKAGE_NAME}@${latest}`]
70
- : ["install", "-g", `${PACKAGE_NAME}@${latest}`];
71
-
72
- console.log(`\nInstalling ${PACKAGE_NAME}@${latest} via ${pm}...\n`);
73
- const result = spawnSync(pm, installArgs, { stdio: "inherit" });
68
+ // Pick a package manager that can actually install global binaries. Prefer
69
+ // pnpm only when its global bin directory is configured; otherwise npm. If
70
+ // the first attempt fails, fall back to the other so a misconfigured pnpm
71
+ // never blocks the update.
72
+ const pnpmStep = ["pnpm", ["add", "-g", `${PACKAGE_NAME}@${latest}`]];
73
+ const npmStep = ["npm", ["install", "-g", `${PACKAGE_NAME}@${latest}`]];
74
+ const steps = hasPnpmGlobal() ? [pnpmStep, npmStep] : [npmStep];
75
+
76
+ let result;
77
+ for (let i = 0; i < steps.length; i++) {
78
+ const [pm, installArgs] = steps[i];
79
+ console.log(`\nInstalling ${PACKAGE_NAME}@${latest} via ${pm}...\n`);
80
+ result = spawnSync(pm, installArgs, { stdio: "inherit" });
81
+ if (result.status === 0) break;
82
+ const next = steps[i + 1];
83
+ if (next) {
84
+ console.log(`\n⚠️ ${pm} install failed — retrying with ${next[0]}...`);
85
+ }
86
+ }
74
87
 
75
88
  if (result.status !== 0) {
76
89
  console.error(`\n❌ Update failed (exit ${result.status})`);
package/src/cli/index.js CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  cmdSessionResume,
33
33
  cmdSessionCompact,
34
34
  } from "./commands/session.js";
35
+ import { cmdSessionsList } from "./commands/sessions.js";
35
36
  import {
36
37
  cmdMcpList,
37
38
  cmdMcpAdd,
@@ -482,6 +483,45 @@ const HELP_TOPICS = new Map(Object.entries({
482
483
  ],
483
484
  examples: ["apx session compact reviewer --conversation abc123"],
484
485
  }),
486
+ sessions: topic({
487
+ title: "apx sessions",
488
+ summary: "List AI engine sessions (Claude Code, Codex, APX) non-interactively.",
489
+ usage: ["apx sessions list [--engine <id>] [--project <name>|--dir <path>] [--limit N]"],
490
+ commands: [
491
+ ["list | ls", "List engine projects, or sessions of one project."],
492
+ ],
493
+ options: [
494
+ ["--engine <id>", "apx (default) | claude | codex | antigravity."],
495
+ ["--project <name>", "A registered APX project to resolve the directory from."],
496
+ ["--dir <path>", "An explicit project directory (for unregistered projects)."],
497
+ ["--limit N", "Show only the N most recent sessions."],
498
+ ],
499
+ examples: [
500
+ "apx sessions list --engine claude",
501
+ "apx sessions list --engine claude --project iacrmar",
502
+ "apx sessions list --engine codex --dir /path/to/project",
503
+ ],
504
+ }),
505
+ "sessions list": topic({
506
+ title: "apx sessions list",
507
+ summary: "List engine projects, or the sessions of one resolved project.",
508
+ usage: [
509
+ "apx sessions list [--engine <id>]",
510
+ "apx sessions list --engine <id> --project <name>",
511
+ "apx sessions list --engine <id> --dir <path>",
512
+ ],
513
+ options: [
514
+ ["--engine <id>", "apx (default) | claude | codex | antigravity."],
515
+ ["--project <name>", "Resolve directory from a registered APX project."],
516
+ ["--dir <path>", "Explicit project directory."],
517
+ ["--limit N", "Show only the N most recent sessions."],
518
+ ],
519
+ examples: [
520
+ "apx sessions list",
521
+ "apx sessions list --engine claude --project iacrmar",
522
+ "apx sessions list --engine codex --dir /Volumes/work/iacrmar",
523
+ ],
524
+ }),
485
525
  mcp: topic({
486
526
  title: "apx mcp",
487
527
  summary: "Manage and call MCP servers merged from APC and supported IDE configs.",
@@ -1049,6 +1089,7 @@ const HELP_ALIASES = new Map(Object.entries({
1049
1089
  "agent vault ls": "agent vault list",
1050
1090
  "session ls": "session list",
1051
1091
  "session show": "session get",
1092
+ "sessions ls": "sessions list",
1052
1093
  "mcp ls": "mcp list",
1053
1094
  "mcp rm": "mcp remove",
1054
1095
  "conv list": "conversations list",
@@ -1140,6 +1181,7 @@ function buildHelp(version) {
1140
1181
  hCmd("apx session close-stale", 36, "auto-close sessions older than 1h"),
1141
1182
  hCmd("apx session resume <id>", 36, "--summary --full (APC + Claude Code transcript)"),
1142
1183
  hCmd("apx session compact <slug>", 36, "--conversation <id> collapse history into summary"),
1184
+ hCmd("apx sessions list", 36, "list AI engine sessions --engine claude|codex|apx --project P | --dir D"),
1143
1185
 
1144
1186
  hSec("MCPs"),
1145
1187
  hCmd("apx mcp list", 36, ""),
@@ -1426,6 +1468,15 @@ async function dispatch(cmd, rest) {
1426
1468
  break;
1427
1469
  }
1428
1470
 
1471
+ case "sessions": {
1472
+ const sub = rest[0];
1473
+ const isListSub = sub === "list" || sub === "ls";
1474
+ const a = parseArgs(isListSub ? rest.slice(1) : rest);
1475
+ if (!sub || isListSub || sub.startsWith("--")) cmdSessionsList(a);
1476
+ else die(`unknown sessions subcommand: ${sub} — try: list`);
1477
+ break;
1478
+ }
1479
+
1429
1480
  case "mcp": {
1430
1481
  const sub = rest[0];
1431
1482
  const a = parseArgs(rest.slice(1));
@@ -53,6 +53,23 @@ For high-trust automation in an already sandboxed environment:
53
53
  claude -p "task" --permission-mode bypassPermissions --output-format json
54
54
  ```
55
55
 
56
+ ## List and resume sessions
57
+
58
+ Claude Code has no `--list`; `--resume` is always an interactive picker. To list
59
+ sessions non-interactively, use APX:
60
+
61
+ ```bash
62
+ apx sessions list --engine claude --project <name> # registered APX project
63
+ apx sessions list --engine claude --dir <path> # any directory
64
+ ```
65
+
66
+ This prints each session's id and title. To resume one (run from the project directory):
67
+
68
+ ```bash
69
+ claude --continue # most recent session
70
+ claude -p --resume <session-id> "..." # specific session, always with -p (print mode)
71
+ ```
72
+
56
73
  ## APX runtime
57
74
 
58
75
  Run a project agent through Claude Code:
@@ -60,6 +60,23 @@ codex exec --json --sandbox workspace-write --skip-git-repo-check "task"
60
60
  `--skip-git-repo-check` matters for APX default runtime dirs such as `~/.apx/projects/default`,
61
61
  which may not be Git repositories.
62
62
 
63
+ ## List and resume sessions
64
+
65
+ List Codex sessions for a project non-interactively with APX:
66
+
67
+ ```bash
68
+ apx sessions list --engine codex --project <name> # registered APX project
69
+ apx sessions list --engine codex --dir <path> # any directory
70
+ ```
71
+
72
+ Resume a session:
73
+
74
+ ```bash
75
+ codex resume <session-id> # interactive
76
+ codex exec resume <session-id> "..." # non-interactive
77
+ codex resume --last # most recent session
78
+ ```
79
+
63
80
  ## APX runtime
64
81
 
65
82
  Run a project agent through Codex: