session-preserver 0.1.0 → 0.1.2
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/index.js +85 -4
- package/package.json +21 -5
package/index.js
CHANGED
|
@@ -218,6 +218,34 @@ function render(sess, pairs) {
|
|
|
218
218
|
return head + pairs.map(([r, t]) => `\n## ${r}\n\n${t}\n`).join("");
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
// ---------- open: resume session in its own CLI ----------
|
|
222
|
+
|
|
223
|
+
const RESUME = {
|
|
224
|
+
claude: (sess) => ({ cmd: "claude", args: ["--resume", sess.id.slice(3)], cwd: expandHome(sess.project) }),
|
|
225
|
+
codex: (sess) => ({ cmd: "codex", args: ["resume", sess.id.slice(3)], cwd: expandHome(sess.project) }),
|
|
226
|
+
opencode: (sess) => ({ cmd: "opencode", args: ["--session", sess.id.slice(3)], cwd: expandHome(sess.project) }),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const expandHome = (p) => p.replace(/^~(?=$|\/)/, os.homedir());
|
|
230
|
+
|
|
231
|
+
function cmdOpen(id) {
|
|
232
|
+
const sess = find(id);
|
|
233
|
+
const { spawnSync } = require("child_process");
|
|
234
|
+
const bin = RESUME[sess.tool](sess);
|
|
235
|
+
const which = spawnSync("which", [bin.cmd]);
|
|
236
|
+
if (which.status !== 0) {
|
|
237
|
+
console.error(`'${bin.cmd}' not on PATH. Install it, or copy context manually with:\n sp summary ${sess.id} | pbcopy`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
const cwd = bin.cwd && fs.existsSync(bin.cwd) ? bin.cwd : process.cwd();
|
|
241
|
+
console.log(`resuming in ${sess.tool}: ${bin.cmd} ${bin.args.join(" ")} (cwd: ${shortHome(cwd)})`);
|
|
242
|
+
// reset terminal for the child TUI (we may be exiting raw/alt-screen state)
|
|
243
|
+
if (process.stdout.isTTY) process.stdout.write("\x1b[?1049l\x1b[0m\x1b[?25h");
|
|
244
|
+
if (process.stdin.isTTY) { try { process.stdin.setRawMode(false); } catch {} }
|
|
245
|
+
const r = spawnSync(bin.cmd, bin.args, { cwd, stdio: "inherit" });
|
|
246
|
+
process.exit(r.status ?? 0);
|
|
247
|
+
}
|
|
248
|
+
|
|
221
249
|
// ---------- commands ----------
|
|
222
250
|
|
|
223
251
|
function cmdLs(args) {
|
|
@@ -227,12 +255,60 @@ function cmdLs(args) {
|
|
|
227
255
|
if (args.query) { const q = args.query.toLowerCase(); rows = rows.filter((r) => (r.title + " " + r.project).toLowerCase().includes(q)); }
|
|
228
256
|
rows = rows.slice(0, args.n);
|
|
229
257
|
if (!rows.length) { console.log("no sessions found"); return; }
|
|
230
|
-
|
|
258
|
+
if (args.plain || !process.stdout.isTTY || !process.stdin.isTTY) return printTable(rows);
|
|
259
|
+
pickInteractive(rows);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function printTable(rows) {
|
|
263
|
+
const idW = Math.max(...rows.map((r) => r.id.length));
|
|
231
264
|
console.log(`${"ID".padEnd(idW + 1)} ${"TOOL".padEnd(9)} ${"WHEN".padEnd(10)} ${"PROJECT".padEnd(38)} TITLE`);
|
|
232
265
|
for (const r of rows)
|
|
233
266
|
console.log(`${r.id.padEnd(idW + 1)} ${r.tool.padEnd(9)} ${reltime(r.updated).padEnd(10)} ${r.project.slice(-37).padEnd(38)} ${r.title.slice(0, 60)}`);
|
|
234
267
|
}
|
|
235
268
|
|
|
269
|
+
const rowLine = (r) =>
|
|
270
|
+
`${r.tool.padEnd(9)} ${reltime(r.updated).padEnd(10)} ${r.project.slice(-30).padEnd(30)} ${r.title.slice(0, 50)}`;
|
|
271
|
+
|
|
272
|
+
function pickInteractive(rows) {
|
|
273
|
+
let sel = 0, offset = 0;
|
|
274
|
+
const height = () => Math.max(5, (process.stdout.rows || 24) - 3);
|
|
275
|
+
|
|
276
|
+
const draw = () => {
|
|
277
|
+
const out = ["\x1b[H\x1b[J↑/↓ move · enter resume in its CLI · s copy summary · q quit\n"];
|
|
278
|
+
const h = height();
|
|
279
|
+
if (sel < offset) offset = sel;
|
|
280
|
+
if (sel >= offset + h) offset = sel - h + 1;
|
|
281
|
+
for (let i = offset; i < Math.min(rows.length, offset + h); i++)
|
|
282
|
+
out.push((i === sel ? "\x1b[7m" : "") + rowLine(rows[i]) + (i === sel ? "\x1b[0m" : ""));
|
|
283
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const done = (action) => {
|
|
287
|
+
process.stdin.setRawMode(false);
|
|
288
|
+
process.stdin.pause();
|
|
289
|
+
process.stdin.removeAllListeners("data");
|
|
290
|
+
process.stdout.write("\x1b[?1049l");
|
|
291
|
+
const sess = rows[sel];
|
|
292
|
+
if (action === "quit") return;
|
|
293
|
+
if (action === "open") return cmdOpen(sess.id);
|
|
294
|
+
if (action === "summary") return cmdSummary(sess.id);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
process.stdin.setRawMode(true);
|
|
298
|
+
process.stdin.resume();
|
|
299
|
+
process.stdin.setEncoding("utf8");
|
|
300
|
+
process.stdin.on("data", (key) => {
|
|
301
|
+
if (key === "q" || key === "\x03" || key === "\x1b") return done("quit");
|
|
302
|
+
if (key === "\x1b[A" || key === "k") { sel = Math.max(0, sel - 1); return draw(); } // up
|
|
303
|
+
if (key === "\x1b[B" || key === "j") { sel = Math.min(rows.length - 1, sel + 1); return draw(); } // down
|
|
304
|
+
if (key === "\r" || key === "\n") return done("open");
|
|
305
|
+
if (key === "s") return done("summary");
|
|
306
|
+
});
|
|
307
|
+
process.stdout.write("\x1b[?1049h"); // alt screen (after raw mode so first draw is clean)
|
|
308
|
+
draw();
|
|
309
|
+
return done;
|
|
310
|
+
}
|
|
311
|
+
|
|
236
312
|
function cmdShow(id, n) {
|
|
237
313
|
const sess = find(id);
|
|
238
314
|
let pairs = [...conversation(sess)];
|
|
@@ -252,10 +328,12 @@ function usage(code) {
|
|
|
252
328
|
console.log(`session-preserver - centralized AI CLI session history
|
|
253
329
|
|
|
254
330
|
Usage:
|
|
255
|
-
session-preserver [ls] [-n N] [--since 2d] [--tool claude|codex|opencode] [query]
|
|
331
|
+
session-preserver [ls] [-n N] [--since 2d] [--tool claude|codex|opencode] [--plain] [query]
|
|
332
|
+
(interactive picker in a TTY: ↑/↓ move, enter resume, s copy summary, q quit; --plain forces table)
|
|
256
333
|
session-preserver show <id> [-n N]
|
|
257
334
|
session-preserver summary <id>
|
|
258
335
|
session-preserver path <id>
|
|
336
|
+
session-preserver open <id> resume the session in its own CLI
|
|
259
337
|
|
|
260
338
|
ids accept unique prefixes, e.g. cx:019efd32`);
|
|
261
339
|
process.exit(code);
|
|
@@ -264,7 +342,7 @@ ids accept unique prefixes, e.g. cx:019efd32`);
|
|
|
264
342
|
const argv = process.argv.slice(2);
|
|
265
343
|
if (argv.includes("-h") || argv.includes("--help")) usage(0);
|
|
266
344
|
|
|
267
|
-
const COMMANDS = new Set(["ls", "show", "summary", "path"]);
|
|
345
|
+
const COMMANDS = new Set(["ls", "show", "summary", "path", "open"]);
|
|
268
346
|
let cmd = "ls";
|
|
269
347
|
if (COMMANDS.has(argv[0])) cmd = argv.shift();
|
|
270
348
|
|
|
@@ -276,7 +354,9 @@ if (cmd === "ls") {
|
|
|
276
354
|
else if (argv[i] === "--tool") {
|
|
277
355
|
args.tool = argv[++i];
|
|
278
356
|
if (!SOURCES[args.tool]) { console.error(`unknown tool '${args.tool}' (claude|codex|opencode)`); process.exit(1); }
|
|
279
|
-
}
|
|
357
|
+
}
|
|
358
|
+
else if (argv[i] === "--plain") args.plain = true;
|
|
359
|
+
else if (!args.query) args.query = argv[i];
|
|
280
360
|
else usage(1);
|
|
281
361
|
}
|
|
282
362
|
cmdLs(args);
|
|
@@ -287,5 +367,6 @@ if (cmd === "ls") {
|
|
|
287
367
|
const i = argv.indexOf("-n");
|
|
288
368
|
cmdShow(id, i > -1 ? Number(argv[i + 1]) : null);
|
|
289
369
|
} else if (cmd === "summary") cmdSummary(id);
|
|
370
|
+
else if (cmd === "open") cmdOpen(id);
|
|
290
371
|
else if (cmd === "path") console.log(find(id).source);
|
|
291
372
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "session-preserver",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Centralized session history for AI CLIs — Claude Code, Codex, OpenCode. Read-only.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
"bin": {
|
|
7
|
+
"session-preserver": "index.js",
|
|
8
|
+
"sp": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude",
|
|
19
|
+
"codex",
|
|
20
|
+
"opencode",
|
|
21
|
+
"ai",
|
|
22
|
+
"cli",
|
|
23
|
+
"history",
|
|
24
|
+
"session"
|
|
25
|
+
]
|
|
10
26
|
}
|