@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 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: Think in Topics
302
+ ### The Core Idea: One Entry Point
303
303
 
304
- The user's mental model is simple: "I did something on a given subject. Where was I working on it? Let me continue."
304
+ `pai <name>` is the universal session command. It does the right thing based on session state:
305
305
 
306
- PAI v0.10.0 is built around this. You think in **topics**, not project names or session UUIDs. PAI handles the lookup.
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 mdf # Find every session where you worked on MDF
310
- pai solar panels # Free-text search across your prompt history
311
- pai 0856d40b # Resume by UUID prefix (from pai sessions)
312
- pai # Show all recent sessions (interactive picker)
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
- When you type `pai mdf`, PAI checks:
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 # Recent sessions picker (live + disk)
328
- pai <topic> # Find + launch session by topic/keyword/UUID
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
- ### Finding Sessions
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
- `pai` (no args) shows two sections:
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
- **Live Sessions** pulled from AIBroker in real time. One row per active iTerm2 pane running Claude Code. Columns: `#`, `id`, `name`, `at prompt`.
350
+ Status values: `live` (active iTerm tab), `resumable` (clean snapshot on disk), `transcript` (history available, not resumable), `stub` (empty or minimal).
347
351
 
348
- **Recent Sessions** — disk scan of `~/.claude/projects/`, sorted by last-modified.
352
+ ### Finding Sessions by Topic
349
353
 
350
- `pai <topic>` searches your prompt history in `~/.claude/history.jsonl`, groups results by session, and shows a table:
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
- Type the number and press Enter. PAI launches Claude, resumes if possible, starts fresh if not.
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 for power users:
371
+ The full session management namespace is still available:
370
372
 
371
373
  ```bash
372
- pai sessions # Live + disk listing (same as bare pai, with more columns)
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>)
@@ -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
- let resumableSession;
9636
- if (session.resumable) {
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 claudeLive = liveSessions.filter((s) => s.kind !== "shell");
9791
- const recentDisk = allSessions.slice(0, maxResults);
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
- if (claudeLive.length > 0) {
9797
- console.log("\n" + header("Live Sessions") + "\n");
9798
- const liveHeaders = [
9799
- "#",
9800
- "id",
9801
- "name",
9802
- "at prompt"
9803
- ];
9804
- const liveRows = claudeLive.map((s, i) => [
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.cyan(s.sessionId.slice(0, 8)),
9807
- s.paiName ?? s.name ?? dim("—"),
9808
- s.atPrompt ? chalk.green("yes") : chalk.dim("no")
9809
- ]);
9810
- console.log(renderTable(liveHeaders, liveRows));
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
- const diskRows = recentDisk.map((s, i) => {
9822
- const snippet = s.lastUserPrompt.replace(/\n+/g, " ").trim().slice(0, 40);
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(" Resume a session: ") + chalk.white("pai <topic>") + dim(" or ") + chalk.white("pai <id>"));
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 byExact = allSessions.filter((s) => s.friendlyName && s.friendlyName.toLowerCase() === qLower);
9853
- if (byExact.length >= 1) {
9854
- launchSession(byExact[0], allSessions, opts.dryRun ?? false);
9855
- return;
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 byPartial = allSessions.filter((s) => s.friendlyName && s.friendlyName.toLowerCase().includes(qLower));
9858
- if (byPartial.length === 1) {
9859
- launchSession(byPartial[0], allSessions, opts.dryRun ?? false);
9860
- return;
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 (byPartial.length > 1) {
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 = byPartial.slice(0, maxResults).map((s, i) => [
9872
- dim(String(i + 1)),
9873
- chalk.cyan(s.shortId),
9874
- dim(fmtAge(s.mtime)),
9875
- s.friendlyName ?? dim("—"),
9876
- dim(shortenProject(s.decodedPath, 36))
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 < byPartial.length) {
9883
- launchSession(byPartial[idx], allSessions, opts.dryRun ?? false);
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
- launchSession(byPartial[0], allSessions, opts.dryRun ?? false);
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(byPartial.length, maxResults));
9895
- if (choice !== null) launchSession(byPartial[choice - 1], allSessions, opts.dryRun ?? false);
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 recent sessions."));
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.10.0 topic-first redesign)
10111
+ * PAI Knowledge OS — CLI entry point (v0.11.0 deduped sessions + universal pai <name>)
9980
10112
  *
9981
- * Top-level surface:
9982
- * pai → recent sessions picker
9983
- * pai <topic> history search + candidate picker + launch
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
- Usage:
10016
- pai Show recent sessions (interactive picker)
10017
- pai <topic> Find sessions by topic and launch the chosen one
10018
- pai <uuid-prefix> Resume a specific session by UUID
10019
- pai cd <name> cd to a project directory (no Claude launch)
10020
- pai pause [all] Save state (or mass-pause every live session)
10021
- pai end Finalize session: save state + mark note Completed
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 mdf Find all sessions where you worked on MDF
10025
- pai solar panels Free-text search across your prompt history
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 the 20 most recent sessions
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.\nAlso: pai <name> (shorter) | pai sessions goto <name> (long form)").option("--dry-run", "Print the exact argv and cwd, then exit without launching").action((name, opts) => {
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) {