@tekmidian/pai 0.9.14 → 0.9.16
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/dist/cli/index.mjs +646 -118
- package/dist/cli/index.mjs.map +1 -1
- package/dist/daemon/index.mjs +5 -5
- package/dist/{daemon-BryEC3as.mjs → daemon-CSKj8Xm9.mjs} +10 -10
- package/dist/{daemon-BryEC3as.mjs.map → daemon-CSKj8Xm9.mjs.map} +1 -1
- package/dist/{helpers-OCVFgprQ.mjs → helpers-crDEr6S2.mjs} +3 -3
- package/dist/helpers-crDEr6S2.mjs.map +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{indexer-backend-BrrCnasS.mjs → indexer-backend-DcYThufd.mjs} +2 -2
- package/dist/{indexer-backend-BrrCnasS.mjs.map → indexer-backend-DcYThufd.mjs.map} +1 -1
- package/dist/{kg-extraction-r5KkTmWY.mjs → kg-extraction-iR1BKKWK.mjs} +37 -3
- package/dist/kg-extraction-iR1BKKWK.mjs.map +1 -0
- package/dist/{sync-CdHSL9Kc.mjs → sync-CmBKOL3K.mjs} +2 -2
- package/dist/{sync-CdHSL9Kc.mjs.map → sync-CmBKOL3K.mjs.map} +1 -1
- package/dist/{vault-indexer-DsPC6wQf.mjs → vault-indexer-vNnL3Azb.mjs} +2 -2
- package/dist/{vault-indexer-DsPC6wQf.mjs.map → vault-indexer-vNnL3Azb.mjs.map} +1 -1
- package/dist/{work-queue-worker-94INtUiz.mjs → work-queue-worker-BxsBtz6j.mjs} +2 -2
- package/dist/{work-queue-worker-94INtUiz.mjs.map → work-queue-worker-BxsBtz6j.mjs.map} +1 -1
- package/package.json +1 -1
- package/dist/helpers-OCVFgprQ.mjs.map +0 -1
- package/dist/kg-extraction-r5KkTmWY.mjs.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -4,13 +4,13 @@ import { _ as warn, a as fmtDate, c as ok, d as scaffoldProjectDirs, f as shorte
|
|
|
4
4
|
import { a as slugify$1, i as parseSessionFilename } from "../migrate-fLD6rAdO.mjs";
|
|
5
5
|
import { n as ensurePaiMarker } from "../pai-marker-CXQPX2P6.mjs";
|
|
6
6
|
import { n as openFederation } from "../db-CYmBWcjh.mjs";
|
|
7
|
-
import "../helpers-
|
|
8
|
-
import { i as indexProject, n as indexAll, t as embedChunks } from "../sync-
|
|
7
|
+
import "../helpers-crDEr6S2.mjs";
|
|
8
|
+
import { i as indexProject, n as indexAll, t as embedChunks } from "../sync-CmBKOL3K.mjs";
|
|
9
9
|
import "../embeddings-DGRAPAYb.mjs";
|
|
10
10
|
import { t as STOP_WORDS } from "../stop-words-BaMEGVeY.mjs";
|
|
11
11
|
import { n as populateSlugs, r as searchMemory } from "../search-i2nlQ-JM.mjs";
|
|
12
12
|
import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "../detect-CdaA48EI.mjs";
|
|
13
|
-
import { a as resolveHome, c as upsertSession, n as cmdScan, o as saveScanConfig, r as loadScanConfig, s as upsertProject, t as extractAndStoreTriples } from "../kg-extraction-
|
|
13
|
+
import { a as resolveHome, c as upsertSession, n as cmdScan, o as saveScanConfig, r as loadScanConfig, s as upsertProject, t as extractAndStoreTriples } from "../kg-extraction-iR1BKKWK.mjs";
|
|
14
14
|
import "../indexer-D7MvSQPY.mjs";
|
|
15
15
|
import { t as PaiClient } from "../ipc-client-C3pjwy2m.mjs";
|
|
16
16
|
import { a as expandHome, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig, t as CONFIG_DIR } from "../config-DqBY3aT0.mjs";
|
|
@@ -20,8 +20,10 @@ import { appendFileSync, chmodSync, copyFileSync, existsSync, lstatSync, mkdirSy
|
|
|
20
20
|
import { homedir, platform, tmpdir } from "node:os";
|
|
21
21
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
22
22
|
import chalk from "chalk";
|
|
23
|
+
import { randomUUID } from "node:crypto";
|
|
23
24
|
import { Command } from "commander";
|
|
24
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { connect } from "node:net";
|
|
25
27
|
import { execSync, spawnSync } from "node:child_process";
|
|
26
28
|
import { createInterface } from "node:readline";
|
|
27
29
|
import { createConnection } from "net";
|
|
@@ -1942,13 +1944,26 @@ function cmdHandover(db, projectSlug, numberOrLatest) {
|
|
|
1942
1944
|
* line of type "system". Sessions that only have a sessions/ counterpart cannot be
|
|
1943
1945
|
* resumed by Claude Code regardless of how much transcript content they have.
|
|
1944
1946
|
*
|
|
1945
|
-
*
|
|
1946
|
-
*
|
|
1947
|
-
*
|
|
1948
|
-
*
|
|
1949
|
-
*
|
|
1947
|
+
* Stale-UUID problem (fixed in this version):
|
|
1948
|
+
* The clc session.json registry stores ONE uuid per named session — the uuid Claude Code
|
|
1949
|
+
* had when the user last named the session. If the user resumes and the session gets a
|
|
1950
|
+
* new uuid (or they Ctrl+C and start fresh), the registry entry still points to the OLD
|
|
1951
|
+
* uuid. The scanner now resolves names to the MOST RECENT top-level jsonl in the project
|
|
1952
|
+
* directory, not the clc-cached uuid.
|
|
1950
1953
|
*
|
|
1951
|
-
*
|
|
1954
|
+
* Resolution strategy:
|
|
1955
|
+
* 1. Walk top-level <project>/<uuid>.jsonl files (Pass 1).
|
|
1956
|
+
* 2. For each, attach the clc name if the uuid matches (exact hit).
|
|
1957
|
+
* 3. After Pass 1, for every clc registry entry:
|
|
1958
|
+
* a. If the cached uuid was found → already handled.
|
|
1959
|
+
* b. Find the encodedDir for this entry's directory.
|
|
1960
|
+
* c. Check ALL sessions in that encodedDir (already in our Pass-1 results).
|
|
1961
|
+
* d. Pick the MOST RECENT resumable session in that dir and attach the name to it.
|
|
1962
|
+
* If no resumable session, fall through to transcript-only pass.
|
|
1963
|
+
* 4. This means the displayed uuid for "Jobs Matthias" is always today's active session,
|
|
1964
|
+
* not the stale cached one.
|
|
1965
|
+
*
|
|
1966
|
+
* Used by: pai sessions, pai resume
|
|
1952
1967
|
*/
|
|
1953
1968
|
const CLC_SESSIONS_FILE = join(homedir(), ".claude", "session.json");
|
|
1954
1969
|
/** Load clc's session registry → uuid → ClcInfo map. Verbatim, never slugified. */
|
|
@@ -2082,7 +2097,11 @@ function resolveFilter(opts) {
|
|
|
2082
2097
|
* Scan ~/.claude/projects/ for all Claude Code sessions.
|
|
2083
2098
|
*
|
|
2084
2099
|
* Pass 1: walk top-level <project>/<uuid>.jsonl files (the resumability source).
|
|
2085
|
-
* Pass 2:
|
|
2100
|
+
* Pass 2: handle clc registry entries whose cached UUID was not found in Pass 1.
|
|
2101
|
+
* For each such entry, scan the entry's project dir for the FRESHEST session
|
|
2102
|
+
* and attach the registry name to it. This fixes the stale-UUID bug where
|
|
2103
|
+
* clc's session.json points to an old uuid after a fresh start.
|
|
2104
|
+
* Pass 3: any remaining clc entries with truly no top-level jsonl → transcript-only.
|
|
2086
2105
|
*
|
|
2087
2106
|
* Filter modes:
|
|
2088
2107
|
* "named" — resumable + registry-known stubs + transcript-only (default)
|
|
@@ -2099,12 +2118,14 @@ function scanSessions(db, opts = {}) {
|
|
|
2099
2118
|
const rootPathMap = buildRegistryRootPathMap(db);
|
|
2100
2119
|
const results = [];
|
|
2101
2120
|
const seenUuids = /* @__PURE__ */ new Set();
|
|
2121
|
+
const attachedClcUuids = /* @__PURE__ */ new Set();
|
|
2102
2122
|
let encodedDirs;
|
|
2103
2123
|
try {
|
|
2104
2124
|
encodedDirs = readdirSync(CLAUDE_PROJECTS_DIR);
|
|
2105
2125
|
} catch {
|
|
2106
2126
|
return [];
|
|
2107
2127
|
}
|
|
2128
|
+
const sessionsByEncodedDir = /* @__PURE__ */ new Map();
|
|
2108
2129
|
for (const encodedDir of encodedDirs) {
|
|
2109
2130
|
const projectDir = join(CLAUDE_PROJECTS_DIR, encodedDir);
|
|
2110
2131
|
try {
|
|
@@ -2131,8 +2152,7 @@ function scanSessions(db, opts = {}) {
|
|
|
2131
2152
|
const clcInfo = clcInfoMap.get(uuid);
|
|
2132
2153
|
const inRegistry = !!clcInfo;
|
|
2133
2154
|
const sessionStatus = resumable ? "resumable" : inRegistry ? "stub" : "orphan";
|
|
2134
|
-
|
|
2135
|
-
if (filterMode === "named" && !resumable && !inRegistry) continue;
|
|
2155
|
+
const passesFilter = filterMode === "resumable" ? resumable : filterMode === "named" ? resumable || inRegistry : true;
|
|
2136
2156
|
const sessionJsonlPath = join(projectDir, "sessions", `${uuid}.jsonl`);
|
|
2137
2157
|
const hasTranscript = existsSync(sessionJsonlPath);
|
|
2138
2158
|
const transcript = hasTranscript ? parseTranscript(sessionJsonlPath) : {
|
|
@@ -2143,8 +2163,7 @@ function scanSessions(db, opts = {}) {
|
|
|
2143
2163
|
};
|
|
2144
2164
|
const mtime = topInfo.mtime || transcript.mtime;
|
|
2145
2165
|
const friendlyName = clcInfo?.name ?? transcript.aiTitle ?? projectBasename ?? void 0;
|
|
2146
|
-
|
|
2147
|
-
results.push({
|
|
2166
|
+
const session = {
|
|
2148
2167
|
uuid,
|
|
2149
2168
|
shortId: uuid.slice(0, 8),
|
|
2150
2169
|
encodedDir,
|
|
@@ -2163,48 +2182,90 @@ function scanSessions(db, opts = {}) {
|
|
|
2163
2182
|
friendlyName,
|
|
2164
2183
|
clcDirectory: clcInfo?.directory,
|
|
2165
2184
|
registryRootPath
|
|
2166
|
-
}
|
|
2185
|
+
};
|
|
2186
|
+
if (!sessionsByEncodedDir.has(encodedDir)) sessionsByEncodedDir.set(encodedDir, []);
|
|
2187
|
+
sessionsByEncodedDir.get(encodedDir).push(session);
|
|
2188
|
+
seenUuids.add(uuid);
|
|
2189
|
+
if (inRegistry) attachedClcUuids.add(uuid);
|
|
2190
|
+
if (passesFilter) results.push(session);
|
|
2167
2191
|
}
|
|
2168
2192
|
}
|
|
2169
|
-
if (filterMode !== "resumable") for (const [
|
|
2170
|
-
if (
|
|
2193
|
+
if (filterMode !== "resumable") for (const [cachedUuid, clcInfo] of clcInfoMap) {
|
|
2194
|
+
if (attachedClcUuids.has(cachedUuid)) continue;
|
|
2171
2195
|
let foundEncodedDir;
|
|
2172
|
-
|
|
2173
|
-
if (clcInfo.directory) try {
|
|
2196
|
+
if (clcInfo.directory) {
|
|
2174
2197
|
const real = realpathSyncSafe(clcInfo.directory);
|
|
2175
2198
|
if (real) {
|
|
2176
2199
|
const encoded = encodeProjectDir(real);
|
|
2177
|
-
|
|
2178
|
-
if (existsSync(candidateTranscript)) {
|
|
2179
|
-
foundEncodedDir = encoded;
|
|
2180
|
-
foundTranscriptPath = candidateTranscript;
|
|
2181
|
-
} else if (existsSync(join(CLAUDE_PROJECTS_DIR, encoded))) foundEncodedDir = encoded;
|
|
2200
|
+
if (existsSync(join(CLAUDE_PROJECTS_DIR, encoded))) foundEncodedDir = encoded;
|
|
2182
2201
|
}
|
|
2183
|
-
}
|
|
2184
|
-
if (!foundEncodedDir)
|
|
2185
|
-
const
|
|
2186
|
-
if (existsSync(candidateTranscript)) {
|
|
2202
|
+
}
|
|
2203
|
+
if (!foundEncodedDir) {
|
|
2204
|
+
for (const encodedDir of encodedDirs) if (existsSync(join(CLAUDE_PROJECTS_DIR, encodedDir, "sessions", `${cachedUuid}.jsonl`))) {
|
|
2187
2205
|
foundEncodedDir = encodedDir;
|
|
2188
|
-
foundTranscriptPath = candidateTranscript;
|
|
2189
2206
|
break;
|
|
2190
2207
|
}
|
|
2191
2208
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2209
|
+
if (foundEncodedDir) {
|
|
2210
|
+
const freshestResumable = (sessionsByEncodedDir.get(foundEncodedDir) ?? []).filter((s) => s.resumable && !s.friendlyName).sort((a, b) => b.mtime - a.mtime)[0];
|
|
2211
|
+
if (freshestResumable) {
|
|
2212
|
+
freshestResumable.friendlyName = clcInfo.name;
|
|
2213
|
+
freshestResumable.clcDirectory = freshestResumable.clcDirectory ?? clcInfo.directory;
|
|
2214
|
+
freshestResumable.sessionStatus = "resumable";
|
|
2215
|
+
attachedClcUuids.add(freshestResumable.uuid);
|
|
2216
|
+
if (!seenUuids.has(freshestResumable.uuid) || !results.includes(freshestResumable)) {
|
|
2217
|
+
if (!results.includes(freshestResumable)) results.push(freshestResumable);
|
|
2218
|
+
}
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
if (!foundEncodedDir) {
|
|
2223
|
+
const encodedDir = "";
|
|
2224
|
+
const decodedPath = clcInfo.directory ?? cachedUuid;
|
|
2225
|
+
const registryRootPath = void 0;
|
|
2226
|
+
const mtime = {
|
|
2227
|
+
userLines: 0,
|
|
2228
|
+
lastUserPrompt: "",
|
|
2229
|
+
msgCount: 0,
|
|
2230
|
+
mtime: 0
|
|
2231
|
+
}.mtime;
|
|
2232
|
+
seenUuids.add(cachedUuid);
|
|
2233
|
+
results.push({
|
|
2234
|
+
uuid: cachedUuid,
|
|
2235
|
+
shortId: cachedUuid.slice(0, 8),
|
|
2236
|
+
encodedDir,
|
|
2237
|
+
decodedPath,
|
|
2238
|
+
topLevelPath: "",
|
|
2239
|
+
topLevelSystemLines: 0,
|
|
2240
|
+
topLevelSize: 0,
|
|
2241
|
+
resumable: false,
|
|
2242
|
+
sessionStatus: "transcript-only",
|
|
2243
|
+
sessionJsonlPath: void 0,
|
|
2244
|
+
userLines: 0,
|
|
2245
|
+
lastUserPrompt: "",
|
|
2246
|
+
msgCount: 0,
|
|
2247
|
+
mtime,
|
|
2248
|
+
friendlyName: clcInfo.name,
|
|
2249
|
+
clcDirectory: clcInfo.directory,
|
|
2250
|
+
registryRootPath
|
|
2251
|
+
});
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
const foundTranscriptPath = existsSync(join(CLAUDE_PROJECTS_DIR, foundEncodedDir, "sessions", `${cachedUuid}.jsonl`)) ? join(CLAUDE_PROJECTS_DIR, foundEncodedDir, "sessions", `${cachedUuid}.jsonl`) : void 0;
|
|
2255
|
+
const decodedPath = clcInfo.directory ?? smartDecodeDir(foundEncodedDir) ?? foundEncodedDir.replace(/-/g, "/");
|
|
2256
|
+
const registryRootPath = rootPathMap.get(foundEncodedDir);
|
|
2257
|
+
const topLevelPath = join(CLAUDE_PROJECTS_DIR, foundEncodedDir, `${cachedUuid}.jsonl`);
|
|
2196
2258
|
const transcript = foundTranscriptPath ? parseTranscript(foundTranscriptPath) : {
|
|
2197
2259
|
userLines: 0,
|
|
2198
2260
|
lastUserPrompt: "",
|
|
2199
2261
|
msgCount: 0,
|
|
2200
2262
|
mtime: 0
|
|
2201
2263
|
};
|
|
2202
|
-
|
|
2203
|
-
seenUuids.add(uuid);
|
|
2264
|
+
seenUuids.add(cachedUuid);
|
|
2204
2265
|
results.push({
|
|
2205
|
-
uuid,
|
|
2206
|
-
shortId:
|
|
2207
|
-
encodedDir,
|
|
2266
|
+
uuid: cachedUuid,
|
|
2267
|
+
shortId: cachedUuid.slice(0, 8),
|
|
2268
|
+
encodedDir: foundEncodedDir,
|
|
2208
2269
|
decodedPath,
|
|
2209
2270
|
topLevelPath,
|
|
2210
2271
|
topLevelSystemLines: 0,
|
|
@@ -2216,7 +2277,7 @@ function scanSessions(db, opts = {}) {
|
|
|
2216
2277
|
lastUserPrompt: transcript.lastUserPrompt,
|
|
2217
2278
|
msgCount: transcript.msgCount,
|
|
2218
2279
|
aiTitle: transcript.aiTitle,
|
|
2219
|
-
mtime,
|
|
2280
|
+
mtime: transcript.mtime,
|
|
2220
2281
|
friendlyName: clcInfo.name,
|
|
2221
2282
|
clcDirectory: clcInfo.directory,
|
|
2222
2283
|
registryRootPath
|
|
@@ -2282,7 +2343,116 @@ function resolveSessionByNameOrId(sessions, query) {
|
|
|
2282
2343
|
const candidates = byUuid.slice(0, 5).map((s, i) => ` ${i + 1}. ${s.shortId} ${s.friendlyName ?? s.decodedPath} (${fmtAge(s.mtime)} ago)`).join("\n");
|
|
2283
2344
|
throw new Error(`UUID prefix "${query}" is ambiguous — ${byUuid.length} matches:\n${candidates}\n\nProvide more characters.`);
|
|
2284
2345
|
}
|
|
2285
|
-
throw new Error(`No
|
|
2346
|
+
throw new Error(`No session found matching "${query}".\n\nRun: pai sessions to list sessions.\nRun: pai sessions --all to include transcript-only sessions.`);
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
//#endregion
|
|
2350
|
+
//#region src/cli/lib/aibroker-client.ts
|
|
2351
|
+
/**
|
|
2352
|
+
* aibroker-client.ts — Lightweight IPC client for AIBroker daemon.
|
|
2353
|
+
*
|
|
2354
|
+
* Connects to the AIBroker Unix Domain Socket, sends a JSON-RPC request,
|
|
2355
|
+
* and reads a single newline-terminated JSON response. No class needed —
|
|
2356
|
+
* just a thin async function matching the WatcherClient protocol.
|
|
2357
|
+
*
|
|
2358
|
+
* Socket path: /tmp/aibroker.sock (default; override via AIBROKER_SOCKET env).
|
|
2359
|
+
*/
|
|
2360
|
+
const DEFAULT_SOCKET = process.env.AIBROKER_SOCKET ?? "/tmp/aibroker.sock";
|
|
2361
|
+
/**
|
|
2362
|
+
* Call an AIBroker IPC method and return the result.
|
|
2363
|
+
*
|
|
2364
|
+
* Resolves with the `result` field of a successful response.
|
|
2365
|
+
* Rejects if the socket is not available, the call times out, or the
|
|
2366
|
+
* daemon returns an error.
|
|
2367
|
+
*
|
|
2368
|
+
* @param method IPC method name (e.g. "session_content", "send_to_session")
|
|
2369
|
+
* @param params Method parameters object
|
|
2370
|
+
* @param timeoutMs Connection + response timeout in milliseconds (default: 8 000)
|
|
2371
|
+
*/
|
|
2372
|
+
function callAiBroker(method, params = {}, timeoutMs = 8e3) {
|
|
2373
|
+
return new Promise((resolve, reject) => {
|
|
2374
|
+
const socketPath = DEFAULT_SOCKET;
|
|
2375
|
+
let done = false;
|
|
2376
|
+
let buffer = "";
|
|
2377
|
+
let timer = null;
|
|
2378
|
+
function finish(err, value) {
|
|
2379
|
+
if (done) return;
|
|
2380
|
+
done = true;
|
|
2381
|
+
if (timer !== null) {
|
|
2382
|
+
clearTimeout(timer);
|
|
2383
|
+
timer = null;
|
|
2384
|
+
}
|
|
2385
|
+
try {
|
|
2386
|
+
socket.destroy();
|
|
2387
|
+
} catch {}
|
|
2388
|
+
if (err) reject(err);
|
|
2389
|
+
else resolve(value);
|
|
2390
|
+
}
|
|
2391
|
+
const socket = connect(socketPath, () => {
|
|
2392
|
+
const request = {
|
|
2393
|
+
id: randomUUID(),
|
|
2394
|
+
sessionId: process.env.TERM_SESSION_ID ?? "pai-cli",
|
|
2395
|
+
method,
|
|
2396
|
+
params
|
|
2397
|
+
};
|
|
2398
|
+
const itermId = process.env.ITERM_SESSION_ID;
|
|
2399
|
+
if (itermId) Object.assign(request, { itermSessionId: itermId });
|
|
2400
|
+
socket.write(JSON.stringify(request) + "\n");
|
|
2401
|
+
});
|
|
2402
|
+
socket.on("data", (chunk) => {
|
|
2403
|
+
buffer += chunk.toString("utf8");
|
|
2404
|
+
const nl = buffer.indexOf("\n");
|
|
2405
|
+
if (nl === -1) return;
|
|
2406
|
+
const line = buffer.slice(0, nl);
|
|
2407
|
+
let response;
|
|
2408
|
+
try {
|
|
2409
|
+
response = JSON.parse(line);
|
|
2410
|
+
} catch {
|
|
2411
|
+
finish(/* @__PURE__ */ new Error(`AIBroker IPC parse error: ${line.slice(0, 120)}`));
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
if (!response.ok) finish(new Error(response.error ?? "AIBroker IPC call failed"));
|
|
2415
|
+
else finish(null, response.result ?? {});
|
|
2416
|
+
});
|
|
2417
|
+
socket.on("error", (e) => {
|
|
2418
|
+
if (e.code === "ENOENT" || e.code === "ECONNREFUSED") finish(/* @__PURE__ */ new Error("AIBroker not running (socket not found)."));
|
|
2419
|
+
else finish(e);
|
|
2420
|
+
});
|
|
2421
|
+
socket.on("end", () => {
|
|
2422
|
+
if (!done) finish(/* @__PURE__ */ new Error("AIBroker IPC connection closed before response."));
|
|
2423
|
+
});
|
|
2424
|
+
timer = setTimeout(() => finish(/* @__PURE__ */ new Error("AIBroker IPC call timed out.")), timeoutMs);
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Fetch all live iTerm2 sessions from AIBroker.
|
|
2429
|
+
* Returns an empty array if AIBroker is not running.
|
|
2430
|
+
*/
|
|
2431
|
+
async function fetchLiveSessions() {
|
|
2432
|
+
try {
|
|
2433
|
+
const sessions = (await callAiBroker("session_content", { lineCount: 0 })).sessions;
|
|
2434
|
+
if (!Array.isArray(sessions)) return [];
|
|
2435
|
+
return sessions;
|
|
2436
|
+
} catch {
|
|
2437
|
+
return [];
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Send text to a specific AIBroker session by its iTerm2 sessionId.
|
|
2442
|
+
*/
|
|
2443
|
+
async function sendToSession(sessionId, text) {
|
|
2444
|
+
try {
|
|
2445
|
+
await callAiBroker("send_to_session", {
|
|
2446
|
+
sessionId,
|
|
2447
|
+
text
|
|
2448
|
+
});
|
|
2449
|
+
return { ok: true };
|
|
2450
|
+
} catch (e) {
|
|
2451
|
+
return {
|
|
2452
|
+
ok: false,
|
|
2453
|
+
error: String(e)
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2286
2456
|
}
|
|
2287
2457
|
|
|
2288
2458
|
//#endregion
|
|
@@ -2300,38 +2470,77 @@ function fmtStatus(status) {
|
|
|
2300
2470
|
case "orphan": return chalk.dim("orphan");
|
|
2301
2471
|
}
|
|
2302
2472
|
}
|
|
2303
|
-
function
|
|
2473
|
+
function renderLiveSessions(allLiveSessions) {
|
|
2474
|
+
const claudeSessions = allLiveSessions.filter((s) => s.paiName !== null && s.paiName !== void 0 && s.paiName !== "");
|
|
2475
|
+
const skippedCount = allLiveSessions.length - claudeSessions.length;
|
|
2476
|
+
if (claudeSessions.length === 0) return;
|
|
2477
|
+
console.log("\n" + header("Live Sessions") + "\n");
|
|
2478
|
+
const liveHeaders = [
|
|
2479
|
+
"#",
|
|
2480
|
+
"iTerm2 id",
|
|
2481
|
+
"name",
|
|
2482
|
+
"at prompt",
|
|
2483
|
+
"paiName"
|
|
2484
|
+
];
|
|
2485
|
+
const liveRows = claudeSessions.map((s, idx) => {
|
|
2486
|
+
const shortId = s.sessionId.slice(0, 8);
|
|
2487
|
+
const name = s.name.length > 32 ? s.name.slice(0, 31) + "…" : s.name;
|
|
2488
|
+
const paiName = s.paiName.length > 20 ? s.paiName.slice(0, 19) + "…" : s.paiName;
|
|
2489
|
+
const atPrompt = s.atPrompt ? chalk.green("yes") : chalk.yellow("busy");
|
|
2490
|
+
return [
|
|
2491
|
+
chalk.dim(String(idx + 1)),
|
|
2492
|
+
chalk.cyan(shortId),
|
|
2493
|
+
name,
|
|
2494
|
+
atPrompt,
|
|
2495
|
+
paiName
|
|
2496
|
+
];
|
|
2497
|
+
});
|
|
2498
|
+
console.log(renderTable(liveHeaders, liveRows));
|
|
2499
|
+
if (skippedCount > 0) console.log(dim(` (${skippedCount} non-Claude tab${skippedCount === 1 ? "" : "s"} hidden — use pai pause all to see full list)`));
|
|
2500
|
+
}
|
|
2501
|
+
async function cmdRecent(db, opts) {
|
|
2304
2502
|
const limit = parseInt(opts.n ?? "20", 10);
|
|
2305
2503
|
const includeAll = opts.all === true;
|
|
2504
|
+
const liveSessions = await fetchLiveSessions();
|
|
2306
2505
|
const sessions = scanSessions(db, {
|
|
2307
2506
|
limit,
|
|
2308
2507
|
filter: includeAll ? "all" : "named"
|
|
2309
2508
|
});
|
|
2310
|
-
if (sessions.length === 0) {
|
|
2311
|
-
if (includeAll) console.log(err("No sessions found in ~/.claude/projects/."));
|
|
2312
|
-
else console.log(err("No named sessions found.\n\n Named sessions appear when you have entries in ~/.claude/session.json\n (set via /Name inside Claude Code) or resumable top-level jsonl files.\n Run: pai session recent --all to list all sessions including unnamed orphans."));
|
|
2313
|
-
return;
|
|
2314
|
-
}
|
|
2315
2509
|
if (opts.json) {
|
|
2316
|
-
const output =
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2510
|
+
const output = {
|
|
2511
|
+
live: liveSessions.map((s) => ({
|
|
2512
|
+
sessionId: s.sessionId,
|
|
2513
|
+
name: s.name,
|
|
2514
|
+
paiName: s.paiName ?? null,
|
|
2515
|
+
atPrompt: s.atPrompt
|
|
2516
|
+
})),
|
|
2517
|
+
paused: sessions.map((s, idx) => ({
|
|
2518
|
+
idx: idx + 1,
|
|
2519
|
+
uuid: s.uuid,
|
|
2520
|
+
shortId: s.shortId,
|
|
2521
|
+
resumable: s.resumable,
|
|
2522
|
+
sessionStatus: s.sessionStatus,
|
|
2523
|
+
age: fmtAge(s.mtime),
|
|
2524
|
+
mtime: s.mtime,
|
|
2525
|
+
name: s.friendlyName,
|
|
2526
|
+
lastUserPrompt: s.lastUserPrompt,
|
|
2527
|
+
userLines: s.userLines,
|
|
2528
|
+
msgCount: s.msgCount,
|
|
2529
|
+
topLevelSize: s.topLevelSize,
|
|
2530
|
+
decodedPath: s.decodedPath
|
|
2531
|
+
}))
|
|
2532
|
+
};
|
|
2331
2533
|
console.log(JSON.stringify(output, null, 2));
|
|
2332
2534
|
return;
|
|
2333
2535
|
}
|
|
2334
|
-
const
|
|
2536
|
+
const hasClaudeLive = liveSessions.some((s) => s.paiName !== null && s.paiName !== void 0 && s.paiName !== "");
|
|
2537
|
+
if (liveSessions.length > 0) renderLiveSessions(liveSessions);
|
|
2538
|
+
if (sessions.length === 0) {
|
|
2539
|
+
if (!hasClaudeLive) if (includeAll) console.log(err("No sessions found in ~/.claude/projects/."));
|
|
2540
|
+
else console.log(err("No named sessions found.\n\n Named sessions appear when you have entries in ~/.claude/session.json\n (set via /Name inside Claude Code) or resumable top-level jsonl files.\n Run: pai session recent --all to list all sessions including unnamed orphans."));
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const title = includeAll ? "Paused / All Sessions (named + orphans)" : "Paused / Resumable Sessions";
|
|
2335
2544
|
console.log("\n" + header(title) + "\n");
|
|
2336
2545
|
const headers = [
|
|
2337
2546
|
"#",
|
|
@@ -2358,11 +2567,48 @@ function cmdRecent(db, opts) {
|
|
|
2358
2567
|
];
|
|
2359
2568
|
});
|
|
2360
2569
|
console.log(renderTable(headers, rows));
|
|
2361
|
-
console.log("\n" + dim("Go to a session: ") + chalk.white("pai resume <name>") + "\n" + (includeAll ? "" : dim("Show all sessions: ") + chalk.white("pai sessions --all") + "\n"));
|
|
2570
|
+
console.log("\n" + dim("Go to a session: ") + chalk.white("pai resume <name>") + "\n" + (hasClaudeLive ? dim("Pause all live: ") + chalk.white("pai pause all") + "\n" : "") + (includeAll ? "" : dim("Show all sessions: ") + chalk.white("pai sessions --all") + "\n"));
|
|
2362
2571
|
}
|
|
2363
2572
|
|
|
2364
2573
|
//#endregion
|
|
2365
2574
|
//#region src/cli/commands/session/goto.ts
|
|
2575
|
+
/**
|
|
2576
|
+
* Run claude --resume <uuid> --print --output-format=json "_" in the given cwd.
|
|
2577
|
+
* Returns ok=true if the session is resumable (exit 0 and no "No conversation found"
|
|
2578
|
+
* in stderr). Timeout: 5 seconds.
|
|
2579
|
+
*/
|
|
2580
|
+
function probeResume(uuid, cwd) {
|
|
2581
|
+
const result = spawnSync("claude", [
|
|
2582
|
+
"--resume",
|
|
2583
|
+
uuid,
|
|
2584
|
+
"--print",
|
|
2585
|
+
"--output-format=json",
|
|
2586
|
+
"_"
|
|
2587
|
+
], {
|
|
2588
|
+
cwd,
|
|
2589
|
+
timeout: 5e3,
|
|
2590
|
+
env: process.env,
|
|
2591
|
+
stdio: [
|
|
2592
|
+
"ignore",
|
|
2593
|
+
"ignore",
|
|
2594
|
+
"pipe"
|
|
2595
|
+
]
|
|
2596
|
+
});
|
|
2597
|
+
if (result.error) return {
|
|
2598
|
+
ok: false,
|
|
2599
|
+
reason: `spawn error: ${result.error.message}`
|
|
2600
|
+
};
|
|
2601
|
+
const stderr = result.stderr?.toString("utf8") ?? "";
|
|
2602
|
+
if (stderr.toLowerCase().includes("no conversation found") || stderr.toLowerCase().includes("session not found")) return {
|
|
2603
|
+
ok: false,
|
|
2604
|
+
reason: "No conversation found for this UUID"
|
|
2605
|
+
};
|
|
2606
|
+
if (result.status !== 0) return {
|
|
2607
|
+
ok: false,
|
|
2608
|
+
reason: `claude exited ${result.status ?? "signal"}${stderr ? `: ${stderr.slice(0, 120).trim()}` : ""}`
|
|
2609
|
+
};
|
|
2610
|
+
return { ok: true };
|
|
2611
|
+
}
|
|
2366
2612
|
function cmdGoto(db, query, opts) {
|
|
2367
2613
|
const allSessions = scanSessions(db, {
|
|
2368
2614
|
limit: 500,
|
|
@@ -2397,44 +2643,84 @@ function cmdGoto(db, query, opts) {
|
|
|
2397
2643
|
console.error(err(`session "${query}": directory does not exist or cannot be resolved.\n Registry says: ${rawDir}\n The directory may have moved or been deleted.`));
|
|
2398
2644
|
process.exit(1);
|
|
2399
2645
|
}
|
|
2400
|
-
const
|
|
2401
|
-
const
|
|
2402
|
-
let promptArg = null;
|
|
2403
|
-
if (useName && friendlyName) promptArg = useGo ? `/Name ${friendlyName}\ngo` : `/Name ${friendlyName}`;
|
|
2404
|
-
else if (useGo) promptArg = "go";
|
|
2405
|
-
const fullArgv = resumableUuid ? [
|
|
2406
|
-
"claude",
|
|
2407
|
-
"--resume",
|
|
2408
|
-
resumableUuid,
|
|
2409
|
-
...promptArg ? [promptArg] : []
|
|
2410
|
-
] : ["claude", ...promptArg ? [promptArg] : []];
|
|
2646
|
+
const name = friendlyName ?? query;
|
|
2647
|
+
const promptArg = `/Name ${name}\ngo`;
|
|
2411
2648
|
if (opts.dryRun) {
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
console.log(
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2649
|
+
if (resumableUuid) {
|
|
2650
|
+
const argvResume = `claude --resume ${resumableUuid} --name "${name}" "/Name ${name}\\ngo"`;
|
|
2651
|
+
const argvFresh = `claude --name "${name}" "/Name ${name}\\ngo"`;
|
|
2652
|
+
console.log("\n" + chalk.bold("Dry run — would probe then exec (RESUME path):") + "\n");
|
|
2653
|
+
console.log(` cwd: ${chalk.cyan(projectDir)}`);
|
|
2654
|
+
console.log(` probe: claude --resume ${resumableUuid} --print --output-format=json "_"`);
|
|
2655
|
+
console.log(` argv: ${chalk.white(argvResume)}`);
|
|
2656
|
+
console.log(` fallback: ${chalk.yellow(argvFresh)}`);
|
|
2657
|
+
if (resumableSession) {
|
|
2658
|
+
console.log(`\n uuid: ${resumableSession.uuid}`);
|
|
2659
|
+
console.log(` age: ${fmtAge(resumableSession.mtime)}`);
|
|
2660
|
+
console.log(` status: ${resumableSession.sessionStatus}`);
|
|
2661
|
+
console.log(` sys: ${resumableSession.topLevelSystemLines} system lines`);
|
|
2662
|
+
}
|
|
2663
|
+
} else {
|
|
2664
|
+
const argvFresh = `claude --name "${name}" "/Name ${name}\\ngo"`;
|
|
2665
|
+
console.log("\n" + chalk.bold("Dry run — would exec (FRESH path, no resumable UUID):") + "\n");
|
|
2666
|
+
console.log(` cwd: ${chalk.cyan(projectDir)}`);
|
|
2667
|
+
console.log(` argv: ${chalk.white(argvFresh)}`);
|
|
2668
|
+
}
|
|
2425
2669
|
console.log();
|
|
2426
2670
|
return;
|
|
2427
2671
|
}
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2672
|
+
if (resumableUuid) {
|
|
2673
|
+
const probe = probeResume(resumableUuid, projectDir);
|
|
2674
|
+
if (probe.ok) {
|
|
2675
|
+
const result = spawnSync("claude", [
|
|
2676
|
+
"--resume",
|
|
2677
|
+
resumableUuid,
|
|
2678
|
+
"--name",
|
|
2679
|
+
name,
|
|
2680
|
+
promptArg
|
|
2681
|
+
], {
|
|
2682
|
+
cwd: projectDir,
|
|
2683
|
+
stdio: "inherit",
|
|
2684
|
+
env: process.env
|
|
2685
|
+
});
|
|
2686
|
+
if (result.error) {
|
|
2687
|
+
console.error(err(`Failed to launch claude: ${result.error.message}`));
|
|
2688
|
+
process.exit(1);
|
|
2689
|
+
}
|
|
2690
|
+
process.exit(result.status ?? 0);
|
|
2691
|
+
} else {
|
|
2692
|
+
process.stderr.write(chalk.yellow(`\n Resume failed for ${resumableUuid.slice(0, 8)}: ${probe.reason ?? "unknown error"}\n Starting fresh session in same directory.\n\n`));
|
|
2693
|
+
const result = spawnSync("claude", [
|
|
2694
|
+
"--name",
|
|
2695
|
+
name,
|
|
2696
|
+
promptArg
|
|
2697
|
+
], {
|
|
2698
|
+
cwd: projectDir,
|
|
2699
|
+
stdio: "inherit",
|
|
2700
|
+
env: process.env
|
|
2701
|
+
});
|
|
2702
|
+
if (result.error) {
|
|
2703
|
+
console.error(err(`Failed to launch claude: ${result.error.message}`));
|
|
2704
|
+
process.exit(1);
|
|
2705
|
+
}
|
|
2706
|
+
process.exit(result.status ?? 0);
|
|
2707
|
+
}
|
|
2708
|
+
} else {
|
|
2709
|
+
const result = spawnSync("claude", [
|
|
2710
|
+
"--name",
|
|
2711
|
+
name,
|
|
2712
|
+
promptArg
|
|
2713
|
+
], {
|
|
2714
|
+
cwd: projectDir,
|
|
2715
|
+
stdio: "inherit",
|
|
2716
|
+
env: process.env
|
|
2717
|
+
});
|
|
2718
|
+
if (result.error) {
|
|
2719
|
+
console.error(err(`Failed to launch claude: ${result.error.message}`));
|
|
2720
|
+
process.exit(1);
|
|
2721
|
+
}
|
|
2722
|
+
process.exit(result.status ?? 0);
|
|
2436
2723
|
}
|
|
2437
|
-
process.exit(result.status ?? 0);
|
|
2438
2724
|
}
|
|
2439
2725
|
|
|
2440
2726
|
//#endregion
|
|
@@ -2564,25 +2850,255 @@ function cmdPause(db, opts) {
|
|
|
2564
2850
|
console.log(box);
|
|
2565
2851
|
}
|
|
2566
2852
|
|
|
2853
|
+
//#endregion
|
|
2854
|
+
//#region src/cli/commands/session/end.ts
|
|
2855
|
+
/** PAI_DIR — mirrors pai-paths.ts resolution. */
|
|
2856
|
+
function getPaiDir() {
|
|
2857
|
+
const envDir = process.env.PAI_DIR;
|
|
2858
|
+
if (envDir) try {
|
|
2859
|
+
return realpathSync(envDir);
|
|
2860
|
+
} catch {
|
|
2861
|
+
return envDir;
|
|
2862
|
+
}
|
|
2863
|
+
return join(homedir(), ".claude");
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Find the notes directory for a project — checks local first, then central.
|
|
2867
|
+
* Returns null if neither exists (dry-run safe: don't create).
|
|
2868
|
+
*/
|
|
2869
|
+
function findNotesDir$1(rootPath, encodedDir) {
|
|
2870
|
+
for (const rel of [
|
|
2871
|
+
"Notes",
|
|
2872
|
+
"notes",
|
|
2873
|
+
".claude/Notes"
|
|
2874
|
+
]) {
|
|
2875
|
+
const p = join(rootPath, rel);
|
|
2876
|
+
if (existsSync(p)) return p;
|
|
2877
|
+
}
|
|
2878
|
+
const central = join(getPaiDir(), "projects", encodedDir, "Notes");
|
|
2879
|
+
if (existsSync(central)) return central;
|
|
2880
|
+
return null;
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* Find the current (latest) session note inside notesDir.
|
|
2884
|
+
* Searches YYYY/MM subdirectory (current month, then previous month),
|
|
2885
|
+
* then flat notesDir as legacy fallback.
|
|
2886
|
+
*/
|
|
2887
|
+
function findLatestNote(notesDir) {
|
|
2888
|
+
const findIn = (dir) => {
|
|
2889
|
+
if (!existsSync(dir)) return null;
|
|
2890
|
+
const files = readdirSync(dir).filter((f) => /^\d{3,4}[\s_-].*\.md$/.test(f)).sort((a, b) => {
|
|
2891
|
+
return parseInt(a.match(/^(\d+)/)?.[1] ?? "0", 10) - parseInt(b.match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
2892
|
+
});
|
|
2893
|
+
return files.length > 0 ? join(dir, files[files.length - 1]) : null;
|
|
2894
|
+
};
|
|
2895
|
+
const now = /* @__PURE__ */ new Date();
|
|
2896
|
+
const current = findIn(join(notesDir, String(now.getFullYear()), String(now.getMonth() + 1).padStart(2, "0")));
|
|
2897
|
+
if (current) return current;
|
|
2898
|
+
const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
2899
|
+
const prevFound = findIn(join(notesDir, String(prev.getFullYear()), String(prev.getMonth() + 1).padStart(2, "0")));
|
|
2900
|
+
if (prevFound) return prevFound;
|
|
2901
|
+
return findIn(notesDir);
|
|
2902
|
+
}
|
|
2903
|
+
function finalizeNote(notePath) {
|
|
2904
|
+
const content = readFileSync(notePath, "utf-8");
|
|
2905
|
+
if (content.includes("**Status:** Completed")) return {
|
|
2906
|
+
finalized: false,
|
|
2907
|
+
path: notePath
|
|
2908
|
+
};
|
|
2909
|
+
let updated = content.replace("**Status:** In Progress", "**Status:** Completed");
|
|
2910
|
+
if (!updated.includes("**Completed:**")) {
|
|
2911
|
+
const completionTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
2912
|
+
updated = updated.replace("---\n\n## Work Done", `**Completed:** ${completionTime}\n\n---\n\n## Work Done`);
|
|
2913
|
+
}
|
|
2914
|
+
const tmp = `${notePath}.end.tmp`;
|
|
2915
|
+
writeFileSync(tmp, updated, "utf-8");
|
|
2916
|
+
renameSync(tmp, notePath);
|
|
2917
|
+
return {
|
|
2918
|
+
finalized: true,
|
|
2919
|
+
path: notePath
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
function cmdEnd(db, opts) {
|
|
2923
|
+
cmdPause(db, opts);
|
|
2924
|
+
const cwd = process.cwd();
|
|
2925
|
+
const project = db.prepare(`SELECT id, slug, display_name, root_path, encoded_dir
|
|
2926
|
+
FROM projects
|
|
2927
|
+
WHERE ? LIKE root_path || '%'
|
|
2928
|
+
ORDER BY length(root_path) DESC
|
|
2929
|
+
LIMIT 1`).get(cwd);
|
|
2930
|
+
if (!project) return;
|
|
2931
|
+
const notesDir = findNotesDir$1(project.root_path, project.encoded_dir ?? "");
|
|
2932
|
+
if (!notesDir) {
|
|
2933
|
+
console.log(chalk.dim(" (No session note found — notes directory does not exist yet.)"));
|
|
2934
|
+
printEndBox(opts.dryRun);
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
const notePath = findLatestNote(notesDir);
|
|
2938
|
+
if (!notePath) {
|
|
2939
|
+
console.log(chalk.dim(" (No session note found in notes directory.)"));
|
|
2940
|
+
printEndBox(opts.dryRun);
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
if (opts.dryRun) {
|
|
2944
|
+
console.log("\n" + chalk.bold("Dry run — would finalize session note:") + " " + chalk.cyan(notePath));
|
|
2945
|
+
console.log(chalk.dim(" Would replace: **Status:** In Progress\n with: **Status:** Completed"));
|
|
2946
|
+
console.log(chalk.dim(" Would add: **Completed:** <timestamp>"));
|
|
2947
|
+
} else {
|
|
2948
|
+
const { finalized, path } = finalizeNote(notePath);
|
|
2949
|
+
if (finalized) console.log(chalk.green(" Session note finalized: ") + chalk.cyan(basename(path)));
|
|
2950
|
+
else console.log(chalk.dim(` Session note already marked Completed: ${basename(notePath)}`));
|
|
2951
|
+
}
|
|
2952
|
+
printEndBox(opts.dryRun);
|
|
2953
|
+
}
|
|
2954
|
+
function printEndBox(dryRun) {
|
|
2955
|
+
const label = dryRun ? " (dry-run)" : "";
|
|
2956
|
+
const box = [
|
|
2957
|
+
"",
|
|
2958
|
+
chalk.bgRed.white.bold(` SESSION ENDING${label}: How to exit safely `),
|
|
2959
|
+
"",
|
|
2960
|
+
chalk.yellow(" Inside the Claude Code session, type: " + chalk.white.bold("/exit") + chalk.yellow(" (then press Enter)")),
|
|
2961
|
+
"",
|
|
2962
|
+
chalk.red.bold(" DO NOT press Ctrl+C."),
|
|
2963
|
+
"",
|
|
2964
|
+
chalk.dim(" Ctrl+C bypasses PAI's stop-hook, which means:"),
|
|
2965
|
+
chalk.dim(" - The session note is NOT written to by the stop-hook"),
|
|
2966
|
+
chalk.dim(" - The session becomes orphaned (cannot --resume next time)"),
|
|
2967
|
+
chalk.dim(" - The final session summary is never generated"),
|
|
2968
|
+
"",
|
|
2969
|
+
chalk.dim(" ## Continue checkpoint and session note status are already saved."),
|
|
2970
|
+
chalk.dim(" Use /exit to let PAI's stop-hook finalize the session fully."),
|
|
2971
|
+
""
|
|
2972
|
+
].join("\n");
|
|
2973
|
+
console.log(box);
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
//#endregion
|
|
2977
|
+
//#region src/cli/commands/session/pause-all.ts
|
|
2978
|
+
/**
|
|
2979
|
+
* pai pause all [--exit] [--dry-run]
|
|
2980
|
+
*
|
|
2981
|
+
* Pause every live Claude Code session that AIBroker knows about.
|
|
2982
|
+
*
|
|
2983
|
+
* For each live session returned by AIBroker's session_content IPC method:
|
|
2984
|
+
* 1. Send "pause session" to the iTerm2 pane via send_to_session.
|
|
2985
|
+
* 2. Optionally send "\n/exit\n" after a short delay (--exit flag).
|
|
2986
|
+
* 3. Print a summary of what was sent and to which sessions.
|
|
2987
|
+
*
|
|
2988
|
+
* If AIBroker is not running, prints a clear error and exits.
|
|
2989
|
+
* If no live sessions are found, reports "nothing to pause".
|
|
2990
|
+
*/
|
|
2991
|
+
function sleep(ms) {
|
|
2992
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2993
|
+
}
|
|
2994
|
+
function sessionLabel(s) {
|
|
2995
|
+
const name = s.paiName ?? s.name;
|
|
2996
|
+
return `${chalk.cyan(s.sessionId.slice(0, 8))} ${chalk.bold(name)}`;
|
|
2997
|
+
}
|
|
2998
|
+
async function cmdPauseAll(opts) {
|
|
2999
|
+
const waitMs = opts.wait ?? 5e3;
|
|
3000
|
+
let liveSessions;
|
|
3001
|
+
try {
|
|
3002
|
+
liveSessions = await fetchLiveSessions();
|
|
3003
|
+
} catch (e) {
|
|
3004
|
+
console.error(err("AIBroker is not running. Cannot list live sessions."));
|
|
3005
|
+
console.error(dim(" Start AIBroker or run `pai pause` from each session manually."));
|
|
3006
|
+
process.exitCode = 1;
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
const claudeSessions = liveSessions.filter((s) => s.paiName !== null && s.paiName !== void 0 && s.paiName !== "");
|
|
3010
|
+
const skipped = liveSessions.length - claudeSessions.length;
|
|
3011
|
+
if (skipped > 0) process.stderr.write(`Skipping ${skipped} non-Claude tab${skipped === 1 ? "" : "s"} (bare shells).\n`);
|
|
3012
|
+
if (claudeSessions.length === 0) {
|
|
3013
|
+
console.log(warn("No live Claude sessions found via AIBroker. Nothing to pause."));
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
if (opts.dryRun) {
|
|
3017
|
+
console.log("\n" + header("Dry Run — Would Pause These Sessions") + "\n");
|
|
3018
|
+
for (const s of claudeSessions) {
|
|
3019
|
+
console.log(" " + sessionLabel(s));
|
|
3020
|
+
console.log(dim(" → send: \"pause session\""));
|
|
3021
|
+
if (opts.exit) console.log(dim(` → wait ${waitMs}ms then send: "/exit"`));
|
|
3022
|
+
}
|
|
3023
|
+
console.log();
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
console.log("\n" + header("Pausing All Live Sessions") + "\n" + dim(` ${claudeSessions.length} Claude session(s) via AIBroker`) + "\n");
|
|
3027
|
+
const results = [];
|
|
3028
|
+
for (const s of claudeSessions) {
|
|
3029
|
+
process.stdout.write(" " + sessionLabel(s) + " … ");
|
|
3030
|
+
const pauseResult = await sendToSession(s.sessionId, "pause session\n");
|
|
3031
|
+
if (!pauseResult.ok) {
|
|
3032
|
+
console.log(err("FAILED: " + (pauseResult.error ?? "unknown error")));
|
|
3033
|
+
results.push({
|
|
3034
|
+
session: s,
|
|
3035
|
+
pauseOk: false,
|
|
3036
|
+
error: pauseResult.error
|
|
3037
|
+
});
|
|
3038
|
+
continue;
|
|
3039
|
+
}
|
|
3040
|
+
console.log(ok("paused"));
|
|
3041
|
+
results.push({
|
|
3042
|
+
session: s,
|
|
3043
|
+
pauseOk: true
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
if (opts.exit) {
|
|
3047
|
+
const pausedSessions = results.filter((r) => r.pauseOk).map((r) => r.session);
|
|
3048
|
+
if (pausedSessions.length > 0) {
|
|
3049
|
+
console.log("\n" + dim(` Waiting ${waitMs / 1e3}s for sessions to save state…`));
|
|
3050
|
+
await sleep(waitMs);
|
|
3051
|
+
console.log();
|
|
3052
|
+
for (const s of pausedSessions) {
|
|
3053
|
+
process.stdout.write(" " + sessionLabel(s) + " exiting … ");
|
|
3054
|
+
const exitResult = await sendToSession(s.sessionId, "/exit\n");
|
|
3055
|
+
const rec = results.find((r) => r.session.sessionId === s.sessionId);
|
|
3056
|
+
rec.exitOk = exitResult.ok;
|
|
3057
|
+
if (!exitResult.ok) console.log(warn("exit failed: " + (exitResult.error ?? "unknown")));
|
|
3058
|
+
else console.log(ok("exited"));
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
const total = results.length;
|
|
3063
|
+
const succeeded = results.filter((r) => r.pauseOk).length;
|
|
3064
|
+
const failed = total - succeeded;
|
|
3065
|
+
console.log();
|
|
3066
|
+
if (failed === 0) console.log(ok(`All ${total} session(s) paused successfully.`));
|
|
3067
|
+
else {
|
|
3068
|
+
console.log(warn(`${succeeded}/${total} session(s) paused. `) + err(`${failed} failed.`));
|
|
3069
|
+
for (const r of results.filter((r) => !r.pauseOk)) {
|
|
3070
|
+
const label = r.session.paiName ?? r.session.name;
|
|
3071
|
+
console.log(err(` ${label}: ${r.error ?? "unknown error"}`));
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
if (opts.exit) {
|
|
3075
|
+
const exitOk = results.filter((r) => r.exitOk).length;
|
|
3076
|
+
console.log(dim(` /exit sent to ${exitOk} session(s).`));
|
|
3077
|
+
}
|
|
3078
|
+
console.log();
|
|
3079
|
+
}
|
|
3080
|
+
|
|
2567
3081
|
//#endregion
|
|
2568
3082
|
//#region src/cli/commands/session/sessions-index.ts
|
|
2569
3083
|
function registerSessionsCommands(sessionsCmd, getDb) {
|
|
2570
|
-
sessionsCmd.command("list", { isDefault: true }).description("Resumable sessions catalog — named sessions with resume status.\nShort form: pai sessions (bare, no subcommand)\nUse --all to also show unnamed orphan sessions.").option("-n <count>", "Maximum sessions to show (default: 20)", "20").option("--all", "Include unnamed orphan sessions (not in clc registry)").option("--json", "Output raw JSON instead of formatted table").action((opts) => {
|
|
2571
|
-
cmdRecent(getDb(), opts);
|
|
3084
|
+
sessionsCmd.command("list", { isDefault: true }).description("Resumable sessions catalog — named sessions with resume status.\nShort form: pai sessions (bare, no subcommand)\nUse --all to also show unnamed orphan sessions.").option("-n <count>", "Maximum sessions to show (default: 20)", "20").option("--all", "Include unnamed orphan sessions (not in clc registry)").option("--json", "Output raw JSON instead of formatted table").action(async (opts) => {
|
|
3085
|
+
await cmdRecent(getDb(), opts);
|
|
2572
3086
|
});
|
|
2573
|
-
sessionsCmd.command("goto <name-or-id>").description("Go to a session: resume if a resumable snapshot exists, start fresh otherwise.\nRecommended short form: pai resume <name>\nResolves by clc/registry name (case-insensitive) or UUID prefix.").option("--
|
|
2574
|
-
cmdGoto(getDb(), nameOrId, {
|
|
2575
|
-
noName: opts.skipName,
|
|
2576
|
-
noGo: opts.skipGo,
|
|
2577
|
-
dryRun: opts.dryRun
|
|
2578
|
-
});
|
|
3087
|
+
sessionsCmd.command("goto <name-or-id>").description("Go to a session: resume if a resumable snapshot exists, start fresh otherwise.\nRecommended short form: pai resume <name>\nResolves by clc/registry name (case-insensitive) or UUID prefix.").option("--dry-run", "Print the exact argv and cwd, then exit without launching").action((nameOrId, opts) => {
|
|
3088
|
+
cmdGoto(getDb(), nameOrId, { dryRun: opts.dryRun });
|
|
2579
3089
|
});
|
|
2580
3090
|
sessionsCmd.command("pause").description("Write a ## Continue checkpoint to the project's TODO.md.\nRecommended short form: pai pause\nUse /exit inside Claude Code to preserve full session resumability.").option("--dry-run", "Preview the ## Continue block without writing it").action((opts) => {
|
|
2581
3091
|
cmdPause(getDb(), opts);
|
|
2582
3092
|
});
|
|
2583
|
-
sessionsCmd.command("
|
|
2584
|
-
|
|
2585
|
-
|
|
3093
|
+
sessionsCmd.command("pause-all").description("Pause every live Claude Code session via AIBroker.\nRequires AIBroker to be running. Sends 'pause session' to each live iTerm2 pane.\nTop-level short form: pai pause all").option("--exit", "Also send /exit to each session after it has saved state").option("--wait <ms>", "Milliseconds to wait before /exit (default: 5000)", "5000").option("--dry-run", "Show what would be sent without actually sending").action(async (opts) => {
|
|
3094
|
+
await cmdPauseAll({
|
|
3095
|
+
exit: opts.exit,
|
|
3096
|
+
dryRun: opts.dryRun,
|
|
3097
|
+
wait: opts.wait !== void 0 ? parseInt(opts.wait, 10) : void 0
|
|
3098
|
+
});
|
|
3099
|
+
});
|
|
3100
|
+
sessionsCmd.command("end").description("Finalize a session: write ## Continue checkpoint + mark session note Completed.\nRecommended short form: pai end\nUse /exit inside Claude Code after running this command.").option("--dry-run", "Preview all changes without writing them").action((opts) => {
|
|
3101
|
+
cmdEnd(getDb(), opts);
|
|
2586
3102
|
});
|
|
2587
3103
|
sessionsCmd.command("info <project-slug> <number>").description("Show full details for a specific session").action((projectSlug, number) => {
|
|
2588
3104
|
cmdInfo(getDb(), projectSlug, number);
|
|
@@ -4185,7 +4701,7 @@ function cmdLogs(opts) {
|
|
|
4185
4701
|
}
|
|
4186
4702
|
function registerDaemonCommands(daemonCmd) {
|
|
4187
4703
|
daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
|
|
4188
|
-
const { serve } = await import("../daemon-
|
|
4704
|
+
const { serve } = await import("../daemon-CSKj8Xm9.mjs").then((n) => n.t);
|
|
4189
4705
|
const { loadConfig: lc, ensureConfigDir } = await import("../config-DqBY3aT0.mjs").then((n) => n.r);
|
|
4190
4706
|
ensureConfigDir();
|
|
4191
4707
|
await serve(lc());
|
|
@@ -8529,7 +9045,7 @@ function registerDbCommands(dbCmd) {
|
|
|
8529
9045
|
* pai version
|
|
8530
9046
|
*
|
|
8531
9047
|
* Daily verb shortcuts (top-level):
|
|
8532
|
-
* pai pause / pai resume / pai cd
|
|
9048
|
+
* pai pause / pai end / pai resume / pai cd
|
|
8533
9049
|
* pai sessions / pai projects / pai notes
|
|
8534
9050
|
*/
|
|
8535
9051
|
function getVersion() {
|
|
@@ -8554,6 +9070,7 @@ const program = new Command();
|
|
|
8554
9070
|
program.name("pai").description("PAI Knowledge OS — Personal AI Infrastructure CLI").version(getVersion(), "-V, --version", "Print version and exit").addHelpText("after", `
|
|
8555
9071
|
Daily verbs (short forms):
|
|
8556
9072
|
pai pause Save state + display safe-exit reminder
|
|
9073
|
+
pai end Finalize session: pause + mark note Completed
|
|
8557
9074
|
pai resume <name> Go to a session (resume or start fresh)
|
|
8558
9075
|
pai cd <name> cd to a project directory
|
|
8559
9076
|
|
|
@@ -8563,7 +9080,7 @@ Listings:
|
|
|
8563
9080
|
pai notes Markdown session notes
|
|
8564
9081
|
|
|
8565
9082
|
Subcommands (power users):
|
|
8566
|
-
pai sessions ... Session management (list, goto, pause, ...)
|
|
9083
|
+
pai sessions ... Session management (list, goto, pause, end, ...)
|
|
8567
9084
|
pai projects ... Project management (cd, list, ...)
|
|
8568
9085
|
pai registry ... Registry maintenance (scan, ...)
|
|
8569
9086
|
pai memory ... Memory engine (index, search, ...)
|
|
@@ -8592,15 +9109,26 @@ registerObservationCommands(program.command("observation").description("Observat
|
|
|
8592
9109
|
program.command("go <query>").description("Jump to a project directory by slug or partial name.\nPrints the root path to stdout — use with: cd $(pai go <query>)\nExample shell function in ~/.zshrc:\n pcd() { cd \"$(pai go \"$@\")\" }").action((query) => {
|
|
8593
9110
|
cmdGo(getDb(), query);
|
|
8594
9111
|
});
|
|
8595
|
-
program.command("pause").description("Save state and display safe-exit instructions for the current session.\nWrites a ## Continue checkpoint to the project's TODO.md.\nLong form: pai sessions pause").option("--dry-run", "Preview
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
noName: opts.skipName,
|
|
8601
|
-
noGo: opts.skipGo,
|
|
8602
|
-
dryRun: opts.dryRun
|
|
9112
|
+
program.command("pause [target]").description("Save state and display safe-exit instructions for the current session.\nWrites a ## Continue checkpoint to the project's TODO.md.\nUse `pai pause all` to pause every live session via AIBroker.\nLong form: pai sessions pause").option("--dry-run", "Preview changes without writing them").option("--exit", "(pause all only) Also send /exit to each session after pausing").option("--wait <ms>", "(pause all only) Milliseconds to wait before /exit (default: 5000)", "5000").action(async (target, opts) => {
|
|
9113
|
+
if (target === "all") await cmdPauseAll({
|
|
9114
|
+
exit: opts.exit,
|
|
9115
|
+
dryRun: opts.dryRun,
|
|
9116
|
+
wait: opts.wait !== void 0 ? parseInt(opts.wait, 10) : void 0
|
|
8603
9117
|
});
|
|
9118
|
+
else {
|
|
9119
|
+
if (target !== void 0) {
|
|
9120
|
+
console.error(`Unknown target: ${target}. Did you mean 'pai pause all'?`);
|
|
9121
|
+
process.exitCode = 1;
|
|
9122
|
+
return;
|
|
9123
|
+
}
|
|
9124
|
+
cmdPause(getDb(), { dryRun: opts.dryRun });
|
|
9125
|
+
}
|
|
9126
|
+
});
|
|
9127
|
+
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) => {
|
|
9128
|
+
cmdEnd(getDb(), opts);
|
|
9129
|
+
});
|
|
9130
|
+
program.command("resume <name>").description("Go to a session by name: resume if resumable, start fresh otherwise.\nLong form: pai sessions goto <name>").option("--dry-run", "Print the exact argv and cwd, then exit without launching").action((name, opts) => {
|
|
9131
|
+
cmdGoto(getDb(), name, { dryRun: opts.dryRun });
|
|
8604
9132
|
});
|
|
8605
9133
|
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.").action((identifier) => {
|
|
8606
9134
|
const project = resolveIdentifier(getDb(), identifier);
|