chron-mcp 0.1.30 → 0.1.31

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/index.js CHANGED
@@ -6162,7 +6162,7 @@ var require_websocket = __commonJS({
6162
6162
  var http = require("http");
6163
6163
  var net = require("net");
6164
6164
  var tls = require("tls");
6165
- var { randomBytes, createHash: createHash6 } = require("crypto");
6165
+ var { randomBytes, createHash: createHash7 } = require("crypto");
6166
6166
  var { Duplex, Readable } = require("stream");
6167
6167
  var { URL: URL2 } = require("url");
6168
6168
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -6842,7 +6842,7 @@ var require_websocket = __commonJS({
6842
6842
  abortHandshake(websocket, socket, "Invalid Upgrade header");
6843
6843
  return;
6844
6844
  }
6845
- const digest = createHash6("sha1").update(key + GUID).digest("base64");
6845
+ const digest = createHash7("sha1").update(key + GUID).digest("base64");
6846
6846
  if (res.headers["sec-websocket-accept"] !== digest) {
6847
6847
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
6848
6848
  return;
@@ -7229,7 +7229,7 @@ var require_websocket_server = __commonJS({
7229
7229
  var EventEmitter = require("events");
7230
7230
  var http = require("http");
7231
7231
  var { Duplex } = require("stream");
7232
- var { createHash: createHash6 } = require("crypto");
7232
+ var { createHash: createHash7 } = require("crypto");
7233
7233
  var extension2 = require_extension();
7234
7234
  var PerMessageDeflate2 = require_permessage_deflate();
7235
7235
  var subprotocol2 = require_subprotocol();
@@ -7538,7 +7538,7 @@ var require_websocket_server = __commonJS({
7538
7538
  }
7539
7539
  if (this._state > RUNNING)
7540
7540
  return abortHandshake(socket, 503);
7541
- const digest = createHash6("sha1").update(key + GUID).digest("base64");
7541
+ const digest = createHash7("sha1").update(key + GUID).digest("base64");
7542
7542
  const headers = [
7543
7543
  "HTTP/1.1 101 Switching Protocols",
7544
7544
  "Upgrade: websocket",
@@ -16526,10 +16526,11 @@ var init_libsql = __esm({
16526
16526
  var schema_exports = {};
16527
16527
  __export(schema_exports, {
16528
16528
  messages: () => messages,
16529
+ review_findings: () => review_findings,
16529
16530
  secrets_detected: () => secrets_detected,
16530
16531
  sessions: () => sessions
16531
16532
  });
16532
- var sessions, messages, secrets_detected;
16533
+ var sessions, messages, secrets_detected, review_findings;
16533
16534
  var init_schema = __esm({
16534
16535
  "src/db/schema.ts"() {
16535
16536
  "use strict";
@@ -16565,6 +16566,16 @@ var init_schema = __esm({
16565
16566
  masked_value: text("masked_value").notNull(),
16566
16567
  detected_at: text("detected_at").notNull()
16567
16568
  });
16569
+ review_findings = sqliteTable("review_findings", {
16570
+ id: text("id").primaryKey(),
16571
+ rule_id: text("rule_id").notNull(),
16572
+ session_id: text("session_id").notNull().references(() => sessions.id),
16573
+ status: text("status", { enum: ["open", "accepted", "dismissed", "resolved"] }).notNull(),
16574
+ note: text("note"),
16575
+ reviewed_at: text("reviewed_at"),
16576
+ created_at: text("created_at").notNull(),
16577
+ updated_at: text("updated_at").notNull()
16578
+ });
16568
16579
  }
16569
16580
  });
16570
16581
 
@@ -16668,11 +16679,24 @@ var init_db2 = __esm({
16668
16679
  detected_at TEXT NOT NULL,
16669
16680
  FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
16670
16681
  FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
16682
+ )`,
16683
+ `CREATE TABLE IF NOT EXISTS review_findings (
16684
+ id TEXT PRIMARY KEY,
16685
+ rule_id TEXT NOT NULL,
16686
+ session_id TEXT NOT NULL,
16687
+ status TEXT NOT NULL CHECK (status IN ('open', 'accepted', 'dismissed', 'resolved')),
16688
+ note TEXT,
16689
+ reviewed_at TEXT,
16690
+ created_at TEXT NOT NULL,
16691
+ updated_at TEXT NOT NULL,
16692
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
16671
16693
  )`,
16672
16694
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title ON sessions(title)`,
16673
16695
  `CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)`,
16674
16696
  `CREATE INDEX IF NOT EXISTS idx_secrets_detected_session_id ON secrets_detected(session_id)`,
16675
16697
  `CREATE INDEX IF NOT EXISTS idx_secrets_detected_message_id ON secrets_detected(message_id)`,
16698
+ `CREATE INDEX IF NOT EXISTS idx_review_findings_session_id ON review_findings(session_id)`,
16699
+ `CREATE INDEX IF NOT EXISTS idx_review_findings_status ON review_findings(status)`,
16676
16700
  `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
16677
16701
  content,
16678
16702
  session_id UNINDEXED,
@@ -17486,7 +17510,7 @@ var require_package = __commonJS({
17486
17510
  "package.json"(exports2, module2) {
17487
17511
  module2.exports = {
17488
17512
  name: "chron-mcp",
17489
- version: "0.1.30",
17513
+ version: "0.1.31",
17490
17514
  mcpName: "io.github.sirinivask/chron",
17491
17515
  description: "Audit-grade timestamped logs for every AI conversation",
17492
17516
  repository: {
@@ -20038,10 +20062,10 @@ var init_rules = __esm({
20038
20062
  id: "soc2.cc6_1.cc6_6.secret_detected",
20039
20063
  framework: "soc2",
20040
20064
  controls: ["CC6.1", "CC6.6"],
20041
- severity: "critical",
20042
- description: "AI session contained a detected secret or credential",
20043
- finding: "A sensitive credential or secret was detected in an AI session.",
20044
- not_claiming: "This is not evidence of a breach. Chron masks all detected values at log time \u2014 no plaintext credentials are stored.",
20065
+ severity: "high",
20066
+ description: "AI session contained detected sensitive data or credentials",
20067
+ finding: "Sensitive data or a credential was detected in an AI session.",
20068
+ not_claiming: "This is not evidence of a breach or credential compromise. Chron masks detected values at log time \u2014 no plaintext values are stored.",
20045
20069
  suggested_evidence: [
20046
20070
  "Confirm no plaintext credential was committed to version control",
20047
20071
  "Rotate the credential if it was a live secret",
@@ -20110,6 +20134,91 @@ var init_rules = __esm({
20110
20134
  }
20111
20135
  });
20112
20136
 
20137
+ // src/review/workflow.ts
20138
+ function findingId(ruleId, sessionId) {
20139
+ return (0, import_crypto7.createHash)("sha256").update(`${ruleId}:${sessionId}`).digest("hex");
20140
+ }
20141
+ async function hydrateFindingStatuses(db, findings) {
20142
+ if (findings.length === 0)
20143
+ return findings;
20144
+ const client = db.$client;
20145
+ const now = (/* @__PURE__ */ new Date()).toISOString();
20146
+ for (const finding of findings) {
20147
+ await client.execute({
20148
+ sql: `INSERT INTO review_findings (id, rule_id, session_id, status, created_at, updated_at)
20149
+ VALUES (?, ?, ?, 'open', ?, ?)
20150
+ ON CONFLICT(id) DO NOTHING`,
20151
+ args: [finding.id, finding.rule_id, finding.session_id, now, now]
20152
+ });
20153
+ }
20154
+ const rows = await loadFindingStates(client, findings.map((f) => f.id));
20155
+ const byId = new Map(rows.map((row) => [row.id, row]));
20156
+ return findings.map((finding) => {
20157
+ const state = byId.get(finding.id);
20158
+ return {
20159
+ ...finding,
20160
+ status: state?.status ?? "open",
20161
+ note: state?.note ?? null,
20162
+ reviewed_at: state?.reviewed_at ?? null
20163
+ };
20164
+ });
20165
+ }
20166
+ async function updateFindingStatus(db, idOrPrefix, status, note) {
20167
+ const client = db.$client;
20168
+ const id = await resolveFindingId(client, idOrPrefix);
20169
+ const reviewedAt = (/* @__PURE__ */ new Date()).toISOString();
20170
+ await client.execute({
20171
+ sql: `UPDATE review_findings
20172
+ SET status = ?, note = ?, reviewed_at = ?, updated_at = ?
20173
+ WHERE id = ?`,
20174
+ args: [status, note ?? null, reviewedAt, reviewedAt, id]
20175
+ });
20176
+ const rows = await loadFindingStates(client, [id]);
20177
+ return rows[0];
20178
+ }
20179
+ async function resolveFindingId(client, idOrPrefix) {
20180
+ const prefix = idOrPrefix.trim();
20181
+ if (!prefix)
20182
+ throw new Error("Finding id is required");
20183
+ const result = await client.execute({
20184
+ sql: `SELECT id FROM review_findings WHERE id LIKE ? ORDER BY id LIMIT 2`,
20185
+ args: [`${prefix}%`]
20186
+ });
20187
+ if (result.rows.length === 0) {
20188
+ throw new Error(`Finding not found: ${prefix}. Run "chron review --framework=soc2" first to register current findings.`);
20189
+ }
20190
+ if (result.rows.length > 1) {
20191
+ throw new Error(`Finding id prefix is ambiguous: ${prefix}. Use more characters.`);
20192
+ }
20193
+ return String(result.rows[0].id);
20194
+ }
20195
+ async function loadFindingStates(client, ids) {
20196
+ if (ids.length === 0)
20197
+ return [];
20198
+ const placeholders = ids.map(() => "?").join(", ");
20199
+ const result = await client.execute({
20200
+ sql: `SELECT id, rule_id, session_id, status, note, reviewed_at
20201
+ FROM review_findings
20202
+ WHERE id IN (${placeholders})`,
20203
+ args: ids
20204
+ });
20205
+ return result.rows.map((row) => ({
20206
+ id: String(row.id),
20207
+ rule_id: String(row.rule_id),
20208
+ session_id: String(row.session_id),
20209
+ status: String(row.status),
20210
+ note: row.note == null ? null : String(row.note),
20211
+ reviewed_at: row.reviewed_at == null ? null : String(row.reviewed_at)
20212
+ }));
20213
+ }
20214
+ var import_crypto7;
20215
+ var init_workflow = __esm({
20216
+ "src/review/workflow.ts"() {
20217
+ "use strict";
20218
+ import_crypto7 = require("crypto");
20219
+ }
20220
+ });
20221
+
20113
20222
  // src/review/engine.ts
20114
20223
  async function runReview(db, rules, since) {
20115
20224
  const client = db.$client;
@@ -20127,6 +20236,27 @@ async function runReview(db, rules, since) {
20127
20236
  );
20128
20237
  return findings;
20129
20238
  }
20239
+ function higherSeverity(a, b) {
20240
+ return (SEVERITY_ORDER[a] ?? 99) <= (SEVERITY_ORDER[b] ?? 99) ? a : b;
20241
+ }
20242
+ function suggestedEvidenceForSecret(type, rule) {
20243
+ const severity = SECRET_SEVERITY[type] ?? rule.severity;
20244
+ if (severity === "medium") {
20245
+ return [
20246
+ "Confirm the personal or contact data was necessary for the AI session",
20247
+ "Verify no unnecessary personal data was committed to version control",
20248
+ "Document handling or remediation if the data was not required"
20249
+ ];
20250
+ }
20251
+ if (severity === "low") {
20252
+ return [
20253
+ "Review whether the detected value contains secret material",
20254
+ "Confirm no plaintext credential was committed to version control",
20255
+ "Document why the value is acceptable or remediate it"
20256
+ ];
20257
+ }
20258
+ return rule.suggested_evidence;
20259
+ }
20130
20260
  async function matchCodeChanges(client, rule, map, since) {
20131
20261
  if (rule.match.type !== "code_change_path")
20132
20262
  return;
@@ -20160,10 +20290,14 @@ async function matchCodeChanges(client, rule, map, since) {
20160
20290
  const evidenceItem = `code_change: ${filePath || "unknown path"}`;
20161
20291
  if (!map.has(mapKey)) {
20162
20292
  map.set(mapKey, {
20293
+ id: findingId(rule.id, sessionId),
20163
20294
  rule_id: rule.id,
20164
20295
  framework: rule.framework,
20165
20296
  controls: rule.controls,
20166
20297
  severity: rule.severity,
20298
+ status: "open",
20299
+ note: null,
20300
+ reviewed_at: null,
20167
20301
  session_id: sessionId,
20168
20302
  session_prefix: sessionId.slice(0, 8),
20169
20303
  session_title: String(row.title ?? ""),
@@ -20200,13 +20334,20 @@ async function matchSecrets(client, rule, map, since) {
20200
20334
  const sessionId = String(row.session_id ?? "");
20201
20335
  const mapKey = `${rule.id}::${sessionId}`;
20202
20336
  const occurredAt = String(row.detected_at ?? "");
20203
- const evidenceItem = `secret_detected: ${row.type} (${row.masked_value})`;
20337
+ const type = String(row.type ?? "");
20338
+ const severity = SECRET_SEVERITY[type] ?? rule.severity;
20339
+ const suggestedEvidence = suggestedEvidenceForSecret(type, rule);
20340
+ const evidenceItem = `secret_detected: ${type} (${row.masked_value})`;
20204
20341
  if (!map.has(mapKey)) {
20205
20342
  map.set(mapKey, {
20343
+ id: findingId(rule.id, sessionId),
20206
20344
  rule_id: rule.id,
20207
20345
  framework: rule.framework,
20208
20346
  controls: rule.controls,
20209
- severity: rule.severity,
20347
+ severity,
20348
+ status: "open",
20349
+ note: null,
20350
+ reviewed_at: null,
20210
20351
  session_id: sessionId,
20211
20352
  session_prefix: sessionId.slice(0, 8),
20212
20353
  session_title: String(row.title ?? ""),
@@ -20216,10 +20357,15 @@ async function matchSecrets(client, rule, map, since) {
20216
20357
  last_occurred_at: occurredAt,
20217
20358
  finding: rule.finding,
20218
20359
  not_claiming: rule.not_claiming,
20219
- suggested_evidence: rule.suggested_evidence
20360
+ suggested_evidence: suggestedEvidence
20220
20361
  });
20221
20362
  }
20222
20363
  const finding = map.get(mapKey);
20364
+ const previousSeverity = finding.severity;
20365
+ finding.severity = higherSeverity(finding.severity, severity);
20366
+ if (finding.severity !== previousSeverity || finding.suggested_evidence.length === 0) {
20367
+ finding.suggested_evidence = suggestedEvidence;
20368
+ }
20223
20369
  if (!finding.evidence_items.includes(evidenceItem)) {
20224
20370
  finding.evidence_items.push(evidenceItem);
20225
20371
  }
@@ -20227,11 +20373,38 @@ async function matchSecrets(client, rule, map, since) {
20227
20373
  finding.last_occurred_at = occurredAt;
20228
20374
  }
20229
20375
  }
20230
- var SEVERITY_ORDER;
20376
+ var SEVERITY_ORDER, SECRET_SEVERITY;
20231
20377
  var init_engine = __esm({
20232
20378
  "src/review/engine.ts"() {
20233
20379
  "use strict";
20380
+ init_workflow();
20234
20381
  SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
20382
+ SECRET_SEVERITY = {
20383
+ private_key: "critical",
20384
+ credit_card: "critical",
20385
+ ssn: "critical",
20386
+ iban: "critical",
20387
+ aws_access_key: "high",
20388
+ anthropic_api_key: "high",
20389
+ openai_api_key: "high",
20390
+ google_api_key: "high",
20391
+ github_token: "high",
20392
+ slack_token: "high",
20393
+ stripe_key: "high",
20394
+ sendgrid_key: "high",
20395
+ huggingface_token: "high",
20396
+ jwt: "high",
20397
+ url_credentials: "high",
20398
+ password: "high",
20399
+ credential_pair: "high",
20400
+ passport: "high",
20401
+ dob: "high",
20402
+ email: "medium",
20403
+ phone_us: "medium",
20404
+ phone_e164: "medium",
20405
+ internal_ip: "low",
20406
+ env_value: "low"
20407
+ };
20235
20408
  }
20236
20409
  });
20237
20410
 
@@ -20240,16 +20413,22 @@ var review_exports = {};
20240
20413
  __export(review_exports, {
20241
20414
  runReview: () => runReview2
20242
20415
  });
20243
- function printFindings(findings, framework, sessionCount) {
20416
+ function printFindings(findings, framework, sessionCount, showAll) {
20244
20417
  const controlsHit = new Set(findings.flatMap((f) => f.controls));
20245
- process.stdout.write(`
20246
- ${BOLD10}Chron Review${RESET10} ${CYAN8}${framework.toUpperCase()}${RESET10} ${DIM9}${sessionCount} session(s) reviewed${RESET10}
20418
+ process.stdout.write(
20419
+ `
20420
+ ${BOLD10}Chron Review${RESET10} ${CYAN8}${framework.toUpperCase()}${RESET10} ${DIM9}${sessionCount} session(s) reviewed${showAll ? " \xB7 all statuses" : " \xB7 open findings"}${RESET10}
20247
20421
 
20248
- `);
20422
+ `
20423
+ );
20249
20424
  if (findings.length === 0) {
20250
- process.stdout.write(`${DIM9}No findings. No sessions matched any control review criteria.${RESET10}
20425
+ process.stdout.write(
20426
+ showAll ? `${DIM9}No findings. No sessions matched any control review criteria.${RESET10}
20251
20427
 
20252
- `);
20428
+ ` : `${DIM9}No open findings. Use --all to include accepted, dismissed, and resolved findings.${RESET10}
20429
+
20430
+ `
20431
+ );
20253
20432
  return;
20254
20433
  }
20255
20434
  process.stdout.write(
@@ -20265,12 +20444,16 @@ It is not a certification of compliance or evidence of any violation.${RESET10}
20265
20444
  );
20266
20445
  for (const f of findings) {
20267
20446
  const sevColor = SEV_COLOR[f.severity] ?? DIM9;
20447
+ const statusColor = STATUS_COLOR[f.status] ?? DIM9;
20268
20448
  const controls = f.controls.join(", ");
20269
20449
  const sev = f.severity.toUpperCase().padEnd(8);
20450
+ const status = f.status.toUpperCase().padEnd(9);
20270
20451
  process.stdout.write(
20271
- ` ${sevColor}${BOLD10}${sev}${RESET10} ${MAGENTA2}${controls.padEnd(16)}${RESET10} ${CYAN8}${f.session_prefix}${RESET10} ${f.session_title}
20452
+ ` ${sevColor}${BOLD10}${sev}${RESET10} ${statusColor}${status}${RESET10} ${MAGENTA2}${controls.padEnd(16)}${RESET10} ${CYAN8}${f.session_prefix}${RESET10} ${f.session_title}
20272
20453
  `
20273
20454
  );
20455
+ process.stdout.write(` ${DIM9}id: ${f.id.slice(0, 16)}${RESET10}
20456
+ `);
20274
20457
  process.stdout.write(` ${f.finding}
20275
20458
  `);
20276
20459
  for (const item of f.evidence_items.slice(0, 5)) {
@@ -20282,6 +20465,9 @@ It is not a certification of compliance or evidence of any violation.${RESET10}
20282
20465
  `);
20283
20466
  }
20284
20467
  process.stdout.write(` ${DIM9}Suggested: ${f.suggested_evidence[0]}${RESET10}
20468
+ `);
20469
+ if (f.note)
20470
+ process.stdout.write(` ${DIM9}Note: ${f.note}${RESET10}
20285
20471
  `);
20286
20472
  process.stdout.write("\n");
20287
20473
  }
@@ -20292,24 +20478,31 @@ function esc2(s) {
20292
20478
  function badgeHtml(severity) {
20293
20479
  return `<span class="badge badge-${esc2(severity)}">${esc2(severity.toUpperCase())}</span>`;
20294
20480
  }
20481
+ function statusBadgeHtml(status) {
20482
+ return `<span class="status-badge status-${esc2(status)}">${esc2(status.toUpperCase())}</span>`;
20483
+ }
20295
20484
  function buildReviewHtml(params) {
20296
20485
  const { framework, host, generatedAt, sessionCount, findings } = params;
20297
20486
  const controlsHit = new Set(findings.flatMap((f) => f.controls));
20298
20487
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
20299
20488
  for (const f of findings)
20300
20489
  bySeverity[f.severity] = (bySeverity[f.severity] ?? 0) + 1;
20301
- const findingCards = findings.length === 0 ? '<p style="color:#6b7280">No findings. No sessions matched any control review criteria.</p>' : findings.map((f) => `
20490
+ const findingCards = findings.length === 0 ? '<p style="color:#6b7280">No findings matched this report scope.</p>' : findings.map((f) => `
20302
20491
  <div class="finding-card">
20303
20492
  <div class="finding-header">
20304
20493
  ${badgeHtml(f.severity)}
20494
+ ${statusBadgeHtml(f.status)}
20305
20495
  <span class="finding-controls">${esc2(f.controls.join(", "))}</span>
20306
20496
  <span style="flex:1"></span>
20307
20497
  <span class="finding-session">${esc2(f.session_prefix)}</span>
20308
20498
  <span style="font-size:12px;color:#374151">${esc2(f.session_title)}</span>
20309
20499
  </div>
20310
20500
  <div class="finding-body">
20501
+ <div style="font-family:monospace;font-size:11px;color:#6b7280;margin-bottom:5px">Finding ID: ${esc2(f.id)}</div>
20311
20502
  <div class="finding-text">${esc2(f.finding)}</div>
20312
20503
  <div class="finding-disclaimer">${esc2(f.not_claiming)}</div>
20504
+ ${f.note ? `<div style="font-size:11px;color:#374151;margin-bottom:8px"><strong>Reviewer note:</strong> ${esc2(f.note)}</div>` : ""}
20505
+ ${f.reviewed_at ? `<div style="font-size:11px;color:#6b7280;margin-bottom:8px">Reviewed: ${esc2(f.reviewed_at)}</div>` : ""}
20313
20506
  <strong style="font-size:11px;color:#374151">Evidence in session:</strong>
20314
20507
  <ul class="evidence-list">
20315
20508
  ${f.evidence_items.map((e) => `<li>${esc2(e)}</li>`).join("")}
@@ -20395,17 +20588,28 @@ ${findingCards}
20395
20588
  </body>
20396
20589
  </html>`;
20397
20590
  }
20398
- async function countSessions(db) {
20591
+ async function countSessions(db, since) {
20399
20592
  const client = db.$client;
20400
- const result = await client.execute("SELECT COUNT(*) AS n FROM sessions");
20593
+ const result = await client.execute(
20594
+ since ? {
20595
+ sql: `SELECT COUNT(*) AS n FROM sessions WHERE updated_at >= ?`,
20596
+ args: [since]
20597
+ } : "SELECT COUNT(*) AS n FROM sessions"
20598
+ );
20401
20599
  return Number(result.rows[0].n ?? 0);
20402
20600
  }
20403
20601
  async function runReview2(args2) {
20602
+ const action = args2[0];
20603
+ if (action === "accept" || action === "dismiss" || action === "resolve") {
20604
+ await runReviewAction(action, args2.slice(1));
20605
+ return;
20606
+ }
20404
20607
  const frameworkArg = args2.find((a) => a.startsWith("--framework="))?.slice("--framework=".length)?.toLowerCase();
20405
20608
  const outputArg = args2.find((a) => a.startsWith("--output="))?.slice("--output=".length);
20406
20609
  const sinceArg = args2.find((a) => a.startsWith("--since="))?.slice("--since=".length);
20610
+ const showAll = args2.includes("--all");
20407
20611
  if (!frameworkArg) {
20408
- process.stderr.write("Usage: chron review --framework=soc2 [--since=<range>] [--output=<file>]\n");
20612
+ process.stderr.write("Usage: chron review --framework=soc2 [--since=<range>] [--all] [--output=<file>]\n");
20409
20613
  process.exit(1);
20410
20614
  }
20411
20615
  const rules = FRAMEWORKS[frameworkArg];
@@ -20425,11 +20629,13 @@ async function runReview2(args2) {
20425
20629
  }
20426
20630
  }
20427
20631
  const db = await initDb();
20428
- const [findings, sessionCount] = await Promise.all([
20632
+ const [rawFindings, sessionCount] = await Promise.all([
20429
20633
  runReview(db, rules, since),
20430
- countSessions(db)
20634
+ countSessions(db, since)
20431
20635
  ]);
20432
- printFindings(findings, frameworkArg, sessionCount);
20636
+ const allFindings = await hydrateFindingStatuses(db, rawFindings);
20637
+ const findings = showAll ? allFindings : allFindings.filter((f) => f.status === "open");
20638
+ printFindings(findings, frameworkArg, sessionCount, showAll);
20433
20639
  if (outputArg) {
20434
20640
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19) + " UTC";
20435
20641
  const html = buildReviewHtml({
@@ -20444,7 +20650,34 @@ async function runReview2(args2) {
20444
20650
  `);
20445
20651
  }
20446
20652
  }
20447
- var import_fs12, import_os11, RESET10, BOLD10, DIM9, RED5, YELLOW8, CYAN8, MAGENTA2, SEV_COLOR, REPORT_CSS;
20653
+ async function runReviewAction(action, args2) {
20654
+ const id = args2.find((a) => !a.startsWith("--"));
20655
+ const note = args2.find((a) => a.startsWith("--note="))?.slice("--note=".length);
20656
+ if (!id) {
20657
+ process.stderr.write(`Usage: chron review ${action} <finding-id> [--note=<text>]
20658
+ `);
20659
+ process.exit(1);
20660
+ }
20661
+ const statusByAction = {
20662
+ accept: "accepted",
20663
+ dismiss: "dismissed",
20664
+ resolve: "resolved"
20665
+ };
20666
+ const status = statusByAction[action];
20667
+ const db = await initDb();
20668
+ try {
20669
+ const row = await updateFindingStatus(db, id, status, note);
20670
+ process.stdout.write(
20671
+ `Finding ${row.id.slice(0, 16)} ${status}${note ? ` with note: ${note}` : ""}
20672
+ `
20673
+ );
20674
+ } catch (err) {
20675
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
20676
+ `);
20677
+ process.exit(1);
20678
+ }
20679
+ }
20680
+ var import_fs12, import_os11, RESET10, BOLD10, DIM9, RED5, YELLOW8, CYAN8, MAGENTA2, SEV_COLOR, STATUS_COLOR, REPORT_CSS;
20448
20681
  var init_review = __esm({
20449
20682
  "src/cli/review.ts"() {
20450
20683
  "use strict";
@@ -20454,6 +20687,7 @@ var init_review = __esm({
20454
20687
  init_report();
20455
20688
  init_rules();
20456
20689
  init_engine();
20690
+ init_workflow();
20457
20691
  RESET10 = "\x1B[0m";
20458
20692
  BOLD10 = "\x1B[1m";
20459
20693
  DIM9 = "\x1B[2m";
@@ -20467,6 +20701,12 @@ var init_review = __esm({
20467
20701
  medium: CYAN8,
20468
20702
  low: DIM9
20469
20703
  };
20704
+ STATUS_COLOR = {
20705
+ open: YELLOW8,
20706
+ accepted: CYAN8,
20707
+ dismissed: DIM9,
20708
+ resolved: CYAN8
20709
+ };
20470
20710
  REPORT_CSS = `
20471
20711
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
20472
20712
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; color: #111; background: #fff; line-height: 1.5; }
@@ -20490,6 +20730,11 @@ tr:nth-child(even) td { background: #fafafa; }
20490
20730
  .badge-high { background:#d97706; }
20491
20731
  .badge-medium { background:#2563eb; }
20492
20732
  .badge-low { background:#6b7280; }
20733
+ .status-badge { display:inline-block; padding:1px 7px; border-radius:3px; font-size:11px; font-weight:600; border:1px solid #d1d5db; color:#374151; background:#fff; }
20734
+ .status-open { border-color:#f59e0b; color:#92400e; background:#fffbeb; }
20735
+ .status-accepted { border-color:#38bdf8; color:#075985; background:#f0f9ff; }
20736
+ .status-dismissed { border-color:#d1d5db; color:#6b7280; background:#f9fafb; }
20737
+ .status-resolved { border-color:#34d399; color:#065f46; background:#ecfdf5; }
20493
20738
  .disclaimer { background: #fffbeb; border: 1px solid #fde68a; border-left: 4px solid #f59e0b; border-radius: 4px; padding: 12px 14px; margin: 14px 0 20px; font-size: 12px; color: #374151; }
20494
20739
  .finding-card { border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 14px; overflow: hidden; }
20495
20740
  .finding-header { display:flex; align-items:center; gap:10px; padding:10px 14px; background:#f9fafb; border-bottom:1px solid #e5e7eb; }
@@ -20642,7 +20887,11 @@ Options (import):
20642
20887
  Options (review):
20643
20888
  --framework=<name> Framework to review against: soc2
20644
20889
  --since=<range> Limit to sessions since: 7d, 30d, or YYYY-MM-DD
20890
+ --all Include accepted, dismissed, and resolved findings
20645
20891
  --output=<file> Write HTML report to file (printable to PDF from browser)
20892
+ accept <id> Mark a finding as accepted; supports --note=<text>
20893
+ dismiss <id> Mark a finding as dismissed; supports --note=<text>
20894
+ resolve <id> Mark a finding as resolved; supports --note=<text>
20646
20895
  `
20647
20896
  );
20648
20897
  process.exit(command ? 1 : 0);
package/dist/index.js CHANGED
@@ -22736,10 +22736,11 @@ var init_libsql = __esm({
22736
22736
  var schema_exports = {};
22737
22737
  __export(schema_exports, {
22738
22738
  messages: () => messages,
22739
+ review_findings: () => review_findings,
22739
22740
  secrets_detected: () => secrets_detected,
22740
22741
  sessions: () => sessions
22741
22742
  });
22742
- var sessions, messages, secrets_detected;
22743
+ var sessions, messages, secrets_detected, review_findings;
22743
22744
  var init_schema = __esm({
22744
22745
  "src/db/schema.ts"() {
22745
22746
  "use strict";
@@ -22775,6 +22776,16 @@ var init_schema = __esm({
22775
22776
  masked_value: text("masked_value").notNull(),
22776
22777
  detected_at: text("detected_at").notNull()
22777
22778
  });
22779
+ review_findings = sqliteTable("review_findings", {
22780
+ id: text("id").primaryKey(),
22781
+ rule_id: text("rule_id").notNull(),
22782
+ session_id: text("session_id").notNull().references(() => sessions.id),
22783
+ status: text("status", { enum: ["open", "accepted", "dismissed", "resolved"] }).notNull(),
22784
+ note: text("note"),
22785
+ reviewed_at: text("reviewed_at"),
22786
+ created_at: text("created_at").notNull(),
22787
+ updated_at: text("updated_at").notNull()
22788
+ });
22778
22789
  }
22779
22790
  });
22780
22791
 
@@ -22878,11 +22889,24 @@ var init_db2 = __esm({
22878
22889
  detected_at TEXT NOT NULL,
22879
22890
  FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
22880
22891
  FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
22892
+ )`,
22893
+ `CREATE TABLE IF NOT EXISTS review_findings (
22894
+ id TEXT PRIMARY KEY,
22895
+ rule_id TEXT NOT NULL,
22896
+ session_id TEXT NOT NULL,
22897
+ status TEXT NOT NULL CHECK (status IN ('open', 'accepted', 'dismissed', 'resolved')),
22898
+ note TEXT,
22899
+ reviewed_at TEXT,
22900
+ created_at TEXT NOT NULL,
22901
+ updated_at TEXT NOT NULL,
22902
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
22881
22903
  )`,
22882
22904
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title ON sessions(title)`,
22883
22905
  `CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)`,
22884
22906
  `CREATE INDEX IF NOT EXISTS idx_secrets_detected_session_id ON secrets_detected(session_id)`,
22885
22907
  `CREATE INDEX IF NOT EXISTS idx_secrets_detected_message_id ON secrets_detected(message_id)`,
22908
+ `CREATE INDEX IF NOT EXISTS idx_review_findings_session_id ON review_findings(session_id)`,
22909
+ `CREATE INDEX IF NOT EXISTS idx_review_findings_status ON review_findings(status)`,
22886
22910
  `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
22887
22911
  content,
22888
22912
  session_id UNINDEXED,
@@ -38502,7 +38526,7 @@ var init_time = __esm({
38502
38526
  var version4;
38503
38527
  var init_package = __esm({
38504
38528
  "package.json"() {
38505
- version4 = "0.1.30";
38529
+ version4 = "0.1.31";
38506
38530
  }
38507
38531
  });
38508
38532
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chron-mcp",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "mcpName": "io.github.sirinivask/chron",
5
5
  "description": "Audit-grade timestamped logs for every AI conversation",
6
6
  "repository": {