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.
Files changed (2) hide show
  1. package/index.js +85 -4
  2. 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
- const idW = Math.max(...rows.map((r) => r.id.length)) ;
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
- } else if (!args.query) args.query = argv[i];
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.0",
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": { "session-preserver": "index.js", "sp": "index.js" },
7
- "engines": { "node": ">=18" },
8
- "files": ["index.js", "README.md"],
9
- "keywords": ["claude", "codex", "opencode", "ai", "cli", "history", "session"]
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
  }