@withoperon/cli 0.2.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.
package/dist/cli.js ADDED
@@ -0,0 +1,4100 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command as Command15, Option as Option5 } from "commander";
5
+
6
+ // src/commands/config.ts
7
+ import { Command } from "commander";
8
+ import { spawn } from "child_process";
9
+
10
+ // src/lib/exit-codes.ts
11
+ var EXIT = {
12
+ ok: 0,
13
+ err: 1,
14
+ noWork: 2,
15
+ // no sources detected, no candidates produced, etc.
16
+ config: 3,
17
+ // bad config / setup
18
+ network: 4,
19
+ // v1
20
+ auth: 5,
21
+ // v1
22
+ evalFail: 6
23
+ // v1
24
+ };
25
+
26
+ // src/lib/globals.ts
27
+ import process2 from "process";
28
+ var resolved;
29
+ function setGlobals(g) {
30
+ resolved = g;
31
+ }
32
+ function getGlobals() {
33
+ if (!resolved) {
34
+ return {
35
+ json: false,
36
+ noColor: !process2.stdout.isTTY,
37
+ quiet: false,
38
+ verbose: false,
39
+ configPath: void 0,
40
+ cwd: process2.cwd()
41
+ };
42
+ }
43
+ return resolved;
44
+ }
45
+
46
+ // src/lib/logger.ts
47
+ import chalk from "chalk";
48
+ var isColor = () => !getGlobals().noColor;
49
+ var dim = (s) => isColor() ? chalk.dim(s) : s;
50
+ var bold = (s) => isColor() ? chalk.bold(s) : s;
51
+ var yellow = (s) => isColor() ? chalk.yellow(s) : s;
52
+ var red = (s) => isColor() ? chalk.red(s) : s;
53
+ var green = (s) => isColor() ? chalk.green(s) : s;
54
+ var log = {
55
+ info(msg) {
56
+ if (getGlobals().quiet) return;
57
+ console.log(msg);
58
+ },
59
+ debug(msg) {
60
+ if (!getGlobals().verbose) return;
61
+ console.log(dim(`[debug] ${msg}`));
62
+ },
63
+ step(msg) {
64
+ if (getGlobals().quiet) return;
65
+ console.log(`${bold(green("\u2192"))} ${msg}`);
66
+ },
67
+ warn(msg) {
68
+ console.warn(`${bold(yellow("!"))} ${msg}`);
69
+ },
70
+ err(msg) {
71
+ console.error(`${bold(red("\u2716"))} ${msg}`);
72
+ },
73
+ json(value) {
74
+ process.stdout.write(`${JSON.stringify(value)}
75
+ `);
76
+ }
77
+ };
78
+
79
+ // src/store/config.ts
80
+ import { readFile, writeFile } from "fs/promises";
81
+ import { z } from "zod";
82
+
83
+ // src/store/paths.ts
84
+ import { mkdir } from "fs/promises";
85
+ import { homedir } from "os";
86
+ import { join } from "path";
87
+ var ROOT = join(homedir(), ".operon");
88
+ var CANDIDATES_DIR = join(ROOT, "candidates");
89
+ var OPERONS_DIR = join(ROOT, "operons");
90
+ var SKILLS_DIR = join(ROOT, "skills");
91
+ var TRASH_DIR = join(ROOT, ".trash");
92
+ var CONFIG_PATH = join(ROOT, "config.json");
93
+ async function ensureDir(path) {
94
+ await mkdir(path, { recursive: true });
95
+ }
96
+ async function ensureLayout() {
97
+ await ensureDir(ROOT);
98
+ await ensureDir(CANDIDATES_DIR);
99
+ await ensureDir(OPERONS_DIR);
100
+ await ensureDir(TRASH_DIR);
101
+ }
102
+ function candidateDir(slug) {
103
+ return join(CANDIDATES_DIR, slug);
104
+ }
105
+ function operonDir(slug) {
106
+ return join(OPERONS_DIR, slug);
107
+ }
108
+ function skillFile(workspaceSlug, slug) {
109
+ return join(SKILLS_DIR, workspaceSlug, slug, "SKILL.md");
110
+ }
111
+ function trashDir(undoId) {
112
+ return join(TRASH_DIR, undoId);
113
+ }
114
+
115
+ // src/store/config.ts
116
+ var configSchema = z.object({
117
+ version: z.literal(1).default(1),
118
+ telemetry: z.object({
119
+ anonStats: z.boolean().default(false),
120
+ patternHashes: z.boolean().default(false),
121
+ anonUserId: z.string().uuid().optional()
122
+ }).default({}),
123
+ redact: z.object({
124
+ patterns: z.array(z.object({ kind: z.string().min(1), regex: z.string().min(1) })).default([]),
125
+ redactEmails: z.boolean().default(true)
126
+ }).default({}),
127
+ paths: z.object({
128
+ // Reserved for `operon scan --cwd` overrides; v0 leaves empty.
129
+ cwdAllowlist: z.array(z.string()).default([])
130
+ }).default({}),
131
+ // Platform endpoint + signed-in identity. apiBase points at the
132
+ // deployed apps/web (or http://localhost:3001 during dogfooding).
133
+ // accessToken may be a Supabase access_token or an op_pat_* token.
134
+ // workspaceSlug is the default --workspace value for publish/install.
135
+ platform: z.object({
136
+ apiBase: z.string().url().default("https://app.withoperon.com"),
137
+ accessToken: z.string().optional(),
138
+ refreshToken: z.string().optional(),
139
+ expiresAt: z.number().int().optional(),
140
+ userEmail: z.string().optional(),
141
+ workspaceSlug: z.string().optional()
142
+ }).default({})
143
+ });
144
+ var DEFAULT_CONFIG = configSchema.parse({});
145
+ async function readConfig() {
146
+ await ensureLayout();
147
+ try {
148
+ const raw = await readFile(CONFIG_PATH, "utf8");
149
+ const parsed = JSON.parse(raw);
150
+ return configSchema.parse(parsed);
151
+ } catch {
152
+ return DEFAULT_CONFIG;
153
+ }
154
+ }
155
+ async function writeConfig(config) {
156
+ await ensureLayout();
157
+ const validated = configSchema.parse(config);
158
+ await writeFile(
159
+ CONFIG_PATH,
160
+ `${JSON.stringify(validated, null, 2)}
161
+ `,
162
+ "utf8"
163
+ );
164
+ }
165
+ function getConfigValue(config, path) {
166
+ const parts = path.split(".");
167
+ let cursor = config;
168
+ for (const p of parts) {
169
+ if (typeof cursor !== "object" || cursor === null) return void 0;
170
+ cursor = cursor[p];
171
+ }
172
+ return cursor;
173
+ }
174
+ function setConfigValue(config, path, value) {
175
+ const parts = path.split(".");
176
+ const next = JSON.parse(JSON.stringify(config));
177
+ let cursor = next;
178
+ for (let i = 0; i < parts.length - 1; i += 1) {
179
+ const key = parts[i];
180
+ const child = cursor[key];
181
+ if (typeof child !== "object" || child === null) {
182
+ cursor[key] = {};
183
+ }
184
+ cursor = cursor[key];
185
+ }
186
+ cursor[parts[parts.length - 1]] = value;
187
+ return configSchema.parse(next);
188
+ }
189
+
190
+ // src/commands/config.ts
191
+ function configCommand() {
192
+ const cmd = new Command("config").description(
193
+ "Read / write ~/.operon/config.json."
194
+ );
195
+ cmd.command("get <key>").description("Read a config value by dot path.").action(async (key) => {
196
+ const config = await readConfig();
197
+ const value = getConfigValue(config, key);
198
+ if (getGlobals().json) {
199
+ log.json({ key, value });
200
+ } else {
201
+ log.info(value === void 0 ? "(unset)" : JSON.stringify(value));
202
+ }
203
+ process.exit(EXIT.ok);
204
+ });
205
+ cmd.command("set <key> <value>").description(
206
+ "Write a config value by dot path. Booleans true/false; JSON literal otherwise."
207
+ ).action(async (key, raw) => {
208
+ const value = parseConfigValue(raw);
209
+ const config = await readConfig();
210
+ const next = setConfigValue(config, key, value);
211
+ await writeConfig(next);
212
+ log.step(`config set ${key} = ${JSON.stringify(value)}`);
213
+ process.exit(EXIT.ok);
214
+ });
215
+ cmd.command("edit").description("Open ~/.operon/config.json in $EDITOR.").action(() => {
216
+ const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? "vi";
217
+ const child = spawn(editor, [CONFIG_PATH], { stdio: "inherit" });
218
+ child.on("exit", (code) => process.exit(code ?? 0));
219
+ });
220
+ return cmd;
221
+ }
222
+ function parseConfigValue(raw) {
223
+ if (raw === "true") return true;
224
+ if (raw === "false") return false;
225
+ try {
226
+ return JSON.parse(raw);
227
+ } catch {
228
+ return raw;
229
+ }
230
+ }
231
+
232
+ // src/commands/doctor.ts
233
+ import { stat as stat4 } from "fs/promises";
234
+ import { Command as Command2 } from "commander";
235
+ import { z as z2 } from "zod";
236
+
237
+ // src/ingestion/claude-code.ts
238
+ import { readdir, stat } from "fs/promises";
239
+ import { homedir as homedir2 } from "os";
240
+ import { basename, join as join2 } from "path";
241
+
242
+ // src/ingestion/jsonl.ts
243
+ import { createReadStream } from "fs";
244
+ import { createInterface } from "readline";
245
+ async function* readJsonl(path, opts = {}) {
246
+ const stream = createReadStream(path, { encoding: "utf8" });
247
+ const rl = createInterface({
248
+ input: stream,
249
+ crlfDelay: Infinity
250
+ });
251
+ let lineNumber = 0;
252
+ for await (const raw of rl) {
253
+ lineNumber += 1;
254
+ if (raw.length === 0 && opts.skipEmpty !== false) continue;
255
+ try {
256
+ yield JSON.parse(raw);
257
+ } catch (e) {
258
+ opts.onError?.(lineNumber, raw, e);
259
+ }
260
+ }
261
+ }
262
+
263
+ // src/ingestion/normalize.ts
264
+ import { extname } from "path";
265
+ function fileExtension(p) {
266
+ const ext = extname(p);
267
+ return ext.length > 0 && ext.length < 8 ? ext.toLowerCase() : "<none>";
268
+ }
269
+ function fileTouchesFromPaths(paths) {
270
+ const counts = /* @__PURE__ */ new Map();
271
+ for (const p of paths) {
272
+ const ext = fileExtension(p);
273
+ counts.set(ext, (counts.get(ext) ?? 0) + 1);
274
+ }
275
+ return [...counts.entries()].map(([extension, count]) => ({
276
+ extension,
277
+ count
278
+ }));
279
+ }
280
+ function aggregateToolCalls(calls) {
281
+ const sums = /* @__PURE__ */ new Map();
282
+ for (const c of calls) {
283
+ const key = c.name;
284
+ const existing = sums.get(key) ?? { name: key, arity: 0, uses: 0 };
285
+ existing.arity = Math.max(existing.arity, c.arity ?? 0);
286
+ existing.uses += 1;
287
+ sums.set(key, existing);
288
+ }
289
+ return [...sums.values()].map(({ name, arity, uses }) => ({
290
+ name,
291
+ arity,
292
+ count: uses
293
+ }));
294
+ }
295
+ function safeDate(value) {
296
+ if (typeof value !== "string" && typeof value !== "number") return void 0;
297
+ const d = new Date(value);
298
+ return Number.isFinite(d.getTime()) ? d : void 0;
299
+ }
300
+ function durationSec(start, end) {
301
+ return Math.max(0, Math.round((end.getTime() - start.getTime()) / 1e3));
302
+ }
303
+ function isAfter(d, cutoff) {
304
+ if (cutoff === void 0) return true;
305
+ if (d === void 0) return true;
306
+ return d.getTime() >= cutoff.getTime();
307
+ }
308
+
309
+ // src/ingestion/claude-code.ts
310
+ var ROOT2 = join2(homedir2(), ".claude", "projects");
311
+ var FILE_TOUCH_TOOLS = /* @__PURE__ */ new Set([
312
+ "Edit",
313
+ "Write",
314
+ "MultiEdit",
315
+ "NotebookEdit"
316
+ ]);
317
+ var claudeCodeAdapter = {
318
+ name: "claude-code",
319
+ async detect() {
320
+ try {
321
+ const s = await stat(ROOT2);
322
+ if (!s.isDirectory()) {
323
+ return { present: false, reason: `${ROOT2} is not a directory` };
324
+ }
325
+ return { present: true };
326
+ } catch {
327
+ return { present: false, reason: `${ROOT2} not found` };
328
+ }
329
+ },
330
+ async *enumerate(opts) {
331
+ let projects;
332
+ try {
333
+ projects = await readdir(ROOT2);
334
+ } catch {
335
+ return;
336
+ }
337
+ let yielded = 0;
338
+ for (const project of projects) {
339
+ const projectDir = join2(ROOT2, project);
340
+ let entries;
341
+ try {
342
+ entries = await readdir(projectDir);
343
+ } catch {
344
+ continue;
345
+ }
346
+ for (const entry of entries) {
347
+ if (!entry.endsWith(".jsonl")) continue;
348
+ const path = join2(projectDir, entry);
349
+ let mtime;
350
+ try {
351
+ const s = await stat(path);
352
+ mtime = s.mtime;
353
+ } catch {
354
+ continue;
355
+ }
356
+ if (!isAfter(mtime, opts.since)) continue;
357
+ if (opts.cwdFilter !== void 0 && opts.cwdFilter.length > 0) {
358
+ }
359
+ yield {
360
+ source: "claude-code",
361
+ path,
362
+ rawId: basename(entry, ".jsonl"),
363
+ records: readJsonl(path, {
364
+ onError: (ln, _raw, e) => {
365
+ process.stderr.write(
366
+ `! claude-code: ${path}:${ln} ${e.message}
367
+ `
368
+ );
369
+ }
370
+ })
371
+ };
372
+ yielded += 1;
373
+ if (opts.limit !== void 0 && yielded >= opts.limit) return;
374
+ }
375
+ }
376
+ },
377
+ async normalize(raw) {
378
+ const messages = [];
379
+ const toolCalls = [];
380
+ const filePaths = [];
381
+ const modelHints = /* @__PURE__ */ new Set();
382
+ let cwd;
383
+ let firstTs;
384
+ let lastTs;
385
+ for await (const record of raw.records) {
386
+ const line = record;
387
+ const ts = safeDate(line.timestamp);
388
+ if (ts !== void 0) {
389
+ if (firstTs === void 0 || ts < firstTs) firstTs = ts;
390
+ if (lastTs === void 0 || ts > lastTs) lastTs = ts;
391
+ }
392
+ if (line.cwd !== void 0 && cwd === void 0) cwd = line.cwd;
393
+ if (line.type === "user" || line.type === "assistant") {
394
+ const role = line.message?.role ?? line.type;
395
+ if (line.message?.model !== void 0) {
396
+ modelHints.add(line.message.model);
397
+ }
398
+ const content = line.message?.content;
399
+ if (typeof content === "string") {
400
+ messages.push({ role, content });
401
+ } else if (Array.isArray(content)) {
402
+ const textParts = [];
403
+ for (const block of content) {
404
+ if (block.type === "text" && typeof block.text === "string") {
405
+ textParts.push(block.text);
406
+ } else if (block.type === "thinking" && typeof block.text === "string") {
407
+ textParts.push(block.text);
408
+ } else if (block.type === "tool_use" && typeof block.name === "string") {
409
+ const arity = typeof block.input === "object" && block.input !== null ? Object.keys(block.input).length : 0;
410
+ toolCalls.push({ name: block.name, arity });
411
+ if (FILE_TOUCH_TOOLS.has(block.name)) {
412
+ const filePath = extractFilePath(
413
+ block.input
414
+ );
415
+ if (filePath !== void 0) filePaths.push(filePath);
416
+ }
417
+ }
418
+ }
419
+ if (textParts.length > 0) {
420
+ messages.push({ role, content: textParts.join("\n") });
421
+ }
422
+ }
423
+ }
424
+ }
425
+ const startedAt = firstTs ?? /* @__PURE__ */ new Date(0);
426
+ const endedAt = lastTs ?? startedAt;
427
+ const session = {
428
+ id: `claude-code:${raw.rawId}`,
429
+ source: "claude-code",
430
+ startedAt,
431
+ endedAt,
432
+ durationSec: durationSec(startedAt, endedAt),
433
+ modelHints: [...modelHints],
434
+ messages,
435
+ toolCalls: aggregateToolCalls(toolCalls),
436
+ fileTouches: fileTouchesFromPaths(filePaths),
437
+ outcome: "unknown"
438
+ };
439
+ if (cwd !== void 0) session.cwd = cwd;
440
+ return session;
441
+ }
442
+ };
443
+ function extractFilePath(input2) {
444
+ if (input2 === void 0) return void 0;
445
+ for (const key of ["file_path", "path", "notebook_path"]) {
446
+ const v = input2[key];
447
+ if (typeof v === "string" && v.length > 0) return v;
448
+ }
449
+ return void 0;
450
+ }
451
+
452
+ // src/ingestion/codex.ts
453
+ import { readdir as readdir2, stat as stat2 } from "fs/promises";
454
+ import { homedir as homedir3 } from "os";
455
+ import { basename as basename2, join as join3 } from "path";
456
+ var ROOT3 = join3(homedir3(), ".codex", "sessions");
457
+ var codexAdapter = {
458
+ name: "codex",
459
+ async detect() {
460
+ try {
461
+ const s = await stat2(ROOT3);
462
+ if (!s.isDirectory()) {
463
+ return { present: false, reason: `${ROOT3} is not a directory` };
464
+ }
465
+ return { present: true };
466
+ } catch {
467
+ return { present: false, reason: `${ROOT3} not found` };
468
+ }
469
+ },
470
+ async *enumerate(opts) {
471
+ let yielded = 0;
472
+ for await (const path of walkJsonl(ROOT3)) {
473
+ let mtime;
474
+ try {
475
+ const s = await stat2(path);
476
+ mtime = s.mtime;
477
+ } catch {
478
+ continue;
479
+ }
480
+ if (!isAfter(mtime, opts.since)) continue;
481
+ const rawId = parseRawIdFromFilename(basename2(path));
482
+ yield {
483
+ source: "codex",
484
+ path,
485
+ rawId,
486
+ records: readJsonl(path, {
487
+ onError: (ln, _raw, e) => {
488
+ process.stderr.write(`! codex: ${path}:${ln} ${e.message}
489
+ `);
490
+ }
491
+ })
492
+ };
493
+ yielded += 1;
494
+ if (opts.limit !== void 0 && yielded >= opts.limit) return;
495
+ }
496
+ },
497
+ async normalize(raw) {
498
+ const messages = [];
499
+ const toolCalls = [];
500
+ const filePaths = [];
501
+ const modelHints = /* @__PURE__ */ new Set();
502
+ let cwd;
503
+ let firstTs;
504
+ let lastTs;
505
+ for await (const record of raw.records) {
506
+ const line = record;
507
+ const ts = safeDate(line.timestamp) ?? safeDate(line.payload?.timestamp);
508
+ if (ts !== void 0) {
509
+ if (firstTs === void 0 || ts < firstTs) firstTs = ts;
510
+ if (lastTs === void 0 || ts > lastTs) lastTs = ts;
511
+ }
512
+ if (line.type === "session_meta") {
513
+ if (line.payload?.cwd !== void 0 && cwd === void 0) {
514
+ cwd = line.payload.cwd;
515
+ }
516
+ continue;
517
+ }
518
+ if (line.type !== "response_item") continue;
519
+ const p = line.payload;
520
+ if (p === void 0) continue;
521
+ if (p.model !== void 0) modelHints.add(p.model);
522
+ if (p.type === "message") {
523
+ const role = p.role ?? "assistant";
524
+ const content = p.content;
525
+ if (typeof content === "string") {
526
+ messages.push({ role, content });
527
+ } else if (Array.isArray(content)) {
528
+ const textParts = [];
529
+ for (const block of content) {
530
+ if (typeof block.text === "string") textParts.push(block.text);
531
+ }
532
+ if (textParts.length > 0) {
533
+ messages.push({ role, content: textParts.join("\n") });
534
+ }
535
+ }
536
+ } else if (p.type === "function_call") {
537
+ if (typeof p.name !== "string") continue;
538
+ const args = parseArguments(p.arguments);
539
+ const arity = args !== void 0 ? Object.keys(args).length : 0;
540
+ toolCalls.push({ name: p.name, arity });
541
+ const filePath = extractFilePath2(args);
542
+ if (filePath !== void 0) filePaths.push(filePath);
543
+ } else if (p.type === "custom_tool_call") {
544
+ if (typeof p.name !== "string") continue;
545
+ toolCalls.push({ name: p.name, arity: 1 });
546
+ const filePath = extractFilePathFromPatch(p.input);
547
+ if (filePath !== void 0) filePaths.push(filePath);
548
+ }
549
+ }
550
+ const startedAt = firstTs ?? /* @__PURE__ */ new Date(0);
551
+ const endedAt = lastTs ?? startedAt;
552
+ const session = {
553
+ id: `codex:${raw.rawId}`,
554
+ source: "codex",
555
+ startedAt,
556
+ endedAt,
557
+ durationSec: durationSec(startedAt, endedAt),
558
+ modelHints: [...modelHints],
559
+ messages,
560
+ toolCalls: aggregateToolCalls(toolCalls),
561
+ fileTouches: fileTouchesFromPaths(filePaths),
562
+ outcome: "unknown"
563
+ };
564
+ if (cwd !== void 0) session.cwd = cwd;
565
+ return session;
566
+ }
567
+ };
568
+ async function* walkJsonl(dir) {
569
+ let entries;
570
+ try {
571
+ entries = await readdir2(dir, { withFileTypes: true });
572
+ } catch {
573
+ return;
574
+ }
575
+ for (const entry of entries) {
576
+ const p = join3(dir, entry.name);
577
+ if (entry.isDirectory()) {
578
+ yield* walkJsonl(p);
579
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
580
+ yield p;
581
+ }
582
+ }
583
+ }
584
+ function parseRawIdFromFilename(name) {
585
+ const stem = name.replace(/\.jsonl$/, "");
586
+ const m = /-([0-9a-f-]{36})$/.exec(stem);
587
+ return m?.[1] ?? stem;
588
+ }
589
+ function parseArguments(s) {
590
+ if (typeof s !== "string" || s.length === 0) return void 0;
591
+ try {
592
+ const parsed = JSON.parse(s);
593
+ if (typeof parsed === "object" && parsed !== null) {
594
+ return parsed;
595
+ }
596
+ } catch {
597
+ }
598
+ return void 0;
599
+ }
600
+ function extractFilePath2(input2) {
601
+ if (input2 === void 0) return void 0;
602
+ for (const key of ["path", "file_path", "filename"]) {
603
+ const v = input2[key];
604
+ if (typeof v === "string" && v.length > 0) return v;
605
+ }
606
+ return void 0;
607
+ }
608
+ function extractFilePathFromPatch(input2) {
609
+ if (typeof input2 !== "string") return void 0;
610
+ const m = /\*\*\* (?:Update|Add|Delete) File: (.+)$/m.exec(input2);
611
+ return m?.[1] ?? void 0;
612
+ }
613
+
614
+ // src/ingestion/cursor.ts
615
+ import { readdir as readdir3, readFile as readFile2, stat as stat3 } from "fs/promises";
616
+ import { createRequire } from "module";
617
+ import { homedir as homedir4 } from "os";
618
+ import { basename as basename3, join as join4 } from "path";
619
+ import { fileURLToPath } from "url";
620
+ var ROOTS = cursorWorkspaceStorageRoots();
621
+ var CHATDATA_KEY = "workbench.panel.aichat.view.aichat.chatdata";
622
+ var require2 = createRequire(import.meta.url);
623
+ var cursorAdapter = {
624
+ name: "cursor",
625
+ async detect() {
626
+ if (loadSqlite() === void 0) {
627
+ return { present: false, reason: "node:sqlite unavailable" };
628
+ }
629
+ for (const root of ROOTS) {
630
+ try {
631
+ const s = await stat3(root);
632
+ if (s.isDirectory()) return { present: true };
633
+ } catch {
634
+ }
635
+ }
636
+ return { present: false, reason: `${ROOTS.join(", ")} not found` };
637
+ },
638
+ async *enumerate(opts) {
639
+ const sqlite = loadSqlite();
640
+ if (sqlite === void 0) return;
641
+ let yielded = 0;
642
+ for (const root of ROOTS) {
643
+ let entries;
644
+ try {
645
+ entries = await readdir3(root, { withFileTypes: true });
646
+ } catch {
647
+ continue;
648
+ }
649
+ for (const entry of entries) {
650
+ if (!entry.isDirectory()) continue;
651
+ const storageDir = join4(root, entry.name);
652
+ const dbPath = join4(storageDir, "state.vscdb");
653
+ let dbMtime;
654
+ try {
655
+ dbMtime = (await stat3(dbPath)).mtime;
656
+ } catch {
657
+ continue;
658
+ }
659
+ if (!isAfter(dbMtime, opts.since)) continue;
660
+ const chatData = readChatData(sqlite, dbPath);
661
+ if (chatData === void 0) continue;
662
+ const cwd = await readWorkspaceFolder(storageDir);
663
+ const tabs = Array.isArray(chatData.tabs) ? chatData.tabs : [];
664
+ for (const tab of tabs) {
665
+ const startedAt = tabStartedAt(tab, dbMtime);
666
+ if (!isAfter(startedAt, opts.since)) continue;
667
+ const tabId = typeof tab.tabId === "string" && tab.tabId.length > 0 ? tab.tabId : String(yielded);
668
+ yield {
669
+ source: "cursor",
670
+ path: dbPath,
671
+ rawId: `${basename3(storageDir)}:${tabId}`,
672
+ records: fromArray([{ tab, cwd, dbMtimeMs: dbMtime.getTime() }])
673
+ };
674
+ yielded += 1;
675
+ if (opts.limit !== void 0 && yielded >= opts.limit) return;
676
+ }
677
+ }
678
+ }
679
+ },
680
+ async normalize(raw) {
681
+ const messages = [];
682
+ const toolCalls = [];
683
+ const filePaths = [];
684
+ const modelHints = /* @__PURE__ */ new Set();
685
+ let cwd;
686
+ let firstTs;
687
+ let lastTs;
688
+ let fallbackTs = /* @__PURE__ */ new Date(0);
689
+ for await (const record of raw.records) {
690
+ const cursorRecord = record;
691
+ if (cursorRecord.cwd !== void 0 && cwd === void 0) {
692
+ cwd = cursorRecord.cwd;
693
+ }
694
+ fallbackTs = new Date(cursorRecord.dbMtimeMs);
695
+ for (const bubble of cursorRecord.tab.bubbles ?? []) {
696
+ const ts = timestampFromBubble(bubble);
697
+ if (ts !== void 0) {
698
+ if (firstTs === void 0 || ts < firstTs) firstTs = ts;
699
+ if (lastTs === void 0 || ts > lastTs) lastTs = ts;
700
+ }
701
+ const content = bubbleText(bubble);
702
+ const role = roleFromBubble(bubble);
703
+ if (role !== void 0 && content !== void 0) {
704
+ messages.push({ role, content });
705
+ }
706
+ if (typeof bubble.modelType === "string") {
707
+ modelHints.add(bubble.modelType);
708
+ }
709
+ if (bubble.type === "ai" || bubble.messageType === 3) {
710
+ toolCalls.push({ name: "CursorChat", arity: 1 });
711
+ }
712
+ for (const path of selectionPaths(bubble)) {
713
+ filePaths.push(path);
714
+ }
715
+ }
716
+ }
717
+ const startedAt = firstTs ?? fallbackTs;
718
+ const endedAt = lastTs ?? startedAt;
719
+ const session = {
720
+ id: `cursor:${raw.rawId}`,
721
+ source: "cursor",
722
+ startedAt,
723
+ endedAt,
724
+ durationSec: durationSec(startedAt, endedAt),
725
+ modelHints: [...modelHints],
726
+ messages,
727
+ toolCalls: aggregateToolCalls(toolCalls),
728
+ fileTouches: fileTouchesFromPaths(filePaths),
729
+ outcome: "unknown"
730
+ };
731
+ if (cwd !== void 0) session.cwd = cwd;
732
+ return session;
733
+ }
734
+ };
735
+ function loadSqlite() {
736
+ try {
737
+ return require2("node:sqlite");
738
+ } catch {
739
+ return void 0;
740
+ }
741
+ }
742
+ function cursorWorkspaceStorageRoots() {
743
+ if (process.platform === "darwin") {
744
+ return [
745
+ join4(
746
+ homedir4(),
747
+ "Library",
748
+ "Application Support",
749
+ "Cursor",
750
+ "User",
751
+ "workspaceStorage"
752
+ )
753
+ ];
754
+ }
755
+ if (process.platform === "win32" && process.env.APPDATA !== void 0) {
756
+ return [join4(process.env.APPDATA, "Cursor", "User", "workspaceStorage")];
757
+ }
758
+ return [join4(homedir4(), ".config", "Cursor", "User", "workspaceStorage")];
759
+ }
760
+ function readChatData(sqlite, dbPath) {
761
+ let db;
762
+ try {
763
+ db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
764
+ const row = db.prepare("select value from ItemTable where key = ?").get(CHATDATA_KEY);
765
+ if (typeof row?.value !== "string") return void 0;
766
+ const parsed = JSON.parse(row.value);
767
+ if (typeof parsed !== "object" || parsed === null) return void 0;
768
+ return parsed;
769
+ } catch {
770
+ return void 0;
771
+ } finally {
772
+ db?.close();
773
+ }
774
+ }
775
+ async function readWorkspaceFolder(storageDir) {
776
+ try {
777
+ const raw = await readFile2(join4(storageDir, "workspace.json"), "utf8");
778
+ const parsed = JSON.parse(raw);
779
+ const folder = typeof parsed.folder === "string" ? parsed.folder : typeof parsed.workspace === "string" ? parsed.workspace : void 0;
780
+ if (folder === void 0) return void 0;
781
+ if (folder.startsWith("file:")) return fileURLToPath(folder);
782
+ return folder;
783
+ } catch {
784
+ return void 0;
785
+ }
786
+ }
787
+ function tabStartedAt(tab, fallback) {
788
+ const timestamps = (tab.bubbles ?? []).map(timestampFromBubble).filter((d) => d !== void 0).sort((a, b) => a.getTime() - b.getTime());
789
+ return timestamps[0] ?? fallback;
790
+ }
791
+ function timestampFromBubble(bubble) {
792
+ if (typeof bubble.contextCacheTimestamp !== "number" || !Number.isFinite(bubble.contextCacheTimestamp)) {
793
+ return void 0;
794
+ }
795
+ return new Date(bubble.contextCacheTimestamp);
796
+ }
797
+ function roleFromBubble(bubble) {
798
+ if (bubble.type === "user" || bubble.messageType === 2) return "user";
799
+ if (bubble.type === "ai" || bubble.messageType === 3) return "assistant";
800
+ return void 0;
801
+ }
802
+ function bubbleText(bubble) {
803
+ for (const value of [bubble.rawText, bubble.initText, bubble.text]) {
804
+ if (typeof value === "string" && value.trim().length > 0) {
805
+ return value;
806
+ }
807
+ }
808
+ return void 0;
809
+ }
810
+ function selectionPaths(bubble) {
811
+ const paths = [];
812
+ for (const selection of [
813
+ ...bubble.selections ?? [],
814
+ ...bubble.fileSelections ?? []
815
+ ]) {
816
+ const uri = selection.uri;
817
+ if (uri === void 0) continue;
818
+ if (typeof uri.fsPath === "string" && uri.fsPath.length > 0) {
819
+ paths.push(uri.fsPath);
820
+ } else if (uri.scheme === "file" && typeof uri.path === "string") {
821
+ paths.push(uri.path);
822
+ }
823
+ }
824
+ return paths;
825
+ }
826
+ async function* fromArray(items) {
827
+ for (const item of items) yield item;
828
+ }
829
+
830
+ // src/ingestion/redact.ts
831
+ var BUILTIN = [
832
+ { kind: "stripe", re: /sk_(live|test)_[A-Za-z0-9]{24,}/g },
833
+ { kind: "github", re: /\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g },
834
+ { kind: "aws", re: /\bAKIA[0-9A-Z]{16}\b/g },
835
+ {
836
+ kind: "jwt",
837
+ re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g
838
+ },
839
+ { kind: "openai", re: /\bsk-[A-Za-z0-9]{32,}\b/g },
840
+ { kind: "anthropic", re: /\bsk-ant-[A-Za-z0-9-]{32,}\b/g },
841
+ {
842
+ kind: "email",
843
+ re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g
844
+ }
845
+ ];
846
+ var ENTROPY_RE = /\b[A-Za-z0-9_+\/=-]{40,}\b/g;
847
+ function redactString(s, cfg = {}) {
848
+ if (s.length === 0) return s;
849
+ let out = s;
850
+ for (const { kind, re } of BUILTIN) {
851
+ if (kind === "email" && cfg.redactEmails === false) continue;
852
+ out = out.replace(re, `[REDACTED:${kind}]`);
853
+ }
854
+ for (const { kind, re } of cfg.userPatterns ?? []) {
855
+ out = out.replace(re, `[REDACTED:${kind}]`);
856
+ }
857
+ out = out.replace(
858
+ ENTROPY_RE,
859
+ (m) => isEntropyHit(m) ? `[REDACTED:high-entropy]` : m
860
+ );
861
+ return out;
862
+ }
863
+ function isEntropyHit(m) {
864
+ let hasDigit = false;
865
+ let hasUpper = false;
866
+ let hasLower = false;
867
+ let hasSymbol = false;
868
+ for (const c of m) {
869
+ if (c >= "0" && c <= "9") hasDigit = true;
870
+ else if (c >= "A" && c <= "Z") hasUpper = true;
871
+ else if (c >= "a" && c <= "z") hasLower = true;
872
+ else hasSymbol = true;
873
+ }
874
+ const classes = Number(hasDigit) + Number(hasUpper) + Number(hasLower) + Number(hasSymbol);
875
+ return classes >= 3;
876
+ }
877
+ function redactSession(session, cfg = {}) {
878
+ return {
879
+ ...session,
880
+ messages: session.messages.map((m) => ({
881
+ ...m,
882
+ content: redactString(m.content, cfg)
883
+ }))
884
+ };
885
+ }
886
+
887
+ // src/ingestion/index.ts
888
+ var ADAPTERS = {
889
+ "claude-code": claudeCodeAdapter,
890
+ codex: codexAdapter,
891
+ cursor: cursorAdapter,
892
+ // ChatGPT is exploratory and not wired here.
893
+ chatgpt: void 0
894
+ };
895
+
896
+ // src/render/doctor-report.ts
897
+ import chalk2 from "chalk";
898
+ function renderDoctorReport(r) {
899
+ const colored = !getGlobals().noColor;
900
+ const ok = (s) => colored ? chalk2.green(s) : s;
901
+ const bad = (s) => colored ? chalk2.red(s) : s;
902
+ const dim2 = (s) => colored ? chalk2.dim(s) : s;
903
+ const lines = [];
904
+ lines.push("Sources detected:");
905
+ for (const s of r.sources) {
906
+ const mark = s.present ? ok("\u2713") : bad("\u2717");
907
+ const reason = s.reason !== void 0 ? dim2(` ${s.reason}`) : "";
908
+ lines.push(` ${mark} ${s.name}${reason}`);
909
+ }
910
+ lines.push("");
911
+ lines.push("Local layout:");
912
+ for (const l of r.layout) {
913
+ const mark = l.ok ? ok("\u2713") : bad("\u2717");
914
+ const detail = l.detail !== void 0 ? dim2(` ${l.detail}`) : "";
915
+ lines.push(` ${mark} ${l.path}${detail}`);
916
+ }
917
+ lines.push("");
918
+ lines.push("Telemetry:");
919
+ lines.push(
920
+ ` anon-stats: ${r.telemetry.anonStats ? ok("opt-in") : dim2("disabled")}`
921
+ );
922
+ lines.push(
923
+ ` pattern-hashes: ${r.telemetry.patternHashes ? ok("opt-in") : dim2("disabled")}`
924
+ );
925
+ lines.push("");
926
+ lines.push(`Operon CLI v${r.cliVersion}`);
927
+ return lines.join("\n");
928
+ }
929
+
930
+ // src/version.ts
931
+ import { readFileSync } from "fs";
932
+ var CLI_VERSION = readCliVersion();
933
+ function readCliVersion() {
934
+ try {
935
+ const packageJson = JSON.parse(
936
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
937
+ );
938
+ return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
939
+ } catch {
940
+ return "0.0.0";
941
+ }
942
+ }
943
+
944
+ // src/commands/doctor.ts
945
+ var optsSchema = z2.object({
946
+ fix: z2.boolean().default(false)
947
+ });
948
+ function doctorCommand() {
949
+ return new Command2("doctor").description("Sanity-check the install and local data layout.").option("--fix", "Attempt to repair config / permissions issues", false).action(async (rawOpts) => {
950
+ const opts = optsSchema.parse(rawOpts);
951
+ if (opts.fix) await ensureLayout();
952
+ const sources = [];
953
+ for (const [name, adapter] of Object.entries(ADAPTERS)) {
954
+ if (adapter === void 0) {
955
+ sources.push({
956
+ name,
957
+ present: false,
958
+ reason: "not enabled in v0"
959
+ });
960
+ continue;
961
+ }
962
+ const detect = await adapter.detect();
963
+ const result = {
964
+ name,
965
+ present: detect.present
966
+ };
967
+ if (detect.reason !== void 0) result.reason = detect.reason;
968
+ sources.push(result);
969
+ }
970
+ const layout = [];
971
+ for (const path of [ROOT, CANDIDATES_DIR, OPERONS_DIR]) {
972
+ try {
973
+ const s = await stat4(path);
974
+ layout.push({
975
+ path,
976
+ ok: s.isDirectory(),
977
+ detail: s.isDirectory() ? "ok" : "not a directory"
978
+ });
979
+ } catch {
980
+ layout.push({
981
+ path,
982
+ ok: false,
983
+ detail: "missing \u2014 run `operon doctor --fix`"
984
+ });
985
+ }
986
+ }
987
+ const config = await readConfig();
988
+ const report = {
989
+ sources,
990
+ layout,
991
+ telemetry: {
992
+ anonStats: config.telemetry.anonStats,
993
+ patternHashes: config.telemetry.patternHashes
994
+ },
995
+ cliVersion: CLI_VERSION
996
+ };
997
+ if (getGlobals().json) {
998
+ log.json(report);
999
+ process.exit(EXIT.ok);
1000
+ }
1001
+ log.info(renderDoctorReport(report));
1002
+ const allOk = sources.some((s) => s.present) && layout.every((l) => l.ok);
1003
+ process.exit(allOk ? EXIT.ok : EXIT.config);
1004
+ });
1005
+ }
1006
+
1007
+ // src/commands/install.ts
1008
+ import { createHash } from "crypto";
1009
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1010
+ import { homedir as homedir5 } from "os";
1011
+ import { dirname, join as join5 } from "path";
1012
+ import { Command as Command3 } from "commander";
1013
+ import { z as z4 } from "zod";
1014
+
1015
+ // src/lib/runs.ts
1016
+ import { z as z3 } from "zod";
1017
+ var slugRe = /^[a-z0-9-]+$/;
1018
+ var semverRe = /^\d+\.\d+\.\d+(-[a-z0-9.]+)?$/;
1019
+ var sha256HexRe = /^[a-f0-9]{64}$/i;
1020
+ var maxInt = 2147483647;
1021
+ var recordRunPayloadSchema = z3.object({
1022
+ workspaceSlug: z3.string().min(1).max(64).regex(slugRe),
1023
+ operonSlug: z3.string().min(1).max(64).regex(slugRe),
1024
+ semver: z3.string().min(1).max(64).regex(semverRe),
1025
+ invocationSource: z3.enum(["cli", "mcp", "webhook", "dashboard"]),
1026
+ status: z3.enum(["running", "succeeded", "failed", "timeout"]),
1027
+ startedAt: z3.string().datetime({ offset: true }).optional(),
1028
+ finishedAt: z3.string().datetime({ offset: true }).optional(),
1029
+ durationMs: z3.number().int().min(0).max(maxInt).optional(),
1030
+ inputHash: z3.string().regex(sha256HexRe).optional(),
1031
+ tokenIn: z3.number().int().min(0).max(maxInt).optional(),
1032
+ tokenOut: z3.number().int().min(0).max(maxInt).optional(),
1033
+ costUsdEstimated: z3.number().finite().min(0).max(9999.999999).optional(),
1034
+ billedRunCount: z3.number().int().min(1).max(maxInt).optional()
1035
+ });
1036
+ var recordRunResponseSchema = z3.object({
1037
+ runId: z3.string().uuid(),
1038
+ status: z3.enum(["running", "succeeded", "failed", "timeout"]),
1039
+ runsUsedMtd: z3.number().int().nonnegative(),
1040
+ runsCap: z3.number().int().nonnegative().nullable(),
1041
+ overCap: z3.boolean()
1042
+ });
1043
+ var RecordRunError = class extends Error {
1044
+ exitCode;
1045
+ status;
1046
+ body;
1047
+ constructor(message, exitCode, status, body) {
1048
+ super(message);
1049
+ this.name = "RecordRunError";
1050
+ this.exitCode = exitCode;
1051
+ this.status = status;
1052
+ this.body = body;
1053
+ }
1054
+ };
1055
+ async function recordRun(input2) {
1056
+ const {
1057
+ apiBase: apiBaseOverride,
1058
+ token: tokenOverride,
1059
+ fetchImpl = fetch,
1060
+ ...payloadInput
1061
+ } = input2;
1062
+ const payload = recordRunPayloadSchema.parse(payloadInput);
1063
+ const config = await readConfig();
1064
+ const apiBase = (apiBaseOverride ?? config.platform.apiBase ?? "https://app.withoperon.com").replace(/\/+$/, "");
1065
+ const token = tokenOverride ?? config.platform.accessToken;
1066
+ if (token === void 0 || token.length === 0) {
1067
+ throw new RecordRunError("not signed in. Run: operon login", EXIT.auth);
1068
+ }
1069
+ let res;
1070
+ try {
1071
+ res = await fetchImpl(`${apiBase}/api/v1/runs`, {
1072
+ method: "POST",
1073
+ headers: {
1074
+ "content-type": "application/json",
1075
+ "x-operon-source": "cli",
1076
+ authorization: `Bearer ${token}`
1077
+ },
1078
+ body: JSON.stringify(payload)
1079
+ });
1080
+ } catch (e) {
1081
+ throw new RecordRunError(
1082
+ `network error: ${e.message}`,
1083
+ EXIT.network
1084
+ );
1085
+ }
1086
+ if (!res.ok) {
1087
+ const body = await res.text().catch(() => "<unreadable>");
1088
+ throw new RecordRunError(
1089
+ `server returned ${res.status}: ${body.slice(0, 400)}`,
1090
+ res.status === 401 ? EXIT.auth : EXIT.err,
1091
+ res.status,
1092
+ body
1093
+ );
1094
+ }
1095
+ return recordRunResponseSchema.parse(await res.json());
1096
+ }
1097
+
1098
+ // src/commands/install.ts
1099
+ var slugSchema = z4.string().min(1).max(64).regex(/^[a-z0-9-]+$/);
1100
+ var workspaceSchema = z4.string().min(1).max(64).regex(/^[a-z0-9-]+$/);
1101
+ var installRefSchema = z4.string().min(1).max(129).regex(/^[a-z0-9-]+(?:\/[a-z0-9-]+)?$/);
1102
+ var optsSchema2 = z4.object({
1103
+ workspace: workspaceSchema.optional(),
1104
+ semver: z4.string().min(1).max(64).regex(/^\d+\.\d+\.\d+(-[a-z0-9.]+)?$/).optional(),
1105
+ target: z4.enum(["all", "claude", "codex"]).default("all"),
1106
+ force: z4.boolean().default(false),
1107
+ json: z4.boolean().default(false),
1108
+ apiBase: z4.string().url().optional()
1109
+ });
1110
+ var installResponseSchema = z4.object({
1111
+ workspace: z4.object({
1112
+ slug: z4.string(),
1113
+ name: z4.string()
1114
+ }),
1115
+ operon: z4.object({
1116
+ slug: z4.string(),
1117
+ name: z4.string(),
1118
+ description: z4.string().nullable(),
1119
+ visibility: z4.string(),
1120
+ tags: z4.array(z4.string())
1121
+ }),
1122
+ version: z4.object({
1123
+ semver: z4.string(),
1124
+ contentHash: z4.string().regex(/^[a-f0-9]{64}$/i),
1125
+ publishStatus: z4.string(),
1126
+ evalStatus: z4.string(),
1127
+ createdAt: z4.string(),
1128
+ changelog: z4.string().nullable()
1129
+ }),
1130
+ skillUrl: z4.string().url(),
1131
+ runRecorded: z4.boolean().optional()
1132
+ });
1133
+ function installCommand() {
1134
+ return new Command3("install").argument("<workspace/slug>", "Operon to install, e.g. acme/review-pr").description("Install an operon's SKILL.md into Claude Code and Codex.").option("--workspace <slug>", "Workspace slug for scriptable install form").option("--semver <x.y.z>", "Version to install; defaults to current").option("--target <target>", "all | claude | codex (default: all)", "all").option("--force", "Overwrite existing different local skill files", false).option("--json", "Machine-readable JSON output", false).option("--api-base <url>", "Override platform base URL").action(async (refArg, rawOpts) => {
1135
+ const ref = installRefSchema.parse(refArg);
1136
+ const opts = optsSchema2.parse(rawOpts);
1137
+ const config = await readConfig();
1138
+ const parsed = parseInstallRef(ref, opts.workspace);
1139
+ const workspaceSlug = parsed.workspace ?? config.platform.workspaceSlug;
1140
+ if (workspaceSlug === void 0 || workspaceSlug.length === 0) {
1141
+ log.err(
1142
+ "missing workspace. Use operon install <workspace>/<slug> or pass --workspace."
1143
+ );
1144
+ process.exit(EXIT.config);
1145
+ return;
1146
+ }
1147
+ const apiBase = (opts.apiBase ?? config.platform.apiBase ?? "https://app.withoperon.com").replace(/\/+$/, "");
1148
+ const token = config.platform.accessToken;
1149
+ const startedAt = /* @__PURE__ */ new Date();
1150
+ const metadata = await fetchMetadata({
1151
+ apiBase,
1152
+ workspaceSlug,
1153
+ slug: parsed.slug,
1154
+ semver: opts.semver,
1155
+ token
1156
+ });
1157
+ if (metadata === void 0) return;
1158
+ const skillBody = await fetchSkill(metadata.skillUrl);
1159
+ if (skillBody === void 0) return;
1160
+ const actualHash = sha256Hex(skillBody);
1161
+ const expectedHash = metadata.version.contentHash.toLowerCase();
1162
+ if (actualHash !== expectedHash) {
1163
+ log.err(
1164
+ `content hash mismatch: expected ${expectedHash}, got ${actualHash}`
1165
+ );
1166
+ process.exit(EXIT.err);
1167
+ return;
1168
+ }
1169
+ const plans = await buildWritePlans({
1170
+ workspaceSlug: metadata.workspace.slug,
1171
+ slug: metadata.operon.slug,
1172
+ body: skillBody,
1173
+ target: opts.target,
1174
+ force: opts.force
1175
+ });
1176
+ if (plans === void 0) return;
1177
+ for (const plan of plans) {
1178
+ await mkdir2(dirname(plan.path), { recursive: true });
1179
+ if (plan.status !== "already-installed") {
1180
+ await writeFile2(plan.path, skillBody, "utf8");
1181
+ }
1182
+ }
1183
+ let runRecorded = metadata.runRecorded === true;
1184
+ if (token !== void 0 && token.length > 0) {
1185
+ const finishedAt = /* @__PURE__ */ new Date();
1186
+ try {
1187
+ await recordRun({
1188
+ workspaceSlug: metadata.workspace.slug,
1189
+ operonSlug: metadata.operon.slug,
1190
+ semver: metadata.version.semver,
1191
+ invocationSource: "cli",
1192
+ status: "succeeded",
1193
+ startedAt: startedAt.toISOString(),
1194
+ finishedAt: finishedAt.toISOString(),
1195
+ durationMs: Math.max(0, finishedAt.getTime() - startedAt.getTime()),
1196
+ inputHash: expectedHash,
1197
+ apiBase,
1198
+ token
1199
+ });
1200
+ runRecorded = true;
1201
+ } catch (e) {
1202
+ if (e instanceof RecordRunError) {
1203
+ log.err(e.message);
1204
+ process.exit(e.exitCode);
1205
+ return;
1206
+ }
1207
+ log.err(`could not record run: ${e.message}`);
1208
+ process.exit(EXIT.err);
1209
+ return;
1210
+ }
1211
+ } else if (!runRecorded) {
1212
+ log.warn(
1213
+ "not signed in; installed public operon without recording a run"
1214
+ );
1215
+ }
1216
+ const json = {
1217
+ workspace: metadata.workspace.slug,
1218
+ slug: metadata.operon.slug,
1219
+ semver: metadata.version.semver,
1220
+ contentHash: expectedHash,
1221
+ runRecorded,
1222
+ targets: plans
1223
+ };
1224
+ if (getGlobals().json || opts.json) {
1225
+ log.json(json);
1226
+ } else {
1227
+ log.step(
1228
+ `installed ${metadata.workspace.slug}/${metadata.operon.slug}@${metadata.version.semver}`
1229
+ );
1230
+ for (const plan of plans) {
1231
+ log.info(` ${plan.target}: ${plan.status} ${plan.path}`);
1232
+ }
1233
+ if (runRecorded) log.info(" run recorded");
1234
+ }
1235
+ process.exit(EXIT.ok);
1236
+ });
1237
+ }
1238
+ function parseInstallRef(ref, workspaceOpt) {
1239
+ const parts = ref.split("/");
1240
+ if (parts.length === 2) {
1241
+ const workspace = workspaceSchema.parse(parts[0]);
1242
+ const slug = slugSchema.parse(parts[1]);
1243
+ if (workspaceOpt !== void 0 && workspaceOpt !== workspace) {
1244
+ log.err(
1245
+ `workspace mismatch: ref uses "${workspace}" but --workspace is "${workspaceOpt}"`
1246
+ );
1247
+ process.exit(EXIT.config);
1248
+ }
1249
+ return { workspace, slug };
1250
+ }
1251
+ return {
1252
+ ...workspaceOpt !== void 0 ? { workspace: workspaceOpt } : {},
1253
+ slug: slugSchema.parse(ref)
1254
+ };
1255
+ }
1256
+ async function buildWritePlans(input2) {
1257
+ const paths = [
1258
+ {
1259
+ target: "cache",
1260
+ path: skillFile(input2.workspaceSlug, input2.slug),
1261
+ status: "installed"
1262
+ }
1263
+ ];
1264
+ if (input2.target === "all" || input2.target === "claude") {
1265
+ paths.push({
1266
+ target: "claude",
1267
+ path: join5(homedir5(), ".claude", "skills", input2.slug, "SKILL.md"),
1268
+ status: "installed"
1269
+ });
1270
+ }
1271
+ if (input2.target === "all" || input2.target === "codex") {
1272
+ paths.push({
1273
+ target: "codex",
1274
+ path: join5(
1275
+ process.env.CODEX_HOME ?? join5(homedir5(), ".codex"),
1276
+ "skills",
1277
+ input2.slug,
1278
+ "SKILL.md"
1279
+ ),
1280
+ status: "installed"
1281
+ });
1282
+ }
1283
+ const conflicts = [];
1284
+ for (const item of paths) {
1285
+ const existing = await readExisting(item.path);
1286
+ if (existing === void 0) continue;
1287
+ if (existing === input2.body) {
1288
+ item.status = "already-installed";
1289
+ continue;
1290
+ }
1291
+ if (item.target === "cache" || input2.force) {
1292
+ item.status = "overwritten";
1293
+ continue;
1294
+ }
1295
+ conflicts.push(`${item.target}: ${item.path}`);
1296
+ }
1297
+ if (conflicts.length > 0) {
1298
+ log.err(
1299
+ `local skill already exists with different content. Re-run with --force to overwrite:
1300
+ ${conflicts.map((p) => ` ${p}`).join("\n")}`
1301
+ );
1302
+ process.exit(EXIT.err);
1303
+ return void 0;
1304
+ }
1305
+ return paths;
1306
+ }
1307
+ async function readExisting(path) {
1308
+ try {
1309
+ return await readFile3(path, "utf8");
1310
+ } catch {
1311
+ return void 0;
1312
+ }
1313
+ }
1314
+ async function fetchMetadata(input2) {
1315
+ const params = new URLSearchParams();
1316
+ if (input2.semver !== void 0) params.set("semver", input2.semver);
1317
+ const query = params.size > 0 ? `?${params.toString()}` : "";
1318
+ const url = `${input2.apiBase}/api/v1/workspaces/${input2.workspaceSlug}/operons/${input2.slug}${query}`;
1319
+ let res;
1320
+ try {
1321
+ const headers = { "x-operon-source": "cli" };
1322
+ if (input2.token !== void 0 && input2.token.length > 0) {
1323
+ headers.authorization = `Bearer ${input2.token}`;
1324
+ }
1325
+ res = await fetch(url, { headers });
1326
+ } catch (e) {
1327
+ log.err(`network error: ${e.message}`);
1328
+ process.exit(EXIT.network);
1329
+ return void 0;
1330
+ }
1331
+ if (!res.ok) {
1332
+ const body = await res.text().catch(() => "<unreadable>");
1333
+ log.err(`server returned ${res.status}: ${body.slice(0, 400)}`);
1334
+ process.exit(res.status === 401 ? EXIT.auth : EXIT.err);
1335
+ return void 0;
1336
+ }
1337
+ try {
1338
+ return installResponseSchema.parse(await res.json());
1339
+ } catch (e) {
1340
+ log.err(`invalid server response: ${e.message}`);
1341
+ process.exit(EXIT.err);
1342
+ return void 0;
1343
+ }
1344
+ }
1345
+ async function fetchSkill(skillUrl) {
1346
+ let res;
1347
+ try {
1348
+ res = await fetch(skillUrl);
1349
+ } catch (e) {
1350
+ log.err(`skill download failed: ${e.message}`);
1351
+ process.exit(EXIT.network);
1352
+ return void 0;
1353
+ }
1354
+ if (!res.ok) {
1355
+ log.err(`skill download returned ${res.status}`);
1356
+ process.exit(EXIT.err);
1357
+ return void 0;
1358
+ }
1359
+ return res.text();
1360
+ }
1361
+ function sha256Hex(value) {
1362
+ return createHash("sha256").update(value).digest("hex");
1363
+ }
1364
+
1365
+ // src/commands/list.ts
1366
+ import { Command as Command4, Option } from "commander";
1367
+ import { z as z5 } from "zod";
1368
+
1369
+ // src/render/candidate-table.ts
1370
+ import chalk3 from "chalk";
1371
+ function renderCandidateTable(candidates) {
1372
+ if (candidates.length === 0) return "(no candidates)";
1373
+ const colored = !getGlobals().noColor;
1374
+ const tint = (s, fn) => colored ? fn(s) : s;
1375
+ const rows = [];
1376
+ rows.push(["#", "slug", "uses", "days", "score", "tools"]);
1377
+ for (const [i, c] of candidates.entries()) {
1378
+ rows.push([
1379
+ String(i + 1),
1380
+ c.slug,
1381
+ String(c.uses),
1382
+ String(c.spanDays),
1383
+ c.score.toFixed(3),
1384
+ c.suggestedTools.slice(0, 3).join(",") || "(none)"
1385
+ ]);
1386
+ }
1387
+ const widths = rows[0].map(
1388
+ (_, col) => rows.reduce((max, r) => Math.max(max, r[col].length), 0)
1389
+ );
1390
+ const lines = [];
1391
+ for (const [i, r] of rows.entries()) {
1392
+ const cells = r.map((cell, col) => cell.padEnd(widths[col]));
1393
+ const joined = cells.join(" ");
1394
+ lines.push(i === 0 ? tint(joined, chalk3.bold) : joined);
1395
+ }
1396
+ return lines.join("\n");
1397
+ }
1398
+
1399
+ // src/store/candidates.ts
1400
+ import { readFile as readFile4, readdir as readdir4, rename, writeFile as writeFile3, rm } from "fs/promises";
1401
+ import { join as join6 } from "path";
1402
+ async function writeCandidate(candidate, exemplars) {
1403
+ const dir = candidateDir(candidate.slug);
1404
+ await ensureDir(dir);
1405
+ await ensureDir(join6(dir, "exemplars"));
1406
+ await writeFile3(
1407
+ join6(dir, "candidate.json"),
1408
+ `${JSON.stringify(candidate, null, 2)}
1409
+ `,
1410
+ "utf8"
1411
+ );
1412
+ await writeFile3(join6(dir, "prompt.draft.md"), candidate.draftPrompt, "utf8");
1413
+ await writeFile3(
1414
+ join6(dir, "tools.suggested.json"),
1415
+ `${JSON.stringify(candidate.suggestedTools, null, 2)}
1416
+ `,
1417
+ "utf8"
1418
+ );
1419
+ await writeFile3(
1420
+ join6(dir, "eval.draft.json"),
1421
+ `${JSON.stringify(candidate.draftEval, null, 2)}
1422
+ `,
1423
+ "utf8"
1424
+ );
1425
+ for (const session of exemplars) {
1426
+ await writeFile3(
1427
+ join6(dir, "exemplars", `${sanitize(session.id)}.summary.md`),
1428
+ renderExemplarSummary(session),
1429
+ "utf8"
1430
+ );
1431
+ }
1432
+ }
1433
+ async function readCandidate(slug) {
1434
+ const dir = candidateDir(slug);
1435
+ try {
1436
+ const raw = await readFile4(join6(dir, "candidate.json"), "utf8");
1437
+ return JSON.parse(raw);
1438
+ } catch {
1439
+ return void 0;
1440
+ }
1441
+ }
1442
+ async function listCandidates() {
1443
+ let entries;
1444
+ try {
1445
+ entries = await readdir4(CANDIDATES_DIR);
1446
+ } catch {
1447
+ return [];
1448
+ }
1449
+ const out = [];
1450
+ for (const slug of entries) {
1451
+ const candidate = await readCandidate(slug);
1452
+ if (candidate !== void 0) out.push(candidate);
1453
+ }
1454
+ return out;
1455
+ }
1456
+ async function trashCandidate(slug) {
1457
+ const undoId = `${Date.now().toString(36)}-cand-${slug}`;
1458
+ const target = trashDir(undoId);
1459
+ await ensureDir(target);
1460
+ try {
1461
+ await rename(candidateDir(slug), join6(target, slug));
1462
+ } catch (e) {
1463
+ throw new Error(
1464
+ `cannot trash candidate "${slug}": ${e.message}`
1465
+ );
1466
+ }
1467
+ return undoId;
1468
+ }
1469
+ var SNIPPET_LEN = 200;
1470
+ function renderExemplarSummary(session) {
1471
+ const userMsg = session.messages.find((m) => m.role === "user");
1472
+ const snippet = userMsg !== void 0 ? userMsg.content.slice(0, SNIPPET_LEN).replace(/\s+/g, " ") : "(no user message)";
1473
+ const tools = session.toolCalls.map((t) => t.name).join(", ") || "(none)";
1474
+ const exts = session.fileTouches.map((f) => `${f.extension}:${f.count}`).join(", ") || "(none)";
1475
+ return `# ${session.id}
1476
+
1477
+ source: ${session.source}
1478
+ started: ${session.startedAt.toISOString()}
1479
+ duration: ${session.durationSec}s
1480
+ cwd: ${session.cwd ?? "(unknown)"}
1481
+
1482
+ ## first user prompt (200c snippet)
1483
+
1484
+ ${snippet}
1485
+
1486
+ ## tools used
1487
+
1488
+ ${tools}
1489
+
1490
+ ## file extensions touched
1491
+
1492
+ ${exts}
1493
+ `;
1494
+ }
1495
+ function sanitize(id) {
1496
+ return id.replace(/[^a-zA-Z0-9._-]/g, "_");
1497
+ }
1498
+
1499
+ // src/store/operons.ts
1500
+ import { readFile as readFile5, readdir as readdir5, rename as rename2, writeFile as writeFile4, rm as rm2 } from "fs/promises";
1501
+ import { join as join7 } from "path";
1502
+ async function readOperon(slug) {
1503
+ try {
1504
+ const raw = await readFile5(join7(operonDir(slug), "manifest.json"), "utf8");
1505
+ return JSON.parse(raw);
1506
+ } catch {
1507
+ return void 0;
1508
+ }
1509
+ }
1510
+ async function listOperons() {
1511
+ let entries;
1512
+ try {
1513
+ entries = await readdir5(OPERONS_DIR);
1514
+ } catch {
1515
+ return [];
1516
+ }
1517
+ const out = [];
1518
+ for (const slug of entries) {
1519
+ const m = await readOperon(slug);
1520
+ if (m !== void 0) out.push(m);
1521
+ }
1522
+ return out;
1523
+ }
1524
+ async function trashOperon(slug) {
1525
+ const undoId = `${Date.now().toString(36)}-${slug}`;
1526
+ const target = trashDir(undoId);
1527
+ await ensureDir(target);
1528
+ try {
1529
+ await rename2(operonDir(slug), join7(target, slug));
1530
+ } catch (e) {
1531
+ throw new Error(`cannot trash operon "${slug}": ${e.message}`);
1532
+ }
1533
+ return undoId;
1534
+ }
1535
+
1536
+ // src/commands/list.ts
1537
+ var SORTS = ["score", "recent", "name"];
1538
+ var optsSchema3 = z5.object({
1539
+ candidates: z5.boolean().default(false),
1540
+ operons: z5.boolean().default(false),
1541
+ sort: z5.enum(SORTS).default("score")
1542
+ });
1543
+ function listCommand() {
1544
+ return new Command4("list").alias("ls").description("List candidates and operons.").option("--candidates", "Show only candidates", false).option("--operons", "Show only operons", false).addOption(new Option("--sort <key>", "Sort key").choices([...SORTS])).action(async (rawOpts) => {
1545
+ const opts = optsSchema3.parse(rawOpts);
1546
+ const showBoth = !opts.candidates && !opts.operons;
1547
+ const candidates = showBoth || opts.candidates ? await listCandidates() : [];
1548
+ const operons = showBoth || opts.operons ? await listOperons() : [];
1549
+ sortCandidates(candidates, opts.sort);
1550
+ if (getGlobals().json) {
1551
+ log.json({ candidates, operons });
1552
+ process.exit(EXIT.ok);
1553
+ }
1554
+ if (showBoth || opts.candidates) {
1555
+ log.info("Candidates:");
1556
+ log.info(renderCandidateTable(candidates));
1557
+ log.info("");
1558
+ }
1559
+ if (showBoth || opts.operons) {
1560
+ log.info("Operons:");
1561
+ if (operons.length === 0) {
1562
+ log.info(" (none)");
1563
+ } else {
1564
+ for (const o of operons) {
1565
+ log.info(` ${o.slug} v${o.semver} promoted ${o.promotedAt}`);
1566
+ }
1567
+ }
1568
+ }
1569
+ const total = candidates.length + operons.length;
1570
+ process.exit(total > 0 ? EXIT.ok : EXIT.noWork);
1571
+ });
1572
+ }
1573
+ function sortCandidates(candidates, key) {
1574
+ switch (key) {
1575
+ case "score":
1576
+ candidates.sort((a, b) => b.score - a.score);
1577
+ return;
1578
+ case "recent":
1579
+ candidates.sort((a, b) => b.spanDays - a.spanDays);
1580
+ return;
1581
+ case "name":
1582
+ candidates.sort((a, b) => a.slug.localeCompare(b.slug));
1583
+ return;
1584
+ }
1585
+ }
1586
+
1587
+ // src/commands/login.ts
1588
+ import { createInterface as createInterface2 } from "readline/promises";
1589
+ import { Command as Command5 } from "commander";
1590
+ import { z as z6 } from "zod";
1591
+ var optsSchema4 = z6.object({
1592
+ apiBase: z6.string().url().optional(),
1593
+ workspace: z6.string().min(1).max(64).optional(),
1594
+ token: z6.string().optional()
1595
+ });
1596
+ function loginCommand() {
1597
+ return new Command5("login").description("Sign in to the Operon platform (paste-token flow for v0).").option(
1598
+ "--api-base <url>",
1599
+ "Override platform base URL (default https://app.withoperon.com)"
1600
+ ).option("--workspace <slug>", "Set default workspace slug for publish").option(
1601
+ "--token <token>",
1602
+ "Skip the prompt and pass a Supabase access token or op_pat_* PAT"
1603
+ ).action(async (rawOpts) => {
1604
+ const opts = optsSchema4.parse(rawOpts);
1605
+ const config = await readConfig();
1606
+ const apiBase = opts.apiBase ?? config.platform.apiBase ?? "https://app.withoperon.com";
1607
+ let accessToken = opts.token;
1608
+ if (accessToken === void 0) {
1609
+ log.step(`Open this URL in your browser to sign in:`);
1610
+ log.info("");
1611
+ log.info(` ${apiBase}/login?from=cli`);
1612
+ log.info("");
1613
+ log.info(
1614
+ "After signing in, copy the access token shown on the device page,"
1615
+ );
1616
+ log.info("then paste it here. You can also paste an op_pat_* token.");
1617
+ log.info("");
1618
+ const rl = createInterface2({
1619
+ input: process.stdin,
1620
+ output: process.stdout
1621
+ });
1622
+ accessToken = (await rl.question("token> ")).trim();
1623
+ rl.close();
1624
+ }
1625
+ if (accessToken === void 0 || accessToken.length === 0) {
1626
+ log.err("no token provided");
1627
+ process.exit(EXIT.config);
1628
+ return;
1629
+ }
1630
+ let email;
1631
+ try {
1632
+ const res = await fetch(`${apiBase}/api/v1/whoami`, {
1633
+ headers: { authorization: `Bearer ${accessToken}` }
1634
+ });
1635
+ if (!res.ok) {
1636
+ const body = await res.text().catch(() => "");
1637
+ log.err(
1638
+ `Token rejected by ${apiBase} (${res.status}): ${body.slice(0, 200)}`
1639
+ );
1640
+ process.exit(EXIT.auth);
1641
+ return;
1642
+ }
1643
+ const json = await res.json();
1644
+ email = json.email;
1645
+ } catch (e) {
1646
+ log.err(`Could not reach ${apiBase}: ${e.message}`);
1647
+ process.exit(EXIT.network);
1648
+ return;
1649
+ }
1650
+ const next = {
1651
+ ...config,
1652
+ platform: {
1653
+ ...config.platform,
1654
+ apiBase,
1655
+ accessToken,
1656
+ userEmail: email,
1657
+ ...opts.workspace !== void 0 ? { workspaceSlug: opts.workspace } : {}
1658
+ }
1659
+ };
1660
+ await writeConfig(next);
1661
+ log.step(
1662
+ `signed in as ${email ?? "(unknown)"} \u2192 ${apiBase}` + (opts.workspace !== void 0 ? ` default workspace: ${opts.workspace}` : "")
1663
+ );
1664
+ log.info("Next: operon publish <slug>");
1665
+ process.exit(EXIT.ok);
1666
+ });
1667
+ }
1668
+
1669
+ // src/commands/mine.ts
1670
+ import { createInterface as createInterface3 } from "readline/promises";
1671
+ import { stdin as input, stdout as output } from "process";
1672
+ import { Command as Command6, Option as Option2 } from "commander";
1673
+
1674
+ // ../../packages/mining/src/prompts.ts
1675
+ var NON_ACTIONABLE_PREFIXES = [
1676
+ "<environment_context>",
1677
+ "<local-command-caveat>",
1678
+ "<user_info>"
1679
+ ];
1680
+ var NON_ACTIONABLE_PATTERNS = [
1681
+ /^# AGENTS\.md instructions[^\n]*(?:\n|$)/i,
1682
+ /<INSTRUCTIONS>[\s\S]*?<\/INSTRUCTIONS>/i,
1683
+ /<environment_context>[\s\S]*?<\/environment_context>/i,
1684
+ /<local-command-caveat>[\s\S]*?<\/local-command-caveat>/i,
1685
+ /<user_info>[\s\S]*?<\/user_info>/i
1686
+ ];
1687
+ function firstActionableUserPrompt(session) {
1688
+ for (const message of session.messages) {
1689
+ if (message.role !== "user") continue;
1690
+ const content = actionablePromptContent(message.content);
1691
+ if (content === void 0) continue;
1692
+ return content;
1693
+ }
1694
+ return void 0;
1695
+ }
1696
+ function actionablePromptContent(content) {
1697
+ const trimmed = content.trim();
1698
+ if (trimmed.length === 0) return void 0;
1699
+ if (NON_ACTIONABLE_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
1700
+ return void 0;
1701
+ }
1702
+ let stripped = trimmed;
1703
+ for (const pattern of NON_ACTIONABLE_PATTERNS) {
1704
+ stripped = stripped.replace(pattern, " ").trim();
1705
+ }
1706
+ return stripped.length === 0 ? void 0 : stripped;
1707
+ }
1708
+
1709
+ // ../../packages/mining/src/filter.ts
1710
+ var DEFAULTS = {
1711
+ minDurationSec: 60,
1712
+ minMessages: 3,
1713
+ minWork: 1
1714
+ };
1715
+ function filterSessions(sessions, opts = {}) {
1716
+ const cfg = { ...DEFAULTS, ...opts };
1717
+ const kept = [];
1718
+ const dropped = [];
1719
+ for (const s of sessions) {
1720
+ if (s.durationSec < cfg.minDurationSec) {
1721
+ dropped.push({ id: s.id, reason: "too-short" });
1722
+ continue;
1723
+ }
1724
+ if (s.messages.length < cfg.minMessages) {
1725
+ dropped.push({ id: s.id, reason: "too-few-messages" });
1726
+ continue;
1727
+ }
1728
+ if (firstActionableUserPrompt(s) === void 0) {
1729
+ dropped.push({ id: s.id, reason: "no-actionable-prompt" });
1730
+ continue;
1731
+ }
1732
+ const fileTouchTotal = s.fileTouches.reduce((a, t) => a + t.count, 0);
1733
+ const toolCallTotal = s.toolCalls.reduce((a, t) => a + (t.count ?? 1), 0);
1734
+ const work = fileTouchTotal + Math.floor(toolCallTotal / 2);
1735
+ if (work < cfg.minWork) {
1736
+ dropped.push({ id: s.id, reason: "no-work" });
1737
+ continue;
1738
+ }
1739
+ kept.push(s);
1740
+ }
1741
+ return { kept, dropped };
1742
+ }
1743
+
1744
+ // ../../packages/mining/src/hashes.ts
1745
+ import { createHash as createHash2 } from "crypto";
1746
+ function sha256(input2) {
1747
+ return createHash2("sha256").update(input2).digest("hex");
1748
+ }
1749
+ function normalizePromptText(s) {
1750
+ let out = s.toLowerCase();
1751
+ out = out.replace(/```[\s\S]*?```/g, " ");
1752
+ out = out.replace(/`[^`]*`/g, " ");
1753
+ out = out.replace(/https?:\/\/\S+/g, "<url>");
1754
+ out = out.replace(/\/[^\s'"`)]+/g, "<id>");
1755
+ out = out.replace(/\d+/g, "#");
1756
+ out = out.replace(/[^\p{L}\s<>#]+/gu, " ");
1757
+ out = out.replace(/\s+/g, " ").trim();
1758
+ return out;
1759
+ }
1760
+ function promptShapeHash(initialPrompt) {
1761
+ return sha256(normalizePromptText(initialPrompt));
1762
+ }
1763
+ function pathFingerprintHash(extensions) {
1764
+ const sorted = [...extensions].sort();
1765
+ return sha256(sorted.join("|"));
1766
+ }
1767
+ function toolFingerprintHash(toolNames) {
1768
+ const sorted = [...toolNames].sort();
1769
+ return sha256(sorted.join("|"));
1770
+ }
1771
+
1772
+ // ../../packages/mining/src/featurize.ts
1773
+ var BASH_TOOLS = /* @__PURE__ */ new Set(["Bash", "exec_command", "shell"]);
1774
+ var EDIT_TOOLS = /* @__PURE__ */ new Set([
1775
+ "Edit",
1776
+ "Write",
1777
+ "MultiEdit",
1778
+ "NotebookEdit",
1779
+ "apply_patch"
1780
+ ]);
1781
+ var SEARCH_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob", "Read", "WebSearch", "WebFetch"]);
1782
+ var SLASH_CMD_TAG_RE = /<command-name>\s*\/?([a-z][\w-]{0,63})\s*<\/command-name>/i;
1783
+ var SLASH_CMD_RAW_RE = /^\s*\/([a-z][\w-]{0,63})(?:\s|$)/i;
1784
+ var VERB_OBJECT_RE = /\b(?:please\s+|can\s+you\s+|let'?s\s+|i\s+want\s+(?:you\s+)?to\s+)?(\w{3,})\s+(?:the\s+|a\s+|this\s+|my\s+|our\s+)?(\w{3,})/i;
1785
+ var STOP_VERBS = /* @__PURE__ */ new Set([
1786
+ "the",
1787
+ "be",
1788
+ "is",
1789
+ "was",
1790
+ "are",
1791
+ "were",
1792
+ "have",
1793
+ "has",
1794
+ "had",
1795
+ "do",
1796
+ "does",
1797
+ "did",
1798
+ "and",
1799
+ "but",
1800
+ "or",
1801
+ "so",
1802
+ "for",
1803
+ "from",
1804
+ "with",
1805
+ "your",
1806
+ "this",
1807
+ "that",
1808
+ "these",
1809
+ "those"
1810
+ ]);
1811
+ function featurize(session) {
1812
+ const initialPrompt = firstActionableUserPrompt(session) ?? "";
1813
+ const promptShape = promptShapeHash(initialPrompt);
1814
+ const commandHint = extractCommand(initialPrompt);
1815
+ const verbObject = commandHint === void 0 ? extractVerbObject(initialPrompt) : void 0;
1816
+ const toolHistogram = {};
1817
+ let toolCallCount = 0;
1818
+ for (const t of session.toolCalls) {
1819
+ const count = t.count ?? 1;
1820
+ toolHistogram[t.name] = (toolHistogram[t.name] ?? 0) + count;
1821
+ toolCallCount += count;
1822
+ }
1823
+ const primaryTool = pickPrimaryTool(toolHistogram);
1824
+ const bashInvocationCount = sumByName(toolHistogram, BASH_TOOLS);
1825
+ const editCount = sumByName(toolHistogram, EDIT_TOOLS);
1826
+ const searchCount = sumByName(toolHistogram, SEARCH_TOOLS);
1827
+ const fileExtensions = [
1828
+ ...session.fileTouches.map((f) => f.extension)
1829
+ ].sort();
1830
+ const pathFingerprint = pathFingerprintHash(fileExtensions);
1831
+ const cwdLeaf = session.cwd === void 0 ? void 0 : leafOfCwd(session.cwd);
1832
+ const actionSignature = buildActionSignature(
1833
+ primaryTool,
1834
+ toolHistogram,
1835
+ fileExtensions,
1836
+ cwdLeaf
1837
+ );
1838
+ const features = {
1839
+ primaryTool,
1840
+ fileExtensions,
1841
+ modelFamily: detectModelFamily(session.modelHints),
1842
+ messageCount: session.messages.length,
1843
+ toolCallCount,
1844
+ bashInvocationCount,
1845
+ editCount,
1846
+ searchCount,
1847
+ promptShape,
1848
+ toolHistogram,
1849
+ pathFingerprint,
1850
+ commandHint,
1851
+ verbObject,
1852
+ actionSignature,
1853
+ cwdLeaf,
1854
+ durationBucket: bucketDuration(session.durationSec),
1855
+ outcome: session.outcome,
1856
+ sessionId: session.id,
1857
+ startedAt: session.startedAt
1858
+ };
1859
+ return features;
1860
+ }
1861
+ function featurizeMany(sessions) {
1862
+ return sessions.map(featurize);
1863
+ }
1864
+ function buildActionSignature(primaryTool, histogram, extensions, cwdLeaf) {
1865
+ const top3 = [...Object.entries(histogram)].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 3).map(([name]) => name).sort().join(",");
1866
+ const primaryExt = extensions[0] ?? "<none>";
1867
+ const leaf = cwdLeaf ?? "<no-cwd>";
1868
+ return sha256(`${primaryTool}|${top3}|${primaryExt}|${leaf}`);
1869
+ }
1870
+ function extractCommand(prompt) {
1871
+ const tagged = SLASH_CMD_TAG_RE.exec(prompt);
1872
+ if (tagged?.[1] !== void 0) {
1873
+ return tagged[1].toLowerCase();
1874
+ }
1875
+ const raw = SLASH_CMD_RAW_RE.exec(prompt);
1876
+ if (raw?.[1] !== void 0) {
1877
+ return raw[1].toLowerCase();
1878
+ }
1879
+ return void 0;
1880
+ }
1881
+ function extractVerbObject(prompt) {
1882
+ const stripped = prompt.replace(/<command-message>[\s\S]*?<\/command-message>/gi, " ").replace(/<command-name>[\s\S]*?<\/command-name>/gi, " ").replace(/<command-args>[\s\S]*?<\/command-args>/gi, " ").replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/gi, " ").replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, " ").trim();
1883
+ const m = VERB_OBJECT_RE.exec(stripped);
1884
+ if (m === null) return void 0;
1885
+ const verb = m[1].toLowerCase();
1886
+ const object = m[2].toLowerCase();
1887
+ if (STOP_VERBS.has(verb) || STOP_VERBS.has(object)) return void 0;
1888
+ if (verb === object) return void 0;
1889
+ const slug = `${verb}-${object}`.replace(/[^a-z0-9-]/g, "");
1890
+ if (slug.length < 6) return void 0;
1891
+ return slug;
1892
+ }
1893
+ function leafOfCwd(cwd) {
1894
+ const parts = cwd.split("/").filter(Boolean);
1895
+ return parts[parts.length - 1] ?? cwd;
1896
+ }
1897
+ function pickPrimaryTool(histogram) {
1898
+ const entries = Object.entries(histogram);
1899
+ if (entries.length === 0) return "<none>";
1900
+ entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
1901
+ return entries[0][0];
1902
+ }
1903
+ function sumByName(histogram, names) {
1904
+ let n = 0;
1905
+ for (const [k, v] of Object.entries(histogram)) {
1906
+ if (names.has(k)) n += v;
1907
+ }
1908
+ return n;
1909
+ }
1910
+ function detectModelFamily(hints) {
1911
+ if (hints.length === 0) return "unknown";
1912
+ const lower = hints.map((h) => h.toLowerCase());
1913
+ const claude = lower.some((h) => h.includes("claude"));
1914
+ const gpt = lower.some(
1915
+ (h) => h.includes("gpt") || h.includes("o1") || h.includes("o3")
1916
+ );
1917
+ if (claude && gpt) return "mixed";
1918
+ if (claude) return "claude";
1919
+ if (gpt) return "gpt";
1920
+ return "unknown";
1921
+ }
1922
+ function bucketDuration(sec) {
1923
+ if (sec < 5 * 60) return "short";
1924
+ if (sec < 30 * 60) return "medium";
1925
+ if (sec < 2 * 60 * 60) return "long";
1926
+ return "very-long";
1927
+ }
1928
+
1929
+ // ../../packages/mining/src/cluster.ts
1930
+ var DEFAULT_WEIGHTS = {
1931
+ actionSignature: 0.4,
1932
+ toolJaccard: 0.3,
1933
+ extJaccard: 0.1,
1934
+ pathFingerprint: 0.1,
1935
+ promptShape: 0.05,
1936
+ outcome: 0.05
1937
+ };
1938
+ var DEFAULTS2 = {
1939
+ // Two-session pairs were too noisy in 90-day dogfood runs: they
1940
+ // often captured one-off debugging, setup, or conversational
1941
+ // follow-up. Require at least three repeats before surfacing a
1942
+ // reusable pattern.
1943
+ minClusterSize: 3,
1944
+ maxDistance: 0.55
1945
+ };
1946
+ function clusterSessions(features, opts = {}) {
1947
+ const cfg = {
1948
+ minClusterSize: opts.minClusterSize ?? DEFAULTS2.minClusterSize,
1949
+ maxDistance: opts.maxDistance ?? DEFAULTS2.maxDistance,
1950
+ weights: opts.weights ?? DEFAULT_WEIGHTS
1951
+ };
1952
+ const consumed = /* @__PURE__ */ new Set();
1953
+ const clusters = [];
1954
+ bucketByKey(
1955
+ features,
1956
+ consumed,
1957
+ cfg.minClusterSize,
1958
+ "cmd",
1959
+ clusters,
1960
+ (f) => f.commandHint === void 0 ? void 0 : `cmd:${f.commandHint}`
1961
+ );
1962
+ bucketByKey(
1963
+ features,
1964
+ consumed,
1965
+ cfg.minClusterSize,
1966
+ "verb",
1967
+ clusters,
1968
+ (f) => f.verbObject === void 0 ? void 0 : `verb:${f.verbObject}`
1969
+ );
1970
+ bucketByKey(
1971
+ features,
1972
+ consumed,
1973
+ cfg.minClusterSize,
1974
+ "action",
1975
+ clusters,
1976
+ (f) => `action:${f.actionSignature}`
1977
+ );
1978
+ bucketByKey(
1979
+ features,
1980
+ consumed,
1981
+ cfg.minClusterSize,
1982
+ "shape",
1983
+ clusters,
1984
+ (f) => `shape:${f.promptShape}`
1985
+ );
1986
+ const remainder = features.filter((f) => !consumed.has(f.sessionId));
1987
+ clusters.push(...densityCluster(remainder, cfg));
1988
+ return clusters;
1989
+ }
1990
+ function bucketByKey(features, consumed, minClusterSize, pass, clusters, keyFn) {
1991
+ const buckets = /* @__PURE__ */ new Map();
1992
+ for (const f of features) {
1993
+ if (consumed.has(f.sessionId)) continue;
1994
+ const key = keyFn(f);
1995
+ if (key === void 0) continue;
1996
+ const arr = buckets.get(key) ?? [];
1997
+ arr.push(f);
1998
+ buckets.set(key, arr);
1999
+ }
2000
+ for (const arr of buckets.values()) {
2001
+ if (arr.length < minClusterSize) continue;
2002
+ clusters.push(buildCluster(arr, pass));
2003
+ for (const f of arr) consumed.add(f.sessionId);
2004
+ }
2005
+ }
2006
+ function densityCluster(features, cfg) {
2007
+ if (features.length === 0) return [];
2008
+ const assigned = /* @__PURE__ */ new Set();
2009
+ const clusters = [];
2010
+ for (const seed of features) {
2011
+ if (assigned.has(seed.sessionId)) continue;
2012
+ const members = [seed];
2013
+ for (const candidate of features) {
2014
+ if (candidate.sessionId === seed.sessionId || assigned.has(candidate.sessionId)) {
2015
+ continue;
2016
+ }
2017
+ if (distance(seed, candidate, cfg.weights) <= cfg.maxDistance) {
2018
+ members.push(candidate);
2019
+ }
2020
+ }
2021
+ if (members.length >= cfg.minClusterSize) {
2022
+ clusters.push(buildCluster(members, "B"));
2023
+ for (const member of members) assigned.add(member.sessionId);
2024
+ }
2025
+ }
2026
+ return clusters;
2027
+ }
2028
+ function distance(a, b, w = DEFAULT_WEIGHTS) {
2029
+ const actionD = a.actionSignature === b.actionSignature ? 0 : 1;
2030
+ const toolD = jaccardDistance(
2031
+ Object.keys(a.toolHistogram),
2032
+ Object.keys(b.toolHistogram)
2033
+ );
2034
+ const extD = jaccardDistance(a.fileExtensions, b.fileExtensions);
2035
+ const pathD = a.pathFingerprint === b.pathFingerprint ? 0 : 1;
2036
+ const promptD = a.promptShape === b.promptShape ? 0 : 1;
2037
+ const outcomeD = a.outcome === b.outcome ? 0 : 1;
2038
+ return w.actionSignature * actionD + w.toolJaccard * toolD + w.extJaccard * extD + w.pathFingerprint * pathD + w.promptShape * promptD + w.outcome * outcomeD;
2039
+ }
2040
+ function jaccardDistance(a, b) {
2041
+ if (a.length === 0 && b.length === 0) return 0;
2042
+ const A = new Set(a);
2043
+ const B = new Set(b);
2044
+ let intersection = 0;
2045
+ for (const x of A) if (B.has(x)) intersection += 1;
2046
+ const union = A.size + B.size - intersection;
2047
+ if (union === 0) return 0;
2048
+ return 1 - intersection / union;
2049
+ }
2050
+ function buildCluster(members, pass) {
2051
+ const sorted = [...members].sort(
2052
+ (a, b) => a.startedAt.getTime() - b.startedAt.getTime()
2053
+ );
2054
+ const centroid = sorted[Math.floor(sorted.length / 2)];
2055
+ const id = sha256(sorted.map((m) => m.sessionId).join("|"));
2056
+ const days = /* @__PURE__ */ new Set();
2057
+ for (const m of sorted) {
2058
+ days.add(m.startedAt.toISOString().slice(0, 10));
2059
+ }
2060
+ return {
2061
+ id,
2062
+ members: sorted.map((m) => m.sessionId),
2063
+ centroidPromptShape: centroid.promptShape,
2064
+ centroidFeatures: centroid,
2065
+ daysSpanned: days.size,
2066
+ pass: passShortLabel(pass)
2067
+ };
2068
+ }
2069
+ function passShortLabel(pass) {
2070
+ return pass === "B" ? "B" : "A";
2071
+ }
2072
+
2073
+ // ../../packages/mining/src/score.ts
2074
+ var DEFAULT_SCORE_WEIGHTS = {
2075
+ uses: 0.4,
2076
+ recurrence: 0.4,
2077
+ toolRichness: 0.2,
2078
+ recentlySeenPenalty: 0.1
2079
+ };
2080
+ function scoreClusters(clusters, opts = {}) {
2081
+ const w = opts.weights ?? DEFAULT_SCORE_WEIGHTS;
2082
+ const usesCeiling = opts.usesCeiling ?? 5;
2083
+ const promoted = opts.alreadyPromotedShapes ?? /* @__PURE__ */ new Set();
2084
+ return clusters.map((c) => {
2085
+ const breakdown = scoreOne(c, w, usesCeiling, promoted);
2086
+ const score = sum(breakdown);
2087
+ return { cluster: c, score, breakdown };
2088
+ }).sort((a, b) => b.score - a.score);
2089
+ }
2090
+ function scoreOne(cluster, w, usesCeiling, promoted) {
2091
+ const uses = cluster.members.length;
2092
+ const days = Math.max(1, cluster.daysSpanned);
2093
+ const usesPerDay = uses / days;
2094
+ const usesScore = w.uses * Math.min(1, usesPerDay / usesCeiling);
2095
+ const recurrenceScore = w.recurrence * Math.min(1, Math.log2(days + 1) / Math.log2(31));
2096
+ const toolRichnessScore = w.toolRichness * toolRichness(cluster.centroidFeatures);
2097
+ const penalty = promoted.has(cluster.centroidPromptShape) ? w.recentlySeenPenalty : 0;
2098
+ return {
2099
+ uses: usesScore,
2100
+ recurrence: recurrenceScore,
2101
+ toolRichness: toolRichnessScore,
2102
+ recentlySeenPenalty: -penalty
2103
+ };
2104
+ }
2105
+ function toolRichness(f) {
2106
+ const distinct = Object.keys(f.toolHistogram).length;
2107
+ if (distinct <= 1) return 0;
2108
+ if (distinct === 2) return 0.4;
2109
+ if (distinct === 3) return 0.7;
2110
+ return 1;
2111
+ }
2112
+ function sum(record) {
2113
+ let total = 0;
2114
+ for (const v of Object.values(record)) total += v;
2115
+ return total;
2116
+ }
2117
+
2118
+ // ../../packages/mining/src/render.ts
2119
+ var DEFAULTS3 = {
2120
+ top: 10,
2121
+ exemplarsPerCandidate: 3
2122
+ };
2123
+ function renderCandidates(scored, sessionsById, opts = {}) {
2124
+ const limit = opts.top ?? DEFAULTS3.top;
2125
+ const k = opts.exemplarsPerCandidate ?? DEFAULTS3.exemplarsPerCandidate;
2126
+ const used = /* @__PURE__ */ new Set();
2127
+ const out = [];
2128
+ for (const sc of scored) {
2129
+ if (out.length >= limit) break;
2130
+ const candidate = renderOne(sc, sessionsById, k, used);
2131
+ if (candidate === void 0) continue;
2132
+ out.push(candidate);
2133
+ }
2134
+ return out;
2135
+ }
2136
+ function renderOne(sc, sessionsById, k, usedSlugs) {
2137
+ const cf = sc.cluster.centroidFeatures;
2138
+ const exemplarIds = sc.cluster.members.slice(0, k);
2139
+ const exemplars = exemplarIds.map((id) => sessionsById.get(id)).filter((s) => s !== void 0);
2140
+ if (exemplars.length === 0) return void 0;
2141
+ const draftPrompt = buildDraftPrompt(exemplars[0]);
2142
+ const suggestedTools = pickSuggestedTools(exemplars);
2143
+ const memberSessions = sc.cluster.members.map((id) => sessionsById.get(id)).filter((s) => s !== void 0);
2144
+ const memberStarts = memberSessions.map((s) => s.startedAt.getTime());
2145
+ const firstSeenAt = new Date(Math.min(...memberStarts)).toISOString();
2146
+ const lastSeenAt = new Date(Math.max(...memberStarts)).toISOString();
2147
+ const slug = mintSlug(
2148
+ cf.commandHint,
2149
+ cf.verbObject,
2150
+ draftPrompt,
2151
+ sc.cluster.id,
2152
+ usedSlugs
2153
+ );
2154
+ usedSlugs.add(slug);
2155
+ return {
2156
+ slug,
2157
+ uses: sc.cluster.members.length,
2158
+ spanDays: sc.cluster.daysSpanned,
2159
+ toolFingerprint: toolFingerprintHash(suggestedTools),
2160
+ pathFingerprint: pathFingerprintHash(cf.fileExtensions),
2161
+ promptShape: cf.promptShape,
2162
+ score: round(sc.score, 4),
2163
+ scoreBreakdown: roundAll(sc.breakdown, 4),
2164
+ exemplarSessionIds: exemplarIds,
2165
+ firstSeenAt,
2166
+ lastSeenAt,
2167
+ draftPrompt,
2168
+ suggestedTools,
2169
+ draftEval: {
2170
+ type: "session-replay",
2171
+ fixtures: exemplarIds,
2172
+ asserts: [{ kind: "tools-used", subset: suggestedTools.slice(0, 3) }]
2173
+ },
2174
+ summary: buildSummary(sc, suggestedTools, exemplars)
2175
+ };
2176
+ }
2177
+ function buildDraftPrompt(centroidSession) {
2178
+ let body = firstActionableUserPrompt(centroidSession);
2179
+ if (body === void 0) return "Pattern: see exemplar sessions for shape.";
2180
+ body = body.trim();
2181
+ if (body.length > 1500) body = `${body.slice(0, 1497)}\u2026`;
2182
+ return body;
2183
+ }
2184
+ function pickSuggestedTools(exemplars) {
2185
+ const sets = exemplars.map((s) => new Set(s.toolCalls.map((t) => t.name)));
2186
+ if (sets.length === 0) return [];
2187
+ const intersection = new Set(sets[0]);
2188
+ for (const s of sets.slice(1)) {
2189
+ for (const t of intersection) if (!s.has(t)) intersection.delete(t);
2190
+ }
2191
+ const union = /* @__PURE__ */ new Set();
2192
+ for (const s of sets) for (const t of s) union.add(t);
2193
+ const inter = [...intersection];
2194
+ const rest = [...union].filter((t) => !intersection.has(t));
2195
+ const all = [...inter, ...rest];
2196
+ return all.slice(0, 8);
2197
+ }
2198
+ function mintSlug(commandHint, verbObject, draftPrompt, clusterId, used) {
2199
+ const candidate = commandHint ?? verbObject ?? extractVerbObject2(draftPrompt) ?? `pattern-${clusterId.slice(0, 8)}`;
2200
+ if (!used.has(candidate)) return candidate;
2201
+ return `${candidate}-${clusterId.slice(0, 4)}`;
2202
+ }
2203
+ var VERB_OBJECT_REGEX = /^(?:please\s+)?(?:can\s+you\s+)?(\w+)\s+(?:the\s+|a\s+|this\s+|my\s+)?(\w+(?:\s+\w+)?)/i;
2204
+ function extractVerbObject2(prompt) {
2205
+ const m = VERB_OBJECT_REGEX.exec(prompt.trim());
2206
+ if (m === null) return void 0;
2207
+ const verb = m[1].toLowerCase();
2208
+ const object = m[2].toLowerCase().replace(/\s+/g, "-");
2209
+ if (verb === object) return void 0;
2210
+ if (verb.length < 3 || object.length < 3) return void 0;
2211
+ return `${verb}-${object}`.replace(/[^a-z0-9-]/g, "");
2212
+ }
2213
+ function buildSummary(sc, tools, exemplars) {
2214
+ const f = sc.cluster.centroidFeatures;
2215
+ const exts = f.fileExtensions.length > 0 ? f.fileExtensions.slice(0, 5).join(", ") : "no-file-touches";
2216
+ const exemplarLine = exemplars.map((e) => e.id).slice(0, 3).join(", ");
2217
+ return `This pattern appears in ${sc.cluster.members.length} sessions across ${sc.cluster.daysSpanned} days. Each session uses ${tools.slice(0, 3).join(", ") || "no tools"}, touches files matching ${exts}. Examples: ${exemplarLine}.`;
2218
+ }
2219
+ function round(n, d) {
2220
+ const f = 10 ** d;
2221
+ return Math.round(n * f) / f;
2222
+ }
2223
+ function roundAll(rec, d) {
2224
+ const out = {};
2225
+ for (const [k, v] of Object.entries(rec)) out[k] = round(v, d);
2226
+ return out;
2227
+ }
2228
+
2229
+ // ../../packages/mining/src/pipeline.ts
2230
+ function mineSessions(sessions, opts = {}) {
2231
+ const filtered = filterSessions(sessions, opts.filter);
2232
+ const features = featurizeMany(filtered.kept);
2233
+ const clusters = clusterSessions(features, opts.cluster);
2234
+ const scored = scoreClusters(clusters, opts.score);
2235
+ const sessionsById = new Map(filtered.kept.map((s) => [s.id, s]));
2236
+ const candidates = renderCandidates(scored, sessionsById, opts.render);
2237
+ return {
2238
+ candidates,
2239
+ filtered,
2240
+ clusterCount: clusters.length,
2241
+ sessionCountInput: sessions.length,
2242
+ sessionCountKept: filtered.kept.length
2243
+ };
2244
+ }
2245
+
2246
+ // src/commands/mine.ts
2247
+ import { z as z8 } from "zod";
2248
+
2249
+ // src/lib/duration.ts
2250
+ var UNITS = {
2251
+ s: 1e3,
2252
+ m: 60 * 1e3,
2253
+ h: 60 * 60 * 1e3,
2254
+ d: 24 * 60 * 60 * 1e3
2255
+ };
2256
+ function parseDuration(input2) {
2257
+ const m = /^(\d+)([smhd])$/.exec(input2.trim());
2258
+ if (!m) {
2259
+ throw new Error(
2260
+ `Invalid duration "${input2}". Use forms like 30d, 24h, 30m, 45s.`
2261
+ );
2262
+ }
2263
+ const n = Number(m[1]);
2264
+ const unit = m[2];
2265
+ return n * UNITS[unit];
2266
+ }
2267
+
2268
+ // src/lib/events.ts
2269
+ import { z as z7 } from "zod";
2270
+ var eventNameSchema = z7.enum([
2271
+ "mining_scan_started",
2272
+ "mining_scan_completed",
2273
+ "pattern_detected",
2274
+ "suggestion_submitted",
2275
+ "suggestion_viewed",
2276
+ "suggestion_voted",
2277
+ "suggestion_accepted",
2278
+ "suggestion_dismissed",
2279
+ "mining_session_completed"
2280
+ ]);
2281
+ var eventValueSchema = z7.union([
2282
+ z7.string(),
2283
+ z7.number(),
2284
+ z7.boolean(),
2285
+ z7.null(),
2286
+ z7.array(z7.union([z7.string(), z7.number(), z7.boolean()]))
2287
+ ]);
2288
+ var eventPropertiesSchema = z7.record(eventValueSchema.optional());
2289
+ var PlatformEventError = class extends Error {
2290
+ exitCode;
2291
+ status;
2292
+ body;
2293
+ constructor(message, exitCode, status, body) {
2294
+ super(message);
2295
+ this.name = "PlatformEventError";
2296
+ this.exitCode = exitCode;
2297
+ this.status = status;
2298
+ this.body = body;
2299
+ }
2300
+ };
2301
+ async function createPlatformEventClient(options = {}) {
2302
+ const config = await readConfig();
2303
+ const apiBase = (options.apiBase ?? config.platform.apiBase ?? "https://app.withoperon.com").replace(/\/+$/, "");
2304
+ const token = options.token ?? config.platform.accessToken;
2305
+ if (token === void 0 || token.length === 0) {
2306
+ throw new PlatformEventError("not signed in. Run: operon login", EXIT.auth);
2307
+ }
2308
+ const workspaceSlug = options.workspaceSlug ?? config.platform.workspaceSlug;
2309
+ if (workspaceSlug === void 0 || workspaceSlug.length === 0) {
2310
+ throw new PlatformEventError(
2311
+ "missing workspace. Pass --workspace or run: operon login --workspace <slug>",
2312
+ EXIT.config
2313
+ );
2314
+ }
2315
+ return {
2316
+ send: (event, properties = {}) => sendPlatformEvent({
2317
+ apiBase,
2318
+ token,
2319
+ workspaceSlug,
2320
+ event,
2321
+ properties,
2322
+ ...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
2323
+ })
2324
+ };
2325
+ }
2326
+ async function sendPlatformEvent(input2) {
2327
+ const event = eventNameSchema.parse(input2.event);
2328
+ const properties = eventPropertiesSchema.parse(input2.properties ?? {});
2329
+ const fetchImpl = input2.fetchImpl ?? fetch;
2330
+ let res;
2331
+ try {
2332
+ res = await fetchImpl(
2333
+ `${input2.apiBase.replace(/\/+$/, "")}/api/v1/events`,
2334
+ {
2335
+ method: "POST",
2336
+ headers: {
2337
+ "content-type": "application/json",
2338
+ "x-operon-source": "cli",
2339
+ authorization: `Bearer ${input2.token}`
2340
+ },
2341
+ body: JSON.stringify({
2342
+ event,
2343
+ properties: {
2344
+ ...properties,
2345
+ workspaceSlug: input2.workspaceSlug,
2346
+ source: "cli"
2347
+ }
2348
+ })
2349
+ }
2350
+ );
2351
+ } catch (e) {
2352
+ throw new PlatformEventError(
2353
+ `network error: ${e.message}`,
2354
+ EXIT.network
2355
+ );
2356
+ }
2357
+ if (!res.ok) {
2358
+ const body = await res.text().catch(() => "<unreadable>");
2359
+ throw new PlatformEventError(
2360
+ `server returned ${res.status}: ${body.slice(0, 400)}`,
2361
+ res.status === 401 ? EXIT.auth : EXIT.err,
2362
+ res.status,
2363
+ body
2364
+ );
2365
+ }
2366
+ }
2367
+
2368
+ // src/lib/push.ts
2369
+ import { readFile as readFile6 } from "fs/promises";
2370
+ import { join as join8 } from "path";
2371
+ var PushOperonError = class extends Error {
2372
+ exitCode;
2373
+ status;
2374
+ body;
2375
+ constructor(message, exitCode, status, body) {
2376
+ super(message);
2377
+ this.name = "PushOperonError";
2378
+ this.exitCode = exitCode;
2379
+ this.status = status;
2380
+ this.body = body;
2381
+ }
2382
+ };
2383
+ async function pushOperon(options) {
2384
+ const config = await readConfig();
2385
+ const apiBase = (options.apiBase ?? config.platform.apiBase ?? "https://app.withoperon.com").replace(/\/+$/, "");
2386
+ const token = options.token ?? config.platform.accessToken;
2387
+ if (token === void 0 || token.length === 0) {
2388
+ throw new PushOperonError("not signed in. Run: operon login", EXIT.auth);
2389
+ }
2390
+ const workspaceSlug = options.workspace ?? config.platform.workspaceSlug;
2391
+ const candidate = options.candidate ?? await readCandidate(options.slug);
2392
+ if (candidate === void 0) {
2393
+ throw new PushOperonError(
2394
+ `no candidate "${options.slug}" found locally. Run scan first.`,
2395
+ EXIT.noWork
2396
+ );
2397
+ }
2398
+ const skill = await readSkillMarkdown(candidate);
2399
+ const testsYaml = await readTestsYaml(candidate.slug);
2400
+ const body = {
2401
+ ...workspaceSlug !== void 0 ? { workspaceSlug } : {},
2402
+ slug: options.slug,
2403
+ name: titleCase(options.slug),
2404
+ description: truncate(candidate.summary, 1800),
2405
+ polishedSkillMarkdown: skill.markdown,
2406
+ ...testsYaml !== void 0 ? { testsYaml } : {},
2407
+ suggestedTools: candidate.suggestedTools,
2408
+ visibility: options.visibility ?? "team",
2409
+ candidateMeta: {
2410
+ uses: candidate.uses,
2411
+ spanDays: candidate.spanDays,
2412
+ firstSeenAt: candidate.firstSeenAt,
2413
+ lastSeenAt: candidate.lastSeenAt,
2414
+ score: candidate.score
2415
+ }
2416
+ };
2417
+ const fetchImpl = options.fetchImpl ?? fetch;
2418
+ let res;
2419
+ try {
2420
+ res = await fetchImpl(`${apiBase}/api/v1/operons`, {
2421
+ method: "POST",
2422
+ headers: {
2423
+ "content-type": "application/json",
2424
+ "x-operon-source": "cli",
2425
+ authorization: `Bearer ${token}`
2426
+ },
2427
+ body: JSON.stringify(body)
2428
+ });
2429
+ } catch (e) {
2430
+ throw new PushOperonError(
2431
+ `network error: ${e.message}`,
2432
+ EXIT.network
2433
+ );
2434
+ }
2435
+ if (!res.ok) {
2436
+ const text = await res.text().catch(() => "<unreadable>");
2437
+ throw new PushOperonError(
2438
+ `server returned ${res.status}: ${text.slice(0, 400)}`,
2439
+ res.status === 401 ? EXIT.auth : EXIT.err,
2440
+ res.status,
2441
+ text
2442
+ );
2443
+ }
2444
+ const json = await res.json();
2445
+ return {
2446
+ ...json,
2447
+ url: `${apiBase}/${json.workspaceSlug}/${options.slug}`,
2448
+ usedDraftPrompt: skill.usedDraftPrompt
2449
+ };
2450
+ }
2451
+ async function readTestsYaml(slug) {
2452
+ try {
2453
+ return await readFile6(join8(candidateDir(slug), "tests.yaml"), "utf8");
2454
+ } catch {
2455
+ return void 0;
2456
+ }
2457
+ }
2458
+ async function readSkillMarkdown(candidate) {
2459
+ try {
2460
+ return {
2461
+ markdown: await readFile6(
2462
+ join8(candidateDir(candidate.slug), "polished.md"),
2463
+ "utf8"
2464
+ ),
2465
+ usedDraftPrompt: false
2466
+ };
2467
+ } catch {
2468
+ return {
2469
+ markdown: `---
2470
+ name: ${candidate.slug}
2471
+ description: ${truncate(candidate.summary, 90)}
2472
+ ---
2473
+
2474
+ ${candidate.draftPrompt}
2475
+ `,
2476
+ usedDraftPrompt: true
2477
+ };
2478
+ }
2479
+ }
2480
+ function titleCase(slug) {
2481
+ return slug.split("-").map((p) => p.length === 0 ? p : p[0].toUpperCase() + p.slice(1)).join(" ");
2482
+ }
2483
+ function truncate(s, max) {
2484
+ if (s.length <= max) return s;
2485
+ return `${s.slice(0, max - 1)}\u2026`;
2486
+ }
2487
+
2488
+ // src/commands/mine.ts
2489
+ var SOURCES = ["claude-code", "codex", "cursor", "chatgpt"];
2490
+ var slugRe2 = /^[a-z0-9-]+$/;
2491
+ var optsSchema5 = z8.object({
2492
+ source: z8.array(z8.enum(SOURCES)).default([...SOURCES]),
2493
+ workspace: z8.string().min(1).max(64).regex(slugRe2).optional(),
2494
+ since: z8.string().default("30d"),
2495
+ top: z8.coerce.number().int().positive().default(5),
2496
+ dryRun: z8.boolean().default(false),
2497
+ yes: z8.boolean().default(false),
2498
+ apiBase: z8.string().url().optional()
2499
+ });
2500
+ function mineCommand() {
2501
+ return new Command6("mine").description(
2502
+ "Mine local sessions, preview suggestions, and publish accepted ones."
2503
+ ).addOption(
2504
+ new Option2("--source <src...>", "Limit to one or more sources").choices([
2505
+ ...SOURCES
2506
+ ])
2507
+ ).option("--workspace <slug>", "Override the default workspace from config").option("--since <duration>", "Window to scan (e.g. 7d, 30d, 90d)", "30d").option("--top <N>", "Max candidates to preview", "5").option("--dry-run", "Print candidates, do not publish", false).option("--yes", "Skip prompts and publish all candidates", false).option("--api-base <url>", "Override platform base URL").action(async (rawOpts) => {
2508
+ const opts = optsSchema5.parse(rawOpts);
2509
+ let since;
2510
+ try {
2511
+ since = new Date(Date.now() - parseDuration(opts.since));
2512
+ } catch (e) {
2513
+ log.err(e.message);
2514
+ process.exit(EXIT.config);
2515
+ return;
2516
+ }
2517
+ const eventClient = await tryCreateEventClient({
2518
+ ...opts.apiBase !== void 0 ? { apiBase: opts.apiBase } : {},
2519
+ ...opts.workspace !== void 0 ? { workspace: opts.workspace } : {}
2520
+ });
2521
+ const startedAt = Date.now();
2522
+ const sources = opts.source.filter(
2523
+ (s) => ADAPTERS[s] !== void 0
2524
+ );
2525
+ await sendMiningEvent(eventClient, "mining_scan_started", {
2526
+ since: opts.since,
2527
+ sources
2528
+ });
2529
+ const allSessions = await collectSessions(sources, since);
2530
+ if (allSessions.length === 0) {
2531
+ await sendMiningEvent(eventClient, "mining_scan_completed", {
2532
+ sessionCount: 0,
2533
+ durationMs: Date.now() - startedAt
2534
+ });
2535
+ log.err(
2536
+ `no sessions in last ${opts.since}. Try --since=90d or --since=365d.`
2537
+ );
2538
+ process.exit(EXIT.noWork);
2539
+ return;
2540
+ }
2541
+ if (!getGlobals().json) {
2542
+ log.step(
2543
+ `mining ${allSessions.length} sessions from last ${opts.since}...`
2544
+ );
2545
+ }
2546
+ const result = mineSessions(allSessions, { render: { top: opts.top } });
2547
+ await sendMiningEvent(eventClient, "mining_scan_completed", {
2548
+ sessionCount: result.sessionCountKept,
2549
+ durationMs: Date.now() - startedAt
2550
+ });
2551
+ const candidates = result.candidates.slice(0, opts.top);
2552
+ if (candidates.length === 0) {
2553
+ log.info("no reusable patterns found. Try widening --since.");
2554
+ process.exit(EXIT.noWork);
2555
+ return;
2556
+ }
2557
+ const sessionsById = new Map(allSessions.map((s) => [s.id, s]));
2558
+ for (const candidate of candidates) {
2559
+ await persistCandidate(candidate, sessionsById);
2560
+ await sendMiningEvent(eventClient, "pattern_detected", {
2561
+ clusterId: candidate.promptShape,
2562
+ clusterSize: candidate.uses,
2563
+ score: candidate.score,
2564
+ keywords: candidate.suggestedTools
2565
+ });
2566
+ }
2567
+ const existing = await loadExistingPreviews(candidates, {
2568
+ ...opts.apiBase !== void 0 ? { apiBase: opts.apiBase } : {},
2569
+ ...opts.workspace !== void 0 ? { workspace: opts.workspace } : {}
2570
+ });
2571
+ const jsonPreviewOnly = getGlobals().json && !opts.yes;
2572
+ const preview = {
2573
+ input: result.sessionCountInput,
2574
+ kept: result.sessionCountKept,
2575
+ clusters: result.clusterCount,
2576
+ dryRun: opts.dryRun || jsonPreviewOnly,
2577
+ candidates: candidates.map(
2578
+ (candidate) => candidateJson(candidate, existing.get(candidate.slug))
2579
+ )
2580
+ };
2581
+ if (!getGlobals().json) {
2582
+ log.info("");
2583
+ log.info("Mined candidates:");
2584
+ log.info("");
2585
+ for (const [index, candidate] of candidates.entries()) {
2586
+ printCandidate(index + 1, candidate, existing.get(candidate.slug));
2587
+ }
2588
+ }
2589
+ if (opts.dryRun || jsonPreviewOnly) {
2590
+ if (getGlobals().json) {
2591
+ log.json(preview);
2592
+ }
2593
+ await sendMiningEvent(eventClient, "mining_session_completed", {
2594
+ candidatesShown: candidates.length,
2595
+ candidatesAccepted: 0,
2596
+ candidatesSkipped: 0,
2597
+ durationMs: Date.now() - startedAt
2598
+ });
2599
+ process.exit(EXIT.ok);
2600
+ return;
2601
+ }
2602
+ const published = [];
2603
+ let dismissed = 0;
2604
+ const rl = opts.yes || getGlobals().json ? void 0 : createInterface3({ input, output });
2605
+ try {
2606
+ for (const candidate of candidates) {
2607
+ let targetSlug = candidate.slug;
2608
+ const decision = opts.yes ? { kind: "publish" } : await promptForDecision(rl, candidate, targetSlug);
2609
+ if (decision.kind === "quit") break;
2610
+ if (decision.kind === "skip") {
2611
+ dismissed += 1;
2612
+ await sendMiningEvent(eventClient, "suggestion_dismissed", {
2613
+ clusterId: candidate.promptShape,
2614
+ slug: targetSlug
2615
+ });
2616
+ continue;
2617
+ }
2618
+ if (decision.kind === "edit") {
2619
+ targetSlug = decision.slug;
2620
+ }
2621
+ try {
2622
+ if (!getGlobals().json) {
2623
+ log.step(`publishing ${targetSlug} ...`);
2624
+ }
2625
+ const pushedResult = await pushOperon({
2626
+ slug: targetSlug,
2627
+ candidate,
2628
+ ...opts.workspace !== void 0 ? { workspace: opts.workspace } : {},
2629
+ ...opts.apiBase !== void 0 ? { apiBase: opts.apiBase } : {}
2630
+ });
2631
+ published.push({ slug: targetSlug, url: pushedResult.url });
2632
+ await sendMiningEvent(eventClient, "suggestion_accepted", {
2633
+ clusterId: candidate.promptShape,
2634
+ slug: targetSlug,
2635
+ score: candidate.score
2636
+ });
2637
+ } catch (e) {
2638
+ if (e instanceof PushOperonError) {
2639
+ log.err(e.message);
2640
+ process.exit(e.exitCode);
2641
+ return;
2642
+ }
2643
+ throw e;
2644
+ }
2645
+ }
2646
+ } finally {
2647
+ rl?.close();
2648
+ }
2649
+ await sendMiningEvent(eventClient, "mining_session_completed", {
2650
+ candidatesShown: candidates.length,
2651
+ candidatesAccepted: published.length,
2652
+ candidatesSkipped: dismissed,
2653
+ durationMs: Date.now() - startedAt
2654
+ });
2655
+ if (getGlobals().json) {
2656
+ log.json({ ...preview, published, dismissed });
2657
+ } else {
2658
+ log.info("");
2659
+ log.step(`Published ${published.length} of ${candidates.length}.`);
2660
+ for (const item of published) {
2661
+ log.info(` ${item.slug}: ${item.url}`);
2662
+ }
2663
+ }
2664
+ process.exit(EXIT.ok);
2665
+ });
2666
+ }
2667
+ async function collectSessions(sources, since) {
2668
+ const allSessions = [];
2669
+ for (const sourceName of sources) {
2670
+ const adapter = ADAPTERS[sourceName];
2671
+ if (adapter === void 0) continue;
2672
+ const detect = await adapter.detect();
2673
+ if (!detect.present) continue;
2674
+ for await (const raw of adapter.enumerate({ since })) {
2675
+ let session = await adapter.normalize(raw);
2676
+ session = redactSession(session);
2677
+ allSessions.push(session);
2678
+ }
2679
+ }
2680
+ return allSessions;
2681
+ }
2682
+ async function persistCandidate(candidate, sessionsById) {
2683
+ const exemplars = candidate.exemplarSessionIds.map((id) => sessionsById.get(id)).filter((s) => s !== void 0);
2684
+ await writeCandidate(candidate, exemplars);
2685
+ }
2686
+ function printCandidate(index, candidate, existing) {
2687
+ const keywords = candidate.suggestedTools.slice(0, 3);
2688
+ log.info(` ${index}. ${titleCase(candidate.slug)} (${candidate.slug})`);
2689
+ log.info(
2690
+ ` cluster ${candidate.uses} sessions, ${candidate.spanDays}d span, score ${candidate.score}`
2691
+ );
2692
+ log.info(` seen ${candidate.firstSeenAt} -> ${candidate.lastSeenAt}`);
2693
+ log.info(` keywords: ${keywords.join(", ") || "(none)"}`);
2694
+ log.info(` existing: ${existing?.summary ?? "unchecked"}`);
2695
+ log.info("");
2696
+ }
2697
+ async function promptForDecision(rl, candidate, targetSlug) {
2698
+ if (rl === void 0) return { kind: "publish" };
2699
+ for (; ; ) {
2700
+ const raw = (await rl.question(
2701
+ `${candidate.slug} [p]ublish, [s]kip, [e]dit slug, [q]uit? `
2702
+ )).trim().toLowerCase();
2703
+ if (raw === "" || raw === "p" || raw === "publish") {
2704
+ return { kind: "publish" };
2705
+ }
2706
+ if (raw === "s" || raw === "skip") return { kind: "skip" };
2707
+ if (raw === "q" || raw === "quit") return { kind: "quit" };
2708
+ if (raw === "e" || raw === "edit") {
2709
+ const next = (await rl.question(`slug (${targetSlug}): `)).trim();
2710
+ if (!slugRe2.test(next)) {
2711
+ log.warn("slug must match /^[a-z0-9-]+$/");
2712
+ continue;
2713
+ }
2714
+ return { kind: "edit", slug: next };
2715
+ }
2716
+ }
2717
+ }
2718
+ async function tryCreateEventClient(opts) {
2719
+ try {
2720
+ return await createPlatformEventClient({
2721
+ ...opts.apiBase !== void 0 ? { apiBase: opts.apiBase } : {},
2722
+ ...opts.workspace !== void 0 ? { workspaceSlug: opts.workspace } : {}
2723
+ });
2724
+ } catch (e) {
2725
+ if (e instanceof PlatformEventError) {
2726
+ log.debug(`telemetry disabled: ${e.message}`);
2727
+ return void 0;
2728
+ }
2729
+ throw e;
2730
+ }
2731
+ }
2732
+ async function sendMiningEvent(client, event, properties) {
2733
+ if (client === void 0) return;
2734
+ try {
2735
+ await client.send(event, properties);
2736
+ } catch (e) {
2737
+ if (e instanceof PlatformEventError) {
2738
+ log.warn(`telemetry skipped: ${e.message}`);
2739
+ return;
2740
+ }
2741
+ throw e;
2742
+ }
2743
+ }
2744
+ async function loadExistingPreviews(candidates, opts) {
2745
+ const out = /* @__PURE__ */ new Map();
2746
+ const config = await readConfig();
2747
+ const workspace = opts.workspace ?? config.platform.workspaceSlug;
2748
+ const token = config.platform.accessToken;
2749
+ if (workspace === void 0 || token === void 0 || token.length === 0) {
2750
+ for (const candidate of candidates) {
2751
+ out.set(candidate.slug, {
2752
+ status: "unchecked",
2753
+ summary: "unchecked (missing workspace or login)"
2754
+ });
2755
+ }
2756
+ return out;
2757
+ }
2758
+ const apiBase = (opts.apiBase ?? config.platform.apiBase ?? "https://app.withoperon.com").replace(/\/+$/, "");
2759
+ for (const candidate of candidates) {
2760
+ out.set(
2761
+ candidate.slug,
2762
+ await loadExistingPreview({ apiBase, workspace, token, candidate })
2763
+ );
2764
+ }
2765
+ return out;
2766
+ }
2767
+ async function loadExistingPreview(input2) {
2768
+ try {
2769
+ const res = await fetch(
2770
+ `${input2.apiBase}/api/v1/workspaces/${input2.workspace}/operons/${input2.candidate.slug}`,
2771
+ {
2772
+ headers: {
2773
+ "x-operon-source": "cli",
2774
+ authorization: `Bearer ${input2.token}`
2775
+ }
2776
+ }
2777
+ );
2778
+ if (res.status === 404) return { status: "new", summary: "new" };
2779
+ if (!res.ok) {
2780
+ return {
2781
+ status: "unchecked",
2782
+ summary: `unchecked (${res.status})`
2783
+ };
2784
+ }
2785
+ const json = await res.json();
2786
+ if (typeof json.skillUrl !== "string") {
2787
+ return { status: "unchecked", summary: "unchecked (no skill url)" };
2788
+ }
2789
+ const current = await fetch(json.skillUrl).then(
2790
+ (r) => r.ok ? r.text() : void 0
2791
+ );
2792
+ if (current === void 0) {
2793
+ return { status: "unchecked", summary: "unchecked (skill unavailable)" };
2794
+ }
2795
+ const next = await readSkillMarkdown(input2.candidate);
2796
+ if (current === next.markdown) {
2797
+ return { status: "same", summary: "no SKILL.md changes" };
2798
+ }
2799
+ return {
2800
+ status: "changed",
2801
+ summary: `SKILL.md differs (${lineCount(current)} -> ${lineCount(next.markdown)} lines)`
2802
+ };
2803
+ } catch (e) {
2804
+ return {
2805
+ status: "unchecked",
2806
+ summary: `unchecked (${e.message})`
2807
+ };
2808
+ }
2809
+ }
2810
+ function lineCount(s) {
2811
+ return s.split("\n").length;
2812
+ }
2813
+ function candidateJson(candidate, existing) {
2814
+ return {
2815
+ slug: candidate.slug,
2816
+ name: titleCase(candidate.slug),
2817
+ uses: candidate.uses,
2818
+ spanDays: candidate.spanDays,
2819
+ score: candidate.score,
2820
+ firstSeenAt: candidate.firstSeenAt,
2821
+ lastSeenAt: candidate.lastSeenAt,
2822
+ keywords: candidate.suggestedTools.slice(0, 3),
2823
+ existing: existing ?? { status: "unchecked", summary: "unchecked" }
2824
+ };
2825
+ }
2826
+
2827
+ // src/commands/mcp.ts
2828
+ import { Command as Command7 } from "commander";
2829
+
2830
+ // ../mcp/src/index.ts
2831
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2832
+
2833
+ // ../mcp/src/tools.ts
2834
+ import { createHash as createHash3 } from "crypto";
2835
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2836
+ import { z as z9 } from "zod";
2837
+ var DEFAULT_API_BASE = "https://app.withoperon.com";
2838
+ var slugRe3 = /^[a-z0-9-]+$/;
2839
+ var semverRe2 = /^\d+\.\d+\.\d+(-[a-z0-9.]+)?$/;
2840
+ var sha256HexRe2 = /^[a-f0-9]{64}$/i;
2841
+ var maxInt2 = 2147483647;
2842
+ var workspaceSchema2 = z9.string().min(1).max(64).regex(slugRe3);
2843
+ var slugSchema2 = z9.string().min(1).max(64).regex(slugRe3);
2844
+ var semverSchema = z9.string().min(1).max(64).regex(semverRe2);
2845
+ var listOperonsResponseSchema = z9.object({
2846
+ workspace: z9.object({
2847
+ slug: z9.string(),
2848
+ name: z9.string()
2849
+ }),
2850
+ operons: z9.array(
2851
+ z9.object({
2852
+ slug: z9.string(),
2853
+ name: z9.string(),
2854
+ description: z9.string().nullable(),
2855
+ visibility: z9.string(),
2856
+ tags: z9.array(z9.string()),
2857
+ updatedAt: z9.string(),
2858
+ currentVersion: z9.object({
2859
+ semver: z9.string(),
2860
+ contentHash: z9.string().regex(sha256HexRe2),
2861
+ publishStatus: z9.string(),
2862
+ evalStatus: z9.string()
2863
+ }).nullable()
2864
+ })
2865
+ )
2866
+ });
2867
+ var pullResponseSchema = z9.object({
2868
+ workspace: z9.object({
2869
+ slug: z9.string(),
2870
+ name: z9.string()
2871
+ }),
2872
+ operon: z9.object({
2873
+ slug: z9.string(),
2874
+ name: z9.string(),
2875
+ description: z9.string().nullable(),
2876
+ visibility: z9.string(),
2877
+ tags: z9.array(z9.string())
2878
+ }),
2879
+ version: z9.object({
2880
+ semver: z9.string(),
2881
+ contentHash: z9.string().regex(sha256HexRe2),
2882
+ publishStatus: z9.string(),
2883
+ evalStatus: z9.string(),
2884
+ createdAt: z9.string(),
2885
+ changelog: z9.string().nullable()
2886
+ }),
2887
+ skillUrl: z9.string().url()
2888
+ });
2889
+ var recordRunResponseSchema2 = z9.object({
2890
+ runId: z9.string().uuid(),
2891
+ status: z9.enum(["running", "succeeded", "failed", "timeout"]),
2892
+ runsUsedMtd: z9.number().int().nonnegative(),
2893
+ runsCap: z9.number().int().nonnegative().nullable(),
2894
+ overCap: z9.boolean()
2895
+ });
2896
+ var OperonApiClient = class {
2897
+ apiBase;
2898
+ token;
2899
+ fetchImpl;
2900
+ constructor(options) {
2901
+ this.apiBase = options.apiBase.replace(/\/+$/, "");
2902
+ this.token = options.token;
2903
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
2904
+ this.fetchImpl = (input2, init) => fetchImpl(input2, init);
2905
+ }
2906
+ async validateToken() {
2907
+ await this.requestJson(`${this.apiBase}/api/v1/whoami`, z9.object({}));
2908
+ }
2909
+ listOperons(workspace) {
2910
+ return this.requestJson(
2911
+ `${this.apiBase}/api/v1/workspaces/${workspace}/operons`,
2912
+ listOperonsResponseSchema
2913
+ );
2914
+ }
2915
+ async getOperon(input2) {
2916
+ const params = new URLSearchParams();
2917
+ if (input2.semver !== void 0) params.set("semver", input2.semver);
2918
+ const query = params.size > 0 ? `?${params.toString()}` : "";
2919
+ const metadata = await this.requestJson(
2920
+ `${this.apiBase}/api/v1/workspaces/${input2.workspace}/operons/${input2.slug}${query}`,
2921
+ pullResponseSchema
2922
+ );
2923
+ const skillRes = await this.fetchImpl(metadata.skillUrl);
2924
+ if (!skillRes.ok) {
2925
+ throw new Error(`skill download returned ${skillRes.status}`);
2926
+ }
2927
+ const skillMarkdown = await skillRes.text();
2928
+ const actualHash = createHash3("sha256").update(skillMarkdown).digest("hex");
2929
+ const expectedHash = metadata.version.contentHash.toLowerCase();
2930
+ if (actualHash !== expectedHash) {
2931
+ throw new Error(
2932
+ `content hash mismatch: expected ${expectedHash}, got ${actualHash}`
2933
+ );
2934
+ }
2935
+ return {
2936
+ workspace: metadata.workspace.slug,
2937
+ slug: metadata.operon.slug,
2938
+ name: metadata.operon.name,
2939
+ semver: metadata.version.semver,
2940
+ contentHash: expectedHash,
2941
+ skillMarkdown
2942
+ };
2943
+ }
2944
+ recordRun(input2) {
2945
+ const payload = {
2946
+ workspaceSlug: input2.workspace,
2947
+ operonSlug: input2.operonSlug,
2948
+ semver: input2.semver,
2949
+ invocationSource: "mcp",
2950
+ status: input2.status,
2951
+ ...input2.startedAt !== void 0 ? { startedAt: input2.startedAt } : {},
2952
+ ...input2.finishedAt !== void 0 ? { finishedAt: input2.finishedAt } : {},
2953
+ ...input2.durationMs !== void 0 ? { durationMs: input2.durationMs } : {},
2954
+ ...input2.inputHash !== void 0 ? { inputHash: input2.inputHash } : {},
2955
+ ...input2.tokenIn !== void 0 ? { tokenIn: input2.tokenIn } : {},
2956
+ ...input2.tokenOut !== void 0 ? { tokenOut: input2.tokenOut } : {},
2957
+ ...input2.costUsdEstimated !== void 0 ? { costUsdEstimated: input2.costUsdEstimated } : {},
2958
+ ...input2.billedRunCount !== void 0 ? { billedRunCount: input2.billedRunCount } : {}
2959
+ };
2960
+ return this.requestJson(
2961
+ `${this.apiBase}/api/v1/runs`,
2962
+ recordRunResponseSchema2,
2963
+ {
2964
+ method: "POST",
2965
+ headers: { "content-type": "application/json" },
2966
+ body: JSON.stringify(payload)
2967
+ }
2968
+ );
2969
+ }
2970
+ async requestJson(url, schema, init = {}) {
2971
+ let res;
2972
+ try {
2973
+ res = await this.fetchImpl(url, {
2974
+ ...init,
2975
+ headers: {
2976
+ ...init.headers,
2977
+ "x-operon-source": "mcp",
2978
+ authorization: `Bearer ${this.token}`
2979
+ }
2980
+ });
2981
+ } catch (e) {
2982
+ throw new Error(`network error: ${e.message}`);
2983
+ }
2984
+ if (!res.ok) {
2985
+ const body = await res.text().catch(() => "<unreadable>");
2986
+ throw new Error(`server returned ${res.status}: ${body.slice(0, 400)}`);
2987
+ }
2988
+ return schema.parse(await res.json());
2989
+ }
2990
+ };
2991
+ async function createOperonMcpServer(options = {}) {
2992
+ const apiBase = options.apiBase ?? process.env["OPERON_API_BASE"] ?? DEFAULT_API_BASE;
2993
+ const token = options.token ?? process.env["OPERON_TOKEN"];
2994
+ if (token === void 0 || token.length === 0) {
2995
+ throw new Error("OPERON_TOKEN is required");
2996
+ }
2997
+ const client = new OperonApiClient({
2998
+ apiBase,
2999
+ token,
3000
+ fetchImpl: options.fetchImpl
3001
+ });
3002
+ if (options.validateOnStart !== false) {
3003
+ await client.validateToken();
3004
+ }
3005
+ const server = new McpServer({
3006
+ name: "operon",
3007
+ version: "0.1.0"
3008
+ });
3009
+ server.registerTool(
3010
+ "list_operons",
3011
+ {
3012
+ title: "List Operons",
3013
+ description: "List operons visible to a workspace member.",
3014
+ inputSchema: {
3015
+ workspace: workspaceSchema2.describe("Workspace slug")
3016
+ }
3017
+ },
3018
+ async ({ workspace }) => jsonResult(await client.listOperons(workspace))
3019
+ );
3020
+ server.registerTool(
3021
+ "get_operon",
3022
+ {
3023
+ title: "Get Operon",
3024
+ description: "Fetch an operon's SKILL.md and version metadata for use as context.",
3025
+ inputSchema: {
3026
+ workspace: workspaceSchema2.describe("Workspace slug"),
3027
+ slug: slugSchema2.describe("Operon slug"),
3028
+ semver: semverSchema.optional().describe("Optional semver; defaults to current version")
3029
+ }
3030
+ },
3031
+ async ({ workspace, slug, semver }) => {
3032
+ const operon = await client.getOperon({ workspace, slug, semver });
3033
+ return {
3034
+ content: [
3035
+ {
3036
+ type: "text",
3037
+ text: `# ${operon.workspace}/${operon.slug}@${operon.semver}
3038
+
3039
+ ${operon.skillMarkdown}`
3040
+ }
3041
+ ]
3042
+ };
3043
+ }
3044
+ );
3045
+ server.registerTool(
3046
+ "record_run",
3047
+ {
3048
+ title: "Record Run",
3049
+ description: "Record an MCP invocation against an operon version.",
3050
+ inputSchema: {
3051
+ workspace: workspaceSchema2.describe("Workspace slug"),
3052
+ operonSlug: slugSchema2.describe("Operon slug"),
3053
+ semver: semverSchema.describe("Version semver"),
3054
+ status: z9.enum(["running", "succeeded", "failed", "timeout"]).describe("Run status"),
3055
+ startedAt: z9.string().datetime({ offset: true }).optional(),
3056
+ finishedAt: z9.string().datetime({ offset: true }).optional(),
3057
+ durationMs: z9.number().int().min(0).max(maxInt2).optional(),
3058
+ inputHash: z9.string().regex(sha256HexRe2).optional(),
3059
+ tokenIn: z9.number().int().min(0).max(maxInt2).optional(),
3060
+ tokenOut: z9.number().int().min(0).max(maxInt2).optional(),
3061
+ costUsdEstimated: z9.number().finite().min(0).max(9999.999999).optional(),
3062
+ billedRunCount: z9.number().int().min(1).max(maxInt2).optional()
3063
+ }
3064
+ },
3065
+ async (input2) => jsonResult(await client.recordRun(input2))
3066
+ );
3067
+ return server;
3068
+ }
3069
+ function jsonResult(value) {
3070
+ return {
3071
+ content: [
3072
+ {
3073
+ type: "text",
3074
+ text: JSON.stringify(value, null, 2)
3075
+ }
3076
+ ]
3077
+ };
3078
+ }
3079
+
3080
+ // ../mcp/src/index.ts
3081
+ async function runStdioServer(options = {}) {
3082
+ const server = await createOperonMcpServer(options);
3083
+ const transport = new StdioServerTransport();
3084
+ await server.connect(transport);
3085
+ }
3086
+
3087
+ // src/commands/mcp.ts
3088
+ function mcpCommand() {
3089
+ return new Command7("mcp").description("Launch the Operon MCP server over stdio.").action(async () => {
3090
+ try {
3091
+ await runStdioServer();
3092
+ } catch (e) {
3093
+ log.err(e instanceof Error ? e.message : String(e));
3094
+ process.exit(EXIT.err);
3095
+ }
3096
+ });
3097
+ }
3098
+
3099
+ // src/commands/polish.ts
3100
+ import { writeFile as writeFile5 } from "fs/promises";
3101
+ import { join as join9 } from "path";
3102
+ import { Command as Command8 } from "commander";
3103
+ import { z as z10 } from "zod";
3104
+
3105
+ // src/llm/polish.ts
3106
+ var API = "https://api.anthropic.com/v1/messages";
3107
+ var MODEL = "claude-sonnet-4-6";
3108
+ var MAX_TOKENS = 1500;
3109
+ async function polishCandidate(candidate, exemplars) {
3110
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
3111
+ if (apiKey === void 0 || apiKey.length === 0) {
3112
+ return { error: { kind: "no-api-key" } };
3113
+ }
3114
+ const prompt = buildPrompt(candidate, exemplars);
3115
+ let res;
3116
+ try {
3117
+ res = await fetch(API, {
3118
+ method: "POST",
3119
+ headers: {
3120
+ "x-api-key": apiKey,
3121
+ "anthropic-version": "2023-06-01",
3122
+ "content-type": "application/json"
3123
+ },
3124
+ body: JSON.stringify({
3125
+ model: MODEL,
3126
+ max_tokens: MAX_TOKENS,
3127
+ messages: [{ role: "user", content: prompt }]
3128
+ })
3129
+ });
3130
+ } catch (e) {
3131
+ return {
3132
+ error: { kind: "network-error", message: e.message }
3133
+ };
3134
+ }
3135
+ if (!res.ok) {
3136
+ const body = await res.text().catch(() => "<unreadable>");
3137
+ return { error: { kind: "http-error", status: res.status, body } };
3138
+ }
3139
+ const json = await res.json();
3140
+ const text = json.content?.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n").trim();
3141
+ if (text === void 0 || text.length === 0) {
3142
+ return { error: { kind: "empty-response" } };
3143
+ }
3144
+ return {
3145
+ slug: candidate.slug,
3146
+ markdown: text,
3147
+ model: MODEL,
3148
+ usage: {
3149
+ inputTokens: json.usage?.input_tokens ?? 0,
3150
+ outputTokens: json.usage?.output_tokens ?? 0
3151
+ }
3152
+ };
3153
+ }
3154
+ function buildPrompt(c, exemplars) {
3155
+ const exemplarBlocks = exemplars.slice(0, 3).map((s, i) => {
3156
+ const userMsg = s.messages.find((m) => m.role === "user");
3157
+ const lastAssistant = [...s.messages].reverse().find((m) => m.role === "assistant");
3158
+ const tools = s.toolCalls.map((t) => t.name).slice(0, 12).join(", ");
3159
+ return `### Exemplar ${i + 1}
3160
+ session: ${s.id}
3161
+ duration: ${s.durationSec}s
3162
+ cwd: ${s.cwd ?? "(unknown)"}
3163
+ tools used: ${tools || "(none)"}
3164
+ file extensions: ${s.fileTouches.map((f) => `${f.extension}:${f.count}`).join(", ") || "(none)"}
3165
+
3166
+ first user prompt (truncated to 800c):
3167
+ ${(userMsg?.content ?? "").slice(0, 800)}
3168
+
3169
+ last assistant message (truncated to 500c):
3170
+ ${(lastAssistant?.content ?? "").slice(0, 500)}`;
3171
+ });
3172
+ return `You are helping a developer turn a recurring Claude Code workflow into a reusable Skill.
3173
+
3174
+ The Operon mining pipeline detected this pattern across ${c.uses} sessions over ${c.spanDays} day(s). The candidate's auto-generated metadata:
3175
+
3176
+ slug: ${c.slug}
3177
+ suggested tools: ${c.suggestedTools.join(", ") || "(none)"}
3178
+ file extensions touched: (see exemplars below)
3179
+
3180
+ Below are ${exemplars.length} exemplar sessions that all matched this pattern. Read them carefully.
3181
+
3182
+ ${exemplarBlocks.join("\n\n")}
3183
+
3184
+ ---
3185
+
3186
+ Now write a clean SKILL.md that:
3187
+
3188
+ 1. Starts with a 1-sentence "When to use this skill" trigger that captures what the user is actually trying to accomplish (NOT what tools they used \u2014 those are an implementation detail).
3189
+ 2. Lists 3-7 specific, actionable steps. Each step should be concrete enough that an agent reading just this skill can execute it. Reference the actual pattern observed across the exemplars \u2014 paths, commands, decisions.
3190
+ 3. Has an "Allowed tools" line listing the most-used tools from the exemplars.
3191
+ 4. Ends with one short example invocation in a fenced code block.
3192
+
3193
+ Output ONLY the SKILL.md content, with frontmatter:
3194
+ ---
3195
+ name: ${c.slug}
3196
+ description: <short description, under 100 chars, what this skill does>
3197
+ allowed-tools: <comma-separated tool list>
3198
+ ---
3199
+
3200
+ (then the body)
3201
+
3202
+ No preamble, no explanation, no "Here is the SKILL.md". Just the file content.`;
3203
+ }
3204
+
3205
+ // src/commands/polish.ts
3206
+ var slugSchema3 = z10.string().min(1).max(64).regex(/^[a-z0-9-]+$/);
3207
+ var optsSchema6 = z10.object({
3208
+ batch: z10.boolean().default(false),
3209
+ minScore: z10.coerce.number().default(0),
3210
+ limit: z10.coerce.number().int().positive().default(10)
3211
+ });
3212
+ function polishCommand() {
3213
+ return new Command8("polish").argument("[slug]", "Candidate slug to polish (omit with --batch)").description(
3214
+ "Use Claude to turn a candidate into a clean SKILL.md. Requires ANTHROPIC_API_KEY."
3215
+ ).option("--batch", "Polish every candidate above --min-score", false).option("--min-score <n>", "Score threshold for --batch (default 0)", "0").option(
3216
+ "--limit <n>",
3217
+ "Cap the number of candidates polished in --batch",
3218
+ "10"
3219
+ ).action(async (slugArg, rawOpts) => {
3220
+ const opts = optsSchema6.parse(rawOpts);
3221
+ if (opts.batch) {
3222
+ await runBatch(opts.minScore, opts.limit);
3223
+ return;
3224
+ }
3225
+ if (slugArg === void 0) {
3226
+ log.err("polish: pass a slug or --batch");
3227
+ process.exit(EXIT.config);
3228
+ return;
3229
+ }
3230
+ const slug = slugSchema3.parse(slugArg);
3231
+ const candidate = await readCandidate(slug);
3232
+ if (candidate === void 0) {
3233
+ log.err(`no candidate "${slug}" found`);
3234
+ process.exit(EXIT.noWork);
3235
+ return;
3236
+ }
3237
+ const result = await polishOne(candidate);
3238
+ if ("error" in result) {
3239
+ exitFromPolishError(result.error);
3240
+ return;
3241
+ }
3242
+ if (getGlobals().json) {
3243
+ log.json({
3244
+ slug: result.slug,
3245
+ path: join9(candidateDir(slug), "polished.md"),
3246
+ model: result.model,
3247
+ usage: result.usage
3248
+ });
3249
+ } else {
3250
+ log.step(
3251
+ `wrote ${join9(candidateDir(slug), "polished.md")} (${result.usage.inputTokens} in / ${result.usage.outputTokens} out tokens)`
3252
+ );
3253
+ log.info("");
3254
+ log.info(result.markdown);
3255
+ }
3256
+ process.exit(EXIT.ok);
3257
+ });
3258
+ }
3259
+ async function runBatch(minScore, limit) {
3260
+ const all = await listCandidates();
3261
+ const eligible = all.filter((c) => c.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
3262
+ if (eligible.length === 0) {
3263
+ log.err(
3264
+ `no candidates above min-score=${minScore} (out of ${all.length} total)`
3265
+ );
3266
+ process.exit(EXIT.noWork);
3267
+ return;
3268
+ }
3269
+ log.step(
3270
+ `batch polishing ${eligible.length} candidate${eligible.length === 1 ? "" : "s"} via Claude...`
3271
+ );
3272
+ const results = [];
3273
+ for (const c of eligible) {
3274
+ const r = await polishOne(c);
3275
+ if ("error" in r) {
3276
+ if (r.error.kind === "no-api-key") {
3277
+ log.err(
3278
+ "ANTHROPIC_API_KEY is not set. Polish requires explicit opt-in via the env var."
3279
+ );
3280
+ process.exit(EXIT.config);
3281
+ return;
3282
+ }
3283
+ results.push({ slug: c.slug, ok: false, error: errorMessage(r.error) });
3284
+ log.warn(` ${c.slug}: ${errorMessage(r.error)}`);
3285
+ continue;
3286
+ }
3287
+ results.push({
3288
+ slug: c.slug,
3289
+ ok: true,
3290
+ inputTokens: r.usage.inputTokens,
3291
+ outputTokens: r.usage.outputTokens
3292
+ });
3293
+ log.info(
3294
+ ` ${c.slug} (${r.usage.inputTokens} in / ${r.usage.outputTokens} out)`
3295
+ );
3296
+ }
3297
+ const okCount = results.filter((r) => r.ok).length;
3298
+ if (getGlobals().json) {
3299
+ log.json({ polished: okCount, total: eligible.length, results });
3300
+ } else {
3301
+ log.step(`polished ${okCount}/${eligible.length}`);
3302
+ }
3303
+ process.exit(okCount > 0 ? EXIT.ok : EXIT.err);
3304
+ }
3305
+ async function polishOne(candidate) {
3306
+ const exemplars = await loadExemplars(candidate.exemplarSessionIds);
3307
+ if (exemplars.length === 0) {
3308
+ return {
3309
+ error: {
3310
+ kind: "empty-response"
3311
+ }
3312
+ };
3313
+ }
3314
+ const result = await polishCandidate(candidate, exemplars);
3315
+ if ("error" in result) return result;
3316
+ const path = join9(candidateDir(candidate.slug), "polished.md");
3317
+ await writeFile5(path, `${result.markdown}
3318
+ `, "utf8");
3319
+ return result;
3320
+ }
3321
+ function errorMessage(e) {
3322
+ switch (e.kind) {
3323
+ case "no-api-key":
3324
+ return "ANTHROPIC_API_KEY not set";
3325
+ case "http-error":
3326
+ return `HTTP ${e.status}: ${e.body.slice(0, 200)}`;
3327
+ case "network-error":
3328
+ return `network: ${e.message}`;
3329
+ case "empty-response":
3330
+ return "empty/missing exemplars or response";
3331
+ }
3332
+ }
3333
+ function exitFromPolishError(e) {
3334
+ switch (e.kind) {
3335
+ case "no-api-key":
3336
+ log.err(
3337
+ "ANTHROPIC_API_KEY is not set. Polish requires explicit opt-in via the env var."
3338
+ );
3339
+ process.exit(EXIT.config);
3340
+ return;
3341
+ case "http-error":
3342
+ log.err(`Anthropic API returned ${e.status}: ${e.body.slice(0, 400)}`);
3343
+ process.exit(EXIT.network);
3344
+ return;
3345
+ case "network-error":
3346
+ log.err(`network error: ${e.message}`);
3347
+ process.exit(EXIT.network);
3348
+ return;
3349
+ case "empty-response":
3350
+ log.err("Anthropic API returned empty content (or no exemplars)");
3351
+ process.exit(EXIT.err);
3352
+ return;
3353
+ }
3354
+ }
3355
+ async function loadExemplars(ids) {
3356
+ const wanted = new Set(ids);
3357
+ const out = [];
3358
+ for (const adapter of Object.values(ADAPTERS)) {
3359
+ if (adapter === void 0) continue;
3360
+ const detect = await adapter.detect();
3361
+ if (!detect.present) continue;
3362
+ for await (const raw of adapter.enumerate({})) {
3363
+ const id = `${adapter.name}:${raw.rawId}`;
3364
+ if (!wanted.has(id)) continue;
3365
+ const session = await adapter.normalize(raw);
3366
+ out.push(session);
3367
+ wanted.delete(id);
3368
+ if (wanted.size === 0) return out;
3369
+ }
3370
+ }
3371
+ return out;
3372
+ }
3373
+
3374
+ // src/commands/publish.ts
3375
+ import { Command as Command9 } from "commander";
3376
+ import { z as z11 } from "zod";
3377
+ var slugSchema4 = z11.string().min(1).max(64).regex(/^[a-z0-9-]+$/);
3378
+ var optsSchema7 = z11.object({
3379
+ workspace: z11.string().min(1).max(64).optional(),
3380
+ visibility: z11.enum(["private", "team"]).default("team"),
3381
+ apiBase: z11.string().url().optional()
3382
+ });
3383
+ function publishCommand() {
3384
+ return new Command9("publish").argument("<slug>", "Candidate slug to publish").description(
3385
+ "Upload a polished candidate to the Operon platform (requires `operon login`)."
3386
+ ).option("--workspace <slug>", "Override the default workspace from config").option("--visibility <v>", "private | team (default: team)", "team").option("--api-base <url>", "Override platform base URL").action(async (slugArg, rawOpts) => {
3387
+ const slug = slugSchema4.parse(slugArg);
3388
+ const opts = optsSchema7.parse(rawOpts);
3389
+ log.step(`publishing ${slug} ...`);
3390
+ let json;
3391
+ try {
3392
+ json = await pushOperon({
3393
+ slug,
3394
+ ...opts.workspace !== void 0 ? { workspace: opts.workspace } : {},
3395
+ visibility: opts.visibility,
3396
+ ...opts.apiBase !== void 0 ? { apiBase: opts.apiBase } : {}
3397
+ });
3398
+ } catch (e) {
3399
+ if (e instanceof PushOperonError) {
3400
+ log.err(e.message);
3401
+ process.exit(e.exitCode);
3402
+ return;
3403
+ }
3404
+ throw e;
3405
+ }
3406
+ if (json.usedDraftPrompt) {
3407
+ log.warn(
3408
+ `${slug} hasn't been polished yet. Publishing the raw draft prompt instead.`
3409
+ );
3410
+ log.warn(
3411
+ `For better results: ANTHROPIC_API_KEY=... operon polish ${slug}`
3412
+ );
3413
+ }
3414
+ if (getGlobals().json) {
3415
+ log.json(json);
3416
+ } else {
3417
+ log.step(`published ${slug}@${json.semver} to ${json.url}`);
3418
+ log.info(` storage: ${json.storagePath}`);
3419
+ }
3420
+ process.exit(EXIT.ok);
3421
+ });
3422
+ }
3423
+
3424
+ // src/commands/prune.ts
3425
+ import { Command as Command10 } from "commander";
3426
+ import { z as z12 } from "zod";
3427
+ var optsSchema8 = z12.object({
3428
+ olderThan: z12.string().default("60d"),
3429
+ apply: z12.boolean().default(false),
3430
+ yes: z12.boolean().default(false)
3431
+ });
3432
+ function pruneCommand() {
3433
+ return new Command10("prune").description(
3434
+ "Archive stale candidates (lastSeenAt older than --older-than)."
3435
+ ).option(
3436
+ "--older-than <duration>",
3437
+ "Staleness threshold (e.g. 30d, 60d, 90d)",
3438
+ "60d"
3439
+ ).option("--apply", "Actually archive (default: dry-run)", false).option("--yes", "Skip the confirmation prompt", false).action(async (rawOpts) => {
3440
+ const opts = optsSchema8.parse(rawOpts);
3441
+ let cutoff;
3442
+ try {
3443
+ cutoff = new Date(Date.now() - parseDuration(opts.olderThan));
3444
+ } catch (e) {
3445
+ log.err(e.message);
3446
+ process.exit(EXIT.config);
3447
+ return;
3448
+ }
3449
+ const all = await listCandidates();
3450
+ const stale = all.filter((c) => new Date(c.lastSeenAt) < cutoff);
3451
+ if (getGlobals().json) {
3452
+ log.json({
3453
+ olderThan: opts.olderThan,
3454
+ cutoff: cutoff.toISOString(),
3455
+ totalCandidates: all.length,
3456
+ staleCount: stale.length,
3457
+ stale: stale.map((c) => ({
3458
+ slug: c.slug,
3459
+ lastSeenAt: c.lastSeenAt,
3460
+ uses: c.uses
3461
+ })),
3462
+ applied: opts.apply
3463
+ });
3464
+ if (opts.apply && stale.length > 0) {
3465
+ for (const c of stale) await trashCandidate(c.slug);
3466
+ }
3467
+ process.exit(stale.length > 0 ? EXIT.ok : EXIT.noWork);
3468
+ }
3469
+ if (stale.length === 0) {
3470
+ log.info(
3471
+ `nothing to prune (0 of ${all.length} candidates older than ${opts.olderThan})`
3472
+ );
3473
+ process.exit(EXIT.noWork);
3474
+ }
3475
+ log.step(
3476
+ `${stale.length} stale candidate${stale.length === 1 ? "" : "s"} (lastSeenAt < ${cutoff.toISOString().slice(0, 10)}):`
3477
+ );
3478
+ for (const c of stale) {
3479
+ const lastSeen = c.lastSeenAt.slice(0, 10);
3480
+ log.info(` ${c.slug} (${c.uses} uses, last seen ${lastSeen})`);
3481
+ }
3482
+ if (!opts.apply) {
3483
+ log.info("");
3484
+ log.info("Dry run. Re-run with --apply to archive.");
3485
+ process.exit(EXIT.ok);
3486
+ }
3487
+ if (!opts.yes) {
3488
+ log.err(
3489
+ "Refusing to archive without --yes. Re-run: operon prune --apply --yes"
3490
+ );
3491
+ process.exit(EXIT.config);
3492
+ return;
3493
+ }
3494
+ const undoIds = [];
3495
+ for (const c of stale) {
3496
+ const undoId = await trashCandidate(c.slug);
3497
+ undoIds.push(undoId);
3498
+ }
3499
+ log.step(
3500
+ `archived ${undoIds.length} candidate${undoIds.length === 1 ? "" : "s"} \u2192 ~/.operon/.trash/`
3501
+ );
3502
+ log.info("Restore via: operon undo --list");
3503
+ process.exit(EXIT.ok);
3504
+ });
3505
+ }
3506
+
3507
+ // src/commands/scan.ts
3508
+ import { Command as Command11, Option as Option3 } from "commander";
3509
+ import { z as z13 } from "zod";
3510
+
3511
+ // src/peer/discover.ts
3512
+ import { readdir as readdir6, readFile as readFile7 } from "fs/promises";
3513
+ import { basename as basename4, join as join10 } from "path";
3514
+ async function discoverPeerSkills(repoPaths) {
3515
+ const out = [];
3516
+ for (const repo of repoPaths) {
3517
+ const skillsDir = join10(repo, ".claude", "skills");
3518
+ let entries;
3519
+ try {
3520
+ entries = await readdir6(skillsDir, { withFileTypes: true });
3521
+ } catch {
3522
+ continue;
3523
+ }
3524
+ for (const entry of entries) {
3525
+ if (!entry.isDirectory()) continue;
3526
+ const path = join10(skillsDir, entry.name, "SKILL.md");
3527
+ let raw;
3528
+ try {
3529
+ raw = await readFile7(path, "utf8");
3530
+ } catch {
3531
+ continue;
3532
+ }
3533
+ const name = parseFrontmatterName(raw) ?? basename4(entry.name);
3534
+ out.push({ name, repo, path });
3535
+ }
3536
+ }
3537
+ return out;
3538
+ }
3539
+ var FM_NAME_RE = /^---[\r\n]+([\s\S]*?)[\r\n]+---/;
3540
+ function parseFrontmatterName(raw) {
3541
+ const fm = FM_NAME_RE.exec(raw);
3542
+ if (fm?.[1] === void 0) return void 0;
3543
+ for (const line of fm[1].split(/\r?\n/)) {
3544
+ const m = /^name:\s*(.+?)\s*$/.exec(line);
3545
+ if (m?.[1] !== void 0) return m[1];
3546
+ }
3547
+ return void 0;
3548
+ }
3549
+ function annotateWithPeers(candidates, peers) {
3550
+ const bySlug = /* @__PURE__ */ new Map();
3551
+ for (const p of peers) {
3552
+ const arr = bySlug.get(p.name) ?? [];
3553
+ arr.push(p.repo);
3554
+ bySlug.set(p.name, arr);
3555
+ }
3556
+ return candidates.map((c) => ({
3557
+ ...c,
3558
+ peerRepos: bySlug.get(c.slug) ?? []
3559
+ }));
3560
+ }
3561
+
3562
+ // src/commands/scan.ts
3563
+ var SOURCES2 = ["claude-code", "codex", "cursor", "chatgpt"];
3564
+ var optsSchema9 = z13.object({
3565
+ source: z13.array(z13.enum(SOURCES2)).default([...SOURCES2]),
3566
+ since: z13.string().optional(),
3567
+ top: z13.coerce.number().int().positive().default(10),
3568
+ redact: z13.boolean().default(true),
3569
+ anonStats: z13.boolean().default(false),
3570
+ peer: z13.array(z13.string()).default([])
3571
+ });
3572
+ function scanCommand() {
3573
+ return new Command11("scan").description(
3574
+ "Mine local Claude Code, Codex, and Cursor sessions and render candidate operons."
3575
+ ).addOption(
3576
+ new Option3("--source <src...>", "Limit to one or more sources").choices([
3577
+ ...SOURCES2
3578
+ ])
3579
+ ).option(
3580
+ "--since <duration>",
3581
+ "Only consider sessions newer than (e.g. 30d, 14d, 24h)"
3582
+ ).option("--top <N>", "Render top N candidates", "10").option("--no-redact", "Skip secrets redaction (warns loudly; dev-only)").option("--anon-stats", "Opt in to anonymous usage stats (one-time)", false).option(
3583
+ "--peer <repo...>",
3584
+ "Cross-reference candidates against peer repos' .claude/skills/"
3585
+ ).action(async (rawOpts) => {
3586
+ const opts = optsSchema9.parse(rawOpts);
3587
+ log.debug(`scan opts: ${JSON.stringify(opts)}`);
3588
+ let since;
3589
+ if (opts.since !== void 0) {
3590
+ try {
3591
+ since = new Date(Date.now() - parseDuration(opts.since));
3592
+ } catch (e) {
3593
+ log.err(e.message);
3594
+ process.exit(EXIT.config);
3595
+ }
3596
+ }
3597
+ if (!opts.redact) {
3598
+ log.warn(
3599
+ "redaction disabled \u2014 secrets in your sessions may surface in output"
3600
+ );
3601
+ }
3602
+ const sources = opts.source.filter(
3603
+ (s) => ADAPTERS[s] !== void 0
3604
+ );
3605
+ if (sources.length === 0) {
3606
+ log.err("no enabled sources detected (claude-code, codex, cursor)");
3607
+ process.exit(EXIT.noWork);
3608
+ }
3609
+ const summary = {};
3610
+ const allSessions = [];
3611
+ for (const sourceName of sources) {
3612
+ const adapter = ADAPTERS[sourceName];
3613
+ if (adapter === void 0) continue;
3614
+ const detect = await adapter.detect();
3615
+ if (!detect.present) {
3616
+ log.debug(`${sourceName}: not present (${detect.reason ?? ""})`);
3617
+ continue;
3618
+ }
3619
+ log.step(`scanning ${sourceName}...`);
3620
+ const stats = { sessions: 0, messages: 0 };
3621
+ const enumerateOpts = {};
3622
+ if (since !== void 0) enumerateOpts.since = since;
3623
+ for await (const raw of adapter.enumerate(enumerateOpts)) {
3624
+ let session = await adapter.normalize(raw);
3625
+ if (opts.redact) session = redactSession(session);
3626
+ stats.sessions += 1;
3627
+ stats.messages += session.messages.length;
3628
+ allSessions.push(session);
3629
+ }
3630
+ summary[sourceName] = stats;
3631
+ }
3632
+ if (allSessions.length === 0) {
3633
+ log.err(
3634
+ "no sessions found in --since window. Try --since=365d to widen."
3635
+ );
3636
+ process.exit(EXIT.noWork);
3637
+ }
3638
+ log.step(`mining ${allSessions.length} sessions (top ${opts.top})...`);
3639
+ const result = mineSessions(allSessions, {
3640
+ render: { top: opts.top }
3641
+ });
3642
+ const peers = opts.peer.length > 0 ? await discoverPeerSkills(opts.peer) : [];
3643
+ const candidatesWithPeers = annotateWithPeers(result.candidates, peers);
3644
+ const teamCount = candidatesWithPeers.filter(
3645
+ (c) => c.peerRepos.length > 0
3646
+ ).length;
3647
+ const sessionsById = new Map(allSessions.map((s) => [s.id, s]));
3648
+ for (const c of result.candidates) {
3649
+ const exemplars = c.exemplarSessionIds.map((id) => sessionsById.get(id)).filter((s) => s !== void 0);
3650
+ await writeCandidate(c, exemplars);
3651
+ }
3652
+ if (getGlobals().json) {
3653
+ log.json({
3654
+ summary,
3655
+ input: result.sessionCountInput,
3656
+ kept: result.sessionCountKept,
3657
+ clusters: result.clusterCount,
3658
+ candidates: candidatesWithPeers
3659
+ });
3660
+ } else {
3661
+ for (const [src, s] of Object.entries(summary)) {
3662
+ log.info(` ${src}: ${s.sessions} sessions, ${s.messages} messages`);
3663
+ }
3664
+ log.info(
3665
+ `kept ${result.sessionCountKept}/${result.sessionCountInput} sessions, ${result.clusterCount} clusters, ${result.candidates.length} candidates` + (teamCount > 0 ? `, ${teamCount} team-recurring` : "")
3666
+ );
3667
+ if (result.candidates.length > 0) {
3668
+ log.info("");
3669
+ log.info(renderCandidateTable(result.candidates));
3670
+ for (const c of candidatesWithPeers) {
3671
+ if (c.peerRepos.length > 0) {
3672
+ log.info(
3673
+ ` \u2605 ${c.slug} also lives at ${c.peerRepos.join(", ")} \u2014 team-recurring`
3674
+ );
3675
+ }
3676
+ }
3677
+ }
3678
+ }
3679
+ process.exit(result.candidates.length > 0 ? EXIT.ok : EXIT.noWork);
3680
+ });
3681
+ }
3682
+
3683
+ // src/commands/show.ts
3684
+ import { Command as Command12 } from "commander";
3685
+ import { z as z14 } from "zod";
3686
+
3687
+ // src/render/show-detail.ts
3688
+ import chalk4 from "chalk";
3689
+ function renderCandidateDetail(c) {
3690
+ const colored = !getGlobals().noColor;
3691
+ const bold2 = (s) => colored ? chalk4.bold(s) : s;
3692
+ const dim2 = (s) => colored ? chalk4.dim(s) : s;
3693
+ const breakdown = Object.entries(c.scoreBreakdown).map(([k, v]) => `${k}=${v.toFixed(3)}`).join(" ");
3694
+ return [
3695
+ `${bold2("slug")} ${c.slug}`,
3696
+ `${bold2("score")} ${c.score.toFixed(3)} ${dim2(`(${breakdown})`)}`,
3697
+ `${bold2("uses")} ${c.uses} across ${c.spanDays} day(s)`,
3698
+ `${bold2("tools")} ${c.suggestedTools.join(", ") || "(none)"}`,
3699
+ `${bold2("exemplars")} ${c.exemplarSessionIds.join(", ")}`,
3700
+ "",
3701
+ bold2("draft prompt"),
3702
+ indent(c.draftPrompt),
3703
+ "",
3704
+ bold2("summary"),
3705
+ indent(c.summary),
3706
+ "",
3707
+ bold2("draft eval"),
3708
+ indent(JSON.stringify(c.draftEval, null, 2))
3709
+ ].join("\n");
3710
+ }
3711
+ function indent(s) {
3712
+ return s.split("\n").map((line) => ` ${line}`).join("\n");
3713
+ }
3714
+
3715
+ // src/commands/show.ts
3716
+ var slugSchema5 = z14.string().min(1).max(64).regex(/^[a-z0-9-]+$/, "lowercase letters, digits, hyphens only");
3717
+ function showCommand() {
3718
+ return new Command12("show").argument("<slug>", "Candidate or operon slug").description("Pretty-print a candidate or operon.").option("--exemplars <N>", "Show N exemplar sessions", "3").option("--full", "Show full prompt and tool details", false).action(async (slugArg) => {
3719
+ const slug = slugSchema5.parse(slugArg);
3720
+ const candidate = await readCandidate(slug);
3721
+ if (candidate === void 0) {
3722
+ log.err(`no candidate found at ~/.operon/candidates/${slug}/`);
3723
+ process.exit(EXIT.noWork);
3724
+ }
3725
+ if (getGlobals().json) {
3726
+ log.json(candidate);
3727
+ process.exit(EXIT.ok);
3728
+ }
3729
+ log.info(renderCandidateDetail(candidate));
3730
+ process.exit(EXIT.ok);
3731
+ });
3732
+ }
3733
+
3734
+ // src/commands/suggest.ts
3735
+ import { Command as Command13, Option as Option4 } from "commander";
3736
+ import { z as z15 } from "zod";
3737
+
3738
+ // src/lib/suggestions.ts
3739
+ var SubmitSuggestionError = class extends Error {
3740
+ exitCode;
3741
+ constructor(message, exitCode) {
3742
+ super(message);
3743
+ this.name = "SubmitSuggestionError";
3744
+ this.exitCode = exitCode;
3745
+ }
3746
+ };
3747
+ async function submitMiningSuggestion(candidate, options = {}) {
3748
+ const config = await readConfig();
3749
+ const apiBase = (options.apiBase ?? config.platform.apiBase ?? "https://app.withoperon.com").replace(/\/+$/, "");
3750
+ const token = options.token ?? config.platform.accessToken;
3751
+ if (token === void 0 || token.length === 0) {
3752
+ throw new SubmitSuggestionError(
3753
+ "not signed in. Run: operon login",
3754
+ EXIT.auth
3755
+ );
3756
+ }
3757
+ const workspaceSlug = options.workspaceSlug ?? config.platform.workspaceSlug;
3758
+ if (workspaceSlug === void 0 || workspaceSlug.length === 0) {
3759
+ throw new SubmitSuggestionError(
3760
+ "missing workspace. Pass --workspace or run: operon login --workspace <slug>",
3761
+ EXIT.config
3762
+ );
3763
+ }
3764
+ const skill = await readSkillMarkdown(candidate);
3765
+ const fetchImpl = options.fetchImpl ?? fetch;
3766
+ let res;
3767
+ try {
3768
+ res = await fetchImpl(
3769
+ `${apiBase}/api/v1/workspaces/${workspaceSlug}/suggestions`,
3770
+ {
3771
+ method: "POST",
3772
+ headers: {
3773
+ "content-type": "application/json",
3774
+ "x-operon-source": "cli",
3775
+ authorization: `Bearer ${token}`
3776
+ },
3777
+ body: JSON.stringify({
3778
+ clusterId: candidate.promptShape,
3779
+ suggestedSlug: candidate.slug,
3780
+ suggestedName: titleCase(candidate.slug),
3781
+ skillMarkdown: skill.markdown,
3782
+ keywords: candidate.suggestedTools.slice(0, 8),
3783
+ clusterSize: candidate.uses,
3784
+ firstSeenAt: candidate.firstSeenAt,
3785
+ lastSeenAt: candidate.lastSeenAt,
3786
+ score: candidate.score
3787
+ })
3788
+ }
3789
+ );
3790
+ } catch (e) {
3791
+ throw new SubmitSuggestionError(
3792
+ `network error: ${e.message}`,
3793
+ EXIT.network
3794
+ );
3795
+ }
3796
+ if (!res.ok) {
3797
+ const body = await res.text().catch(() => "<unreadable>");
3798
+ throw new SubmitSuggestionError(
3799
+ `server returned ${res.status}: ${body.slice(0, 400)}`,
3800
+ res.status === 401 ? EXIT.auth : EXIT.err
3801
+ );
3802
+ }
3803
+ return await res.json();
3804
+ }
3805
+
3806
+ // src/commands/suggest.ts
3807
+ import { writeFile as writeFile6 } from "fs/promises";
3808
+ import { join as join11 } from "path";
3809
+ var SOURCES3 = ["claude-code", "codex", "cursor", "chatgpt"];
3810
+ var optsSchema10 = z15.object({
3811
+ source: z15.array(z15.enum(SOURCES3)).default([...SOURCES3]),
3812
+ since: z15.string().default("30d"),
3813
+ top: z15.coerce.number().int().positive().default(5),
3814
+ polish: z15.boolean().default(false),
3815
+ minUses: z15.coerce.number().int().positive().default(2),
3816
+ submit: z15.boolean().default(false),
3817
+ workspace: z15.string().min(1).max(64).optional(),
3818
+ apiBase: z15.string().url().optional()
3819
+ });
3820
+ function suggestCommand() {
3821
+ return new Command13("suggest").description(
3822
+ "Scan + rank + (optionally) polish \u2014 surface what's actually worth saving."
3823
+ ).addOption(
3824
+ new Option4("--source <src...>", "Limit to one or more sources").choices([
3825
+ ...SOURCES3
3826
+ ])
3827
+ ).option("--since <duration>", "Window to scan (e.g. 7d, 30d)", "30d").option("--top <N>", "Show the top N suggestions", "5").option(
3828
+ "--polish",
3829
+ "Also call Claude to draft a clean SKILL.md per suggestion (needs ANTHROPIC_API_KEY)",
3830
+ false
3831
+ ).option(
3832
+ "--min-uses <n>",
3833
+ "Only suggest patterns with at least this many sessions",
3834
+ "2"
3835
+ ).option(
3836
+ "--submit",
3837
+ "Submit mining telemetry to the Operon platform (requires operon login)",
3838
+ false
3839
+ ).option("--workspace <slug>", "Workspace slug for submitted telemetry").option("--api-base <url>", "Override platform base URL").action(async (rawOpts) => {
3840
+ const opts = optsSchema10.parse(rawOpts);
3841
+ let since;
3842
+ try {
3843
+ since = new Date(Date.now() - parseDuration(opts.since));
3844
+ } catch (e) {
3845
+ log.err(e.message);
3846
+ process.exit(EXIT.config);
3847
+ return;
3848
+ }
3849
+ let eventClient;
3850
+ if (opts.submit) {
3851
+ try {
3852
+ eventClient = await createPlatformEventClient({
3853
+ ...opts.apiBase !== void 0 ? { apiBase: opts.apiBase } : {},
3854
+ ...opts.workspace !== void 0 ? { workspaceSlug: opts.workspace } : {}
3855
+ });
3856
+ } catch (e) {
3857
+ if (e instanceof PlatformEventError) {
3858
+ log.err(e.message);
3859
+ process.exit(e.exitCode);
3860
+ return;
3861
+ }
3862
+ throw e;
3863
+ }
3864
+ }
3865
+ const sources = opts.source.filter(
3866
+ (s) => ADAPTERS[s] !== void 0
3867
+ );
3868
+ const startedAt = Date.now();
3869
+ await sendMiningEvent2(eventClient, "mining_scan_started", {
3870
+ since: opts.since,
3871
+ sources
3872
+ });
3873
+ const allSessions = [];
3874
+ for (const sourceName of sources) {
3875
+ const adapter = ADAPTERS[sourceName];
3876
+ if (adapter === void 0) continue;
3877
+ const detect = await adapter.detect();
3878
+ if (!detect.present) continue;
3879
+ for await (const raw of adapter.enumerate({ since })) {
3880
+ let session = await adapter.normalize(raw);
3881
+ session = redactSession(session);
3882
+ allSessions.push(session);
3883
+ }
3884
+ }
3885
+ if (allSessions.length === 0) {
3886
+ await sendMiningEvent2(eventClient, "mining_scan_completed", {
3887
+ sessionCount: 0,
3888
+ durationMs: Date.now() - startedAt
3889
+ });
3890
+ log.err(
3891
+ `no sessions in last ${opts.since}. Try --since=90d or --since=365d.`
3892
+ );
3893
+ process.exit(EXIT.noWork);
3894
+ return;
3895
+ }
3896
+ log.step(
3897
+ `mining ${allSessions.length} sessions from last ${opts.since}...`
3898
+ );
3899
+ const result = mineSessions(allSessions, { render: { top: opts.top } });
3900
+ const top = result.candidates.filter((c) => c.uses >= opts.minUses);
3901
+ await sendMiningEvent2(eventClient, "mining_scan_completed", {
3902
+ sessionCount: result.sessionCountKept,
3903
+ durationMs: Date.now() - startedAt
3904
+ });
3905
+ if (top.length === 0) {
3906
+ log.info(
3907
+ `no patterns with >= ${opts.minUses} uses found. Either widen --since or work some more :)`
3908
+ );
3909
+ process.exit(EXIT.noWork);
3910
+ return;
3911
+ }
3912
+ for (const c of top) {
3913
+ await sendMiningEvent2(eventClient, "pattern_detected", {
3914
+ clusterId: c.promptShape,
3915
+ clusterSize: c.uses,
3916
+ score: c.score,
3917
+ keywords: c.suggestedTools
3918
+ });
3919
+ }
3920
+ const sessionsById = new Map(allSessions.map((s) => [s.id, s]));
3921
+ for (const c of top) {
3922
+ const exemplars = c.exemplarSessionIds.map((id) => sessionsById.get(id)).filter((s) => s !== void 0);
3923
+ await writeCandidate(c, exemplars);
3924
+ }
3925
+ const polished = /* @__PURE__ */ new Set();
3926
+ if (opts.polish) {
3927
+ log.step(`polishing ${top.length} via Claude...`);
3928
+ for (const c of top) {
3929
+ const exemplars = c.exemplarSessionIds.map((id) => sessionsById.get(id)).filter((s) => s !== void 0);
3930
+ const r = await polishCandidate(c, exemplars);
3931
+ if ("error" in r) {
3932
+ if (r.error.kind === "no-api-key") {
3933
+ log.warn(
3934
+ "ANTHROPIC_API_KEY not set \u2014 skipping --polish for the rest."
3935
+ );
3936
+ break;
3937
+ }
3938
+ log.warn(` ${c.slug}: polish failed (${r.error.kind})`);
3939
+ continue;
3940
+ }
3941
+ await writeFile6(
3942
+ join11(candidateDir(c.slug), "polished.md"),
3943
+ `${r.markdown}
3944
+ `,
3945
+ "utf8"
3946
+ );
3947
+ polished.add(c.slug);
3948
+ }
3949
+ }
3950
+ if (opts.submit) {
3951
+ let submitted = 0;
3952
+ for (const c of top) {
3953
+ try {
3954
+ await submitMiningSuggestion(c, {
3955
+ ...opts.apiBase !== void 0 ? { apiBase: opts.apiBase } : {},
3956
+ ...opts.workspace !== void 0 ? { workspaceSlug: opts.workspace } : {}
3957
+ });
3958
+ submitted += 1;
3959
+ } catch (e) {
3960
+ if (e instanceof SubmitSuggestionError) {
3961
+ log.warn(` ${c.slug}: submit failed (${e.message})`);
3962
+ continue;
3963
+ }
3964
+ throw e;
3965
+ }
3966
+ }
3967
+ log.step(
3968
+ `${submitted} suggestion${submitted === 1 ? "" : "s"} submitted`
3969
+ );
3970
+ }
3971
+ if (getGlobals().json) {
3972
+ log.json({
3973
+ suggestions: top.map((c) => ({
3974
+ slug: c.slug,
3975
+ uses: c.uses,
3976
+ spanDays: c.spanDays,
3977
+ score: c.score,
3978
+ firstSeenAt: c.firstSeenAt,
3979
+ lastSeenAt: c.lastSeenAt,
3980
+ polished: polished.has(c.slug)
3981
+ }))
3982
+ });
3983
+ process.exit(EXIT.ok);
3984
+ }
3985
+ log.info("");
3986
+ log.info("You've done these recently:");
3987
+ log.info("");
3988
+ for (const [i, c] of top.entries()) {
3989
+ const days = c.spanDays === 0 ? "today" : `${c.spanDays}d span`;
3990
+ const ready = polished.has(c.slug) ? " \u2605 polished" : "";
3991
+ log.info(
3992
+ ` ${i + 1}. ${c.slug.padEnd(28)} (${c.uses} uses, ${days})${ready}`
3993
+ );
3994
+ }
3995
+ log.info("");
3996
+ log.info("Save the most useful ones as team skills:");
3997
+ const example = top[0];
3998
+ if (!polished.has(example.slug) && !opts.polish) {
3999
+ log.info(
4000
+ ` operon polish ${example.slug} # draft a clean SKILL.md`
4001
+ );
4002
+ }
4003
+ log.info(
4004
+ ` operon publish ${example.slug} --workspace <slug> # upload to your team`
4005
+ );
4006
+ log.info(
4007
+ ` operon install <workspace>/${example.slug} # install after publish`
4008
+ );
4009
+ process.exit(EXIT.ok);
4010
+ });
4011
+ }
4012
+ async function sendMiningEvent2(client, event, properties) {
4013
+ if (client === void 0) return;
4014
+ try {
4015
+ await client.send(event, properties);
4016
+ } catch (e) {
4017
+ if (e instanceof PlatformEventError) {
4018
+ log.warn(`telemetry skipped: ${e.message}`);
4019
+ return;
4020
+ }
4021
+ throw e;
4022
+ }
4023
+ }
4024
+
4025
+ // src/commands/undo.ts
4026
+ import { readdir as readdir7 } from "fs/promises";
4027
+ import { Command as Command14 } from "commander";
4028
+ import { z as z16 } from "zod";
4029
+ var optsSchema11 = z16.object({
4030
+ list: z16.boolean().default(false),
4031
+ id: z16.string().min(1).max(64).optional()
4032
+ });
4033
+ function undoCommand() {
4034
+ return new Command14("undo").description("Reverse the most recent destructive action.").argument("[slug]", "Operon slug whose promotion to undo").option("--list", "List recent undoable actions", false).option("--id <undo-id>", "Undo a specific action by id").action(async (slugArg, rawOpts) => {
4035
+ const opts = optsSchema11.parse(rawOpts);
4036
+ if (opts.list) {
4037
+ let entries;
4038
+ try {
4039
+ entries = await readdir7(TRASH_DIR);
4040
+ } catch {
4041
+ entries = [];
4042
+ }
4043
+ if (getGlobals().json) {
4044
+ log.json({ trashed: entries });
4045
+ } else if (entries.length === 0) {
4046
+ log.info("nothing in trash");
4047
+ } else {
4048
+ for (const e of entries) log.info(` ${e}`);
4049
+ }
4050
+ process.exit(EXIT.ok);
4051
+ }
4052
+ if (slugArg === void 0) {
4053
+ log.err("undo: pass a slug or --list to inspect the trash bin");
4054
+ process.exit(EXIT.config);
4055
+ }
4056
+ try {
4057
+ const undoId = await trashOperon(slugArg);
4058
+ log.step(
4059
+ `moved operons/${slugArg} \u2192 .trash/${undoId}/ \u2014 restore with mv`
4060
+ );
4061
+ process.exit(EXIT.ok);
4062
+ } catch (e) {
4063
+ log.err(e.message);
4064
+ process.exit(EXIT.err);
4065
+ }
4066
+ });
4067
+ }
4068
+
4069
+ // src/cli.ts
4070
+ var program = new Command15();
4071
+ program.name("operon").description("Team agent memory for AI-native engineering teams.").version(CLI_VERSION).option("--json", "Machine-readable JSON output", false).option("--no-color", "Disable ANSI color").option("-q, --quiet", "Suppress non-error output", false).option("-v, --verbose", "Show debug-level info", false).option("--config <path>", "Override default ~/.operon/config.toml").option("--cwd <path>", "Override the inferred project root").addOption(new Option5("--no-redact", "Skip secrets redaction").hideHelp()).hook("preAction", (thisCommand) => {
4072
+ const o = thisCommand.opts();
4073
+ setGlobals({
4074
+ json: o.json ?? false,
4075
+ noColor: o.color === false,
4076
+ quiet: o.quiet ?? false,
4077
+ verbose: o.verbose ?? false,
4078
+ configPath: o.config,
4079
+ cwd: o.cwd ?? process.cwd()
4080
+ });
4081
+ });
4082
+ program.addCommand(scanCommand());
4083
+ program.addCommand(suggestCommand());
4084
+ program.addCommand(mineCommand());
4085
+ program.addCommand(listCommand());
4086
+ program.addCommand(showCommand());
4087
+ program.addCommand(polishCommand());
4088
+ program.addCommand(pruneCommand());
4089
+ program.addCommand(loginCommand());
4090
+ program.addCommand(mcpCommand());
4091
+ program.addCommand(installCommand());
4092
+ program.addCommand(publishCommand());
4093
+ program.addCommand(doctorCommand());
4094
+ program.addCommand(undoCommand());
4095
+ program.addCommand(configCommand());
4096
+ program.parseAsync().catch((err) => {
4097
+ log.err(err instanceof Error ? err.message : String(err));
4098
+ process.exit(1);
4099
+ });
4100
+ //# sourceMappingURL=cli.js.map