@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
package/skills/apx/SKILL.md
CHANGED
|
@@ -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
|
|
21
|
-
|
|
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
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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:
|