codiedev 0.10.0-beta.15 → 0.10.0-beta.17

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.
@@ -1,10 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GENERIC_RULES = void 0;
3
+ exports.SHAPE_RULES = exports.isTestFile = exports.GENERIC_RULES = void 0;
4
4
  exports.shannonEntropy = shannonEntropy;
5
5
  exports.scrubSecrets = scrubSecrets;
6
6
  exports.redactSpans = redactSpans;
7
7
  exports.looksLikeTestValue = looksLikeTestValue;
8
+ exports.locateInLine = locateInLine;
9
+ exports.locateMarkers = locateMarkers;
10
+ exports.namesASecret = namesASecret;
11
+ exports.notWorthRotating = notWorthRotating;
12
+ exports.hintOf = hintOf;
8
13
  exports.secretFindings = secretFindings;
9
14
  exports.possibleSecrets = possibleSecrets;
10
15
  exports.redactPossible = redactPossible;
@@ -228,7 +233,10 @@ function looksLikeTestValue(value) {
228
233
  const filler = parts.filter((p) => /^\d+$/.test(p) || /^(.)\1*$/.test(p)).length;
229
234
  return words >= 2 && words + filler === parts.length;
230
235
  }
231
- const TEST_FILE = /(?:\.test\.|\.spec\.|__tests__|(?:^|\/)tests?\/|fixtures?\/|__mocks__)/i;
236
+ const TEST_FILE = /(?:\.test\.|\.spec\.|__tests__|__mocks__|fixtures?\/|(?:^|[\/_.-])tests?[\/_.-])/i;
237
+ const TEST_FILE_SUFFIX = /[a-z0-9]Tests?\.\w+$/;
238
+ const isTestFile = (file) => TEST_FILE.test(file) || TEST_FILE_SUFFIX.test(file);
239
+ exports.isTestFile = isTestFile;
232
240
  const NAME_BEFORE = /([A-Za-z_][A-Za-z0-9_.-]{0,63})\??["']?\s*[:=]\s*\??["']?(?:Bearer\s+)?$/;
233
241
  function nameBefore(input, start, lineStart) {
234
242
  const window = input.slice(Math.max(lineStart, start - 160), start).replace(/\\[nrt]/g, " ");
@@ -238,7 +246,8 @@ function nameBefore(input, start, lineStart) {
238
246
  return m;
239
247
  }
240
248
  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;
249
+ const REDIRECT = /(?<![-=>\w]|\\n|\\r)(?<!^)(?:>>?|\btee(?:\s+-a)?)[ \t]*['"]?([\w.~/-]{2,300})/gm;
250
+ const PATH_LIKE = /[./]/;
242
251
  const CONFIG_FILE = /((?:[\w.~-]+\/)*\.env(?:\.[\w-]+)?|(?:[\w.~-]+\/)*[\w.-]+\.(?:env|ya?ml|toml|ini|cfg|conf|properties|tfvars|npmrc|netrc|pgpass))(?![\w/])/g;
243
252
  const lastTwo = (p) => p.replace(/\\\//g, "/").split("/").filter(Boolean).slice(-2).join("/");
244
253
  function lastMatch(re, text) {
@@ -251,8 +260,9 @@ function fileOf(input, start, lineStart) {
251
260
  const lineEnd = input.indexOf("\n", start);
252
261
  const line = input.slice(lineStart, lineEnd === -1 ? input.length : lineEnd);
253
262
  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")
263
+ const redirect = lastMatch(REDIRECT, before);
264
+ const hit = lastMatch(FILE_PATH_FIELD, line) ?? (redirect && PATH_LIKE.test(redirect) && !/^[.\d-]+$/.test(redirect) ? redirect : undefined) ?? lastMatch(CONFIG_FILE, before);
265
+ if (!hit || hit.startsWith("/dev/"))
256
266
  return undefined;
257
267
  const short = lastTwo(hit);
258
268
  return short && short.length <= 120 ? short : undefined;
@@ -261,11 +271,13 @@ const TOOL_CALL_TYPE = /^(?:tool_use|function_call|custom_tool_call|tool_call|to
261
271
  const TOOL_OUTPUT_TYPE = /^(?:tool_result|function_call_output|custom_tool_call_output|tool_output|toolResult|tool\.execution_complete|mcp_tool_result)$/;
262
272
  const SHELL_TOOL = /bash|shell|exec|terminal|run_command|run_terminal/i;
263
273
  const GIT_COMMIT = /\bgit\s+(?:[^\n]*\s)?(?:commit|push)\b/;
264
- function locate(input, r, cache) {
265
- const lineStart = input.lastIndexOf("\n", r.start - 1) + 1;
274
+ const PASTE_LINES = 8;
275
+ const PASTE_CHARS = 800;
276
+ function lineAt(input, at, cache) {
277
+ const lineStart = input.lastIndexOf("\n", at - 1) + 1;
266
278
  let parsed = cache.get(lineStart);
267
279
  if (parsed === undefined) {
268
- const lineEnd = input.indexOf("\n", r.end);
280
+ const lineEnd = input.indexOf("\n", at);
269
281
  try {
270
282
  parsed = JSON.parse(input.slice(lineStart, lineEnd === -1 ? input.length : lineEnd));
271
283
  }
@@ -274,12 +286,60 @@ function locate(input, r, cache) {
274
286
  }
275
287
  cache.set(lineStart, parsed);
276
288
  }
289
+ return parsed;
290
+ }
291
+ function toolForResult(input, id, cache) {
292
+ if (!/^[\w-]{6,}$/.test(id))
293
+ return undefined;
294
+ const at = input.indexOf(`"${id}"`);
295
+ if (at === -1)
296
+ return undefined;
297
+ let name;
298
+ const walk = (node) => {
299
+ if (name || !node || typeof node !== "object")
300
+ return;
301
+ if (Array.isArray(node)) {
302
+ node.forEach(walk);
303
+ return;
304
+ }
305
+ const n = node;
306
+ if ((n.id === id || n.call_id === id || n.toolCallId === id) && typeof n.name === "string" && TOOL_CALL_TYPE.test(typeof n.type === "string" ? n.type : "tool_use")) {
307
+ name = n.name;
308
+ return;
309
+ }
310
+ Object.values(n).forEach(walk);
311
+ };
312
+ walk(lineAt(input, at, cache));
313
+ return name;
314
+ }
315
+ function locate(input, r, cache, exact) {
316
+ const parsed = lineAt(input, r.start, cache);
277
317
  if (parsed === null || typeof parsed !== "object")
278
318
  return { where: "unknown" };
279
- const runs = input.slice(r.start, r.end).match(/[A-Za-z0-9]{8,}/g);
280
- const needle = runs ? runs.reduce((a, b) => (b.length > a.length ? b : a)) : null;
319
+ const runs = exact ? null : input.slice(r.start, r.end).match(/[A-Za-z0-9]{8,}/g);
320
+ const needle = exact ?? (runs ? runs.reduce((a, b) => (b.length > a.length ? b : a)) : null);
281
321
  if (!needle)
282
322
  return { where: "unknown" };
323
+ return classify(parsed, needle, input, cache);
324
+ }
325
+ function locateInLine(line, text) {
326
+ let parsed;
327
+ try {
328
+ parsed = JSON.parse(line);
329
+ }
330
+ catch {
331
+ parsed = null;
332
+ }
333
+ const fileHit = lastMatch(FILE_PATH_FIELD, line);
334
+ const file = fileHit && !fileHit.startsWith("/dev/") ? lastTwo(fileHit).slice(0, 120) : undefined;
335
+ if (parsed === null || typeof parsed !== "object")
336
+ return { where: "unknown", ...(file ? { file } : {}) };
337
+ const runs = text.match(/[A-Za-z0-9]{4,}/g);
338
+ const needle = runs ? runs.reduce((a, b) => (b.length > a.length ? b : a)) : null;
339
+ const loc = needle ? classify(parsed, needle, line, new Map()) : { where: "unknown" };
340
+ return { ...loc, ...(file ? { file } : {}) };
341
+ }
342
+ function classify(parsed, needle, input, cache) {
283
343
  let path = null;
284
344
  const walk = (node, trail) => {
285
345
  if (path)
@@ -314,32 +374,211 @@ function locate(input, r, cache) {
314
374
  return { where: "commit", tool };
315
375
  return tool ? { where: "tool_call", tool } : { where: "tool_call" };
316
376
  }
317
- if (TOOL_OUTPUT_TYPE.test(type))
318
- return tool ? { where: "tool_output", tool } : { where: "tool_output" };
319
- if (n.role === "user" || (type === "user" && i === 0))
320
- return { where: "prompt" };
377
+ if (TOOL_OUTPUT_TYPE.test(type)) {
378
+ const id = n.tool_use_id ?? n.call_id ?? n.toolCallId;
379
+ const named = tool ?? (typeof id === "string" ? toolForResult(input, id, cache) : undefined);
380
+ return named ? { where: "tool_output", tool: named } : { where: "tool_output" };
381
+ }
382
+ if (n.role === "user" || (type === "user" && i === 0)) {
383
+ const line = frames[0].node;
384
+ if (frames.some((f) => f.key === "toolUseResult")) {
385
+ const content = line.message?.content;
386
+ const id = Array.isArray(content) ? content.find((c) => c && typeof c === "object" && "tool_use_id" in c)?.tool_use_id : undefined;
387
+ const named = typeof id === "string" ? toolForResult(input, id, cache) : undefined;
388
+ return named ? { where: "tool_output", tool: named } : { where: "tool_output" };
389
+ }
390
+ if (line.isMeta === true || line.isCompactSummary === true)
391
+ return { where: "attached" };
392
+ const text = frames[frames.length - 1].node;
393
+ if (typeof text === "string" && (text.length >= PASTE_CHARS || text.split("\n").length >= PASTE_LINES))
394
+ return { where: "pasted" };
395
+ return { where: "typed" };
396
+ }
321
397
  if (n.role === "assistant" || (type === "assistant" && i === 0))
322
398
  return { where: "assistant" };
323
399
  }
324
400
  return { where: "unknown" };
325
401
  }
402
+ function locateMarkers(scrubbed, rule) {
403
+ const marker = `[REDACTED:${rule}]`;
404
+ const cache = new Map();
405
+ const out = [];
406
+ for (let at = scrubbed.indexOf(marker); at !== -1; at = scrubbed.indexOf(marker, at + marker.length)) {
407
+ const loc = locate(scrubbed, { rule, start: at, end: at + marker.length }, cache, marker);
408
+ if (!out.some((l) => l.where === loc.where && l.tool === loc.tool))
409
+ out.push(loc);
410
+ if (out.length >= 16)
411
+ break;
412
+ }
413
+ return out;
414
+ }
415
+ exports.SHAPE_RULES = new Set([
416
+ "secret-assignment", "bearer-token", "jwt", "pem-private-key", "url-password", "curl-auth-header", "curl-auth-user",
417
+ ]);
418
+ const FILLER_WORD = /secret|password|passwd|passw|pass|pwd|token|apikey|key|auth|bearer|basic|api|app|access|refresh|client|user|name|admin|root|guest|test(?:ing)?|dummy|sample|example|demo|default|change(?:me|it)?|service|local(?:host)?|super|your|mine|my|the|new|old|temp|tmp|dev|prod|stag(?:ing)?|value|string|here|foo|bar|baz|qux|private|public|hello|world|welcome|letmein/gi;
419
+ const LEET = { "0": "o", "1": "l", "3": "e", "4": "a", "5": "s", "7": "t", "@": "a", $: "s" };
420
+ function madeOfFillerWords(v) {
421
+ const residue = (t) => {
422
+ FILLER_WORD.lastIndex = 0;
423
+ if (!FILLER_WORD.test(t))
424
+ return Infinity;
425
+ return t.replace(FILLER_WORD, "").replace(/[^A-Za-z]/g, "").length;
426
+ };
427
+ return residue(v) <= 3 || residue(v.replace(/[013457@$]/g, (c) => LEET[c])) <= 3;
428
+ }
429
+ function hasCountingRuns(v) {
430
+ const t = v;
431
+ const need = t.length > 24 ? 3 : 2;
432
+ let runs = 0;
433
+ for (let i = 0; i + 2 < t.length; i++) {
434
+ const a = t.charCodeAt(i);
435
+ if (!/[A-Za-z0-9]/.test(t[i]) || t.charCodeAt(i + 1) !== a + 1 || t.charCodeAt(i + 2) !== a + 2)
436
+ continue;
437
+ if (++runs >= need)
438
+ return true;
439
+ i += 2;
440
+ }
441
+ return false;
442
+ }
443
+ const WORD_PAIRS = new Set(("ab ac ad ae ag ai ak al am an ao ap ar as at au av aw ax ay az ba be bi bj bl bo br bs bu by ca ce ch ci ck cl co " +
444
+ "cr cs ct cu cy da db de dg di dm do dr ds du dw dy ea eb ec ed ee ef eg ei el em en eo ep er es et eu ev ew ex ey " +
445
+ "fa fe ff fi fl fo fr ft fu ga ge gh gi gl gn go gr gs gu gy ha hc he hi ho hs ht hu ia ib ic id ie if ig il im in " +
446
+ "io ip ir is it iu iv ix iz ja je jo ju ka ke ki kn ko ks ku la ld le lf li lk ll lm lo lp ls lt lu lv ly ma mb me " +
447
+ "mi mm mo mp ms mu my na nc nd ne nf ng ni nk nm nn no ns nt nv ny oa ob oc od oe of og oi ok ol om on oo op or os " +
448
+ "ot ou ov ow ox oz pa pe ph pi pl po pp pr ps pt pu py qu ra rc rd re rg ri rk rl rm rn ro rr rs rt ru ry sa sc se " +
449
+ "sh si sk sl sm sn so sp ss st su sw sy ta te th ti tl to tr ts tt tu tw ty ua ub uc ud ue ug ui ul um un uo up ur " +
450
+ "us ut va ve vi vo vu wa we wh wi wo ws xp xt ya yc ye yl ym yn yo yp ys yt za ze zi zo").split(" "));
451
+ function readsAsWord(t) {
452
+ if (t.length < 3 || !/^(?:[a-z]+|[A-Z]+|[A-Z][a-z]+)$/.test(t))
453
+ return false;
454
+ const l = t.toLowerCase();
455
+ if (!/[aeiouy]/.test(l))
456
+ return false;
457
+ let odd = 0;
458
+ for (let i = 0; i + 1 < l.length; i++)
459
+ if (!WORD_PAIRS.has(l.slice(i, i + 2)))
460
+ odd++;
461
+ return odd <= (l.length >= 7 ? 1 : 0);
462
+ }
463
+ function namesASecret(v) {
464
+ if (/^arn:[a-z-]+:/i.test(v))
465
+ return true;
466
+ if (/^(?:vault|ssm|secretsmanager|keyvault|op|ref|env|file):/i.test(v))
467
+ return true;
468
+ if (/(?:^|\/)secrets?\//i.test(v))
469
+ return true;
470
+ const tokens = v
471
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
472
+ .replace(/([A-Za-z])(\d)/g, "$1 $2")
473
+ .replace(/(\d)([A-Za-z])/g, "$1 $2")
474
+ .split(/[^A-Za-z0-9]+/)
475
+ .filter(Boolean);
476
+ let total = 0;
477
+ let letters = 0;
478
+ let inWords = 0;
479
+ for (const t of tokens) {
480
+ total += t.length;
481
+ if (/^\d+$/.test(t))
482
+ continue;
483
+ letters += t.length;
484
+ if (readsAsWord(t))
485
+ inWords += t.length;
486
+ }
487
+ return inWords >= 6 && inWords / letters >= 0.7 && letters / total >= 0.5;
488
+ }
489
+ const DAY_MS = 24 * 60 * 60 * 1000;
490
+ function jwtNotWorthRotating(v, now) {
491
+ const parts = v.split(".");
492
+ if (parts.length < 3)
493
+ return true;
494
+ let payload;
495
+ try {
496
+ payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
497
+ }
498
+ catch {
499
+ return true;
500
+ }
501
+ if (!payload || typeof payload.exp !== "number")
502
+ return false;
503
+ if (payload.exp * 1000 < now + DAY_MS)
504
+ return true;
505
+ return typeof payload.iat === "number" && (payload.exp - payload.iat) * 1000 <= DAY_MS;
506
+ }
507
+ function pemBody(block) {
508
+ return block
509
+ .replace(/-----BEGIN[A-Z ]*PRIVATE KEY-----/, "")
510
+ .split("-----END")[0]
511
+ .split(/\\r|\\n|\r|\n/)
512
+ .filter((l) => !l.includes(":"))
513
+ .join("")
514
+ .replace(/\s+/g, "");
515
+ }
516
+ function notWorthRotating(k, now = Date.now()) {
517
+ const { rule, name, file } = k;
518
+ const value = k.value.trim();
519
+ if (looksLikeTestValue(value))
520
+ return true;
521
+ if (exports.SHAPE_RULES.has(rule) && file && (0, exports.isTestFile)(file))
522
+ return true;
523
+ if (rule === "pem-private-key") {
524
+ const body = pemBody(value);
525
+ return body.length < 60 || !/^[A-Za-z0-9+/=]+$/.test(body) || shannonEntropy(body) < 4;
526
+ }
527
+ if (PLACEHOLDER.test(value))
528
+ return true;
529
+ if (hasCountingRuns(value))
530
+ return true;
531
+ if (rule === "aws-access-key" && value.startsWith("ASIA"))
532
+ return true;
533
+ if (name && /(?:session|security)[_-]?token$/i.test(name))
534
+ return true;
535
+ if (/^(?:IQoJ|FwoG|FQoG|AQoD)[A-Za-z0-9+/]{20,}/.test(value))
536
+ return true;
537
+ if (value.startsWith("eyJ"))
538
+ return jwtNotWorthRotating(value, now);
539
+ if (!exports.SHAPE_RULES.has(rule))
540
+ return false;
541
+ if (name && /^x-atlassian-token$/i.test(name))
542
+ return true;
543
+ let secret = value;
544
+ if (rule === "curl-auth-user") {
545
+ const at = value.indexOf(":");
546
+ const user = value.slice(0, at).replace(/["']/g, "");
547
+ secret = value.slice(at + 1).replace(/["']/g, "");
548
+ if (at < 0 || !secret || secret === user)
549
+ return true;
550
+ }
551
+ if (rule === "url-password" && name && secret === name)
552
+ return true;
553
+ if (secret.includes("$"))
554
+ return true;
555
+ if (/^[A-Za-z._-]+$/.test(secret))
556
+ return true;
557
+ if (/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/.test(secret))
558
+ return true;
559
+ if (madeOfFillerWords(secret))
560
+ return true;
561
+ if (rule === "secret-assignment" && namesASecret(secret))
562
+ return true;
563
+ return rule !== "url-password" && shannonEntropy(secret) < 3;
564
+ }
565
+ function hintOf(rule, value) {
566
+ const body = rule === "pem-private-key" ? value.split("-----END")[0].replace(/\\[nr]/g, "") : value;
567
+ return body.replace(/[^A-Za-z0-9]/g, "").slice(-4);
568
+ }
326
569
  const MAX_LOCATIONS = 8;
327
570
  function secretFindings(input, redactions, opts = {}) {
328
571
  const seen = new Map();
329
572
  const cache = new Map();
330
573
  for (const r of redactions) {
331
- let value = input.slice(r.start, r.end);
332
- if (r.rule === "pem-private-key")
333
- value = value.split("-----END")[0];
334
- const hint = value.replace(/[^A-Za-z0-9]/g, "").slice(-4);
574
+ const value = input.slice(r.start, r.end);
575
+ const hint = hintOf(r.rule, value);
335
576
  if (!hint)
336
577
  continue;
337
578
  const lineStart = input.lastIndexOf("\n", r.start - 1) + 1;
338
579
  const name = nameBefore(input, r.start, lineStart);
339
580
  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))
581
+ if (notWorthRotating({ rule: r.rule, value, ...(name ? { name } : {}), ...(file ? { file } : {}) }, opts.now))
343
582
  continue;
344
583
  if (opts.skip?.({ rule: r.rule, value, ...(name ? { name } : {}), ...(file ? { file } : {}) }))
345
584
  continue;
@@ -4,7 +4,7 @@ exports.TEAM_SKILL_CONTEXT = void 0;
4
4
  exports.teamSkillSessionOutput = teamSkillSessionOutput;
5
5
  exports.TEAM_SKILL_CONTEXT = `This machine is connected to Codiedev, the team's artifact and skill library.
6
6
  Before a substantial coding task, run codiedev search "<brief task description>" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.
7
- Use codiedev push <file.md> to share an artifact when asked, codiedev ask "<question>" for team knowledge, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.`;
7
+ Use codiedev push <file.md> to share an artifact when asked, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.`;
8
8
  function teamSkillSessionOutput(source, context = exports.TEAM_SKILL_CONTEXT) {
9
9
  return source === "cursor" ? { additional_context: context } : { additionalContext: context };
10
10
  }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.UNREAD_NOTICE_INTERVAL_MS = void 0;
7
+ exports.parseUnreadPings = parseUnreadPings;
8
+ exports.unreadNoticeLine = unreadNoticeLine;
9
+ exports.takeUnreadNotice = takeUnreadNotice;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const configDir_1 = require("./configDir");
13
+ exports.UNREAD_NOTICE_INTERVAL_MS = 20 * 60 * 60 * 1000;
14
+ const STATE_FILE = "unread-notice.json";
15
+ function parseUnreadPings(value) {
16
+ if (!value || typeof value !== "object")
17
+ return null;
18
+ const v = value;
19
+ if (typeof v.count !== "number" || !Number.isFinite(v.count) || v.count <= 0)
20
+ return null;
21
+ return {
22
+ count: Math.floor(v.count),
23
+ newestPingId: typeof v.newestPingId === "string" ? v.newestPingId : null,
24
+ from: Array.isArray(v.from) ? v.from.filter((n) => typeof n === "string" && n.trim() !== "") : [],
25
+ senderCount: typeof v.senderCount === "number" ? v.senderCount : undefined,
26
+ };
27
+ }
28
+ function nameList(from, senderCount) {
29
+ if (from.length === 0)
30
+ return "";
31
+ const others = Math.max(0, senderCount - from.length);
32
+ const names = others > 0 ? [...from, `${others} other${others === 1 ? "" : "s"}`] : from;
33
+ if (names.length === 1)
34
+ return ` from ${names[0]}`;
35
+ return ` from ${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
36
+ }
37
+ function unreadNoticeLine(u) {
38
+ const who = nameList(u.from, u.senderCount ?? u.from.length);
39
+ return (`Codiedev: ${u.count} unread message${u.count === 1 ? "" : "s"}${who}. ` +
40
+ "Tell the user at the start of your first reply, and offer to show them with `codiedev inbox --unread`. " +
41
+ 'Answer one with `codiedev reply <ping-id> "<message>"`.');
42
+ }
43
+ function readState(dir) {
44
+ try {
45
+ const raw = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, STATE_FILE), "utf8"));
46
+ if (typeof raw?.at !== "number")
47
+ return null;
48
+ return { newestPingId: typeof raw.newestPingId === "string" ? raw.newestPingId : null, at: raw.at };
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function takeUnreadNotice(value, options = {}) {
55
+ try {
56
+ const unread = parseUnreadPings(value);
57
+ if (!unread)
58
+ return null;
59
+ const dir = options.dir ?? (0, configDir_1.codiedevStateDir)();
60
+ const now = options.now ?? Date.now();
61
+ const last = readState(dir);
62
+ const sameNewest = !!last && last.newestPingId === unread.newestPingId;
63
+ if (sameNewest && last.at <= now && now - last.at < exports.UNREAD_NOTICE_INTERVAL_MS)
64
+ return null;
65
+ fs_1.default.mkdirSync(dir, { recursive: true });
66
+ fs_1.default.writeFileSync(path_1.default.join(dir, STATE_FILE), JSON.stringify({ newestPingId: unread.newestPingId, at: now }));
67
+ return unreadNoticeLine(unread);
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }