@illuminis/comprism 0.1.3 → 0.1.4

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 (131) hide show
  1. package/README.md +61 -2
  2. package/out/agent/command.d.ts +75 -4
  3. package/out/agent/command.js +220 -25
  4. package/out/agent/render.d.ts +17 -2
  5. package/out/agent/render.js +157 -14
  6. package/out/agent/session.d.ts +141 -2
  7. package/out/agent/session.js +735 -149
  8. package/out/commands/agents.d.ts +2 -0
  9. package/out/commands/agents.js +79 -0
  10. package/out/commands/ask.d.ts +1 -1
  11. package/out/commands/ask.js +78 -11
  12. package/out/commands/commands-thin.js +79 -17
  13. package/out/commands/config.d.ts +1 -0
  14. package/out/commands/config.js +138 -0
  15. package/out/commands/cost.d.ts +1 -0
  16. package/out/commands/cost.js +167 -0
  17. package/out/commands/hooks.d.ts +1 -0
  18. package/out/commands/hooks.js +83 -0
  19. package/out/commands/install.d.ts +44 -1
  20. package/out/commands/install.js +198 -4
  21. package/out/commands/instructions.d.ts +1 -0
  22. package/out/commands/instructions.js +113 -0
  23. package/out/commands/integrations.d.ts +3 -0
  24. package/out/commands/integrations.js +215 -0
  25. package/out/commands/jobs.d.ts +5 -0
  26. package/out/commands/jobs.js +157 -0
  27. package/out/commands/login.js +188 -36
  28. package/out/commands/memory.d.ts +3 -0
  29. package/out/commands/memory.js +113 -0
  30. package/out/commands/permissions.d.ts +1 -0
  31. package/out/commands/permissions.js +94 -0
  32. package/out/commands/plugins.d.ts +4 -0
  33. package/out/commands/plugins.js +192 -0
  34. package/out/commands/privacy.d.ts +1 -0
  35. package/out/commands/privacy.js +57 -0
  36. package/out/commands/providerKey.d.ts +32 -0
  37. package/out/commands/providerKey.js +108 -0
  38. package/out/commands/repl.d.ts +8 -1
  39. package/out/commands/repl.js +1207 -118
  40. package/out/commands/report.d.ts +39 -0
  41. package/out/commands/report.js +115 -0
  42. package/out/commands/review.d.ts +5 -0
  43. package/out/commands/review.js +223 -0
  44. package/out/commands/sessions.d.ts +23 -0
  45. package/out/commands/sessions.js +115 -0
  46. package/out/commands/settings.d.ts +3 -1
  47. package/out/commands/settings.js +18 -16
  48. package/out/commands/skills.d.ts +21 -0
  49. package/out/commands/skills.js +207 -0
  50. package/out/commands/unattended.d.ts +7 -0
  51. package/out/commands/unattended.js +351 -0
  52. package/out/commands/update.d.ts +1 -0
  53. package/out/commands/update.js +123 -0
  54. package/out/commands/worktrees.d.ts +5 -0
  55. package/out/commands/worktrees.js +186 -0
  56. package/out/executor/browser.d.ts +14 -0
  57. package/out/executor/browser.js +270 -0
  58. package/out/executor/diagnostics.d.ts +2 -0
  59. package/out/executor/diagnostics.js +181 -0
  60. package/out/executor/files.js +270 -40
  61. package/out/executor/git.js +42 -29
  62. package/out/executor/hooks.d.ts +42 -58
  63. package/out/executor/hooks.js +89 -182
  64. package/out/executor/index.d.ts +21 -5
  65. package/out/executor/index.js +160 -14
  66. package/out/executor/paths.d.ts +6 -1
  67. package/out/executor/paths.js +34 -6
  68. package/out/executor/sandbox.d.ts +40 -0
  69. package/out/executor/sandbox.js +299 -0
  70. package/out/executor/shell.d.ts +49 -4
  71. package/out/executor/shell.js +302 -56
  72. package/out/executor/toolservers.d.ts +20 -0
  73. package/out/executor/toolservers.js +189 -0
  74. package/out/executor/worktree.d.ts +9 -0
  75. package/out/executor/worktree.js +119 -0
  76. package/out/graph/read-python.js +2 -1
  77. package/out/lib/attach.d.ts +56 -12
  78. package/out/lib/attach.js +230 -63
  79. package/out/lib/clipboard.d.ts +23 -0
  80. package/out/lib/clipboard.js +182 -0
  81. package/out/lib/commandlist.d.ts +20 -0
  82. package/out/lib/commandlist.js +58 -0
  83. package/out/lib/decision.d.ts +22 -0
  84. package/out/lib/decision.js +50 -0
  85. package/out/lib/fingerprint.d.ts +25 -0
  86. package/out/lib/fingerprint.js +58 -0
  87. package/out/lib/gateway.d.ts +186 -2
  88. package/out/lib/gateway.js +59 -4
  89. package/out/lib/history.d.ts +24 -0
  90. package/out/lib/history.js +137 -0
  91. package/out/lib/ide.d.ts +19 -0
  92. package/out/lib/ide.js +131 -0
  93. package/out/lib/keyboard.d.ts +95 -0
  94. package/out/lib/keyboard.js +383 -0
  95. package/out/lib/machine.d.ts +21 -0
  96. package/out/lib/machine.js +91 -0
  97. package/out/lib/notify.d.ts +4 -0
  98. package/out/lib/notify.js +52 -0
  99. package/out/lib/output.d.ts +48 -0
  100. package/out/lib/output.js +108 -0
  101. package/out/lib/project-ops.d.ts +19 -0
  102. package/out/lib/project-ops.js +146 -0
  103. package/out/lib/project.d.ts +28 -0
  104. package/out/lib/project.js +114 -0
  105. package/out/lib/prompt.js +15 -2
  106. package/out/lib/queue.d.ts +13 -0
  107. package/out/lib/queue.js +116 -0
  108. package/out/lib/readiness.d.ts +19 -0
  109. package/out/lib/readiness.js +170 -1
  110. package/out/lib/self.d.ts +23 -0
  111. package/out/lib/self.js +124 -0
  112. package/out/lib/sessions.d.ts +19 -0
  113. package/out/lib/sessions.js +221 -0
  114. package/out/lib/stdin.d.ts +32 -0
  115. package/out/lib/stdin.js +117 -0
  116. package/out/lib/store.d.ts +40 -0
  117. package/out/lib/store.js +138 -0
  118. package/out/lib/sync.d.ts +18 -0
  119. package/out/lib/sync.js +81 -0
  120. package/out/lib/ui.d.ts +2 -4
  121. package/out/lib/ui.js +31 -25
  122. package/out/lib/voice.js +24 -0
  123. package/out/postinstall.js +42 -17
  124. package/out/providers/anthropic.d.ts +22 -0
  125. package/out/providers/anthropic.js +80 -0
  126. package/out/providers/index.d.ts +8 -0
  127. package/out/providers/index.js +83 -0
  128. package/out/providers/openai.d.ts +11 -0
  129. package/out/providers/openai.js +57 -0
  130. package/out/thin.js +594 -32
  131. package/package.json +9 -49
@@ -0,0 +1,39 @@
1
+ export interface Bucket {
2
+ key: string;
3
+ calls: number;
4
+ inputTokens: number;
5
+ outputTokens: number;
6
+ costUsd: number;
7
+ unpriced: number;
8
+ /**
9
+ * The weakest provenance among the rows in this bucket, in words.
10
+ *
11
+ * Derived, never asserted. The report used to print "observed, first party"
12
+ * on every line regardless of what the rows actually said, which meant the
13
+ * desktop connector's inferred, unpriced turns were displayed as measured
14
+ * spend. A product whose whole claim is "you can see where this number came
15
+ * from" cannot print a provenance it did not read off the rows.
16
+ */
17
+ provenance: string;
18
+ }
19
+ export interface Report {
20
+ since: Date | null;
21
+ rows: number;
22
+ totalCostUsd: number;
23
+ unpricedCalls: number;
24
+ totalInputTokens: number;
25
+ totalOutputTokens: number;
26
+ byModel: Bucket[];
27
+ byTier: Bucket[];
28
+ bySurface: Bucket[];
29
+ sessions: number;
30
+ /** Calls at attempt_index > 0: work that had to be done twice. */
31
+ remediationCalls: number;
32
+ remediationCostUsd: number;
33
+ /** Repeat attempts the provider never priced. Counted, never summed as zero. */
34
+ remediationUnpriced: number;
35
+ firstTs: string | null;
36
+ lastTs: string | null;
37
+ }
38
+ export declare function parseSince(spec: string | undefined): Date | null;
39
+ export declare function buildReport(since: Date | null): Report;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSince = parseSince;
4
+ exports.buildReport = buildReport;
5
+ /**
6
+ * Spend reporting, computed from the ledger and nothing else.
7
+ *
8
+ * Every figure below is a sum over rows that were written once and never edited,
9
+ * so `report` can be reconciled against the provider's own invoice line by line.
10
+ * That reconciliation is the acceptance test for this file, and it is also the
11
+ * first thing a finance team will ask for.
12
+ *
13
+ * Rows the provider never priced are counted on their own line rather than
14
+ * folded into the total. A total that quietly includes zeros for calls of
15
+ * unknown cost is exactly the kind of figure this product exists to replace.
16
+ */
17
+ const store_1 = require("../lib/store");
18
+ function parseSince(spec) {
19
+ if (!spec)
20
+ return null;
21
+ const m = /^(\d+)([dhw])$/.exec(spec.trim());
22
+ if (!m)
23
+ return null;
24
+ const n = Number(m[1]);
25
+ const unit = m[2];
26
+ const ms = unit === 'h' ? 3_600_000 : unit === 'w' ? 604_800_000 : 86_400_000;
27
+ return new Date(Date.now() - n * ms);
28
+ }
29
+ function bucketise(rows, keyOf) {
30
+ const map = new Map();
31
+ const derivations = new Map();
32
+ const provenances = new Map();
33
+ for (const r of rows) {
34
+ const key = keyOf(r);
35
+ const b = map.get(key) || {
36
+ key,
37
+ calls: 0,
38
+ inputTokens: 0,
39
+ outputTokens: 0,
40
+ costUsd: 0,
41
+ unpriced: 0,
42
+ provenance: '',
43
+ };
44
+ b.calls += 1;
45
+ b.inputTokens += r.input_tokens ?? 0;
46
+ b.outputTokens += r.output_tokens ?? 0;
47
+ if (r.cost_usd === null)
48
+ b.unpriced += 1;
49
+ else
50
+ b.costUsd += r.cost_usd;
51
+ map.set(key, b);
52
+ const seen = derivations.get(key) || new Set();
53
+ seen.add(r.meta?.derivation || 'observed');
54
+ derivations.set(key, seen);
55
+ const sources = provenances.get(key) || new Set();
56
+ sources.add(r.meta?.provenance || 'first_party');
57
+ provenances.set(key, sources);
58
+ }
59
+ for (const b of map.values()) {
60
+ b.provenance = describe(derivations.get(b.key) || new Set(), provenances.get(b.key) || new Set(), b.unpriced, b.calls);
61
+ }
62
+ return [...map.values()].sort((a, b) => b.costUsd - a.costUsd);
63
+ }
64
+ /**
65
+ * Say what a bucket's figures actually rest on.
66
+ *
67
+ * A displayed figure inherits the WEAKEST provenance among its inputs, so a
68
+ * bucket mixing observed and inferred rows is described as mixed, not as
69
+ * observed. And a bucket where nothing was priced says so in place of a cost,
70
+ * because "$0.00" and "we could not price this" are different facts that look
71
+ * identical on a screen.
72
+ */
73
+ function describe(derivations, provenances, unpriced, calls) {
74
+ const source = provenances.has('imported')
75
+ ? provenances.size > 1
76
+ ? 'mixed source'
77
+ : 'imported'
78
+ : 'first party';
79
+ let strength;
80
+ if (derivations.size > 1)
81
+ strength = 'mixed';
82
+ else if (derivations.has('estimated'))
83
+ strength = 'estimated';
84
+ else if (derivations.has('inferred'))
85
+ strength = 'inferred';
86
+ else
87
+ strength = 'observed';
88
+ if (unpriced === calls && calls > 0)
89
+ return `${strength}, ${source}, none priced`;
90
+ if (unpriced > 0)
91
+ return `${strength}, ${source}, ${unpriced} unpriced`;
92
+ return `${strength}, ${source}`;
93
+ }
94
+ function buildReport(since) {
95
+ const all = (0, store_1.readRows)(store_1.LEDGER_FILE);
96
+ const rows = since ? all.filter((r) => new Date(r.ts) >= since) : all;
97
+ const remediation = rows.filter((r) => r.attempt_index > 0);
98
+ return {
99
+ since,
100
+ rows: rows.length,
101
+ totalCostUsd: rows.reduce((s, r) => s + (r.cost_usd ?? 0), 0),
102
+ unpricedCalls: rows.filter((r) => r.cost_usd === null).length,
103
+ totalInputTokens: rows.reduce((s, r) => s + (r.input_tokens ?? 0), 0),
104
+ totalOutputTokens: rows.reduce((s, r) => s + (r.output_tokens ?? 0), 0),
105
+ byModel: bucketise(rows, (r) => r.model_dispatched),
106
+ byTier: bucketise(rows, (r) => r.task_tier),
107
+ bySurface: bucketise(rows, (r) => r.source_system),
108
+ sessions: new Set(rows.map((r) => r.session_id)).size,
109
+ remediationCalls: remediation.length,
110
+ remediationCostUsd: remediation.reduce((s, r) => s + (r.cost_usd ?? 0), 0),
111
+ remediationUnpriced: remediation.filter((r) => r.cost_usd === null).length,
112
+ firstTs: rows[0]?.ts ?? null,
113
+ lastTs: rows[rows.length - 1]?.ts ?? null,
114
+ };
115
+ }
@@ -0,0 +1,5 @@
1
+ export declare function review(op: string, payload: Record<string, unknown>, cwd?: string): Promise<import("../lib/gateway").GatewayReply>;
2
+ export declare function cmdChanges(args: string[]): Promise<number>;
3
+ export declare function cmdDiff(args: string[]): Promise<number>;
4
+ /** `confirmWith`: a session's own keyboard, so two readers never share it. */
5
+ export declare function cmdRewind(args: string[], confirmWith?: (question: string) => Promise<boolean>): Promise<number>;
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.review = review;
37
+ exports.cmdChanges = cmdChanges;
38
+ exports.cmdDiff = cmdDiff;
39
+ exports.cmdRewind = cmdRewind;
40
+ /**
41
+ * `comprism changes`, `comprism diff` and `comprism rewind` (manual 7.1 to 7.4,
42
+ * 7.7).
43
+ *
44
+ * The service knows what the agent changed, from the job's own records and its
45
+ * checkpoints, and decides what putting a file back means: restore, delete or
46
+ * rename back, and what cannot be undone. This file prints its words, and does
47
+ * the physical part of a rewind: before writing each file it checks the file
48
+ * still holds what the agent left, and skips it otherwise unless `--force`.
49
+ */
50
+ const crypto = __importStar(require("crypto"));
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const ui = __importStar(require("../lib/ui"));
54
+ const gateway_1 = require("../lib/gateway");
55
+ const project_1 = require("../lib/project");
56
+ const project_2 = require("../lib/project");
57
+ const out = (s = "") => process.stdout.write(`${s}\n`);
58
+ const err = (s = "") => process.stderr.write(`${s}\n`);
59
+ async function review(op, payload, cwd = process.cwd()) {
60
+ const project = (0, project_1.resolveProject)(cwd);
61
+ return (0, gateway_1.ask)({ intent: "review", prompt: "", op, payload: { ...(0, project_2.asWire)(project), ...payload } });
62
+ }
63
+ function refused(reply) {
64
+ if (!reply.ok)
65
+ return reply.message || "Your workspace could not be reached.";
66
+ if (reply.served === false)
67
+ return "Your workspace is running an older build that does not answer this yet.";
68
+ if (reply.granted === false)
69
+ return reply.message || "Refused.";
70
+ return null;
71
+ }
72
+ function words(args, valueFlags) {
73
+ return args.filter((a, i) => !a.startsWith("--") && !valueFlags.includes(String(args[i - 1])));
74
+ }
75
+ async function cmdChanges(args) {
76
+ const [job] = words(args, []);
77
+ const reply = await review("changes", { job: job ?? "" });
78
+ const why = refused(reply);
79
+ if (why) {
80
+ err(ui.fail(why));
81
+ return 1;
82
+ }
83
+ out();
84
+ for (const line of reply.lines ?? [])
85
+ out(` ${line}`);
86
+ out();
87
+ return 0;
88
+ }
89
+ async function cmdDiff(args) {
90
+ const [first, second] = words(args, []);
91
+ // `diff calc.py` is a file of the latest job; `diff j_7c21 calc.py` names both.
92
+ const isJob = first && !second && !first.includes(".") && !first.includes("/") ? true : Boolean(second);
93
+ const reply = await review("diff", { job: isJob ? first : "", file: isJob ? (second ?? "") : (first ?? "") });
94
+ const why = refused(reply);
95
+ if (why) {
96
+ err(ui.fail(why));
97
+ return 1;
98
+ }
99
+ out();
100
+ for (const line of reply.lines ?? []) {
101
+ const shown = line.startsWith("+") && !line.startsWith("+++") ? ui.c.green(line)
102
+ : line.startsWith("-") && !line.startsWith("---") ? ui.c.red(line) : line;
103
+ out(` ${shown}`);
104
+ }
105
+ out();
106
+ return 0;
107
+ }
108
+ function sha(file) {
109
+ try {
110
+ return { exists: true, sha: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") };
111
+ }
112
+ catch {
113
+ return { exists: false, sha: null };
114
+ }
115
+ }
116
+ function inside(root, rel) {
117
+ const base = fs.realpathSync(root);
118
+ const where = path.resolve(base, rel);
119
+ return where.startsWith(base + path.sep) ? where : null;
120
+ }
121
+ async function confirm(question) {
122
+ if (!process.stdin.isTTY)
123
+ return false;
124
+ const { ask: prompt, closePrompt } = await Promise.resolve().then(() => __importStar(require("../lib/prompt")));
125
+ const answer = await prompt(` ${question} `);
126
+ closePrompt();
127
+ return /^y(es)?$/i.test(answer.trim());
128
+ }
129
+ /** `confirmWith`: a session's own keyboard, so two readers never share it. */
130
+ async function cmdRewind(args, confirmWith) {
131
+ const force = args.includes("--force");
132
+ const yes = args.includes("--yes");
133
+ const stepAt = args.indexOf("--to-step");
134
+ const toStep = stepAt === -1 ? null : Number(args[stepAt + 1]);
135
+ if (stepAt !== -1 && (!Number.isInteger(toStep) || (toStep ?? -1) < 0)) {
136
+ err(ui.fail("--to-step takes a step number, for example --to-step 2."));
137
+ return 2;
138
+ }
139
+ const [job, file] = words(args, ["--to-step"]);
140
+ const project = (0, project_1.resolveProject)(process.cwd());
141
+ const reply = await review("rewind_plan", { job: job ?? "", to_step: toStep, file: file ?? "" });
142
+ const why = refused(reply);
143
+ if (why) {
144
+ err(ui.fail(why));
145
+ return 1;
146
+ }
147
+ const data = (reply.data ?? {});
148
+ const items = data.items ?? [];
149
+ out();
150
+ for (const line of reply.lines ?? [])
151
+ out(` ${line}`);
152
+ if (!items.length) {
153
+ out();
154
+ return 0;
155
+ }
156
+ if (!yes && !(await (confirmWith ?? confirm)("Go ahead? [y/N]"))) {
157
+ out(ui.c.dim(" Nothing was changed."));
158
+ out();
159
+ return process.stdin.isTTY ? 0 : 4;
160
+ }
161
+ const results = [];
162
+ for (const item of items) {
163
+ const where = inside(project.root, item.path);
164
+ if (!where) {
165
+ results.push({ path: item.path, result: "outside this project, not restored" });
166
+ continue;
167
+ }
168
+ // Your later changes are never overwritten (7.4): the file must still be
169
+ // what the agent left, or it is skipped unless --force.
170
+ const now = sha(where);
171
+ const agentLeft = item.expected_exists === false ? { exists: false, sha: null }
172
+ : { exists: true, sha: item.expected_sha ?? null };
173
+ const same = now.exists === agentLeft.exists && (!now.exists || !agentLeft.sha || now.sha === agentLeft.sha);
174
+ if (!same && !force) {
175
+ results.push({ path: item.path, result: "changed since the job, not restored (--force restores it)" });
176
+ continue;
177
+ }
178
+ if (!same && force && now.exists) {
179
+ out(ui.c.amber(` --force: ${item.path} was changed since the job; those changes are lost:`));
180
+ const current = fs.readFileSync(where, "utf8").split("\n").slice(0, 20);
181
+ for (const l of current)
182
+ out(ui.c.dim(` ${l}`));
183
+ }
184
+ try {
185
+ if (item.action === "delete") {
186
+ if (now.exists)
187
+ fs.rmSync(where);
188
+ results.push({ path: item.path, result: "deleted (created by the agent)" });
189
+ }
190
+ else if (item.action === "rename_back" && item.to) {
191
+ const from = inside(project.root, item.to);
192
+ if (!from)
193
+ throw new Error("the new name is outside this project");
194
+ fs.mkdirSync(path.dirname(where), { recursive: true });
195
+ if (fs.existsSync(from))
196
+ fs.renameSync(from, where);
197
+ if (typeof item.content === "string")
198
+ fs.writeFileSync(where, item.content);
199
+ results.push({ path: item.path, result: `renamed back from ${item.to}` });
200
+ }
201
+ else {
202
+ if (typeof item.content !== "string")
203
+ throw new Error("no copy was kept");
204
+ fs.mkdirSync(path.dirname(where), { recursive: true });
205
+ fs.writeFileSync(where, item.content);
206
+ results.push({ path: item.path, result: "restored" });
207
+ }
208
+ }
209
+ catch (e) {
210
+ results.push({ path: item.path, result: `not restored: ${e.message}` });
211
+ }
212
+ }
213
+ const ok = results.filter((r) => /^(restored|deleted|renamed back)/.test(r.result));
214
+ out();
215
+ for (const r of results)
216
+ out(` ${ok.includes(r) ? ui.c.green("done") : ui.c.amber("left")} ${r.path} ${r.result}`);
217
+ out(ok.length === results.length
218
+ ? ui.ok(`Rewound ${ok.length} file${ok.length === 1 ? "" : "s"}.`)
219
+ : ui.warn(`Rewound ${ok.length} of ${results.length} files. The others were left as they are.`));
220
+ out();
221
+ await review("rewound", { job_id: data.job_id, to_step: toStep, results });
222
+ return ok.length === results.length ? 0 : 1;
223
+ }
@@ -0,0 +1,23 @@
1
+ import type { Message, PersistedTurn, Provider, Session, SourceSurface } from '../lib/types';
2
+ export declare function listSessions(): Session[];
3
+ export declare function getSession(sessionId: string): Session | undefined;
4
+ export declare function newSession(title?: string, surface?: SourceSurface): Session;
5
+ /** Context size in tokens, from what has already been persisted. */
6
+ export declare function contextTokens(session: Session): number;
7
+ export declare function isFresh(session: Session): boolean;
8
+ export declare function appendTurn(sessionId: string, role: 'user' | 'assistant', content: string, meta?: {
9
+ model?: string;
10
+ provider?: Provider;
11
+ }): PersistedTurn;
12
+ /**
13
+ * Rebuild the message history a provider needs.
14
+ *
15
+ * Only possible when text storage is on. Otherwise the honest answer is an
16
+ * empty history and a caller that says so - being content-blind has a cost, and
17
+ * paying it visibly beats storing the text and calling the product
18
+ * content-blind anyway.
19
+ */
20
+ export declare function replayableHistory(session: Session): {
21
+ messages: Message[];
22
+ replayable: boolean;
23
+ };
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.listSessions = listSessions;
7
+ exports.getSession = getSession;
8
+ exports.newSession = newSession;
9
+ exports.contextTokens = contextTokens;
10
+ exports.isFresh = isFresh;
11
+ exports.appendTurn = appendTurn;
12
+ exports.replayableHistory = replayableHistory;
13
+ /**
14
+ * Work sessions: a person's working conversation, recorded content-blind.
15
+ *
16
+ * `sessions.json` is the one store here that is rewritten rather than appended.
17
+ * That is deliberate and it is not a hole in the append-only rule: a session is
18
+ * current state (a title, a turn count, a context size), not evidence. Every
19
+ * evidentiary claim - what was dispatched, what it cost, what was decided -
20
+ * lives in the ledger and the decision records, which are never rewritten.
21
+ *
22
+ * What is persisted per turn is the SHAPE of the turn: role, fingerprint, size,
23
+ * and which model answered. Not its words, unless the tenant has explicitly
24
+ * turned text storage on.
25
+ */
26
+ const node_fs_1 = __importDefault(require("node:fs"));
27
+ const config_1 = require("../lib/config");
28
+ const fingerprint_1 = require("../lib/fingerprint");
29
+ const store_1 = require("../lib/store");
30
+ function readAll() {
31
+ const p = (0, store_1.storePath)(store_1.SESSIONS_FILE);
32
+ if (!node_fs_1.default.existsSync(p))
33
+ return {};
34
+ try {
35
+ return JSON.parse(node_fs_1.default.readFileSync(p, 'utf8'));
36
+ }
37
+ catch {
38
+ // A corrupt session index must not stop someone working. Start clean; the
39
+ // ledger and the decision records are the parts that matter and they are
40
+ // append-only, so nothing evidentiary is lost here.
41
+ return {};
42
+ }
43
+ }
44
+ function writeAll(map) {
45
+ (0, config_1.ensureHome)();
46
+ node_fs_1.default.writeFileSync((0, store_1.storePath)(store_1.SESSIONS_FILE), `${JSON.stringify(map, null, 2)}\n`, { mode: 0o600 });
47
+ }
48
+ function listSessions() {
49
+ return Object.values(readAll()).sort((a, b) => b.updated.localeCompare(a.updated));
50
+ }
51
+ function getSession(sessionId) {
52
+ return readAll()[sessionId];
53
+ }
54
+ function newSession(title, surface = 'sdk') {
55
+ const now = new Date().toISOString();
56
+ const session = {
57
+ session_id: (0, fingerprint_1.newId)('ses'),
58
+ title: title || 'Untitled session',
59
+ created: now,
60
+ updated: now,
61
+ source_surface: surface,
62
+ turns: [],
63
+ };
64
+ const map = readAll();
65
+ map[session.session_id] = session;
66
+ writeAll(map);
67
+ return session;
68
+ }
69
+ /** Context size in tokens, from what has already been persisted. */
70
+ function contextTokens(session) {
71
+ return session.turns.reduce((sum, t) => sum + t.approx_tokens, 0);
72
+ }
73
+ function isFresh(session) {
74
+ return session.turns.length === 0;
75
+ }
76
+ function appendTurn(sessionId, role, content, meta = {}) {
77
+ const cfg = (0, config_1.loadConfig)();
78
+ const turn = {
79
+ role,
80
+ ts: new Date().toISOString(),
81
+ fingerprint: (0, fingerprint_1.fingerprint)(content),
82
+ chars: content.length,
83
+ approx_tokens: (0, fingerprint_1.approxTokens)(content),
84
+ ...(meta.model ? { model: meta.model } : {}),
85
+ ...(meta.provider ? { provider: meta.provider } : {}),
86
+ // Off by default. The whole product is built to work without it.
87
+ ...(cfg.contentPolicy.storeText ? { content } : {}),
88
+ };
89
+ const map = readAll();
90
+ const session = map[sessionId];
91
+ if (!session)
92
+ throw new Error(`unknown session: ${sessionId}`);
93
+ session.turns.push(turn);
94
+ session.updated = turn.ts;
95
+ writeAll(map);
96
+ return turn;
97
+ }
98
+ /**
99
+ * Rebuild the message history a provider needs.
100
+ *
101
+ * Only possible when text storage is on. Otherwise the honest answer is an
102
+ * empty history and a caller that says so - being content-blind has a cost, and
103
+ * paying it visibly beats storing the text and calling the product
104
+ * content-blind anyway.
105
+ */
106
+ function replayableHistory(session) {
107
+ const withText = session.turns.filter((t) => typeof t.content === 'string');
108
+ if (session.turns.length > 0 && withText.length !== session.turns.length) {
109
+ return { messages: [], replayable: false };
110
+ }
111
+ return {
112
+ messages: withText.map((t) => ({ role: t.role, content: t.content })),
113
+ replayable: true,
114
+ };
115
+ }
@@ -18,4 +18,6 @@ export declare function cmdProviders(): Promise<void>;
18
18
  * have used, which is the only thing a saving can honestly be measured against.
19
19
  */
20
20
  export declare function cmdModel(args: string[]): Promise<void>;
21
- export declare function cmdWorkspace(args: string[]): Promise<void>;
21
+ export declare function cmdProject(args: string[]): Promise<void>;
22
+ /** The older name, kept so nothing that calls it breaks. */
23
+ export declare const cmdWorkspace: typeof cmdProject;
@@ -36,10 +36,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.cmdWorkspace = void 0;
39
40
  exports.currentWorkspace = currentWorkspace;
40
41
  exports.cmdProviders = cmdProviders;
41
42
  exports.cmdModel = cmdModel;
42
- exports.cmdWorkspace = cmdWorkspace;
43
+ exports.cmdProject = cmdProject;
43
44
  /**
44
45
  * The three things a person sets once and then forgets: which model they are
45
46
  * compared against, which vendors they can reach, and which folder they work in.
@@ -62,6 +63,7 @@ const node_path_1 = __importDefault(require("node:path"));
62
63
  const prompt_1 = require("../lib/prompt");
63
64
  const gateway_1 = require("../lib/gateway");
64
65
  const config_1 = require("../lib/config");
66
+ const project_1 = require("../lib/project");
65
67
  const ui = __importStar(require("../lib/ui"));
66
68
  const out = (s = '') => process.stdout.write(s + '\n');
67
69
  /** Where the chosen folder is remembered. Beside the rest of the settings. */
@@ -193,10 +195,10 @@ async function cmdModel(args) {
193
195
  out(` ${ui.c.dim('every receipt from now on prices your work against this model')}`);
194
196
  out();
195
197
  }
196
- async function cmdWorkspace(args) {
198
+ async function cmdProject(args) {
197
199
  const named = args.find((a) => !a.startsWith('--'));
198
200
  const current = currentWorkspace();
199
- out(ui.banner('Working folder'));
201
+ out(ui.banner('Project folder'));
200
202
  out();
201
203
  out(` ${ui.c.bold(current.path)}`);
202
204
  out(` ${ui.c.dim(current.source === 'cwd'
@@ -212,8 +214,12 @@ async function cmdWorkspace(args) {
212
214
  out();
213
215
  return;
214
216
  }
217
+ // `comprism project` on its own shows the folder (manual 2.1). Asking only
218
+ // when a person is at the keyboard and typed nothing, as before.
215
219
  let target = named;
216
220
  if (!target) {
221
+ if (!process.stdin.isTTY)
222
+ return;
217
223
  const answer = await (0, prompt_1.ask)(' new folder, or enter to keep it: ');
218
224
  (0, prompt_1.closePrompt)();
219
225
  if (!answer) {
@@ -223,22 +229,18 @@ async function cmdWorkspace(args) {
223
229
  }
224
230
  target = answer;
225
231
  }
226
- const resolved = node_path_1.default.resolve(target.replace(/^~/, process.env.HOME || '~'));
227
- if (!node_fs_1.default.existsSync(resolved)) {
228
- out(ui.fail(`There is no folder at ${resolved}.`));
229
- out();
230
- process.exitCode = 1;
231
- return;
232
- }
233
- if (!node_fs_1.default.statSync(resolved).isDirectory()) {
234
- out(ui.fail(`${resolved} is a file, not a folder.`));
232
+ const found = (0, project_1.checkFolder)(target);
233
+ if (!found.ok) {
234
+ out(ui.fail(found.why));
235
235
  out();
236
- process.exitCode = 1;
236
+ process.exitCode = 2;
237
237
  return;
238
238
  }
239
239
  node_fs_1.default.mkdirSync((0, config_1.homeDir)(), { recursive: true });
240
- node_fs_1.default.writeFileSync(workspaceFile(), JSON.stringify({ path: resolved }, null, 2), { mode: 0o600 });
241
- out(ui.ok(`Working folder set to ${ui.c.bold(resolved)}.`));
242
- out(` ${ui.c.dim('clear it with')} ${ui.c.bold('comprism workspace --clear')}`);
240
+ node_fs_1.default.writeFileSync(workspaceFile(), JSON.stringify({ path: found.path }, null, 2), { mode: 0o600 });
241
+ out(ui.ok(`Project folder set to ${ui.c.bold(found.path)}.`));
242
+ out(` ${ui.c.dim('clear it with')} ${ui.c.bold('comprism project --clear')}`);
243
243
  out();
244
244
  }
245
+ /** The older name, kept so nothing that calls it breaks. */
246
+ exports.cmdWorkspace = cmdProject;
@@ -0,0 +1,21 @@
1
+ type Row = {
2
+ name: string;
3
+ description: string;
4
+ where: string;
5
+ active: boolean;
6
+ note?: string | null;
7
+ };
8
+ type Checked = {
9
+ level: string;
10
+ name: string;
11
+ line: string;
12
+ };
13
+ export declare function skillRows(folder: string, op?: "list" | "check"): Promise<{
14
+ rows: Row[];
15
+ checked?: Checked[];
16
+ empty?: string | null;
17
+ error?: string;
18
+ }>;
19
+ export declare const SKILLS_HELP = " comprism skills the skills here and where each came from\n comprism skills add --personal <folder> keep a skill folder on your account\n comprism skills remove --personal <name> remove one of your own skills\n comprism skills check what will not load, and how to fix it\n In a session, /name followed by anything runs that skill with it.";
20
+ export declare function cmdSkills(args: string[]): Promise<number>;
21
+ export {};