@tekmidian/pai 0.10.1 → 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 +237 -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));
|
|
@@ -9594,6 +9617,64 @@ async function searchHistory(query, maxResults) {
|
|
|
9594
9617
|
|
|
9595
9618
|
//#endregion
|
|
9596
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
|
+
}
|
|
9597
9678
|
function probeResume(uuid, cwd) {
|
|
9598
9679
|
const result = spawnSync("claude", [
|
|
9599
9680
|
"--resume",
|
|
@@ -9626,23 +9707,13 @@ function probeResume(uuid, cwd) {
|
|
|
9626
9707
|
};
|
|
9627
9708
|
return { ok: true };
|
|
9628
9709
|
}
|
|
9629
|
-
/**
|
|
9630
|
-
* Launch Claude for the given session. Handles resume probe + fallback to fresh.
|
|
9631
|
-
* Never returns on success (process.exit inside spawnSync block).
|
|
9632
|
-
*/
|
|
9633
9710
|
function launchSession(session, allSessions, dryRun) {
|
|
9634
9711
|
let resumableUuid;
|
|
9635
|
-
|
|
9636
|
-
if (session.
|
|
9637
|
-
resumableUuid = session.uuid;
|
|
9638
|
-
resumableSession = session;
|
|
9639
|
-
} else if (session.encodedDir) {
|
|
9712
|
+
if (session.resumable) resumableUuid = session.uuid;
|
|
9713
|
+
else if (session.encodedDir) {
|
|
9640
9714
|
const sameProject = allSessions.filter((s) => s.encodedDir === session.encodedDir && s.resumable);
|
|
9641
9715
|
sameProject.sort((a, b) => b.mtime - a.mtime);
|
|
9642
|
-
if (sameProject.length > 0)
|
|
9643
|
-
resumableSession = sameProject[0];
|
|
9644
|
-
resumableUuid = resumableSession.uuid;
|
|
9645
|
-
}
|
|
9716
|
+
if (sameProject.length > 0) resumableUuid = sameProject[0].uuid;
|
|
9646
9717
|
}
|
|
9647
9718
|
const rawDir = session.clcDirectory ?? session.registryRootPath ?? session.decodedPath;
|
|
9648
9719
|
let projectDir;
|
|
@@ -9723,11 +9794,6 @@ function launchSession(session, allSessions, dryRun) {
|
|
|
9723
9794
|
process.exit(result.status ?? 0);
|
|
9724
9795
|
}
|
|
9725
9796
|
}
|
|
9726
|
-
/**
|
|
9727
|
-
* Given a SessionMatch from history search, find the corresponding ScannedSession
|
|
9728
|
-
* (for launch). Falls back to a minimal synthetic session using the decodedPath
|
|
9729
|
-
* from the history entry's project field.
|
|
9730
|
-
*/
|
|
9731
9797
|
function matchToSession(match, allSessions) {
|
|
9732
9798
|
if (!match.sessionId) return null;
|
|
9733
9799
|
const catalogMatch = allSessions.find((s) => s.uuid === match.sessionId);
|
|
@@ -9762,6 +9828,15 @@ function shortenProject(p, maxLen = 44) {
|
|
|
9762
9828
|
if (!p || p.length <= maxLen) return p || dim("—");
|
|
9763
9829
|
return "…" + p.slice(-(maxLen - 1));
|
|
9764
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
|
+
}
|
|
9765
9840
|
async function askForChoice(max) {
|
|
9766
9841
|
return new Promise((resolve) => {
|
|
9767
9842
|
const rl = createInterface({
|
|
@@ -9787,51 +9862,36 @@ async function cmdMain(db, query, pickN, opts) {
|
|
|
9787
9862
|
try {
|
|
9788
9863
|
liveSessions = await fetchLiveSessions();
|
|
9789
9864
|
} catch {}
|
|
9790
|
-
const
|
|
9791
|
-
|
|
9792
|
-
if (claudeLive.length === 0 && recentDisk.length === 0) {
|
|
9865
|
+
const deduped = buildDeduped(liveSessions, allSessions);
|
|
9866
|
+
if (deduped.length === 0) {
|
|
9793
9867
|
console.log(warn("No sessions found. Start Claude Code in a project directory first."));
|
|
9794
9868
|
return;
|
|
9795
9869
|
}
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
|
|
9803
|
-
|
|
9804
|
-
|
|
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 [
|
|
9805
9884
|
dim(String(i + 1)),
|
|
9806
|
-
chalk.
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
}
|
|
9812
|
-
if (recentDisk.length > 0) {
|
|
9813
|
-
console.log("\n" + header("Recent Sessions") + "\n");
|
|
9814
|
-
const diskHeaders = [
|
|
9815
|
-
"#",
|
|
9816
|
-
"id",
|
|
9817
|
-
"age",
|
|
9818
|
-
"project",
|
|
9819
|
-
"last prompt"
|
|
9885
|
+
chalk.white(entry.name),
|
|
9886
|
+
fmtStatus(entry.status),
|
|
9887
|
+
age,
|
|
9888
|
+
project,
|
|
9889
|
+
chalk.dim(snippet ? `"${snippet}"` : "—")
|
|
9820
9890
|
];
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
return [
|
|
9824
|
-
dim(String(i + 1)),
|
|
9825
|
-
chalk.cyan(s.shortId),
|
|
9826
|
-
dim(fmtAge(s.mtime)),
|
|
9827
|
-
dim(shortenProject(s.friendlyName ?? s.decodedPath, 30)),
|
|
9828
|
-
chalk.dim(snippet ? `"${snippet}"` : "—")
|
|
9829
|
-
];
|
|
9830
|
-
});
|
|
9831
|
-
console.log(renderTable(diskHeaders, diskRows));
|
|
9832
|
-
}
|
|
9891
|
+
});
|
|
9892
|
+
console.log(renderTable(tableHeaders, tableRows));
|
|
9833
9893
|
console.log();
|
|
9834
|
-
console.log(dim("
|
|
9894
|
+
console.log(dim(" Switch/resume/start: ") + chalk.white("pai <name>") + dim(" or ") + chalk.white("pai <uuid-prefix>"));
|
|
9835
9895
|
console.log();
|
|
9836
9896
|
return;
|
|
9837
9897
|
}
|
|
@@ -9848,39 +9908,94 @@ async function cmdMain(db, query, pickN, opts) {
|
|
|
9848
9908
|
}
|
|
9849
9909
|
}
|
|
9850
9910
|
{
|
|
9911
|
+
let liveSessions = [];
|
|
9912
|
+
try {
|
|
9913
|
+
liveSessions = await fetchLiveSessions();
|
|
9914
|
+
} catch {}
|
|
9915
|
+
const deduped = buildDeduped(liveSessions, allSessions);
|
|
9851
9916
|
const qLower = query.toLowerCase();
|
|
9852
|
-
const
|
|
9853
|
-
if (
|
|
9854
|
-
|
|
9855
|
-
|
|
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
|
+
}
|
|
9856
9940
|
}
|
|
9857
|
-
const
|
|
9858
|
-
if (
|
|
9859
|
-
|
|
9860
|
-
|
|
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
|
+
}
|
|
9861
9964
|
}
|
|
9862
|
-
if (
|
|
9965
|
+
if (partialMatches.length > 1) {
|
|
9863
9966
|
console.log("\n" + header(`Sessions matching "${query}"`) + "\n");
|
|
9864
9967
|
const headers = [
|
|
9865
9968
|
"#",
|
|
9866
|
-
"id",
|
|
9867
|
-
"age",
|
|
9868
9969
|
"name",
|
|
9970
|
+
"status",
|
|
9971
|
+
"age",
|
|
9869
9972
|
"project"
|
|
9870
9973
|
];
|
|
9871
|
-
const rows =
|
|
9872
|
-
dim(
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
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
|
+
});
|
|
9878
9985
|
console.log(renderTable(headers, rows));
|
|
9879
9986
|
console.log();
|
|
9880
9987
|
if (pickN !== void 0) {
|
|
9881
9988
|
const idx = pickN - 1;
|
|
9882
|
-
if (idx >= 0 && idx <
|
|
9883
|
-
|
|
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);
|
|
9884
9999
|
return;
|
|
9885
10000
|
}
|
|
9886
10001
|
console.error(err(`Invalid choice: ${pickN}`));
|
|
@@ -9888,17 +10003,34 @@ async function cmdMain(db, query, pickN, opts) {
|
|
|
9888
10003
|
return;
|
|
9889
10004
|
}
|
|
9890
10005
|
if (opts.auto) {
|
|
9891
|
-
|
|
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);
|
|
9892
10015
|
return;
|
|
9893
10016
|
}
|
|
9894
|
-
const choice = await askForChoice(Math.min(
|
|
9895
|
-
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
|
+
}
|
|
9896
10027
|
return;
|
|
9897
10028
|
}
|
|
9898
10029
|
}
|
|
9899
10030
|
if (!existsSync(HISTORY_FILE)) {
|
|
9900
10031
|
console.error(err("~/.claude/history.jsonl not found."));
|
|
9901
10032
|
console.error(dim(" No prompt history available for search."));
|
|
10033
|
+
console.error(dim(` Try: pai (no args) to see all sessions.`));
|
|
9902
10034
|
process.exitCode = 1;
|
|
9903
10035
|
return;
|
|
9904
10036
|
}
|
|
@@ -9907,7 +10039,7 @@ async function cmdMain(db, query, pickN, opts) {
|
|
|
9907
10039
|
if (matches.length === 0) {
|
|
9908
10040
|
console.log(warn(`No sessions found matching "${query}".`));
|
|
9909
10041
|
console.log(dim(` Try a shorter or different search term.`));
|
|
9910
|
-
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."));
|
|
9911
10043
|
return;
|
|
9912
10044
|
}
|
|
9913
10045
|
console.log("\n" + header(`Sessions matching "${query}"`) + "\n");
|
|
@@ -9976,13 +10108,12 @@ async function cmdMain(db, query, pickN, opts) {
|
|
|
9976
10108
|
//#endregion
|
|
9977
10109
|
//#region src/cli/index.ts
|
|
9978
10110
|
/**
|
|
9979
|
-
* PAI Knowledge OS — CLI entry point (v0.
|
|
10111
|
+
* PAI Knowledge OS — CLI entry point (v0.11.0 deduped sessions + universal pai <name>)
|
|
9980
10112
|
*
|
|
9981
|
-
*
|
|
9982
|
-
* pai →
|
|
9983
|
-
* pai <
|
|
10113
|
+
* Daily surface:
|
|
10114
|
+
* pai → deduped session listing (one row per name)
|
|
10115
|
+
* pai <name> → universal: switch live tab / resume / fresh
|
|
9984
10116
|
* pai <uuid-prefix> → direct session resume via filesystem scan
|
|
9985
|
-
* pai cd <name> → cd to project directory (no Claude launch)
|
|
9986
10117
|
* pai pause [all] → save state (or mass-pause every live session)
|
|
9987
10118
|
* pai end → finalize session
|
|
9988
10119
|
*
|
|
@@ -10012,28 +10143,25 @@ function getDb() {
|
|
|
10012
10143
|
}
|
|
10013
10144
|
const program = new Command();
|
|
10014
10145
|
program.name("pai").description("PAI Knowledge OS — Personal AI Infrastructure CLI").version(getVersion(), "-V, --version", "Print version and exit").addHelpText("after", `
|
|
10015
|
-
|
|
10016
|
-
pai
|
|
10017
|
-
pai <
|
|
10018
|
-
pai
|
|
10019
|
-
pai
|
|
10020
|
-
pai
|
|
10021
|
-
|
|
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)")
|
|
10022
10159
|
|
|
10023
10160
|
Examples:
|
|
10024
|
-
pai
|
|
10025
|
-
pai
|
|
10161
|
+
pai aibroker Switch to the live AIBroker tab in iTerm
|
|
10162
|
+
pai mdf Find sessions where you worked on MDF
|
|
10026
10163
|
pai 81c5c3dc Resume session by UUID prefix
|
|
10027
|
-
pai See
|
|
10028
|
-
|
|
10029
|
-
Power-user namespaces (run "pai sessions --help" etc. for details):
|
|
10030
|
-
pai sessions ... Full session management (list, goto, info, ...)
|
|
10031
|
-
pai projects ... Project management (cd, rebind, list, ...)
|
|
10032
|
-
pai registry ... Registry maintenance (scan, ...)
|
|
10033
|
-
pai memory ... Memory engine (index, search, ...)
|
|
10034
|
-
|
|
10035
|
-
Shell integration:
|
|
10036
|
-
eval "$(pai shell-init)" # Add to ~/.zshrc for pai cd to work`);
|
|
10164
|
+
pai See all sessions (deduped, one row per name)`);
|
|
10037
10165
|
registerProjectsCommands(program.command("projects").description("Manage registered projects (list, cd, add, info, ...)"), getDb);
|
|
10038
10166
|
const sessionsCmd = program.command("sessions").description("Session management (list, goto, pause, ...)");
|
|
10039
10167
|
registerSessionsCommands(sessionsCmd, getDb);
|
|
@@ -10071,10 +10199,10 @@ program.command("pause [target]").description("Save state and display safe-exit
|
|
|
10071
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) => {
|
|
10072
10200
|
cmdEnd(getDb(), opts);
|
|
10073
10201
|
});
|
|
10074
|
-
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) => {
|
|
10075
10203
|
cmdGoto(getDb(), name, { dryRun: opts.dryRun });
|
|
10076
10204
|
});
|
|
10077
|
-
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) => {
|
|
10078
10206
|
const db = getDb();
|
|
10079
10207
|
const project = resolveIdentifier(db, identifier);
|
|
10080
10208
|
if (!project) {
|