@druumen/sessions-db 0.1.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +249 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +10 -0
  4. package/README.md +250 -0
  5. package/cli/_write-helpers.mjs +99 -0
  6. package/cli/alias.mjs +115 -0
  7. package/cli/argparse.mjs +296 -0
  8. package/cli/close.mjs +116 -0
  9. package/cli/find.mjs +185 -0
  10. package/cli/format.mjs +277 -0
  11. package/cli/link-parent.mjs +133 -0
  12. package/cli/link.mjs +132 -0
  13. package/cli/rebuild.mjs +98 -0
  14. package/cli/sessions-db-session-start-main.mjs +454 -0
  15. package/cli/sessions-db-session-start.mjs +56 -0
  16. package/cli/sessions-db.mjs +119 -0
  17. package/cli/sweep.mjs +171 -0
  18. package/cli/tree.mjs +127 -0
  19. package/lib/git-context.mjs +479 -0
  20. package/lib/identity.mjs +616 -0
  21. package/lib/index.mjs +145 -0
  22. package/lib/init.mjs +185 -0
  23. package/lib/lock.mjs +86 -0
  24. package/lib/operations.mjs +490 -0
  25. package/lib/paths.mjs +199 -0
  26. package/lib/projection.mjs +496 -0
  27. package/lib/sanitize.mjs +131 -0
  28. package/lib/storage.mjs +759 -0
  29. package/lib/sweep.mjs +209 -0
  30. package/lib/transcript.mjs +230 -0
  31. package/lib/types.mjs +276 -0
  32. package/lib/uuid.mjs +116 -0
  33. package/lib/watch.mjs +217 -0
  34. package/package.json +53 -0
  35. package/types/git-context.d.mts +98 -0
  36. package/types/identity.d.mts +658 -0
  37. package/types/index.d.mts +10 -0
  38. package/types/index.d.ts +127 -0
  39. package/types/init.d.mts +53 -0
  40. package/types/lock.d.mts +18 -0
  41. package/types/operations.d.mts +204 -0
  42. package/types/paths.d.mts +54 -0
  43. package/types/projection.d.mts +79 -0
  44. package/types/sanitize.d.mts +39 -0
  45. package/types/storage.d.mts +276 -0
  46. package/types/sweep.d.mts +58 -0
  47. package/types/transcript.d.mts +59 -0
  48. package/types/types.d.mts +255 -0
  49. package/types/uuid.d.mts +17 -0
  50. package/types/watch.d.mts +33 -0
