codiedev 0.10.0-beta.14 → 0.10.0-beta.16

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 CHANGED
@@ -44,7 +44,6 @@ const ask_1 = require("./commands/ask");
44
44
  const ping_1 = require("./commands/ping");
45
45
  const inbox_1 = require("./commands/inbox");
46
46
  const note_1 = require("./commands/note");
47
- const createTicket_1 = require("./commands/createTicket");
48
47
  const doctor_1 = require("./commands/doctor");
49
48
  const search_1 = require("./commands/search");
50
49
  const library_1 = require("./commands/library");
@@ -90,14 +89,6 @@ Artifacts:
90
89
  team's collective memory. Returns an
91
90
  LLM-synthesized answer with citations
92
91
  pointing at sessions + artifacts.
93
- codiedev create-ticket [<prompt>] Forward-write a scoped ticket plan.
94
- Claude investigates the codebase, asks
95
- "single repo or multiple? where?", then
96
- emits a plan with N+1 split. Lands in
97
- /portal/research/<id>; push to Jira from there.
98
- codiedev create-ticket --submit <path|->
99
- Internal: Claude calls this itself with
100
- a plan JSON to submit. Use - for stdin.
101
92
 
102
93
  Messaging:
103
94
  codiedev ping <user> "<msg>" [--with <key>]
@@ -337,25 +328,6 @@ codiedev note — capture a passing thought, linked to your session
337
328
  unignore: IGNORE_HELP,
338
329
  ignored: IGNORE_HELP,
339
330
  capture: IGNORE_HELP,
340
- "create-ticket": `
341
- codiedev create-ticket — forward-write a scoped ticket plan
342
-
343
- Default path:
344
- codiedev create-ticket "I want to add SSO with Google"
345
-
346
- Bare mode (no --submit) prints skill instructions for Claude to follow.
347
- Claude asks where your repos live, investigates them locally, and emits a
348
- plan as JSON. The CLI then re-invokes itself with --submit to upload it.
349
-
350
- The plan lands at /portal/research/<id> as a ready-state research session.
351
- Single ticket renders as one ticket; multi-ticket renders as a folder/group.
352
- Push to Jira from the portal.
353
-
354
- Examples:
355
- codiedev create-ticket
356
- codiedev create-ticket "I want to add Google SSO"
357
- codiedev create-ticket --submit /tmp/plan.json # internal, called by Claude
358
- `.trim(),
359
331
  };
360
332
  const DOCS = `
361
333
  Codiedev — usage guide
@@ -466,8 +438,10 @@ Run 'codiedev help <command>' for each, or see the quick list:
466
438
  `.trim();
467
439
  const REMOVED_COMMANDS = {
468
440
  promote: "Share an auto-saved spec with 'Promote to team' in the portal's My artifacts list.",
469
- "reverse-ticket": "Use 'codiedev create-ticket' to draft Jira tickets.",
470
- reverseTicket: "Use 'codiedev create-ticket' to draft Jira tickets.",
441
+ "reverse-ticket": "Ask your agent to file tickets with your team's own Jira or Linear tooling.",
442
+ reverseTicket: "Ask your agent to file tickets with your team's own Jira or Linear tooling.",
443
+ "create-ticket": "Ask your agent to file tickets with your team's own Jira or Linear tooling.",
444
+ createTicket: "Ask your agent to file tickets with your team's own Jira or Linear tooling.",
471
445
  react: "React to feed posts in the portal.",
472
446
  };
473
447
  function removedCommand(command) {
@@ -591,11 +565,9 @@ async function main() {
591
565
  case "reverse-ticket":
592
566
  case "reverseTicket":
593
567
  case "react":
594
- removedCommand(command);
595
- return;
596
568
  case "create-ticket":
597
569
  case "createTicket":
598
- await (0, createTicket_1.runCreateTicket)(rest);
570
+ removedCommand(command);
599
571
  return;
600
572
  case "search":
601
573
  await (0, search_1.runSearch)(rest);
@@ -21,6 +21,14 @@ themselves never leave this machine.
21
21
  --path <file> Only this file (repeatable)
22
22
  --session <id> With --path <opencode.db>: only this OpenCode/Codie session
23
23
  --min-age <min> Skip files changed in the last <min> minutes (default 60)
24
+
25
+ The hourly background run can also remove your own secrets (values from
26
+ your credential files and .env files) if your admin allows it. It reads
27
+ ~/.aws/credentials, ~/.npmrc, ~/.pypirc, ~/.netrc, ~/.git-credentials,
28
+ ~/.config/gh/hosts.yml, ~/.docker/config.json, ~/.config/hub,
29
+ ~/.gem/credentials, ~/.config/openai/credentials, and .env / .env.* in the
30
+ repos your sessions start in. Values stay on this machine; only if your admin
31
+ chose to check uploaded sessions too is a keyed fingerprint of each sent.
24
32
  `;
25
33
  function parseScrubArgs(args) {
26
34
  let dryRun = false;
package/dist/hook.js CHANGED
@@ -445,9 +445,8 @@ async function main() {
445
445
  }
446
446
  if (process.argv[2] === "local-scrub") {
447
447
  try {
448
- const { runAndReport, scrubAllowed } = await Promise.resolve().then(() => __importStar(require("./localScrub")));
449
- if (await scrubAllowed())
450
- await runAndReport();
448
+ const { runScheduledScrub } = await Promise.resolve().then(() => __importStar(require("./localScrub")));
449
+ await runScheduledScrub();
451
450
  }
452
451
  catch {
453
452
  }
@@ -501,7 +500,7 @@ async function main() {
501
500
  if (source === "codex")
502
501
  (0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
503
502
  if (source === "codex")
504
- (0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()) });
503
+ (0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()), cwd: hookInput.cwd });
505
504
  if (source !== "copilot")
506
505
  await runDiscover(hookInput.prompt);
507
506
  return;
@@ -543,7 +542,7 @@ async function main() {
543
542
  process.argv[2] === "session-start-opencode" ||
544
543
  process.argv[2] === "session-start-pi") {
545
544
  (0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
546
- (0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()) });
545
+ (0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()), cwd: hookInput.cwd });
547
546
  const packNag = process.argv[2] === "session-start-opencode" || process.argv[2] === "session-start-pi" ? null : (0, sync_1.packNagLine)();
548
547
  if (packNag && process.argv[2] === "session-start")
549
548
  process.stdout.write(`${packNag}\n`);
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.hintOf = exports.CREDENTIAL_FILES = void 0;
37
+ exports.worthMatching = worthMatching;
38
+ exports.parseAssignments = parseAssignments;
39
+ exports.credentialFiles = credentialFiles;
40
+ exports.collectKnownSecrets = collectKnownSecrets;
41
+ exports.knownSecretRule = knownSecretRule;
42
+ exports.knownHash = knownHash;
43
+ exports.fingerprints = fingerprints;
44
+ exports.uploadKnownSecrets = uploadKnownSecrets;
45
+ exports.rememberCwd = rememberCwd;
46
+ exports.recentCwds = recentCwds;
47
+ const crypto = __importStar(require("crypto"));
48
+ const fs = __importStar(require("fs"));
49
+ const os = __importStar(require("os"));
50
+ const path = __importStar(require("path"));
51
+ const rules_1 = require("./scrub/rules");
52
+ const MAX_SECRETS = 500;
53
+ const MAX_FILE_BYTES = 256 * 1024;
54
+ const MAX_CWDS = 30;
55
+ function worthMatching(v) {
56
+ if (v.length < 16 || v.length > 512)
57
+ return false;
58
+ if (/\s/.test(v) || /^[a-z][a-z0-9+.-]*:\/\//i.test(v))
59
+ return false;
60
+ if (/^(?:true|false|null|undefined|localhost)$/i.test(v) || /^[\d.]+$/.test(v))
61
+ return false;
62
+ if (/^[~./]/.test(v) || /\$\{|\$\(/.test(v))
63
+ return false;
64
+ if ((0, rules_1.looksLikeTestValue)(v))
65
+ return false;
66
+ return (0, rules_1.shannonEntropy)(v) >= 3.5;
67
+ }
68
+ const unquote = (v) => v.trim().replace(/^(['"])(.*)\1$/, "$2");
69
+ const short = (p) => p.split(path.sep).filter(Boolean).slice(-2).join("/");
70
+ function parseAssignments(text) {
71
+ const out = [];
72
+ for (const line of text.split(/\r?\n/)) {
73
+ const m = line.match(/^\s*(?:export\s+)?([A-Za-z_./:@-][\w./:@-]*)\s*[=:]\s*(.+?)\s*$/);
74
+ if (!m || line.trim().startsWith("#") || line.trim().startsWith(";"))
75
+ continue;
76
+ out.push({ name: m[1].replace(/^\/\/[^:]*:/, ""), value: unquote(m[2].replace(/\s+#.*$/, "")) });
77
+ }
78
+ return out;
79
+ }
80
+ function read(file) {
81
+ try {
82
+ const st = fs.statSync(file);
83
+ if (!st.isFile() || st.size > MAX_FILE_BYTES)
84
+ return null;
85
+ return fs.readFileSync(file, "utf8");
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ }
91
+ exports.CREDENTIAL_FILES = [
92
+ ".aws/credentials", ".npmrc", ".pypirc", ".netrc", ".git-credentials", ".config/gh/hosts.yml",
93
+ ".docker/config.json", ".config/hub", ".gem/credentials", ".config/openai/credentials",
94
+ ];
95
+ function credentialFiles(home, cwds) {
96
+ const global = exports.CREDENTIAL_FILES.map((p) => path.join(home, p));
97
+ const envs = [];
98
+ for (const cwd of cwds) {
99
+ let entries = [];
100
+ try {
101
+ entries = fs.readdirSync(cwd);
102
+ }
103
+ catch {
104
+ continue;
105
+ }
106
+ for (const e of entries) {
107
+ if (/^\.env(?:\.[\w-]+)?$/.test(e) && !/\.(?:example|sample|template|dist|defaults?)$/i.test(e))
108
+ envs.push(path.join(cwd, e));
109
+ }
110
+ }
111
+ return [...global, ...envs];
112
+ }
113
+ function collectKnownSecrets(home = os.homedir(), cwds = []) {
114
+ const seen = new Map();
115
+ const add = (value, file, name) => {
116
+ if (seen.size >= MAX_SECRETS || !worthMatching(value) || seen.has(value))
117
+ return;
118
+ seen.set(value, { value, file: short(file), ...(name ? { name: name.slice(0, 64) } : {}) });
119
+ };
120
+ for (const file of credentialFiles(home, cwds)) {
121
+ const text = read(file);
122
+ if (!text)
123
+ continue;
124
+ const base = path.basename(file);
125
+ if (base === "config.json") {
126
+ try {
127
+ const auths = JSON.parse(text).auths ?? {};
128
+ for (const [host, a] of Object.entries(auths)) {
129
+ if (a.auth)
130
+ add(a.auth, file, `docker ${host}`);
131
+ if (a.identitytoken)
132
+ add(a.identitytoken, file, `docker ${host}`);
133
+ }
134
+ }
135
+ catch { }
136
+ continue;
137
+ }
138
+ if (base === ".git-credentials") {
139
+ for (const m of text.matchAll(/^https?:\/\/[^:\s]+:([^@\s]+)@(\S+)$/gm))
140
+ add(decodeURIComponent(m[1]), file, `git ${m[2]}`);
141
+ continue;
142
+ }
143
+ if (base === ".netrc") {
144
+ for (const m of text.matchAll(/\bpassword\s+(\S+)/g))
145
+ add(m[1], file, "netrc password");
146
+ continue;
147
+ }
148
+ for (const { name, value } of parseAssignments(text))
149
+ add(value, file, name);
150
+ }
151
+ return [...seen.values()];
152
+ }
153
+ function knownSecretRule(secrets) {
154
+ if (secrets.length === 0)
155
+ return null;
156
+ const forms = new Set();
157
+ for (const s of secrets) {
158
+ forms.add(s.value);
159
+ const escaped = JSON.stringify(s.value).slice(1, -1);
160
+ if (escaped !== s.value)
161
+ forms.add(escaped);
162
+ }
163
+ const alt = [...forms].sort((a, b) => b.length - a.length).map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
164
+ return { name: "known-secret", re: new RegExp(`(?<![A-Za-z0-9])(?:${alt})(?![A-Za-z0-9])`, "g") };
165
+ }
166
+ function knownHash(keyHex, value) {
167
+ return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(value).digest("hex").slice(0, 32);
168
+ }
169
+ const hintOf = (value) => value.replace(/[^A-Za-z0-9]/g, "").slice(-4);
170
+ exports.hintOf = hintOf;
171
+ function fingerprints(keyHex, secrets) {
172
+ return secrets.map((s) => ({ hash: knownHash(keyHex, s.value), hint: (0, exports.hintOf)(s.value), ...(s.name ? { name: s.name } : {}), file: s.file }));
173
+ }
174
+ async function uploadKnownSecrets(config, keyHex, secrets, fetchImpl = fetch) {
175
+ if (!config?.backendUrl || !config.token || !/^[0-9a-f]{64}$/.test(keyHex))
176
+ return false;
177
+ try {
178
+ const res = await fetchImpl(`${config.backendUrl}/api/cli/knownSecrets`, {
179
+ method: "POST",
180
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.token}` },
181
+ body: JSON.stringify({ secrets: fingerprints(keyHex, secrets) }),
182
+ signal: AbortSignal.timeout(15_000),
183
+ });
184
+ return res.ok;
185
+ }
186
+ catch {
187
+ return false;
188
+ }
189
+ }
190
+ function rememberCwd(stateDir, cwd) {
191
+ if (!cwd || !path.isAbsolute(cwd))
192
+ return;
193
+ try {
194
+ const file = path.join(stateDir, "cwds.json");
195
+ let list = [];
196
+ try {
197
+ list = JSON.parse(fs.readFileSync(file, "utf8"));
198
+ }
199
+ catch { }
200
+ list = [cwd, ...list.filter((c) => c !== cwd)].slice(0, MAX_CWDS);
201
+ fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
202
+ fs.writeFileSync(file, JSON.stringify(list), { mode: 0o600 });
203
+ }
204
+ catch { }
205
+ }
206
+ function recentCwds(stateDir) {
207
+ try {
208
+ const list = JSON.parse(fs.readFileSync(path.join(stateDir, "cwds.json"), "utf8"));
209
+ return Array.isArray(list) ? list.filter((c) => typeof c === "string") : [];
210
+ }
211
+ catch {
212
+ return [];
213
+ }
214
+ }
@@ -47,13 +47,17 @@ exports.cleanOpenCodeDb = cleanOpenCodeDb;
47
47
  exports.countCursorDb = countCursorDb;
48
48
  exports.runLocalScrub = runLocalScrub;
49
49
  exports.runAndReport = runAndReport;
50
+ exports.scrubPolicy = scrubPolicy;
50
51
  exports.scrubAllowed = scrubAllowed;
52
+ exports.formatRules = formatRules;
53
+ exports.runScheduledScrub = runScheduledScrub;
51
54
  exports.maybeSpawnLocalScrub = maybeSpawnLocalScrub;
52
55
  const fs = __importStar(require("fs"));
53
56
  const os = __importStar(require("os"));
54
57
  const path = __importStar(require("path"));
55
58
  const child_process_1 = require("child_process");
56
- const rules_1 = require("./scrub/rules");
59
+ const extraRules_1 = require("./scrub/extraRules");
60
+ const knownSecrets_1 = require("./knownSecrets");
57
61
  const harnessDirs_1 = require("./harnessDirs");
58
62
  const copilotHooks_1 = require("./copilotHooks");
59
63
  const configDir_1 = require("./configDir");
@@ -93,6 +97,12 @@ function sessionStores(env = process.env, home = os.homedir(), stateDir = (0, co
93
97
  root: path.join((0, copilotHooks_1.copilotHome)(env, home), "session-state"),
94
98
  match: (f) => path.basename(f) === "workspace.yaml",
95
99
  },
100
+ {
101
+ harness: "copilot",
102
+ kind: "json",
103
+ root: path.join((0, copilotHooks_1.copilotHome)(env, home), "session-state"),
104
+ match: (f) => path.basename(f) === "index.json" && path.basename(path.dirname(f)) === "rewind-file-snapshots",
105
+ },
96
106
  { harness: "pi", kind: "jsonl", root: path.join((0, harnessDirs_1.piAgentDir)(env, home), "sessions"), match: isJsonl },
97
107
  { harness: "pi", kind: "jsonl", root: path.join(stateDir, "managed", "pi", "agent", "sessions"), match: isJsonl },
98
108
  { harness: "opencode", kind: "json", root: path.join(stateDir, "opencode-captures"), match: (f) => f.endsWith(".json") },
@@ -117,7 +127,7 @@ function cursorIdeDb(env = process.env, home = os.homedir(), platform = process.
117
127
  }
118
128
  function cleanValue(value, count) {
119
129
  if (typeof value === "string") {
120
- const r = (0, rules_1.scrubSecrets)(value);
130
+ const r = (0, extraRules_1.scrub)(value);
121
131
  count.n += r.redactions.length;
122
132
  return r.text;
123
133
  }
@@ -141,7 +151,7 @@ function parses(text) {
141
151
  }
142
152
  }
143
153
  function cleanJson(text) {
144
- const raw = (0, rules_1.scrubSecrets)(text);
154
+ const raw = (0, extraRules_1.scrub)(text);
145
155
  if (raw.redactions.length === 0)
146
156
  return { text, redactions: 0 };
147
157
  if (parses(raw.text) || !parses(text))
@@ -168,13 +178,13 @@ function cleanJsonl(text) {
168
178
  return redactions === 0 ? { text, redactions: 0 } : { text: lines.join("\n"), redactions };
169
179
  }
170
180
  function cleanText(text) {
171
- const r = (0, rules_1.scrubSecrets)(text);
181
+ const r = (0, extraRules_1.scrub)(text);
172
182
  return { text: r.text, redactions: r.redactions.length };
173
183
  }
174
184
  function cleanYaml(text) {
175
185
  let redactions = 0;
176
186
  const lines = text.split("\n").map((line) => {
177
- const r = (0, rules_1.scrubSecrets)(line);
187
+ const r = (0, extraRules_1.scrub)(line);
178
188
  if (r.redactions.length === 0)
179
189
  return line;
180
190
  redactions += r.redactions.length;
@@ -333,7 +343,7 @@ function cleanOpenCodeDb(file, deps) {
333
343
  for (const [id, d] of sessions) {
334
344
  const row = get.all(id)[0];
335
345
  if (typeof row?.title === "string")
336
- d.title = (0, rules_1.scrubSecrets)(row.title).text.slice(0, 80);
346
+ d.title = (0, extraRules_1.scrub)(row.title).text.slice(0, 80);
337
347
  if (typeof row?.directory === "string")
338
348
  d.cwd = row.directory;
339
349
  }
@@ -416,7 +426,7 @@ function countCursorDb(file, kind) {
416
426
  const details = [...chats].map(([chat, keys]) => ({
417
427
  harness: "cursor",
418
428
  sessionKey: chat,
419
- ...(titles.get(chat) ? { title: (0, rules_1.scrubSecrets)(titles.get(chat)).text.slice(0, 80) } : {}),
429
+ ...(titles.get(chat) ? { title: (0, extraRules_1.scrub)(titles.get(chat)).text.slice(0, 80) } : {}),
420
430
  keys,
421
431
  status: "not_removed",
422
432
  reason: "cursor_chat_store",
@@ -657,26 +667,65 @@ async function runAndReport(opts = {}) {
657
667
  }
658
668
  return { ...summary, reported };
659
669
  }
660
- async function scrubAllowed(fetchImpl = fetch, getConfig = () => {
670
+ const defaultConfig = () => {
661
671
  const { readConfig } = require("./utils");
662
672
  return readConfig();
663
- }) {
673
+ };
674
+ const OFF = { enabled: false, formats: [], ownSecrets: "off" };
675
+ async function scrubPolicy(fetchImpl = fetch, getConfig = defaultConfig) {
664
676
  try {
665
677
  const config = getConfig();
666
678
  if (!config?.token || !config.backendUrl)
667
- return false;
679
+ return OFF;
668
680
  const res = await fetchImpl(`${config.backendUrl}/api/cli/localScrubPolicy`, {
669
681
  headers: { Authorization: `Bearer ${config.token}` },
670
682
  signal: AbortSignal.timeout(10_000),
671
683
  });
672
684
  if (!res.ok)
673
- return false;
685
+ return OFF;
674
686
  const body = (await res.json());
675
- return body.enabled === true;
687
+ const formats = Array.isArray(body.formats)
688
+ ? body.formats.filter((f) => f && typeof f.name === "string" && typeof f.source === "string" && f.source.length <= 300).slice(0, 50)
689
+ : [];
690
+ const ownSecrets = body.ownSecrets === "off" || body.ownSecrets === "server" ? body.ownSecrets : "laptop";
691
+ const key = typeof body.fingerprintKey === "string" && /^[0-9a-f]{64}$/.test(body.fingerprintKey) ? body.fingerprintKey : undefined;
692
+ return { enabled: body.enabled === true, formats, ownSecrets, ...(key ? { fingerprintKey: key } : {}) };
676
693
  }
677
694
  catch {
695
+ return OFF;
696
+ }
697
+ }
698
+ async function scrubAllowed(fetchImpl = fetch, getConfig = defaultConfig) {
699
+ return (await scrubPolicy(fetchImpl, getConfig)).enabled;
700
+ }
701
+ function formatRules(formats) {
702
+ const out = [];
703
+ for (const f of formats) {
704
+ try {
705
+ out.push({ name: `custom:${f.name}`, re: new RegExp(`(?<![A-Za-z0-9])${f.source}(?![A-Za-z0-9_-])`, "g"), ...(f.keyword ? { keywords: [f.keyword] } : {}) });
706
+ }
707
+ catch { }
708
+ }
709
+ return out;
710
+ }
711
+ async function runScheduledScrub(opts = {}) {
712
+ const getConfig = opts.getConfig ?? defaultConfig;
713
+ const policy = await scrubPolicy(opts.fetchImpl ?? fetch, getConfig);
714
+ if (!policy.enabled)
678
715
  return false;
716
+ const stateDir = opts.stateDir ?? (0, configDir_1.codiedevStateDir)();
717
+ const known = policy.ownSecrets === "off" ? [] : (0, knownSecrets_1.collectKnownSecrets)(opts.home ?? os.homedir(), (0, knownSecrets_1.recentCwds)(scrubDir(stateDir)));
718
+ if (policy.ownSecrets === "server" && policy.fingerprintKey)
719
+ await (0, knownSecrets_1.uploadKnownSecrets)(getConfig(), policy.fingerprintKey, known, opts.fetchImpl ?? fetch);
720
+ const knownRule = (0, knownSecrets_1.knownSecretRule)(known);
721
+ (0, extraRules_1.setExtraRules)([...formatRules(policy.formats), ...(knownRule ? [knownRule] : [])]);
722
+ try {
723
+ await runAndReport({ stateDir });
724
+ }
725
+ finally {
726
+ (0, extraRules_1.setExtraRules)([]);
679
727
  }
728
+ return true;
680
729
  }
681
730
  function maybeSpawnLocalScrub(opts = {}) {
682
731
  try {
@@ -685,6 +734,7 @@ function maybeSpawnLocalScrub(opts = {}) {
685
734
  return "disabled";
686
735
  const now = opts.now ?? Date.now();
687
736
  const dir = scrubDir(opts.stateDir ?? (0, configDir_1.codiedevStateDir)());
737
+ (0, knownSecrets_1.rememberCwd)(dir, opts.cwd);
688
738
  const stamp = path.join(dir, "last-run");
689
739
  try {
690
740
  const last = fs.statSync(stamp).mtimeMs;
@@ -46,11 +46,12 @@ const fs = __importStar(require("fs"));
46
46
  const os = __importStar(require("os"));
47
47
  const path = __importStar(require("path"));
48
48
  const rules_1 = require("./scrub/rules");
49
+ const extraRules_1 = require("./scrub/extraRules");
49
50
  const captureExclusions_1 = require("./captureExclusions");
50
51
  const MAX_KEYS_PER_SESSION = 20;
51
52
  const TITLE_LEN = 80;
52
53
  function keysIn(text) {
53
- const { redactions } = (0, rules_1.scrubSecrets)(text);
54
+ const { redactions } = (0, extraRules_1.scrub)(text);
54
55
  if (redactions.length === 0)
55
56
  return [];
56
57
  const hints = new Map();
@@ -181,8 +182,10 @@ function sessionKeyOf(harness, file) {
181
182
  const base = path.basename(file);
182
183
  if (base === "history.jsonl")
183
184
  return "history";
184
- if (harness === "copilot")
185
- return path.basename(path.dirname(file));
185
+ if (harness === "copilot") {
186
+ const dir = path.dirname(file);
187
+ return path.basename(path.basename(dir) === "rewind-file-snapshots" ? path.dirname(dir) : dir);
188
+ }
186
189
  const uuid = base.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
187
190
  if (uuid)
188
191
  return uuid[1];
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setExtraRules = setExtraRules;
4
+ exports.scrub = scrub;
5
+ const rules_1 = require("./rules");
6
+ let extra = [];
7
+ function setExtraRules(rules) {
8
+ extra = rules;
9
+ }
10
+ function scrub(text) {
11
+ return extra.length ? (0, rules_1.scrubSecrets)(text, { extraRules: extra }) : (0, rules_1.scrubSecrets)(text);
12
+ }
@@ -1,9 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GENERIC_RULES = void 0;
4
+ exports.shannonEntropy = shannonEntropy;
3
5
  exports.scrubSecrets = scrubSecrets;
6
+ exports.redactSpans = redactSpans;
7
+ exports.looksLikeTestValue = looksLikeTestValue;
4
8
  exports.secretFindings = secretFindings;
5
9
  exports.possibleSecrets = possibleSecrets;
6
10
  exports.redactPossible = redactPossible;
11
+ const scrubProviderRules_1 = require("./scrubProviderRules");
12
+ function shannonEntropy(s) {
13
+ if (!s)
14
+ return 0;
15
+ const counts = new Map();
16
+ for (const ch of s)
17
+ counts.set(ch, (counts.get(ch) ?? 0) + 1);
18
+ let h = 0;
19
+ for (const n of counts.values()) {
20
+ const p = n / s.length;
21
+ h -= p * Math.log2(p);
22
+ }
23
+ return h;
24
+ }
7
25
  const SECRET_REF_KEY = /_(?:NAME|ID|ARN|FILE|PATH|URL|URI|HOST|REGION|BUCKET|PREFIX|ENABLED|TYPE|VERSION|COUNT)$/i;
8
26
  function looksSecret(v) {
9
27
  if (v.includes("<") || v.includes(">"))
@@ -34,6 +52,7 @@ function looksSecret(v) {
34
52
  }
35
53
  const PLACEHOLDER = /x{6,}|\.\.\.|your|example|placeholder|changeme|redacted|dummy|fake|<|\$\{|\*{4,}/i;
36
54
  const notPlaceholder = (v) => !PLACEHOLDER.test(v);
55
+ const notTestValue = (v) => notPlaceholder(v) && !looksLikeTestValue(v);
37
56
  const URL_PASSWORD_PLACEHOLDER = /^(?:pass(?:word)?|secret|pwd|test|admin|root|guest)$/i;
38
57
  const RULES = [
39
58
  {
@@ -82,19 +101,61 @@ const RULES = [
82
101
  m.input[m.index - 1] !== ":" &&
83
102
  looksSecret(v),
84
103
  },
104
+ { name: "supabase-token", re: /\bsbp_[a-f0-9]{40}\b/g, validate: notTestValue },
105
+ { name: "groq-key", re: /\bgsk_[A-Za-z0-9]{48,56}\b/g, validate: notTestValue },
106
+ { name: "xai-key", re: /\bxai-[A-Za-z0-9]{70,90}\b/g, validate: notTestValue },
107
+ { name: "replicate-token", re: /\br8_[A-Za-z0-9]{37}\b/g, validate: notTestValue },
108
+ ...scrubProviderRules_1.PROVIDER_RULES.map((r) => ({
109
+ name: r.name,
110
+ re: r.re,
111
+ ...(r.group !== undefined ? { group: r.group } : {}),
112
+ ...(r.firstGroup ? { firstGroup: true } : {}),
113
+ keywords: r.keywords,
114
+ fallback: true,
115
+ validate: (v) => notTestValue(v) && (r.entropy === undefined || shannonEntropy(v) >= r.entropy),
116
+ })),
85
117
  ];
86
- function scrubSecrets(input) {
118
+ const ALL_KEYWORDS = [...new Set(RULES.flatMap((r) => r.keywords ?? []))].sort((a, b) => b.length - a.length);
119
+ const KEYWORD_RE = new RegExp(ALL_KEYWORDS.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "gi");
120
+ const KEYWORD_PARTS = new Map(ALL_KEYWORDS.map((k) => [k, ALL_KEYWORDS.filter((p) => k.includes(p))]));
121
+ function keywordsIn(input) {
122
+ const found = new Set();
123
+ if (!ALL_KEYWORDS.length)
124
+ return found;
125
+ KEYWORD_RE.lastIndex = 0;
126
+ let m;
127
+ while ((m = KEYWORD_RE.exec(input)) !== null) {
128
+ for (const k of KEYWORD_PARTS.get(m[0].toLowerCase()) ?? [])
129
+ found.add(k);
130
+ if (found.size === ALL_KEYWORDS.length)
131
+ break;
132
+ }
133
+ return found;
134
+ }
135
+ function scrubSecrets(input, opts = {}) {
87
136
  if (!input)
88
137
  return { text: input, redactions: [] };
89
138
  const candidates = [];
90
- for (const rule of RULES) {
139
+ const fallbacks = [];
140
+ const generic = [];
141
+ const extra = (opts.extraRules ?? []).map((r) => ({ name: r.name, re: new RegExp(r.re.source, r.re.flags.includes("g") ? r.re.flags : r.re.flags + "g"), validate: notTestValue }));
142
+ const present = keywordsIn(input);
143
+ for (const rule of extra.length ? [...RULES, ...extra] : RULES) {
144
+ if (rule.keywords?.length && !rule.keywords.some((k) => present.has(k)))
145
+ continue;
91
146
  rule.re.lastIndex = 0;
92
147
  let m;
93
148
  while ((m = rule.re.exec(input)) !== null) {
94
149
  let start = m.index;
95
150
  let end = m.index + m[0].length;
96
- if (rule.group !== undefined) {
97
- const gv = m[rule.group];
151
+ const groupIndex = rule.firstGroup ? m.findIndex((g, i) => i > 0 && g) : rule.group;
152
+ if (rule.firstGroup && groupIndex === -1) {
153
+ if (m[0].length === 0)
154
+ rule.re.lastIndex++;
155
+ continue;
156
+ }
157
+ if (groupIndex !== undefined && groupIndex > 0) {
158
+ const gv = m[groupIndex];
98
159
  if (gv === undefined || gv === "")
99
160
  continue;
100
161
  if (rule.validate && !rule.validate(gv, m))
@@ -111,30 +172,90 @@ function scrubSecrets(input) {
111
172
  continue;
112
173
  }
113
174
  if (end > start)
114
- candidates.push({ rule: rule.name, start, end });
175
+ (rule.fallback ? fallbacks : exports.GENERIC_RULES.has(rule.name) ? generic : candidates).push({ rule: rule.name, start, end });
115
176
  if (m[0].length === 0)
116
177
  rule.re.lastIndex++;
117
178
  }
118
179
  }
119
- if (candidates.length === 0)
180
+ if (candidates.length === 0 && fallbacks.length === 0 && generic.length === 0)
120
181
  return { text: input, redactions: [] };
121
- candidates.sort((a, b) => a.start - b.start || b.end - a.end);
122
- const chosen = [];
123
- let cursor = -1;
124
- for (const c of candidates) {
125
- if (c.start >= cursor) {
126
- chosen.push(c);
182
+ const pick = (list, taken) => {
183
+ list.sort((a, b) => a.start - b.start || b.end - a.end);
184
+ const out = [];
185
+ let cursor = -1;
186
+ let t = 0;
187
+ for (const c of list) {
188
+ if (c.start < cursor)
189
+ continue;
190
+ while (t < taken.length && taken[t].end <= c.start)
191
+ t++;
192
+ if (t < taken.length && taken[t].start < c.end)
193
+ continue;
194
+ out.push(c);
127
195
  cursor = c.end;
128
196
  }
197
+ return out;
198
+ };
199
+ let chosen = pick(candidates, []);
200
+ for (const tier of [fallbacks, generic]) {
201
+ if (tier.length)
202
+ chosen = [...chosen, ...pick(tier, chosen)].sort((a, b) => a.start - b.start);
129
203
  }
204
+ return { text: redactSpans(input, chosen), redactions: chosen };
205
+ }
206
+ function redactSpans(input, spans) {
130
207
  const parts = [];
131
208
  let last = 0;
132
- for (const c of chosen) {
209
+ for (const c of spans) {
133
210
  parts.push(input.slice(last, c.start), `[REDACTED:${c.rule}]`);
134
211
  last = c.end;
135
212
  }
136
213
  parts.push(input.slice(last));
137
- return { text: parts.join(""), redactions: chosen };
214
+ return parts.join("");
215
+ }
216
+ exports.GENERIC_RULES = new Set(["secret-assignment", "bearer-token", "jwt"]);
217
+ const SEQUENTIAL = /0123456|1234567|2345678|3456789|abcdefg|bcdefgh|qwertyu/i;
218
+ const WORDISH = /^[A-Za-z]{2,}$/;
219
+ function looksLikeTestValue(value) {
220
+ if (SEQUENTIAL.test(value))
221
+ return true;
222
+ if (/(.)\1{9,}$/.test(value))
223
+ return true;
224
+ const parts = value.split(/[_-]+/).filter(Boolean);
225
+ if (parts.length < 3)
226
+ return false;
227
+ const words = parts.filter((p) => WORDISH.test(p)).length;
228
+ const filler = parts.filter((p) => /^\d+$/.test(p) || /^(.)\1*$/.test(p)).length;
229
+ return words >= 2 && words + filler === parts.length;
230
+ }
231
+ const TEST_FILE = /(?:\.test\.|\.spec\.|__tests__|(?:^|\/)tests?\/|fixtures?\/|__mocks__)/i;
232
+ const NAME_BEFORE = /([A-Za-z_][A-Za-z0-9_.-]{0,63})\??["']?\s*[:=]\s*\??["']?(?:Bearer\s+)?$/;
233
+ function nameBefore(input, start, lineStart) {
234
+ const window = input.slice(Math.max(lineStart, start - 160), start).replace(/\\[nrt]/g, " ");
235
+ const m = window.match(NAME_BEFORE)?.[1];
236
+ if (!m || /^(?:https?|Bearer|Authorization)$/i.test(m))
237
+ return m === "Authorization" ? m : undefined;
238
+ return m;
239
+ }
240
+ const FILE_PATH_FIELD = /"(?:file_path|filePath|path|target_file)"\s*:\s*"([^"]{1,400})"/g;
241
+ const REDIRECT = /(?:>>?|\btee(?:\s+-a)?)\s*['"]?([^\s'"<>|;&()]{2,300})/g;
242
+ const CONFIG_FILE = /((?:[\w.~-]+\/)*\.env(?:\.[\w-]+)?|(?:[\w.~-]+\/)*[\w.-]+\.(?:env|ya?ml|toml|ini|cfg|conf|properties|tfvars|npmrc|netrc|pgpass))(?![\w/])/g;
243
+ const lastTwo = (p) => p.replace(/\\\//g, "/").split("/").filter(Boolean).slice(-2).join("/");
244
+ function lastMatch(re, text) {
245
+ let last;
246
+ for (const m of text.matchAll(re))
247
+ last = m[1];
248
+ return last;
249
+ }
250
+ function fileOf(input, start, lineStart) {
251
+ const lineEnd = input.indexOf("\n", start);
252
+ const line = input.slice(lineStart, lineEnd === -1 ? input.length : lineEnd);
253
+ const before = input.slice(Math.max(lineStart, start - 400), start);
254
+ const hit = lastMatch(FILE_PATH_FIELD, line) ?? lastMatch(REDIRECT, before) ?? lastMatch(CONFIG_FILE, before);
255
+ if (!hit || hit.startsWith("/dev/") || hit === "&1" || hit === "&2")
256
+ return undefined;
257
+ const short = lastTwo(hit);
258
+ return short && short.length <= 120 ? short : undefined;
138
259
  }
139
260
  const TOOL_CALL_TYPE = /^(?:tool_use|function_call|custom_tool_call|tool_call|toolCall|tool_request|tool\.execution_start|server_tool_use|mcp_tool_use)$/;
140
261
  const TOOL_OUTPUT_TYPE = /^(?:tool_result|function_call_output|custom_tool_call_output|tool_output|toolResult|tool\.execution_complete|mcp_tool_result)$/;
@@ -203,7 +324,7 @@ function locate(input, r, cache) {
203
324
  return { where: "unknown" };
204
325
  }
205
326
  const MAX_LOCATIONS = 8;
206
- function secretFindings(input, redactions) {
327
+ function secretFindings(input, redactions, opts = {}) {
207
328
  const seen = new Map();
208
329
  const cache = new Map();
209
330
  for (const r of redactions) {
@@ -213,8 +334,21 @@ function secretFindings(input, redactions) {
213
334
  const hint = value.replace(/[^A-Za-z0-9]/g, "").slice(-4);
214
335
  if (!hint)
215
336
  continue;
337
+ const lineStart = input.lastIndexOf("\n", r.start - 1) + 1;
338
+ const name = nameBefore(input, r.start, lineStart);
339
+ const file = fileOf(input, r.start, lineStart);
340
+ if (looksLikeTestValue(value))
341
+ continue;
342
+ if (exports.GENERIC_RULES.has(r.rule) && file && TEST_FILE.test(file))
343
+ continue;
344
+ if (opts.skip?.({ rule: r.rule, value, ...(name ? { name } : {}), ...(file ? { file } : {}) }))
345
+ continue;
216
346
  const key = `${r.rule}\u0000${hint}`;
217
347
  const finding = seen.get(key) ?? { rule: r.rule, hint, locations: [] };
348
+ if (name && !finding.name)
349
+ finding.name = name;
350
+ if (file && !finding.file)
351
+ finding.file = file;
218
352
  seen.set(key, finding);
219
353
  const loc = locate(input, r, cache);
220
354
  if (finding.locations.length < MAX_LOCATIONS && !finding.locations.some((l) => l.where === loc.where && l.tool === loc.tool)) {
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PROVIDER_RULE_LABELS = exports.PROVIDER_RULES = void 0;
4
+ exports.PROVIDER_RULES = [
5
+ { name: "1password-secret-key", re: new RegExp("\\bA3-[A-Z0-9]{6}-(?:(?:[A-Z0-9]{11})|(?:[A-Z0-9]{6}-[A-Z0-9]{5}))-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}\\b", "g"), entropy: 3.8, keywords: ["a3-"] },
6
+ { name: "1password-service-account-token", re: new RegExp("ops_eyJ[a-zA-Z0-9+/]{250,}={0,3}", "g"), entropy: 4, keywords: ["ops_"] },
7
+ { name: "adobe-client-secret", re: new RegExp("\\b(p8e-[a-z0-9]{32})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 2, keywords: ["p8e-"] },
8
+ { name: "age-secret-key", re: new RegExp("AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}", "g"), keywords: ["age-secret-key-1"] },
9
+ { name: "airtable-personnal-access-token", re: new RegExp("\\b(pat[A-Za-z0-9]{14}\\.[a-f0-9]{64})\\b", "g"), firstGroup: true, keywords: ["airtable"] },
10
+ { name: "alibaba-access-key-id", re: new RegExp("\\b(LTAI[a-z0-9]{20})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 2, keywords: ["ltai"] },
11
+ { name: "artifactory-api-key", re: new RegExp("\\bAKCp[A-Za-z0-9]{69}\\b", "g"), entropy: 4.5, keywords: ["akcp"] },
12
+ { name: "artifactory-reference-token", re: new RegExp("\\bcmVmd[A-Za-z0-9]{59}\\b", "g"), entropy: 4.5, keywords: ["cmvmd"] },
13
+ { name: "clickhouse-cloud-api-secret-key", re: new RegExp("\\b(4b1d[A-Za-z0-9]{38})\\b", "g"), firstGroup: true, entropy: 3, keywords: ["4b1d"] },
14
+ { name: "clojars-api-token", re: new RegExp("CLOJARS_[a-z0-9]{60}", "gi"), entropy: 2, keywords: ["clojars_"] },
15
+ { name: "cloudflare-origin-ca-key", re: new RegExp("\\b(v1\\.0-[a-f0-9]{24}-[a-f0-9]{146})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["cloudflare", "v1.0-"] },
16
+ { name: "curl-auth-header", re: new RegExp("\\bcurl\\b(?:.*?|.*?(?:[\\r\\n]{1,2}.*?){1,5})[ \\t\\n\\r](?:-H|--header)(?:=|[ \\t]{0,5})(?:\"(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,})|([\\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,}))\"|'(?:Authorization:[ \\t]{0,5}(?:Basic[ \\t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \\t]([\\w=~@.+/-]{8,})|([\\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \\t]{0,5}([\\w=~@.+/-]{8,}))')(?:\\B|\\s|$)", "gi"), firstGroup: true, entropy: 2.75, keywords: ["curl"] },
17
+ { name: "curl-auth-user", re: new RegExp("\\bcurl\\b(?:.*|.*(?:[\\r\\n]{1,2}.*){1,5})[ \\t\\n\\r](?:-u|--user)(?:=|[ \\t]{0,5})(\"(:[^\"]{3,}|[^:\"]{3,}:|[^:\"]{3,}:[^\"]{3,})\"|'([^:']{3,}:[^']{3,})'|((?:\"[^\"]{3,}\"|'[^']{3,}'|[\\w$@.-]+):(?:\"[^\"]{3,}\"|'[^']{3,}'|[\\w${}@.-]+)))(?:\\s|$)", "g"), firstGroup: true, entropy: 2, keywords: ["curl"] },
18
+ { name: "databricks-api-token", re: new RegExp("\\b(dapi[a-f0-9]{32}(?:-\\d)?)(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["dapi"] },
19
+ { name: "digitalocean-access-token", re: new RegExp("\\b(doo_v1_[a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["doo_v1_"] },
20
+ { name: "digitalocean-pat", re: new RegExp("\\b(dop_v1_[a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["dop_v1_"] },
21
+ { name: "digitalocean-refresh-token", re: new RegExp("\\b(dor_v1_[a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, keywords: ["dor_v1_"] },
22
+ { name: "doppler-api-token", re: new RegExp("dp\\.pt\\.[a-z0-9]{43}", "gi"), entropy: 2, keywords: ["dp.pt."] },
23
+ { name: "duffel-api-token", re: new RegExp("duffel_(?:test|live)_[a-z0-9_\\-=]{43}", "gi"), entropy: 2, keywords: ["duffel_"] },
24
+ { name: "dynatrace-api-token", re: new RegExp("dt0c01\\.[a-z0-9]{24}\\.[a-z0-9]{64}", "gi"), entropy: 4, keywords: ["dt0c01."] },
25
+ { name: "easypost-api-token", re: new RegExp("\\bEZAK[a-z0-9]{54}\\b", "gi"), entropy: 2, keywords: ["ezak"] },
26
+ { name: "easypost-test-api-token", re: new RegExp("\\bEZTK[a-z0-9]{54}\\b", "gi"), entropy: 2, keywords: ["eztk"] },
27
+ { name: "facebook-access-token", re: new RegExp("\\b(\\d{15,16}(\\||%)[0-9a-z\\-_]{27,40})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["facebook"] },
28
+ { name: "facebook-page-access-token", re: new RegExp("\\b(EAA[MC][a-z0-9]{100,})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 4, keywords: ["eaam", "eaac"] },
29
+ { name: "flutterwave-encryption-key", re: new RegExp("FLWSECK_TEST-[a-h0-9]{12}", "gi"), entropy: 2, keywords: ["flwseck_test"] },
30
+ { name: "flutterwave-secret-key", re: new RegExp("FLWSECK_TEST-[a-h0-9]{32}-X", "gi"), entropy: 2, keywords: ["flwseck_test"] },
31
+ { name: "flyio-access-token", re: new RegExp("\\b((?:fo1_[\\w-]{43}|fm1[ar]_[a-zA-Z0-9+\\/]{100,}={0,3}|fm2_[a-zA-Z0-9+\\/]{100,}={0,3}))(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 4, keywords: ["fo1_", "fm1", "fm2_"] },
32
+ { name: "frameio-api-token", re: new RegExp("fio-u-[a-z0-9\\-_=]{64}", "gi"), keywords: ["fio-u-"] },
33
+ { name: "grafana-api-key", re: new RegExp("\\b(eyJrIjoi[A-Za-z0-9]{70,400}={0,3})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["eyjrijoi"] },
34
+ { name: "grafana-cloud-api-token", re: new RegExp("\\b(glc_[A-Za-z0-9+/]{32,400}={0,3})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["glc_"] },
35
+ { name: "grafana-service-account-token", re: new RegExp("\\b(glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["glsa_"] },
36
+ { name: "harness-api-key", re: new RegExp("(?:pat|sat)\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9]{24}\\.[a-zA-Z0-9]{20}", "g"), keywords: ["pat.", "sat."] },
37
+ { name: "heroku-api-key-v2", re: new RegExp("\\b((HRKU-AA[0-9a-zA-Z_-]{58}))(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 4, keywords: ["hrku-aa"] },
38
+ { name: "huggingface-access-token", re: new RegExp("\\b(hf_(?:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 2, keywords: ["hf_"] },
39
+ { name: "huggingface-organization-api-token", re: new RegExp("\\b(api_org_(?:[a-z]{34}))(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 2, keywords: ["api_org_"] },
40
+ { name: "infracost-api-token", re: new RegExp("\\b(ico-[a-zA-Z0-9]{32})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["ico-"] },
41
+ { name: "intra42-client-secret", re: new RegExp("\\b(s-s4t2(?:ud|af)-[abcdef0123456789]{64})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["intra", "s-s4t2ud-", "s-s4t2af-"] },
42
+ { name: "maxmind-license-key", re: new RegExp("\\b([A-Za-z0-9]{6}_[A-Za-z0-9]{29}_mmk)(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 4, keywords: ["_mmk"] },
43
+ { name: "microsoft-teams-webhook", re: new RegExp("https://[a-z0-9]+\\.webhook\\.office\\.com/webhookb2/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}@[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}/IncomingWebhook/[a-z0-9]{32}/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}", "g"), firstGroup: true, keywords: ["webhook.office.com", "webhookb2", "incomingwebhook"] },
44
+ { name: "notion-api-token", re: new RegExp("\\b(ntn_[0-9]{11}[A-Za-z0-9]{32}[A-Za-z0-9]{3})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 4, keywords: ["ntn_"] },
45
+ { name: "octopus-deploy-api-key", re: new RegExp("\\b(API-[A-Z0-9]{26})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["api-"] },
46
+ { name: "openshift-user-token", re: new RegExp("\\b(sha256~[\\w-]{43})(?:[^\\w-]|$)", "g"), firstGroup: true, entropy: 3.5, keywords: ["sha256~"] },
47
+ { name: "perplexity-api-key", re: new RegExp("\\b(pplx-[a-zA-Z0-9]{48})(?:[\\x60'\"\\s;]|\\\\|$|\\b)", "g"), firstGroup: true, entropy: 4, keywords: ["pplx-"] },
48
+ { name: "planetscale-api-token", re: new RegExp("\\b(pscale_tkn_[\\w=\\.-]{32,64})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["pscale_tkn_"] },
49
+ { name: "planetscale-oauth-token", re: new RegExp("\\b(pscale_oauth_[\\w=\\.-]{32,64})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["pscale_oauth_"] },
50
+ { name: "planetscale-password", re: new RegExp("\\b(pscale_pw_[\\w=\\.-]{32,64})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["pscale_pw_"] },
51
+ { name: "postman-api-token", re: new RegExp("\\b(PMAK-[a-f0-9]{24}\\-[a-f0-9]{34})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 3, keywords: ["pmak-"] },
52
+ { name: "prefect-api-token", re: new RegExp("\\b(pnu_[a-zA-Z0-9]{36})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["pnu_"] },
53
+ { name: "pulumi-api-token", re: new RegExp("\\b(pul-[a-f0-9]{40})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["pul-"] },
54
+ { name: "pypi-upload-token", re: new RegExp("pypi-AgEIcHlwaS5vcmc[\\w-]{50,1000}", "g"), entropy: 3, keywords: ["pypi-ageichlwas5vcmc"] },
55
+ { name: "readme-api-token", re: new RegExp("\\b(rdme_[a-z0-9]{70})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["rdme_"] },
56
+ { name: "rubygems-api-token", re: new RegExp("\\b(rubygems_[a-f0-9]{48})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["rubygems_"] },
57
+ { name: "scalingo-api-token", re: new RegExp("\\b(tk-us-[\\w-]{48})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["tk-us-"] },
58
+ { name: "sendinblue-api-token", re: new RegExp("\\b(xkeysib-[a-f0-9]{64}\\-[a-z0-9]{16})(?:[\\x60'\"\\s;]|\\\\|$)", "gi"), firstGroup: true, entropy: 2, keywords: ["xkeysib-"] },
59
+ { name: "sentry-org-token", re: new RegExp("\\bsntrys_eyJpYXQiO[a-zA-Z0-9+/]{10,200}(?:LCJyZWdpb25fdXJs|InJlZ2lvbl91cmwi|cmVnaW9uX3VybCI6)[a-zA-Z0-9+/]{10,200}={0,2}_[a-zA-Z0-9+/]{43}(?:[^a-zA-Z0-9+/]|$)", "g"), entropy: 4.5, keywords: ["sntrys_eyjpyxqio"] },
60
+ { name: "sentry-user-token", re: new RegExp("\\b(sntryu_[a-f0-9]{64})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3.5, keywords: ["sntryu_"] },
61
+ { name: "settlemint-application-access-token", re: new RegExp("\\b(sm_aat_[a-zA-Z0-9]{16})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["sm_aat"] },
62
+ { name: "settlemint-personal-access-token", re: new RegExp("\\b(sm_pat_[a-zA-Z0-9]{16})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["sm_pat"] },
63
+ { name: "settlemint-service-access-token", re: new RegExp("\\b(sm_sat_[a-zA-Z0-9]{16})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 3, keywords: ["sm_sat"] },
64
+ { name: "shippo-api-token", re: new RegExp("\\b(shippo_(?:live|test)_[a-fA-F0-9]{40})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["shippo_"] },
65
+ { name: "shopify-access-token", re: new RegExp("shpat_[a-fA-F0-9]{32}", "g"), entropy: 2, keywords: ["shpat_"] },
66
+ { name: "shopify-custom-access-token", re: new RegExp("shpca_[a-fA-F0-9]{32}", "g"), entropy: 2, keywords: ["shpca_"] },
67
+ { name: "shopify-private-app-access-token", re: new RegExp("shppa_[a-fA-F0-9]{32}", "g"), entropy: 2, keywords: ["shppa_"] },
68
+ { name: "shopify-shared-secret", re: new RegExp("shpss_[a-fA-F0-9]{32}", "g"), entropy: 2, keywords: ["shpss_"] },
69
+ { name: "sidekiq-sensitive-url", re: new RegExp("\\bhttps?://([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\\/|\\#|\\?|:]|$)", "gi"), firstGroup: true, keywords: ["gems.contribsys.com", "enterprise.contribsys.com"] },
70
+ { name: "sourcegraph-access-token", re: new RegExp("\\b(sgp_(?:[a-fA-F0-9]{16}|local)_[a-fA-F0-9]{40}|sgp_[a-fA-F0-9]{40})\\b", "g"), firstGroup: true, entropy: 3, keywords: ["sgp_", "sourcegraph"] },
71
+ { name: "square-access-token", re: new RegExp("\\b((?:EAAA|sq0atp-)[\\w-]{22,60})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 2, keywords: ["sq0atp-", "eaaa"] },
72
+ { name: "vault-batch-token", re: new RegExp("\\b(hvb\\.[\\w-]{138,300})(?:[\\x60'\"\\s;]|\\\\|$)", "g"), firstGroup: true, entropy: 4, keywords: ["hvb."] },
73
+ { name: "vault-service-token", re: new RegExp("\\b(hvs\\.[\\w-]{90,120})", "g"), firstGroup: true, entropy: 3.5, keywords: ["hvs."] },
74
+ ];
75
+ exports.PROVIDER_RULE_LABELS = {
76
+ "1password-secret-key": "1password secret key",
77
+ "1password-service-account-token": "1password service account token",
78
+ "adobe-client-secret": "Adobe client secret",
79
+ "age-secret-key": "Age secret key",
80
+ "airtable-personnal-access-token": "Airtable personnal access token",
81
+ "alibaba-access-key-id": "Alibaba access key id",
82
+ "artifactory-api-key": "Artifactory api key",
83
+ "artifactory-reference-token": "Artifactory reference token",
84
+ "clickhouse-cloud-api-secret-key": "Clickhouse cloud api secret key",
85
+ "clojars-api-token": "Clojars api token",
86
+ "cloudflare-origin-ca-key": "Cloudflare origin ca key",
87
+ "curl-auth-header": "Curl auth header",
88
+ "curl-auth-user": "Curl auth user",
89
+ "databricks-api-token": "Databricks api token",
90
+ "digitalocean-access-token": "Digitalocean access token",
91
+ "digitalocean-pat": "Digitalocean pat",
92
+ "digitalocean-refresh-token": "Digitalocean refresh token",
93
+ "doppler-api-token": "Doppler api token",
94
+ "duffel-api-token": "Duffel api token",
95
+ "dynatrace-api-token": "Dynatrace api token",
96
+ "easypost-api-token": "Easypost api token",
97
+ "easypost-test-api-token": "Easypost test api token",
98
+ "facebook-access-token": "Facebook access token",
99
+ "facebook-page-access-token": "Facebook page access token",
100
+ "flutterwave-encryption-key": "Flutterwave encryption key",
101
+ "flutterwave-secret-key": "Flutterwave secret key",
102
+ "flyio-access-token": "Flyio access token",
103
+ "frameio-api-token": "Frameio api token",
104
+ "grafana-api-key": "Grafana api key",
105
+ "grafana-cloud-api-token": "Grafana cloud api token",
106
+ "grafana-service-account-token": "Grafana service account token",
107
+ "harness-api-key": "Harness api key",
108
+ "heroku-api-key-v2": "Heroku api key v2",
109
+ "huggingface-access-token": "Huggingface access token",
110
+ "huggingface-organization-api-token": "Huggingface organization api token",
111
+ "infracost-api-token": "Infracost api token",
112
+ "intra42-client-secret": "Intra42 client secret",
113
+ "maxmind-license-key": "Maxmind license key",
114
+ "microsoft-teams-webhook": "Microsoft teams webhook",
115
+ "notion-api-token": "Notion api token",
116
+ "octopus-deploy-api-key": "Octopus deploy api key",
117
+ "openshift-user-token": "Openshift user token",
118
+ "perplexity-api-key": "Perplexity api key",
119
+ "planetscale-api-token": "Planetscale api token",
120
+ "planetscale-oauth-token": "Planetscale oauth token",
121
+ "planetscale-password": "Planetscale password",
122
+ "postman-api-token": "Postman api token",
123
+ "prefect-api-token": "Prefect api token",
124
+ "pulumi-api-token": "Pulumi api token",
125
+ "pypi-upload-token": "Pypi upload token",
126
+ "readme-api-token": "Readme api token",
127
+ "rubygems-api-token": "Rubygems api token",
128
+ "scalingo-api-token": "Scalingo api token",
129
+ "sendinblue-api-token": "Sendinblue api token",
130
+ "sentry-org-token": "Sentry org token",
131
+ "sentry-user-token": "Sentry user token",
132
+ "settlemint-application-access-token": "Settlemint application access token",
133
+ "settlemint-personal-access-token": "Settlemint personal access token",
134
+ "settlemint-service-access-token": "Settlemint service access token",
135
+ "shippo-api-token": "Shippo api token",
136
+ "shopify-access-token": "Shopify access token",
137
+ "shopify-custom-access-token": "Shopify custom access token",
138
+ "shopify-private-app-access-token": "Shopify private app access token",
139
+ "shopify-shared-secret": "Shopify shared secret",
140
+ "sidekiq-sensitive-url": "Sidekiq sensitive url",
141
+ "sourcegraph-access-token": "Sourcegraph access token",
142
+ "square-access-token": "Square access token",
143
+ "vault-batch-token": "Vault batch token",
144
+ "vault-service-token": "Vault service token",
145
+ };
package/dist/utils.js CHANGED
@@ -1421,7 +1421,6 @@ but does not install a global \`codiedev\` executable.
1421
1421
  | "any messages?" / "check my inbox" / "what did a teammate say?" | \`codiedev inbox\` |
1422
1422
  | "read that reply" / "mark that ping read" | \`codiedev read <ping-id>\` |
1423
1423
  | "note that X is a follow-up" / "remember X" | \`codiedev note "<text>"\` |
1424
- | "create a ticket" / "scope this into tickets" / "ticket this for me" | \`codiedev create-ticket [optional-inline-prompt]\` |
1425
1424
  | "share with a teammate" / "give someone access" (no notification) | \`codiedev share <key> <name> [--role read\\|edit]\` |
1426
1425
  | "send this to a teammate" / "ask someone to look at this" | \`codiedev send <key> <name> ["<msg>"]\` |
1427
1426
  | "find something about X" / "has anyone solved Y?" | \`codiedev search "<query>" [--limit N]\` |
@@ -1470,11 +1469,6 @@ iterate rather than duplicating.
1470
1469
  notification). \`codiedev send\` both grants access AND pings the
1471
1470
  recipient. Use send when the user is actively looping someone in.
1472
1471
 
1473
- **\`create-ticket\` flow:** when invoked without \`--submit\`, the CLI prints
1474
- skill instructions you should follow. Step 1 is asking the user about repos.
1475
- Step 4 calls \`codiedev create-ticket --submit <path>\` with the plan JSON.
1476
- Tell the user the portal URL when the submit returns.
1477
-
1478
1472
  **Errors:**
1479
1473
  - "not connected" → user needs to run \`npx codiedev connect\` with their
1480
1474
  API token from https://codiedev.com/portal/integrations/claude-code
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.10.0-beta.14",
3
+ "version": "0.10.0-beta.16",
4
4
  "description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",
@@ -12,12 +12,12 @@
12
12
  "dev": "tsc --watch",
13
13
  "prepublishOnly": "tsc",
14
14
  "test": "vitest run",
15
- "test:functional": "vitest run src/__tests__/limitEvent.functional.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/piCapture.functional.test.ts",
16
- "test:harness-parity": "vitest run src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/piCapture.functional.test.ts",
15
+ "test:functional": "vitest run src/__tests__/localScrub.functional.test.ts src/__tests__/limitEvent.functional.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/piCapture.functional.test.ts",
16
+ "test:harness-parity": "vitest run src/__tests__/localScrub.functional.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/piCapture.functional.test.ts",
17
17
  "test:harness-binaries": "node scripts/checkHarnessBinaries.mjs",
18
18
  "test:opencode-attribution": "vitest run src/__tests__/repoResolver.test.ts src/__tests__/openCodeCapture.test.ts src/__tests__/openCodePluginGitRefs.test.ts src/__tests__/capturePayloadFinal.test.ts src/__tests__/captureDirectUpload.test.ts src/__tests__/resolveCaptureFinal.test.ts && tsc",
19
19
  "test:watch": "vitest",
20
- "test:metadata-parity": "vitest run src/__tests__/sourceMetadataMatrix.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/copilotCapture.test.ts src/__tests__/piCapture.functional.test.ts src/__tests__/piHooks.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/nativeSourceParity.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/subagents.test.ts src/__tests__/transcriptModels.test.ts src/__tests__/captureRetry.test.ts src/__tests__/limitEvent.functional.test.ts src/__tests__/captureExclusions.test.ts",
20
+ "test:metadata-parity": "vitest run src/__tests__/localScrub.functional.test.ts src/__tests__/sourceMetadataMatrix.test.ts src/__tests__/copilotCapture.functional.test.ts src/__tests__/copilotCapture.test.ts src/__tests__/piCapture.functional.test.ts src/__tests__/piHooks.test.ts src/__tests__/cursorParity.functional.test.ts src/__tests__/nativeSourceParity.test.ts src/__tests__/harnessParity.functional.test.ts src/__tests__/subagents.test.ts src/__tests__/transcriptModels.test.ts src/__tests__/captureRetry.test.ts src/__tests__/limitEvent.functional.test.ts src/__tests__/captureExclusions.test.ts",
21
21
  "test:local-scrub-live": "npm run build && node scripts/localScrubLive.mjs"
22
22
  },
23
23
  "files": [
@@ -1,182 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.substitutePrompt = substitutePrompt;
37
- exports.validatePlanHasRepo = validatePlanHasRepo;
38
- exports.resolveRepoIds = resolveRepoIds;
39
- exports.parseCreateTicketArgs = parseCreateTicketArgs;
40
- exports.runCreateTicket = runCreateTicket;
41
- exports.runBare = runBare;
42
- exports.runSubmit = runSubmit;
43
- exports.readSubmitInput = readSubmitInput;
44
- const fs = __importStar(require("node:fs"));
45
- const shared_1 = require("./shared");
46
- const PROMPT_PLACEHOLDER = "{{USER_PROMPT}}";
47
- const MISSING_PROMPT_FALLBACK = "(not yet provided — ask the user a single combined question that covers both the intent AND repo locations: \"What are you working on, and is this in a single repo or multiple? If multiple, give me the paths.\" Skip Step 1 since you've folded it into this question. Then proceed to Step 2.)";
48
- function substitutePrompt(protocol, prompt) {
49
- return protocol.replace(PROMPT_PLACEHOLDER, prompt && prompt.length > 0 ? prompt : MISSING_PROMPT_FALLBACK);
50
- }
51
- function validatePlanHasRepo(parsedPlan) {
52
- const fullNames = Array.isArray(parsedPlan?.repoFullNames) ? parsedPlan.repoFullNames : [];
53
- const explicitIds = Array.isArray(parsedPlan?.repoIds) ? parsedPlan.repoIds : [];
54
- if (fullNames.length === 0 && explicitIds.length === 0) {
55
- return "Plan must include at least one repo (repoFullNames: ['owner/name'] or repoIds: [...])";
56
- }
57
- return null;
58
- }
59
- function resolveRepoIds(fullNames, repos) {
60
- const lookup = new Map();
61
- for (const r of repos) {
62
- lookup.set(r.fullName.toLowerCase(), r.repoId);
63
- }
64
- const ids = [];
65
- for (const fn of fullNames) {
66
- const id = lookup.get(fn.toLowerCase());
67
- if (!id) {
68
- throw new Error(`Repo "${fn}" is not connected to your Codiedev account. Run \`codiedev connect\` to refresh, or push the missing repo through Settings.`);
69
- }
70
- ids.push(id);
71
- }
72
- return ids;
73
- }
74
- function parseCreateTicketArgs(args) {
75
- let mode = "bare";
76
- let prompt;
77
- let submitSource;
78
- for (let i = 0; i < args.length; i++) {
79
- const a = args[i];
80
- if (a === "--submit" && i + 1 < args.length) {
81
- mode = "submit";
82
- const next = args[++i];
83
- submitSource = next === "-" ? "stdin" : next;
84
- }
85
- else if (a.startsWith("--submit=")) {
86
- mode = "submit";
87
- const v = a.slice("--submit=".length);
88
- submitSource = v === "-" ? "stdin" : v;
89
- }
90
- else if (!a.startsWith("--") && !prompt && mode === "bare") {
91
- prompt = a;
92
- }
93
- }
94
- return { mode, prompt, submitSource };
95
- }
96
- async function runCreateTicket(args) {
97
- const parsed = parseCreateTicketArgs(args);
98
- if (parsed.mode === "bare") {
99
- const config = (0, shared_1.requireConfig)();
100
- await runBare(config, parsed.prompt);
101
- return;
102
- }
103
- if (!parsed.submitSource) {
104
- console.error("--submit requires a path or '-' for stdin");
105
- process.exit(1);
106
- }
107
- const config = (0, shared_1.requireConfig)();
108
- let raw;
109
- try {
110
- raw = readSubmitInput(parsed.submitSource);
111
- }
112
- catch (err) {
113
- console.error(`Failed to read plan: ${err.message}`);
114
- process.exit(1);
115
- }
116
- let parsedPlan;
117
- try {
118
- parsedPlan = JSON.parse(raw);
119
- }
120
- catch (err) {
121
- console.error(`Plan JSON did not parse: ${err.message}`);
122
- process.exit(1);
123
- }
124
- const validationError = validatePlanHasRepo(parsedPlan);
125
- if (validationError) {
126
- console.error(validationError);
127
- process.exit(1);
128
- }
129
- const fullNames = Array.isArray(parsedPlan.repoFullNames) ? parsedPlan.repoFullNames : [];
130
- const explicitIds = Array.isArray(parsedPlan.repoIds) ? parsedPlan.repoIds : [];
131
- if (fullNames.length > 0 && explicitIds.length === 0) {
132
- parsedPlan.repoIds = resolveRepoIds(fullNames, config.repos);
133
- }
134
- for (const t of parsedPlan.plan?.tickets ?? []) {
135
- if (t.targetRepoFullName && !t.targetRepoId) {
136
- const ids = resolveRepoIds([t.targetRepoFullName], config.repos);
137
- t.targetRepoId = ids[0];
138
- }
139
- }
140
- await runSubmit(config, JSON.stringify(parsedPlan));
141
- }
142
- async function runBare(config, prompt) {
143
- let res;
144
- try {
145
- res = await (0, shared_1.apiRequest)("GET", "/api/cli/createTicketProtocol", { config });
146
- }
147
- catch (err) {
148
- console.error(`Failed to fetch create-ticket protocol: ${err.message}. Check your network connection and re-run.`);
149
- process.exit(1);
150
- }
151
- process.stdout.write(substitutePrompt(res.protocol, prompt));
152
- }
153
- async function runSubmit(config, rawJson) {
154
- let payload;
155
- try {
156
- payload = JSON.parse(rawJson);
157
- }
158
- catch (err) {
159
- throw new Error(`Plan JSON did not parse — emit valid JSON matching the schema printed by \`codiedev create-ticket\`. Original error: ${err.message}`);
160
- }
161
- if (!payload.plan || !payload.prompt || !payload.repoIds) {
162
- throw new Error("Plan JSON missing one of: plan, prompt, repoIds");
163
- }
164
- const res = await (0, shared_1.withTelemetry)(config, "codiedev_create_ticket", () => (0, shared_1.apiRequest)("POST", "/api/cli/createPlan", {
165
- config,
166
- body: payload,
167
- }));
168
- const portalHost = (process.env.CODIEDEV_PORTAL_URL || "https://codiedev.com").replace(/\/$/, "");
169
- const portalLink = res.portalUrl.startsWith("http")
170
- ? res.portalUrl
171
- : `${portalHost}${res.portalUrl}`;
172
- console.log(`✓ Created ${res.ticketCount} ticket(s).`);
173
- console.log(` portal: ${portalLink}`);
174
- console.log("");
175
- console.log("Open in your portal to review and push to Jira.");
176
- }
177
- function readSubmitInput(source) {
178
- if (source === "stdin") {
179
- return fs.readFileSync(0, "utf8");
180
- }
181
- return fs.readFileSync(source, "utf8");
182
- }