codiedev 0.10.0-beta.13 → 0.10.0-beta.15

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,8 +445,8 @@ async function main() {
445
445
  }
446
446
  if (process.argv[2] === "local-scrub") {
447
447
  try {
448
- const { runAndReport } = await Promise.resolve().then(() => __importStar(require("./localScrub")));
449
- await runAndReport();
448
+ const { runScheduledScrub } = await Promise.resolve().then(() => __importStar(require("./localScrub")));
449
+ await runScheduledScrub();
450
450
  }
451
451
  catch {
452
452
  }
@@ -500,7 +500,7 @@ async function main() {
500
500
  if (source === "codex")
501
501
  (0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
502
502
  if (source === "codex")
503
- (0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()) });
503
+ (0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()), cwd: hookInput.cwd });
504
504
  if (source !== "copilot")
505
505
  await runDiscover(hookInput.prompt);
506
506
  return;
@@ -542,7 +542,7 @@ async function main() {
542
542
  process.argv[2] === "session-start-opencode" ||
543
543
  process.argv[2] === "session-start-pi") {
544
544
  (0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
545
- (0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()) });
545
+ (0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()), cwd: hookInput.cwd });
546
546
  const packNag = process.argv[2] === "session-start-opencode" || process.argv[2] === "session-start-pi" ? null : (0, sync_1.packNagLine)();
547
547
  if (packNag && process.argv[2] === "session-start")
548
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,12 +47,17 @@ exports.cleanOpenCodeDb = cleanOpenCodeDb;
47
47
  exports.countCursorDb = countCursorDb;
48
48
  exports.runLocalScrub = runLocalScrub;
49
49
  exports.runAndReport = runAndReport;
50
+ exports.scrubPolicy = scrubPolicy;
51
+ exports.scrubAllowed = scrubAllowed;
52
+ exports.formatRules = formatRules;
53
+ exports.runScheduledScrub = runScheduledScrub;
50
54
  exports.maybeSpawnLocalScrub = maybeSpawnLocalScrub;
51
55
  const fs = __importStar(require("fs"));
52
56
  const os = __importStar(require("os"));
53
57
  const path = __importStar(require("path"));
54
58
  const child_process_1 = require("child_process");
55
- const rules_1 = require("./scrub/rules");
59
+ const extraRules_1 = require("./scrub/extraRules");
60
+ const knownSecrets_1 = require("./knownSecrets");
56
61
  const harnessDirs_1 = require("./harnessDirs");
57
62
  const copilotHooks_1 = require("./copilotHooks");
58
63
  const configDir_1 = require("./configDir");
@@ -116,7 +121,7 @@ function cursorIdeDb(env = process.env, home = os.homedir(), platform = process.
116
121
  }
117
122
  function cleanValue(value, count) {
118
123
  if (typeof value === "string") {
119
- const r = (0, rules_1.scrubSecrets)(value);
124
+ const r = (0, extraRules_1.scrub)(value);
120
125
  count.n += r.redactions.length;
121
126
  return r.text;
122
127
  }
@@ -140,7 +145,7 @@ function parses(text) {
140
145
  }
141
146
  }
