@tekmidian/pai 0.10.0 → 0.11.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 +32 -30
- package/dist/cli/index.mjs +296 -109
- package/dist/cli/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -299,34 +299,28 @@ All hooks are TypeScript compiled to `.mjs` modules. They run as separate proces
|
|
|
299
299
|
|
|
300
300
|
PAI gives you a complete picture of every Claude Code session running on your machine — live tabs in iTerm2, paused snapshots on disk, and everything in between.
|
|
301
301
|
|
|
302
|
-
### The Core Idea:
|
|
302
|
+
### The Core Idea: One Entry Point
|
|
303
303
|
|
|
304
|
-
|
|
304
|
+
`pai <name>` is the universal session command. It does the right thing based on session state:
|
|
305
305
|
|
|
306
|
-
|
|
306
|
+
- **Live session** — switches the iTerm2 tab to front (no new Claude launched)
|
|
307
|
+
- **Resumable session** — probes `claude --resume`, launches with the saved UUID
|
|
308
|
+
- **Transcript/stub** — starts a fresh Claude in the same project directory
|
|
309
|
+
- **No match** — searches `~/.claude/history.jsonl`, shows a candidate picker
|
|
307
310
|
|
|
308
311
|
```bash
|
|
309
|
-
pai
|
|
310
|
-
pai
|
|
311
|
-
pai
|
|
312
|
-
pai
|
|
312
|
+
pai aibroker # Switch to the live AIBroker tab (iTerm comes to front)
|
|
313
|
+
pai youdrill # Resume the youdrill session, or fresh if snapshot is stale
|
|
314
|
+
pai mdf # Free-text search across your prompt history
|
|
315
|
+
pai 0856d40b # Resume by UUID prefix
|
|
316
|
+
pai # Show all sessions (deduped, one row per name)
|
|
313
317
|
```
|
|
314
318
|
|
|
315
|
-
|
|
316
|
-
1. Is "mdf" a named session in the catalog? → launch immediately, no picker
|
|
317
|
-
2. Is "mdf" a UUID prefix? → direct filesystem resume
|
|
318
|
-
3. Otherwise → grep `~/.claude/history.jsonl` for matching prompts, show candidate list, you pick a number
|
|
319
|
-
|
|
320
|
-
The picked session resolves to the best resumable snapshot in the same project directory. PAI probes `claude --resume` automatically. If the snapshot is stale, it starts a fresh Claude in the same directory.
|
|
321
|
-
|
|
322
|
-
### Daily Verbs
|
|
323
|
-
|
|
324
|
-
The commands you'll use every day:
|
|
319
|
+
### Daily Commands
|
|
325
320
|
|
|
326
321
|
```bash
|
|
327
|
-
pai #
|
|
328
|
-
pai <
|
|
329
|
-
pai cd <name> # cd to a project directory (no Claude launch)
|
|
322
|
+
pai # List sessions (deduped by name, one row per name)
|
|
323
|
+
pai <name> # Switch / resume / fresh — universal
|
|
330
324
|
pai pause # Save state checkpoint (write ## Continue to TODO.md)
|
|
331
325
|
pai pause all # Pause every live Claude session at once
|
|
332
326
|
pai end # Finalize: save state + mark session note Completed
|
|
@@ -339,15 +333,25 @@ And inside Claude Code, the two slash commands that matter:
|
|
|
339
333
|
/end → same as /pause, plus marks the session note Completed
|
|
340
334
|
```
|
|
341
335
|
|
|
342
|
-
###
|
|
336
|
+
### Session Listing
|
|
337
|
+
|
|
338
|
+
`pai` (no args) shows a single deduped table — one row per session name, regardless of how many snapshots exist on disk:
|
|
343
339
|
|
|
344
|
-
|
|
340
|
+
```
|
|
341
|
+
Sessions:
|
|
342
|
+
|
|
343
|
+
# name status age project last prompt
|
|
344
|
+
-- ---------- ---------- -------- ---------------------------- --------------------------
|
|
345
|
+
1 AIBroker live now — —
|
|
346
|
+
2 PAI resumable 2m ago /…dev/ai/PAI "refactor session listing…"
|
|
347
|
+
3 MDF transcript 3d ago /…MDF/Infrastruktur/Webseiten "ok so we recently had…"
|
|
348
|
+
```
|
|
345
349
|
|
|
346
|
-
|
|
350
|
+
Status values: `live` (active iTerm tab), `resumable` (clean snapshot on disk), `transcript` (history available, not resumable), `stub` (empty or minimal).
|
|
347
351
|
|
|
348
|
-
|
|
352
|
+
### Finding Sessions by Topic
|
|
349
353
|
|
|
350
|
-
`pai <topic>`
|
|
354
|
+
`pai <topic>` first checks session names, then falls back to searching your prompt history:
|
|
351
355
|
|
|
352
356
|
```
|
|
353
357
|
Sessions matching "mdf":
|
|
@@ -360,16 +364,14 @@ Sessions matching "mdf":
|
|
|
360
364
|
Enter # to launch (1-2), or press Enter to cancel:
|
|
361
365
|
```
|
|
362
366
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
Use `pai <topic> --auto` (or `-y`) to auto-pick #1 without the prompt. Use `pai <topic> 2` to pick #2 directly.
|
|
367
|
+
Use `pai <topic> --auto` (or `-y`) to auto-pick #1. Use `pai <topic> 2` to pick directly.
|
|
366
368
|
|
|
367
369
|
### Power User Access
|
|
368
370
|
|
|
369
|
-
The full session management namespace is still available
|
|
371
|
+
The full session management namespace is still available:
|
|
370
372
|
|
|
371
373
|
```bash
|
|
372
|
-
pai sessions # Live + disk listing (
|
|
374
|
+
pai sessions # Live + disk listing (with more columns)
|
|
373
375
|
pai sessions --all # Include unnamed orphan sessions
|
|
374
376
|
pai sessions --all-tabs # Include shell tabs in the live section
|
|
375
377
|
pai sessions goto <name> # Named-session resolver (same as pai <name>)
|
package/dist/cli/index.mjs
CHANGED
|
@@ -2758,6 +2758,29 @@ async function sendToSession(sessionId, text) {
|
|
|
2758
2758
|
};
|
|
2759
2759
|
}
|
|
2760
2760
|
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Switch iTerm2 focus to the session identified by `target`.
|
|
2763
|
+
*
|
|
2764
|
+
* `target` can be a sessionId, paiName, or tab index number (as string).
|
|
2765
|
+
* After switching, activates the iTerm2 application itself so the window
|
|
2766
|
+
* comes to the foreground.
|
|
2767
|
+
*
|
|
2768
|
+
* Returns { ok: true } if AIBroker confirmed the switch, or
|
|
2769
|
+
* { ok: false, error } if AIBroker is not running or the session was not found.
|
|
2770
|
+
*/
|
|
2771
|
+
async function switchToSession(target) {
|
|
2772
|
+
try {
|
|
2773
|
+
await callAiBroker("switch", { target });
|
|
2774
|
+
const { spawnSync } = await import("node:child_process");
|
|
2775
|
+
spawnSync("osascript", ["-e", "tell application \"iTerm\" to activate"], { stdio: "ignore" });
|
|
2776
|
+
return { ok: true };
|
|
2777
|
+
} catch (e) {
|
|
2778
|
+
return {
|
|
2779
|
+
ok: false,
|
|
2780
|
+
error: String(e)
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2761
2784
|
|
|
2762
2785
|
//#endregion
|
|
2763
2786
|
//#region src/cli/commands/session/recent.ts
|
|
@@ -2766,7 +2789,7 @@ function fmtSize(bytes) {
|
|
|
2766
2789
|
if (bytes < 1048576) return `${Math.round(bytes / 1024)}K`;
|
|
2767
2790
|
return `${(bytes / 1048576).toFixed(1)}M`;
|
|
2768
2791
|
}
|
|
2769
|
-
function fmtStatus(status) {
|
|
2792
|
+
function fmtStatus$1(status) {
|
|
2770
2793
|
switch (status) {
|
|
2771
2794
|
case "resumable": return chalk.green("resumable");
|
|
2772
2795
|
case "stub": return chalk.yellow("stub");
|
|
@@ -2872,7 +2895,7 @@ async function cmdRecent(db, opts) {
|
|
|
2872
2895
|
prompt,
|
|
2873
2896
|
s.userLines > 0 ? String(s.userLines) : dim("—"),
|
|
2874
2897
|
dim(fmtSize(s.topLevelSize)),
|
|
2875
|
-
fmtStatus(s.sessionStatus)
|
|
2898
|
+
fmtStatus$1(s.sessionStatus)
|
|
2876
2899
|
];
|
|
2877
2900
|
});
|
|
2878
2901
|
console.log(renderTable(headers, rows));
|
|
@@ -3389,6 +3412,62 @@ async function cmdPauseAll(opts) {
|
|
|
3389
3412
|
console.log();
|
|
3390
3413
|
}
|
|
3391
3414
|
|
|
3415
|
+
//#endregion
|
|
3416
|
+
//#region src/cli/commands/session/clear-names.ts
|
|
3417
|
+
/**
|
|
3418
|
+
* pai sessions clear-names
|
|
3419
|
+
*
|
|
3420
|
+
* Recovery command: wipes corrupted iTerm2 session name state.
|
|
3421
|
+
*
|
|
3422
|
+
* Two-step reset:
|
|
3423
|
+
* 1. Clear ~/.aibroker/session-names.json (the persistent name store).
|
|
3424
|
+
* 2. Clear user.paiName variable from all live iTerm2 sessions.
|
|
3425
|
+
*
|
|
3426
|
+
* After this runs the user can `/Name` each tab fresh. Nothing is
|
|
3427
|
+
* set automatically — the user re-asserts names via the /Name skill.
|
|
3428
|
+
*/
|
|
3429
|
+
async function cmdClearNames(opts) {
|
|
3430
|
+
if (opts.dryRun) {
|
|
3431
|
+
console.log(dim("Dry run — would:"));
|
|
3432
|
+
console.log(dim(" 1. Call AIBroker clear_session_names (wipe session-names.json)"));
|
|
3433
|
+
console.log(dim(" 2. Call AIBroker clear_pai_names (wipe user.paiName from all iTerm sessions)"));
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
let anyFailed = false;
|
|
3437
|
+
process.stdout.write("Clearing session-names.json … ");
|
|
3438
|
+
try {
|
|
3439
|
+
const n = (await callAiBroker("clear_session_names", {}, 1e4)).cleared ?? 0;
|
|
3440
|
+
console.log(ok(`cleared ${n} entr${n === 1 ? "y" : "ies"}`));
|
|
3441
|
+
} catch (e) {
|
|
3442
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3443
|
+
if (msg.includes("ENOENT") || msg.includes("ECONNREFUSED")) {
|
|
3444
|
+
console.log(warn("AIBroker not running — session-names.json not cleared"));
|
|
3445
|
+
anyFailed = true;
|
|
3446
|
+
} else {
|
|
3447
|
+
console.log(err(msg));
|
|
3448
|
+
anyFailed = true;
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
process.stdout.write("Clearing user.paiName from iTerm2 sessions … ");
|
|
3452
|
+
try {
|
|
3453
|
+
const n = (await callAiBroker("clear_pai_names", {}, 3e4)).cleared ?? 0;
|
|
3454
|
+
console.log(ok(`cleared ${n} session${n === 1 ? "" : "s"}`));
|
|
3455
|
+
} catch (e) {
|
|
3456
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3457
|
+
if (msg.includes("ENOENT") || msg.includes("ECONNREFUSED")) {
|
|
3458
|
+
console.log(warn("AIBroker not running — iTerm2 paiName variables not cleared"));
|
|
3459
|
+
anyFailed = true;
|
|
3460
|
+
} else {
|
|
3461
|
+
console.log(err(msg));
|
|
3462
|
+
anyFailed = true;
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
if (anyFailed) {
|
|
3466
|
+
console.log(warn("\nSome steps could not complete. Restart AIBroker and try again."));
|
|
3467
|
+
process.exitCode = 1;
|
|
3468
|
+
} else console.log(ok("\nDone. Use /Name to re-label each tab."));
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3392
3471
|
//#endregion
|
|
3393
3472
|
//#region src/cli/commands/session/sessions-index.ts
|
|
3394
3473
|
function registerSessionsCommands(sessionsCmd, getDb) {
|
|
@@ -3435,6 +3514,9 @@ function registerSessionsCommands(sessionsCmd, getDb) {
|
|
|
3435
3514
|
sessionsCmd.command("active").description("Show currently active Claude Code sessions.\nDetects live sessions by checking which JSONL transcript files\nwere recently modified in ~/.claude/projects/.").option("--minutes <n>", "Consider sessions active if modified within N minutes (default: 60)", "60").option("--json", "Output raw JSON instead of formatted display").action((opts) => {
|
|
3436
3515
|
cmdActive(getDb(), opts);
|
|
3437
3516
|
});
|
|
3517
|
+
sessionsCmd.command("clear-names").description("Recovery: wipe corrupted iTerm2 session name state.\nClears ~/.aibroker/session-names.json AND user.paiName from all live iTerm2 sessions.\nAfter running this, use /Name to re-label each tab.").option("--dry-run", "Preview what would be cleared without making changes").action(async (opts) => {
|
|
3518
|
+
await cmdClearNames(opts);
|
|
3519
|
+
});
|
|
3438
3520
|
sessionsCmd.command("auto-route").description("Auto-detect which project this session belongs to.\nTries: (1) path match in registry, (2) Notes/PAI.md marker walk, (3) topic detection.").option("--cwd <path>", "Working directory to detect from (default: process.cwd())").option("--context <text>", "Conversation context for topic-based fallback routing").option("--json", "Output raw JSON instead of formatted display").action(async (opts) => {
|
|
3439
3521
|
await cmdAutoRoute(opts);
|
|
3440
3522
|
});
|
|
@@ -9535,6 +9617,64 @@ async function searchHistory(query, maxResults) {
|
|
|
9535
9617
|
|
|
9536
9618
|
//#endregion
|
|
9537
9619
|
//#region src/cli/commands/main-resolver.ts
|
|
9620
|
+
const STATUS_PRIORITY = {
|
|
9621
|
+
live: 0,
|
|
9622
|
+
resumable: 1,
|
|
9623
|
+
"transcript-only": 2,
|
|
9624
|
+
stub: 3,
|
|
9625
|
+
orphan: 4
|
|
9626
|
+
};
|
|
9627
|
+
function buildDeduped(liveSessions, diskSessions) {
|
|
9628
|
+
const byName = /* @__PURE__ */ new Map();
|
|
9629
|
+
for (const s of liveSessions) {
|
|
9630
|
+
if (s.kind === "shell") continue;
|
|
9631
|
+
const rawName = s.paiName ?? s.name ?? s.sessionId.slice(0, 8);
|
|
9632
|
+
const key = rawName.toLowerCase();
|
|
9633
|
+
const entry = {
|
|
9634
|
+
name: rawName,
|
|
9635
|
+
status: "live",
|
|
9636
|
+
liveSessionId: s.sessionId,
|
|
9637
|
+
lastActivity: Date.now(),
|
|
9638
|
+
project: "",
|
|
9639
|
+
lastPrompt: ""
|
|
9640
|
+
};
|
|
9641
|
+
const existing = byName.get(key);
|
|
9642
|
+
if (!existing || STATUS_PRIORITY["live"] < STATUS_PRIORITY[existing.status]) byName.set(key, entry);
|
|
9643
|
+
}
|
|
9644
|
+
for (const s of diskSessions) {
|
|
9645
|
+
const rawName = s.friendlyName ?? s.shortId;
|
|
9646
|
+
const key = rawName.toLowerCase();
|
|
9647
|
+
let status;
|
|
9648
|
+
if (s.resumable) status = "resumable";
|
|
9649
|
+
else if (s.sessionStatus === "transcript-only") status = "transcript-only";
|
|
9650
|
+
else if (s.sessionStatus === "stub") status = "stub";
|
|
9651
|
+
else status = "orphan";
|
|
9652
|
+
const entry = {
|
|
9653
|
+
name: rawName,
|
|
9654
|
+
status,
|
|
9655
|
+
diskSession: s,
|
|
9656
|
+
lastActivity: s.mtime,
|
|
9657
|
+
project: s.decodedPath ?? "",
|
|
9658
|
+
lastPrompt: s.lastUserPrompt ?? ""
|
|
9659
|
+
};
|
|
9660
|
+
const existing = byName.get(key);
|
|
9661
|
+
if (!existing) byName.set(key, entry);
|
|
9662
|
+
else {
|
|
9663
|
+
const existingPriority = STATUS_PRIORITY[existing.status];
|
|
9664
|
+
const newPriority = STATUS_PRIORITY[status];
|
|
9665
|
+
if (newPriority < existingPriority) byName.set(key, entry);
|
|
9666
|
+
else if (newPriority === existingPriority && entry.lastActivity > existing.lastActivity) byName.set(key, entry);
|
|
9667
|
+
}
|
|
9668
|
+
}
|
|
9669
|
+
const entries = Array.from(byName.values());
|
|
9670
|
+
entries.sort((a, b) => {
|
|
9671
|
+
const pa = STATUS_PRIORITY[a.status];
|
|
9672
|
+
const pb = STATUS_PRIORITY[b.status];
|
|
9673
|
+
if (pa !== pb) return pa - pb;
|
|
9674
|
+
return b.lastActivity - a.lastActivity;
|
|
9675
|
+
});
|
|
9676
|
+
return entries;
|
|
9677
|
+
}
|
|
9538
9678
|
function probeResume(uuid, cwd) {
|
|
9539
9679
|
const result = spawnSync("claude", [
|
|
9540
9680
|
"--resume",
|
|
@@ -9567,23 +9707,13 @@ function probeResume(uuid, cwd) {
|
|
|
9567
9707
|
};
|
|
9568
9708
|
return { ok: true };
|
|
9569
9709
|
}
|
|
9570
|
-
/**
|
|
9571
|
-
* Launch Claude for the given session. Handles resume probe + fallback to fresh.
|
|
9572
|
-
* Never returns on success (process.exit inside spawnSync block).
|
|
9573
|
-
*/
|
|
9574
9710
|
function launchSession(session, allSessions, dryRun) {
|
|
9575
9711
|
let resumableUuid;
|
|
9576
|
-
|
|
9577
|
-
if (session.
|
|
9578
|
-
resumableUuid = session.uuid;
|
|
9579
|
-
resumableSession = session;
|
|
9580
|
-
} else if (session.encodedDir) {
|
|
9712
|
+
if (session.resumable) resumableUuid = session.uuid;
|
|
9713
|
+
else if (session.encodedDir) {
|
|
9581
9714
|
const sameProject = allSessions.filter((s) => s.encodedDir === session.encodedDir && s.resumable);
|
|
9582
9715
|
sameProject.sort((a, b) => b.mtime - a.mtime);
|
|
9583
|
-
if (sameProject.length > 0)
|
|
9584
|
-
resumableSession = sameProject[0];
|
|
9585
|
-
resumableUuid = resumableSession.uuid;
|
|
9586
|
-
}
|
|
9716
|
+
if (sameProject.length > 0) resumableUuid = sameProject[0].uuid;
|
|
9587
9717
|
}
|
|
9588
9718
|
const rawDir = session.clcDirectory ?? session.registryRootPath ?? session.decodedPath;
|
|
9589
9719
|
let projectDir;
|
|
@@ -9664,11 +9794,6 @@ function launchSession(session, allSessions, dryRun) {
|
|
|
9664
9794
|
process.exit(result.status ?? 0);
|
|
9665
9795
|
}
|
|
9666
9796
|
}
|
|
9667
|
-
/**
|
|
9668
|
-
* Given a SessionMatch from history search, find the corresponding ScannedSession
|
|
9669
|
-
* (for launch). Falls back to a minimal synthetic session using the decodedPath
|
|
9670
|
-
* from the history entry's project field.
|
|
9671
|
-
*/
|
|
9672
9797
|
function matchToSession(match, allSessions) {
|
|
9673
9798
|
if (!match.sessionId) return null;
|
|
9674
9799
|
const catalogMatch = allSessions.find((s) => s.uuid === match.sessionId);
|
|
@@ -9703,6 +9828,15 @@ function shortenProject(p, maxLen = 44) {
|
|
|
9703
9828
|
if (!p || p.length <= maxLen) return p || dim("—");
|
|
9704
9829
|
return "…" + p.slice(-(maxLen - 1));
|
|
9705
9830
|
}
|
|
9831
|
+
function fmtStatus(s) {
|
|
9832
|
+
switch (s) {
|
|
9833
|
+
case "live": return chalk.green("live");
|
|
9834
|
+
case "resumable": return chalk.cyan("resumable");
|
|
9835
|
+
case "transcript-only": return chalk.dim("transcript");
|
|
9836
|
+
case "stub": return chalk.dim("stub");
|
|
9837
|
+
case "orphan": return chalk.dim("orphan");
|
|
9838
|
+
}
|
|
9839
|
+
}
|
|
9706
9840
|
async function askForChoice(max) {
|
|
9707
9841
|
return new Promise((resolve) => {
|
|
9708
9842
|
const rl = createInterface({
|
|
@@ -9728,51 +9862,36 @@ async function cmdMain(db, query, pickN, opts) {
|
|
|
9728
9862
|
try {
|
|
9729
9863
|
liveSessions = await fetchLiveSessions();
|
|
9730
9864
|
} catch {}
|
|
9731
|
-
const
|
|
9732
|
-
|
|
9733
|
-
if (claudeLive.length === 0 && recentDisk.length === 0) {
|
|
9865
|
+
const deduped = buildDeduped(liveSessions, allSessions);
|
|
9866
|
+
if (deduped.length === 0) {
|
|
9734
9867
|
console.log(warn("No sessions found. Start Claude Code in a project directory first."));
|
|
9735
9868
|
return;
|
|
9736
9869
|
}
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9870
|
+
console.log("\n" + header("Sessions") + "\n");
|
|
9871
|
+
const tableHeaders = [
|
|
9872
|
+
"#",
|
|
9873
|
+
"name",
|
|
9874
|
+
"status",
|
|
9875
|
+
"age",
|
|
9876
|
+
"project",
|
|
9877
|
+
"last prompt"
|
|
9878
|
+
];
|
|
9879
|
+
const tableRows = deduped.slice(0, maxResults).map((entry, i) => {
|
|
9880
|
+
const age = entry.status === "live" ? chalk.green("now") : dim(fmtAge(entry.lastActivity));
|
|
9881
|
+
const snippet = entry.lastPrompt.replace(/\n+/g, " ").trim().slice(0, 36);
|
|
9882
|
+
const project = entry.diskSession ? dim(shortenProject(entry.diskSession.friendlyName ?? entry.diskSession.decodedPath, 28)) : dim("—");
|
|
9883
|
+
return [
|
|
9746
9884
|
dim(String(i + 1)),
|
|
9747
|
-
chalk.
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
}
|
|
9753
|
-
if (recentDisk.length > 0) {
|
|
9754
|
-
console.log("\n" + header("Recent Sessions") + "\n");
|
|
9755
|
-
const diskHeaders = [
|
|
9756
|
-
"#",
|
|
9757
|
-
"id",
|
|
9758
|
-
"age",
|
|
9759
|
-
"project",
|
|
9760
|
-
"last prompt"
|
|
9885
|
+
chalk.white(entry.name),
|
|
9886
|
+
fmtStatus(entry.status),
|
|
9887
|
+
age,
|
|
9888
|
+
project,
|
|
9889
|
+
chalk.dim(snippet ? `"${snippet}"` : "—")
|
|
9761
9890
|
];
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
return [
|
|
9765
|
-
dim(String(i + 1)),
|
|
9766
|
-
chalk.cyan(s.shortId),
|
|
9767
|
-
dim(fmtAge(s.mtime)),
|
|
9768
|
-
dim(shortenProject(s.friendlyName ?? s.decodedPath, 30)),
|
|
9769
|
-
chalk.dim(snippet ? `"${snippet}"` : "—")
|
|
9770
|
-
];
|
|
9771
|
-
});
|
|
9772
|
-
console.log(renderTable(diskHeaders, diskRows));
|
|
9773
|
-
}
|
|
9891
|
+
});
|
|
9892
|
+
console.log(renderTable(tableHeaders, tableRows));
|
|
9774
9893
|
console.log();
|
|
9775
|
-
console.log(dim("
|
|
9894
|
+
console.log(dim(" Switch/resume/start: ") + chalk.white("pai <name>") + dim(" or ") + chalk.white("pai <uuid-prefix>"));
|
|
9776
9895
|
console.log();
|
|
9777
9896
|
return;
|
|
9778
9897
|
}
|
|
@@ -9789,39 +9908,94 @@ async function cmdMain(db, query, pickN, opts) {
|
|
|
9789
9908
|
}
|
|
9790
9909
|
}
|
|
9791
9910
|
{
|
|
9911
|
+
let liveSessions = [];
|
|
9912
|
+
try {
|
|
9913
|
+
liveSessions = await fetchLiveSessions();
|
|
9914
|
+
} catch {}
|
|
9915
|
+
const deduped = buildDeduped(liveSessions, allSessions);
|
|
9792
9916
|
const qLower = query.toLowerCase();
|
|
9793
|
-
const
|
|
9794
|
-
if (
|
|
9795
|
-
|
|
9796
|
-
|
|
9917
|
+
const exactMatch = deduped.find((e) => e.name.toLowerCase() === qLower);
|
|
9918
|
+
if (exactMatch) {
|
|
9919
|
+
if (exactMatch.status === "live" && exactMatch.liveSessionId) {
|
|
9920
|
+
if (opts.dryRun) {
|
|
9921
|
+
console.log("\n" + chalk.bold("Dry run — would switch iTerm tab:") + "\n");
|
|
9922
|
+
console.log(` target: ${exactMatch.name} (${exactMatch.liveSessionId.slice(0, 8)})`);
|
|
9923
|
+
console.log(` action: aibroker_switch + osascript iTerm activate`);
|
|
9924
|
+
console.log();
|
|
9925
|
+
return;
|
|
9926
|
+
}
|
|
9927
|
+
const result = await switchToSession(exactMatch.liveSessionId);
|
|
9928
|
+
if (result.ok) console.log(ok(`Switched to live session: ${chalk.white(exactMatch.name)}`));
|
|
9929
|
+
else {
|
|
9930
|
+
console.error(warn(`Could not switch via AIBroker: ${result.error ?? "unknown error"}`));
|
|
9931
|
+
console.error(dim(" Falling back to disk launch..."));
|
|
9932
|
+
if (exactMatch.diskSession) launchSession(exactMatch.diskSession, allSessions, opts.dryRun ?? false);
|
|
9933
|
+
}
|
|
9934
|
+
return;
|
|
9935
|
+
}
|
|
9936
|
+
if (exactMatch.diskSession) {
|
|
9937
|
+
launchSession(exactMatch.diskSession, allSessions, opts.dryRun ?? false);
|
|
9938
|
+
return;
|
|
9939
|
+
}
|
|
9797
9940
|
}
|
|
9798
|
-
const
|
|
9799
|
-
if (
|
|
9800
|
-
|
|
9801
|
-
|
|
9941
|
+
const partialMatches = deduped.filter((e) => e.name.toLowerCase().includes(qLower));
|
|
9942
|
+
if (partialMatches.length === 1) {
|
|
9943
|
+
const match = partialMatches[0];
|
|
9944
|
+
if (match.status === "live" && match.liveSessionId) {
|
|
9945
|
+
if (opts.dryRun) {
|
|
9946
|
+
console.log("\n" + chalk.bold("Dry run — would switch iTerm tab:") + "\n");
|
|
9947
|
+
console.log(` target: ${match.name} (${match.liveSessionId.slice(0, 8)})`);
|
|
9948
|
+
console.log(` action: aibroker_switch + osascript iTerm activate`);
|
|
9949
|
+
console.log();
|
|
9950
|
+
return;
|
|
9951
|
+
}
|
|
9952
|
+
const result = await switchToSession(match.liveSessionId);
|
|
9953
|
+
if (result.ok) console.log(ok(`Switched to live session: ${chalk.white(match.name)}`));
|
|
9954
|
+
else {
|
|
9955
|
+
console.error(warn(`Could not switch via AIBroker: ${result.error ?? "unknown error"}`));
|
|
9956
|
+
if (match.diskSession) launchSession(match.diskSession, allSessions, opts.dryRun ?? false);
|
|
9957
|
+
}
|
|
9958
|
+
return;
|
|
9959
|
+
}
|
|
9960
|
+
if (match.diskSession) {
|
|
9961
|
+
launchSession(match.diskSession, allSessions, opts.dryRun ?? false);
|
|
9962
|
+
return;
|
|
9963
|
+
}
|
|
9802
9964
|
}
|
|
9803
|
-
if (
|
|
9965
|
+
if (partialMatches.length > 1) {
|
|
9804
9966
|
console.log("\n" + header(`Sessions matching "${query}"`) + "\n");
|
|
9805
9967
|
const headers = [
|
|
9806
9968
|
"#",
|
|
9807
|
-
"id",
|
|
9808
|
-
"age",
|
|
9809
9969
|
"name",
|
|
9970
|
+
"status",
|
|
9971
|
+
"age",
|
|
9810
9972
|
"project"
|
|
9811
9973
|
];
|
|
9812
|
-
const rows =
|
|
9813
|
-
dim(
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9974
|
+
const rows = partialMatches.slice(0, maxResults).map((entry, i) => {
|
|
9975
|
+
const age = entry.status === "live" ? chalk.green("now") : dim(fmtAge(entry.lastActivity));
|
|
9976
|
+
const project = entry.diskSession ? dim(shortenProject(entry.diskSession.decodedPath, 36)) : dim("—");
|
|
9977
|
+
return [
|
|
9978
|
+
dim(String(i + 1)),
|
|
9979
|
+
chalk.white(entry.name),
|
|
9980
|
+
fmtStatus(entry.status),
|
|
9981
|
+
age,
|
|
9982
|
+
project
|
|
9983
|
+
];
|
|
9984
|
+
});
|
|
9819
9985
|
console.log(renderTable(headers, rows));
|
|
9820
9986
|
console.log();
|
|
9821
9987
|
if (pickN !== void 0) {
|
|
9822
9988
|
const idx = pickN - 1;
|
|
9823
|
-
if (idx >= 0 && idx <
|
|
9824
|
-
|
|
9989
|
+
if (idx >= 0 && idx < partialMatches.length) {
|
|
9990
|
+
const match = partialMatches[idx];
|
|
9991
|
+
if (match.status === "live" && match.liveSessionId) {
|
|
9992
|
+
if (!opts.dryRun) {
|
|
9993
|
+
await switchToSession(match.liveSessionId);
|
|
9994
|
+
console.log(ok(`Switched to live session: ${chalk.white(match.name)}`));
|
|
9995
|
+
}
|
|
9996
|
+
return;
|
|
9997
|
+
}
|
|
9998
|
+
if (match.diskSession) launchSession(match.diskSession, allSessions, opts.dryRun ?? false);
|
|
9825
9999
|
return;
|
|
9826
10000
|
}
|
|
9827
10001
|
console.error(err(`Invalid choice: ${pickN}`));
|
|
@@ -9829,17 +10003,34 @@ async function cmdMain(db, query, pickN, opts) {
|
|
|
9829
10003
|
return;
|
|
9830
10004
|
}
|
|
9831
10005
|
if (opts.auto) {
|
|
9832
|
-
|
|
10006
|
+
const match = partialMatches[0];
|
|
10007
|
+
if (match.status === "live" && match.liveSessionId) {
|
|
10008
|
+
if (!opts.dryRun) {
|
|
10009
|
+
await switchToSession(match.liveSessionId);
|
|
10010
|
+
console.log(ok(`Switched to live session: ${chalk.white(match.name)}`));
|
|
10011
|
+
}
|
|
10012
|
+
return;
|
|
10013
|
+
}
|
|
10014
|
+
if (match.diskSession) launchSession(match.diskSession, allSessions, opts.dryRun ?? false);
|
|
9833
10015
|
return;
|
|
9834
10016
|
}
|
|
9835
|
-
const choice = await askForChoice(Math.min(
|
|
9836
|
-
if (choice !== null)
|
|
10017
|
+
const choice = await askForChoice(Math.min(partialMatches.length, maxResults));
|
|
10018
|
+
if (choice !== null) {
|
|
10019
|
+
const match = partialMatches[choice - 1];
|
|
10020
|
+
if (match.status === "live" && match.liveSessionId) {
|
|
10021
|
+
await switchToSession(match.liveSessionId);
|
|
10022
|
+
console.log(ok(`Switched to live session: ${chalk.white(match.name)}`));
|
|
10023
|
+
return;
|
|
10024
|
+
}
|
|
10025
|
+
if (match.diskSession) launchSession(match.diskSession, allSessions, opts.dryRun ?? false);
|
|
10026
|
+
}
|
|
9837
10027
|
return;
|
|
9838
10028
|
}
|
|
9839
10029
|
}
|
|
9840
10030
|
if (!existsSync(HISTORY_FILE)) {
|
|
9841
10031
|
console.error(err("~/.claude/history.jsonl not found."));
|
|
9842
10032
|
console.error(dim(" No prompt history available for search."));
|
|
10033
|
+
console.error(dim(` Try: pai (no args) to see all sessions.`));
|
|
9843
10034
|
process.exitCode = 1;
|
|
9844
10035
|
return;
|
|
9845
10036
|
}
|
|
@@ -9848,7 +10039,7 @@ async function cmdMain(db, query, pickN, opts) {
|
|
|
9848
10039
|
if (matches.length === 0) {
|
|
9849
10040
|
console.log(warn(`No sessions found matching "${query}".`));
|
|
9850
10041
|
console.log(dim(` Try a shorter or different search term.`));
|
|
9851
|
-
console.log(dim(` Or run: `) + chalk.white("pai") + dim(" (no args) to see all
|
|
10042
|
+
console.log(dim(` Or run: `) + chalk.white("pai") + dim(" (no args) to see all sessions."));
|
|
9852
10043
|
return;
|
|
9853
10044
|
}
|
|
9854
10045
|
console.log("\n" + header(`Sessions matching "${query}"`) + "\n");
|
|
@@ -9917,13 +10108,12 @@ async function cmdMain(db, query, pickN, opts) {
|
|
|
9917
10108
|
//#endregion
|
|
9918
10109
|
//#region src/cli/index.ts
|
|
9919
10110
|
/**
|
|
9920
|
-
* PAI Knowledge OS — CLI entry point (v0.
|
|
10111
|
+
* PAI Knowledge OS — CLI entry point (v0.11.0 deduped sessions + universal pai <name>)
|
|
9921
10112
|
*
|
|
9922
|
-
*
|
|
9923
|
-
* pai →
|
|
9924
|
-
* pai <
|
|
10113
|
+
* Daily surface:
|
|
10114
|
+
* pai → deduped session listing (one row per name)
|
|
10115
|
+
* pai <name> → universal: switch live tab / resume / fresh
|
|
9925
10116
|
* pai <uuid-prefix> → direct session resume via filesystem scan
|
|
9926
|
-
* pai cd <name> → cd to project directory (no Claude launch)
|
|
9927
10117
|
* pai pause [all] → save state (or mass-pause every live session)
|
|
9928
10118
|
* pai end → finalize session
|
|
9929
10119
|
*
|
|
@@ -9953,28 +10143,25 @@ function getDb() {
|
|
|
9953
10143
|
}
|
|
9954
10144
|
const program = new Command();
|
|
9955
10145
|
program.name("pai").description("PAI Knowledge OS — Personal AI Infrastructure CLI").version(getVersion(), "-V, --version", "Print version and exit").addHelpText("after", `
|
|
9956
|
-
|
|
9957
|
-
pai
|
|
9958
|
-
pai <
|
|
9959
|
-
pai
|
|
9960
|
-
pai
|
|
9961
|
-
pai
|
|
9962
|
-
|
|
10146
|
+
Daily commands:
|
|
10147
|
+
pai List your sessions (deduped by name)
|
|
10148
|
+
pai <name> Switch / resume / fresh — universal
|
|
10149
|
+
pai pause Save state checkpoint for the current session
|
|
10150
|
+
pai pause all Pause every live Claude session at once
|
|
10151
|
+
pai end Finalize current session: save state + mark Completed
|
|
10152
|
+
|
|
10153
|
+
Subcommands (advanced):
|
|
10154
|
+
pai sessions ... Full session management
|
|
10155
|
+
pai projects ... Project management
|
|
10156
|
+
pai registry ... Registry maintenance
|
|
10157
|
+
pai memory ... Memory engine
|
|
10158
|
+
pai shell-init Shell integration (eval "$(pai shell-init)")
|
|
9963
10159
|
|
|
9964
10160
|
Examples:
|
|
9965
|
-
pai
|
|
9966
|
-
pai
|
|
10161
|
+
pai aibroker Switch to the live AIBroker tab in iTerm
|
|
10162
|
+
pai mdf Find sessions where you worked on MDF
|
|
9967
10163
|
pai 81c5c3dc Resume session by UUID prefix
|
|
9968
|
-
pai See
|
|
9969
|
-
|
|
9970
|
-
Power-user namespaces (run "pai sessions --help" etc. for details):
|
|
9971
|
-
pai sessions ... Full session management (list, goto, info, ...)
|
|
9972
|
-
pai projects ... Project management (cd, rebind, list, ...)
|
|
9973
|
-
pai registry ... Registry maintenance (scan, ...)
|
|
9974
|
-
pai memory ... Memory engine (index, search, ...)
|
|
9975
|
-
|
|
9976
|
-
Shell integration:
|
|
9977
|
-
eval "$(pai shell-init)" # Add to ~/.zshrc for pai cd to work`);
|
|
10164
|
+
pai See all sessions (deduped, one row per name)`);
|
|
9978
10165
|
registerProjectsCommands(program.command("projects").description("Manage registered projects (list, cd, add, info, ...)"), getDb);
|
|
9979
10166
|
const sessionsCmd = program.command("sessions").description("Session management (list, goto, pause, ...)");
|
|
9980
10167
|
registerSessionsCommands(sessionsCmd, getDb);
|
|
@@ -10012,10 +10199,10 @@ program.command("pause [target]").description("Save state and display safe-exit
|
|
|
10012
10199
|
program.command("end").description("Finalize a session: save state, mark note Completed, display safe-exit instructions.\nLong form: pai sessions end").option("--dry-run", "Preview all changes without writing them").action((opts) => {
|
|
10013
10200
|
cmdEnd(getDb(), opts);
|
|
10014
10201
|
});
|
|
10015
|
-
program.command("resume <name>").description("Go to a session by name or UUID: resume if resumable, start fresh otherwise.\
|
|
10202
|
+
program.command("resume <name>", { hidden: true }).description("Go to a session by name or UUID: resume if resumable, start fresh otherwise.\nPrefer: pai <name> | Long form: pai sessions goto <name>").option("--dry-run", "Print the exact argv and cwd, then exit without launching").action((name, opts) => {
|
|
10016
10203
|
cmdGoto(getDb(), name, { dryRun: opts.dryRun });
|
|
10017
10204
|
});
|
|
10018
|
-
program.command("cd <identifier>").description("cd to a project directory (shell wrapper handles the actual cd).\nLong form: pai projects cd <identifier>\nThe shell function installed by pai shell-init intercepts this command\nand calls builtin cd with the resolved path.\nAuto-detects moved projects when the registered path no longer exists.").action((identifier) => {
|
|
10205
|
+
program.command("cd <identifier>", { hidden: true }).description("cd to a project directory (shell wrapper handles the actual cd).\nLong form: pai projects cd <identifier>\nThe shell function installed by pai shell-init intercepts this command\nand calls builtin cd with the resolved path.\nAuto-detects moved projects when the registered path no longer exists.").action((identifier) => {
|
|
10019
10206
|
const db = getDb();
|
|
10020
10207
|
const project = resolveIdentifier(db, identifier);
|
|
10021
10208
|
if (!project) {
|