@agentprojectcontext/apx 1.20.0 → 1.22.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.20.0",
3
+ "version": "1.22.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -10,13 +10,11 @@
10
10
  "bin": {
11
11
  "apx": "./src/cli/index.js",
12
12
  "apx-daemon": "./src/daemon/index.js",
13
- "apx-mcp": "./src/mcp/index.js",
14
- "apx-ng": "./dist/cli/index.js"
13
+ "apx-mcp": "./src/mcp/index.js"
15
14
  },
16
15
  "files": [
17
16
  "src/",
18
17
  "skills/",
19
- "dist/",
20
18
  "README.md"
21
19
  ],
22
20
  "engines": {
@@ -26,9 +24,6 @@
26
24
  "start": "node src/daemon/index.js",
27
25
  "smoke": "node src/daemon/smoke.js",
28
26
  "test": "node --test --test-reporter=spec tests/*.test.js",
29
- "build": "node scripts/build-cli.js",
30
- "build:watch": "node scripts/build-cli.js --watch",
31
- "typecheck": "tsc --noEmit -p tsconfig.cli.json",
32
27
  "upgrade": "pnpm install && pnpm add -g .",
33
28
  "prepack": "node scripts/sync-apc-skill.js",
34
29
  "postinstall": "node src/cli/postinstall.js"
@@ -36,50 +31,23 @@
36
31
  "packageManager": "pnpm@10.25.0",
37
32
  "dependencies": {
38
33
  "@modelcontextprotocol/sdk": "^1.29.0",
39
- "@opentui/core": "^0.2.8",
40
- "@opentui/keymap": "^0.2.8",
41
- "@opentui/solid": "^0.2.8",
42
- "@solid-primitives/event-bus": "^1.1.3",
43
- "@solid-primitives/keyboard": "^1.3.5",
44
- "@solid-primitives/scheduled": "^1.5.3",
45
- "ansi-regex": "^6.2.2",
46
34
  "chalk": "^5.6.2",
47
35
  "cli-cursor": "^5.0.0",
48
- "cli-sound": "^1.1.3",
49
- "clipboardy": "^5.3.1",
50
36
  "cron-parser": "^5.5.0",
51
- "effect": "^3.21.2",
52
37
  "express": "^4.21.0",
53
- "fuzzysort": "^3.1.0",
54
- "jsonc-parser": "^3.3.1",
55
- "node-fetch": "^3.3.2",
56
- "open": "^11.0.0",
57
- "opentui-spinner": "^0.0.6",
58
- "react": "^19.2.6",
59
- "remeda": "^2.34.1",
60
- "semver": "^7.8.0",
61
- "solid-js": "^1.9.12",
62
- "strip-ansi": "^7.2.0",
63
- "yargs": "^18.0.0",
64
- "zod": "^3.25.76"
38
+ "node-fetch": "^3.3.2"
65
39
  },
66
40
  "optionalDependencies": {
67
41
  "fast-glob": "^3.3.2",
68
- "puppeteer": "^22.0.0",
69
- "ws": "^8.18.0"
42
+ "puppeteer": "^22.0.0"
70
43
  },
71
44
  "devDependencies": {
72
- "@babel/core": "^7.29.0",
73
45
  "@semantic-release/changelog": "^6.0.3",
74
46
  "@semantic-release/git": "^10.0.1",
75
47
  "@types/node": "^25.7.0",
76
- "@types/yargs": "^17.0.35",
77
- "babel-preset-solid": "^1.9.12",
78
48
  "better-sqlite3": "^11.3.0",
79
49
  "conventional-changelog-conventionalcommits": "^9.3.1",
80
- "electron": "^33.4.11",
81
50
  "esbuild": "^0.28.0",
82
- "esbuild-plugin-solid": "^0.6.0",
83
51
  "typescript": "^6.0.3"
84
52
  },
85
53
  "keywords": [
@@ -94,7 +62,6 @@
94
62
  "pnpm": {
95
63
  "onlyBuiltDependencies": [
96
64
  "better-sqlite3",
97
- "electron",
98
65
  "puppeteer"
99
66
  ]
100
67
  },
@@ -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
+ }