142
147
  function cleanJson(text) {
143
- const raw = (0, rules_1.scrubSecrets)(text);
148
+ const raw = (0, extraRules_1.scrub)(text);
144
149
  if (raw.redactions.length === 0)
145
150
  return { text, redactions: 0 };
146
151
  if (parses(raw.text) || !parses(text))
@@ -167,13 +172,13 @@ function cleanJsonl(text) {
167
172
  return redactions === 0 ? { text, redactions: 0 } : { text: lines.join("\n"), redactions };
168
173
  }
169
174
  function cleanText(text) {
170
- const r = (0, rules_1.scrubSecrets)(text);
175
+ const r = (0, extraRules_1.scrub)(text);
171
176
  return { text: r.text, redactions: r.redactions.length };
172
177
  }
173
178
  function cleanYaml(text) {
174
179
  let redactions = 0;
175
180
  const lines = text.split("\n").map((line) => {
176
- const r = (0, rules_1.scrubSecrets)(line);
181
+ const r = (0, extraRules_1.scrub)(line);
177
182
  if (r.redactions.length === 0)
178
183
  return line;
179
184
  redactions += r.redactions.length;
@@ -332,7 +337,7 @@ function cleanOpenCodeDb(file, deps) {
332
337
  for (const [id, d] of sessions) {
333
338
  const row = get.all(id)[0];
334
339
  if (typeof row?.title === "string")
335
- d.title = (0, rules_1.scrubSecrets)(row.title).text.slice(0, 80);
340
+ d.title = (0, extraRules_1.scrub)(row.title).text.slice(0, 80);
336
341
  if (typeof row?.directory === "string")
337
342
  d.cwd = row.directory;
338
343
  }
@@ -415,7 +420,7 @@ function countCursorDb(file, kind) {
415
420
  const details = [...chats].map(([chat, keys]) => ({
416
421
  harness: "cursor",
417
422
  sessionKey: chat,
418
- ...(titles.get(chat) ? { title: (0, rules_1.scrubSecrets)(titles.get(chat)).text.slice(0, 80) } : {}),
423
+ ...(titles.get(chat) ? { title: (0, extraRules_1.scrub)(titles.get(chat)).text.slice(0, 80) } : {}),
419
424
  keys,
420
425
  status: "not_removed",
421
426
  reason: "cursor_chat_store",
@@ -656,6 +661,66 @@ async function runAndReport(opts = {}) {
656
661
  }
657
662
  return { ...summary, reported };
658
663
  }
664
+ const defaultConfig = () => {
665
+ const { readConfig } = require("./utils");
666
+ return readConfig();
667
+ };
668
+ const OFF = { enabled: false, formats: [], ownSecrets: "off" };
669
+ async function scrubPolicy(fetchImpl = fetch, getConfig = defaultConfig) {
670
+ try {
671
+ const config = getConfig();
672
+ if (!config?.token || !config.backendUrl)
673
+ return OFF;
674
+ const res = await fetchImpl(`${config.backendUrl}/api/cli/localScrubPolicy`, {
675
+ headers: { Authorization: `Bearer ${config.token}` },
676
+ signal: AbortSignal.timeout(10_000),
677
+ });
678
+ if (!res.ok)
679
+ return OFF;
680
+ const body = (await res.json());
681
+ const formats = Array.isArray(body.formats)
682
+ ? body.formats.filter((f) => f && typeof f.name === "string" && typeof f.source === "string" && f.source.length <= 300).slice(0, 50)
683
+ : [];
684
+ const ownSecrets = body.ownSecrets === "off" || body.ownSecrets === "server" ? body.ownSecrets : "laptop";
685
+ const key = typeof body.fingerprintKey === "string" && /^[0-9a-f]{64}$/.test(body.fingerprintKey) ? body.fingerprintKey : undefined;
686
+ return { enabled: body.enabled === true, formats, ownSecrets, ...(key ? { fingerprintKey: key } : {}) };
687
+ }
688
+ catch {
689
+ return OFF;
690
+ }
691
+ }
692
+ async function scrubAllowed(fetchImpl = fetch, getConfig = defaultConfig) {
693
+ return (await scrubPolicy(fetchImpl, getConfig)).enabled;
694
+ }
695
+ function formatRules(formats) {
696
+ const out = [];
697
+ for (const f of formats) {
698
+ try {
699
+ 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] } : {}) });
700
+ }
701
+ catch { }
702
+ }
703
+ return out;
704
+ }
705
+ async function runScheduledScrub(opts = {}) {
706
+ const getConfig = opts.getConfig ?? defaultConfig;
707
+ const policy = await scrubPolicy(opts.fetchImpl ?? fetch, getConfig);
708
+ if (!policy.enabled)
709
+ return false;
710
+ const stateDir = opts.stateDir ?? (0, configDir_1.codiedevStateDir)();
711
+ const known = policy.ownSecrets === "off" ? [] : (0, knownSecrets_1.collectKnownSecrets)(opts.home ?? os.homedir(), (0, knownSecrets_1.recentCwds)(scrubDir(stateDir)));
712
+ if (policy.ownSecrets === "server" && policy.fingerprintKey)
713
+ await (0, knownSecrets_1.uploadKnownSecrets)(getConfig(), policy.fingerprintKey, known, opts.fetchImpl ?? fetch);
714
+ const knownRule = (0, knownSecrets_1.knownSecretRule)(known);
715
+ (0, extraRules_1.setExtraRules)([...formatRules(policy.formats), ...(knownRule ? [knownRule] : [])]);
716
+ try {
717
+ await runAndReport({ stateDir });
718
+ }
719
+ finally {
720
+ (0, extraRules_1.setExtraRules)([]);
721
+ }
722
+ return true;
723
+ }
659
724
  function maybeSpawnLocalScrub(opts = {}) {
660
725
  try {
661
726
  const env = opts.env ?? process.env;
@@ -663,6 +728,7 @@ function maybeSpawnLocalScrub(opts = {}) {
663
728
  return "disabled";
664
729
  const now = opts.now ?? Date.now();
665
730
  const dir = scrubDir(opts.stateDir ?? (0, configDir_1.codiedevStateDir)());
731
+ (0, knownSecrets_1.rememberCwd)(dir, opts.cwd);
666
732
  const stamp = path.join(dir, "last-run");
667
733
  try {
668
734
  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();
@@ -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.13",
3
+ "version": "0.10.0-beta.15",
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
- }