package/cli/sweep.mjs ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * `sessions-db sweep` — compute and apply activity_state transitions
3
+ * (active → idle → archived) by recency.
4
+ *
5
+ * Day 3 refactor: the planning + commit loop lives in
6
+ * `lib/operations.runSweep`. This handler is a thin wrapper that handles
7
+ * argparse, output rendering (human / JSON / quiet), and exit code mapping.
8
+ *
9
+ * Threshold validation (positive numbers; archive >= idle) stays in the
10
+ * CLI as exit-2 argparse errors so the test suite can pin both message
11
+ * and code without a library prefix in the message.
12
+ */
13
+
14
+ import { runSweep } from '../lib/operations.mjs';
15
+ import { ArgparseError, formatHelp, parseArgs } from './argparse.mjs';
16
+ import { formatJSON } from './format.mjs';
17
+
18
+ const SPEC = {
19
+ positional: [],
20
+ flags: {
21
+ '--dry-run': { type: 'boolean' },
22
+ '--idle-threshold-days': { type: 'number' },
23
+ '--archive-threshold-days': { type: 'number' },
24
+ '--json': { type: 'boolean' },
25
+ '--root': { type: 'string' },
26
+ '--quiet': { type: 'boolean' },
27
+ },
28
+ };
29
+
30
+ export const HELP = formatHelp({
31
+ usage: 'sessions-db sweep [--dry-run] [--idle-threshold-days N] [--archive-threshold-days N]',
32
+ summary: 'Compute activity_state transitions (active → idle → archived) and write sweep events.',
33
+ flags: [
34
+ { name: '--dry-run', desc: 'print planned transitions without writing events' },
35
+ { name: '--idle-threshold-days <N>', desc: 'override idle threshold (default: _meta.idle_threshold_days || 14)' },
36
+ { name: '--archive-threshold-days <N>', desc: 'override archive threshold (default: _meta.archive_threshold_days || 30)' },
37
+ { name: '--json', desc: 'JSON output (machine-readable)' },
38
+ { name: '--root <p>', desc: 'override storage root (default cwd)' },
39
+ { name: '--quiet', desc: 'silent stdout (exit code only)' },
40
+ ],
41
+ examples: [
42
+ 'sessions-db sweep --dry-run # preview transitions',
43
+ 'sessions-db sweep # apply transitions',
44
+ 'sessions-db sweep --idle-threshold-days 7 # one-off override',
45
+ ],
46
+ });
47
+
48
+ export async function run(argv) {
49
+ let parsed;
50
+ try {
51
+ parsed = parseArgs(argv, SPEC);
52
+ } catch (err) {
53
+ if (err instanceof ArgparseError) {
54
+ process.stderr.write(`error: ${err.message}\n\n${HELP}`);
55
+ process.exit(err.exitCode);
56
+ }
57
+ throw err;
58
+ }
59
+
60
+ if (parsed.helpRequested) {
61
+ process.stdout.write(HELP);
62
+ return;
63
+ }
64
+
65
+ const root = parsed.flags['--root'];
66
+ const dryRun = parsed.flags['--dry-run'] === true;
67
+ const json = parsed.flags['--json'] === true;
68
+ const quiet = parsed.flags['--quiet'] === true;
69
+
70
+ // Argparse-class threshold validation. parseArgs already rejected
71
+ // non-numeric values; we additionally reject 0 / negative so a typo
72
+ // like `--idle-threshold-days 0` does not silently disable the
73
+ // threshold (which would auto-archive every session). These exit codes
74
+ // (2, "must be a positive number" / "must be >= --idle-threshold-days")
75
+ // are what the test suite pins against.
76
+ const idleThresholdDays = parsed.flags['--idle-threshold-days'];
77
+ if (idleThresholdDays !== undefined
78
+ && (!Number.isFinite(idleThresholdDays) || idleThresholdDays <= 0)) {
79
+ process.stderr.write(
80
+ `error: --idle-threshold-days must be a positive number (got: ${idleThresholdDays})\n`,
81
+ );
82
+ process.exit(2);
83
+ }
84
+ const archiveThresholdDays = parsed.flags['--archive-threshold-days'];
85
+ if (archiveThresholdDays !== undefined
86
+ && (!Number.isFinite(archiveThresholdDays) || archiveThresholdDays <= 0)) {
87
+ process.stderr.write(
88
+ `error: --archive-threshold-days must be a positive number (got: ${archiveThresholdDays})\n`,
89
+ );
90
+ process.exit(2);
91
+ }
92
+ if (idleThresholdDays !== undefined
93
+ && archiveThresholdDays !== undefined
94
+ && archiveThresholdDays < idleThresholdDays) {
95
+ process.stderr.write(
96
+ `error: --archive-threshold-days (${archiveThresholdDays}) must be >= --idle-threshold-days (${idleThresholdDays})\n`,
97
+ );
98
+ process.exit(2);
99
+ }
100
+
101
+ const opts = {
102
+ idleThresholdDays,
103
+ archiveThresholdDays,
104
+ dryRun,
105
+ ...(root ? { root } : {}),
106
+ };
107
+ const result = await runSweep(opts);
108
+
109
+ // Dry-run path — print plan and bail. ok is always true for dry-run.
110
+ if (dryRun) {
111
+ const transitions = result.transitions || [];
112
+ if (json) {
113
+ process.stdout.write(formatJSON({
114
+ ok: true,
115
+ dry_run: true,
116
+ transitions,
117
+ count: transitions.length,
118
+ }));
119
+ } else if (!quiet) {
120
+ if (transitions.length === 0) {
121
+ process.stdout.write('ok: sweep dry-run — no transitions needed\n');
122
+ } else {
123
+ process.stdout.write(
124
+ `ok: sweep dry-run — ${transitions.length} transition${transitions.length === 1 ? '' : 's'} planned:\n`,
125
+ );
126
+ for (const t of transitions) {
127
+ process.stdout.write(
128
+ ` ${t.stable_id} ${t.from_state} → ${t.to_state} (age ${t.age_days}d, last_progress ${t.effective_last_progress})\n`,
129
+ );
130
+ }
131
+ }
132
+ }
133
+ return;
134
+ }
135
+
136
+ const applied = result.applied || [];
137
+ const failed = result.failed || [];
138
+ const summary = result.summary || {
139
+ total: 0, applied: 0, failed: 0, to_idle: 0, to_archived: 0,
140
+ };
141
+
142
+ if (json) {
143
+ process.stdout.write(formatJSON({
144
+ ok: failed.length === 0,
145
+ applied,
146
+ failed,
147
+ summary,
148
+ }));
149
+ } else if (!quiet) {
150
+ if (summary.total === 0) {
151
+ process.stdout.write('ok: sweep — no transitions needed\n');
152
+ } else {
153
+ process.stdout.write(
154
+ `ok: sweep — ${applied.length} of ${summary.total} transition${summary.total === 1 ? '' : 's'} applied (${summary.to_idle} to idle, ${summary.to_archived} to archived)\n`,
155
+ );
156
+ for (const a of applied) {
157
+ process.stdout.write(
158
+ ` ${a.stable_id} ${a.from_state} → ${a.to_state} (age ${a.age_days}d)\n`,
159
+ );
160
+ }
161
+ if (failed.length > 0) {
162
+ process.stderr.write(`error: ${failed.length} transition${failed.length === 1 ? '' : 's'} failed:\n`);
163
+ for (const f of failed) {
164
+ process.stderr.write(` ${f.stable_id} ${f.from_state} → ${f.to_state}: ${f.error}\n`);
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ if (failed.length > 0) process.exit(1);
171
+ }
package/cli/tree.mjs ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * `sessions-db tree <stable_id>` — render the hub-spoke subtree rooted at
3
+ * `stable_id` (parent + recursive children). Read-only.
4
+ *
5
+ * The tree is built by inverting `parent_session_id` across all sessions in
6
+ * the projection. Depth is capped (formatTree internals) to defend against
7
+ * accidental circular chains.
8
+ *
9
+ * Exit codes:
10
+ * 0 — root rendered
11
+ * 1 — root stable_id not found in projection
12
+ * 2 — argparse error (missing positional, etc.)
13
+ */
14
+
15
+ import { loadProjection } from '../lib/storage.mjs';
16
+ import { ArgparseError, formatHelp, parseArgs } from './argparse.mjs';
17
+ import { formatJSON, formatTree, shouldUseColor } from './format.mjs';
18
+
19
+ const SPEC = {
20
+ positional: [{ name: 'stable_id', required: true }],
21
+ flags: {
22
+ '--json': { type: 'boolean' },
23
+ '--no-color': { type: 'boolean' },
24
+ '--root': { type: 'string' },
25
+ '--quiet': { type: 'boolean' },
26
+ },
27
+ };
28
+
29
+ export const HELP = formatHelp({
30
+ usage: 'sessions-db tree <stable_id> [--json]',
31
+ summary: 'Render the hub-spoke parent → children subtree rooted at stable_id.',
32
+ flags: [
33
+ { name: '--json', desc: 'JSON output: { root, children: [{stable_id, alias, ...}] }' },
34
+ { name: '--no-color', desc: 'disable ANSI color' },
35
+ { name: '--root <path>', desc: 'override storage root (default cwd)' },
36
+ ],
37
+ examples: [
38
+ 'sessions-db tree sess_01970000-0000-7000-8000-00000000000a',
39
+ ],
40
+ });
41
+
42
+ export async function run(argv) {
43
+ let parsed;
44
+ try {
45
+ parsed = parseArgs(argv, SPEC);
46
+ } catch (err) {
47
+ if (err instanceof ArgparseError) {
48
+ process.stderr.write(`error: ${err.message}\n\n${HELP}`);
49
+ process.exit(err.exitCode);
50
+ }
51
+ throw err;
52
+ }
53
+
54
+ if (parsed.helpRequested) {
55
+ process.stdout.write(HELP);
56
+ return;
57
+ }
58
+
59
+ const stableId = parsed.positional.stable_id;
60
+ const root = parsed.flags['--root'];
61
+ const projection = await loadProjection(root ? { root } : {});
62
+
63
+ if (!projection.sessions || !projection.sessions[stableId]) {
64
+ process.stderr.write(`error: stable_id not found: ${stableId}\n`);
65
+ process.exit(1);
66
+ }
67
+
68
+ if (parsed.flags['--quiet']) return;
69
+
70
+ if (parsed.flags['--json']) {
71
+ process.stdout.write(formatJSON(buildTreeJSON(stableId, projection)));
72
+ return;
73
+ }
74
+
75
+ const useColor = shouldUseColor(
76
+ process.stdout.isTTY,
77
+ process.env,
78
+ parsed.flags['--no-color'] === true,
79
+ );
80
+ process.stdout.write(formatTree(stableId, projection, { useColor }));
81
+ }
82
+
83
+ /**
84
+ * Build a JSON-friendly tree object. Exposed for tests.
85
+ */
86
+ export function buildTreeJSON(rootId, projection) {
87
+ const sessions = projection && projection.sessions ? projection.sessions : {};
88
+ const childIdx = new Map();
89
+ for (const [sid, s] of Object.entries(sessions)) {
90
+ const parent = s && s.parent_session_id;
91
+ if (parent && parent !== sid) {
92
+ if (!childIdx.has(parent)) childIdx.set(parent, []);
93
+ childIdx.get(parent).push(sid);
94
+ }
95
+ }
96
+ const visited = new Set();
97
+ const MAX_DEPTH = 32;
98
+
99
+ function build(sid, depth) {
100
+ if (depth > MAX_DEPTH) return { stable_id: sid, truncated: 'max-depth' };
101
+ if (visited.has(sid)) return { stable_id: sid, truncated: 'circular' };
102
+ visited.add(sid);
103
+ const s = sessions[sid];
104
+ const kids = (childIdx.get(sid) || [])
105
+ .slice()
106
+ .sort((a, b) => {
107
+ const ca = sessions[a] && sessions[a].created_at;
108
+ const cb = sessions[b] && sessions[b].created_at;
109
+ if (!ca && !cb) return 0;
110
+ if (!ca) return 1;
111
+ if (!cb) return -1;
112
+ return ca < cb ? -1 : ca > cb ? 1 : 0;
113
+ })
114
+ .map((kid) => build(kid, depth + 1));
115
+ return {
116
+ stable_id: sid,
117
+ alias: s ? s.alias : null,
118
+ activity_state: s ? s.activity_state : null,
119
+ outcome: s ? s.outcome : null,
120
+ last_progress_at: s ? s.last_progress_at : null,
121
+ branch_current: s ? s.branch_current : null,
122
+ children: kids,
123
+ };
124
+ }
125
+
126
+ return build(rootId, 0);
127
+ }