chron-mcp 0.1.28 → 0.1.30

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
@@ -16615,6 +16615,14 @@ async function initDb(dbPath3) {
16615
16615
  if (!sessCols.includes("metadata")) {
16616
16616
  await client.execute("ALTER TABLE sessions ADD COLUMN metadata TEXT");
16617
16617
  }
16618
+ const ftsCount = await client.execute("SELECT count(*) as n FROM messages_fts");
16619
+ const msgCount = await client.execute("SELECT count(*) as n FROM messages");
16620
+ if (Number(ftsCount.rows[0][0]) < Number(msgCount.rows[0][0])) {
16621
+ await client.execute("DELETE FROM messages_fts");
16622
+ await client.execute(
16623
+ "INSERT INTO messages_fts(rowid, content, session_id) SELECT rowid, content, session_id FROM messages"
16624
+ );
16625
+ }
16618
16626
  return drizzle(client, { schema: schema_exports });
16619
16627
  }
16620
16628
  var import_os, import_fs, import_path, CREATE_SQL;
@@ -16664,11 +16672,81 @@ var init_db2 = __esm({
16664
16672
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title ON sessions(title)`,
16665
16673
  `CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)`,
16666
16674
  `CREATE INDEX IF NOT EXISTS idx_secrets_detected_session_id ON secrets_detected(session_id)`,
16667
- `CREATE INDEX IF NOT EXISTS idx_secrets_detected_message_id ON secrets_detected(message_id)`
16675
+ `CREATE INDEX IF NOT EXISTS idx_secrets_detected_message_id ON secrets_detected(message_id)`,
16676
+ `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
16677
+ content,
16678
+ session_id UNINDEXED,
16679
+ tokenize = 'porter unicode61'
16680
+ )`,
16681
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ai
16682
+ AFTER INSERT ON messages BEGIN
16683
+ INSERT INTO messages_fts(rowid, content, session_id)
16684
+ VALUES (new.rowid, new.content, new.session_id);
16685
+ END`,
16686
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ad
16687
+ AFTER DELETE ON messages BEGIN
16688
+ DELETE FROM messages_fts WHERE rowid = old.rowid;
16689
+ END`
16668
16690
  ];
16669
16691
  }
16670
16692
  });
16671
16693
 
16694
+ // src/tools/search.ts
16695
+ async function runSearch(db, query, limit = 10) {
16696
+ const client = db.$client;
16697
+ let rows;
16698
+ try {
16699
+ const result = await client.execute({
16700
+ sql: `
16701
+ SELECT
16702
+ messages_fts.session_id AS session_id,
16703
+ snippet(messages_fts, 0, '>>>', '<<<', '\u2026', 24) AS excerpt,
16704
+ s.title AS title,
16705
+ s.ai_tool AS ai_tool,
16706
+ s.updated_at AS updated_at
16707
+ FROM messages_fts
16708
+ JOIN sessions s ON s.id = messages_fts.session_id
16709
+ WHERE messages_fts MATCH ?
16710
+ ORDER BY rank
16711
+ LIMIT ?
16712
+ `,
16713
+ args: [query, limit * 5]
16714
+ // over-fetch so we can group into limit distinct sessions
16715
+ });
16716
+ rows = result.rows;
16717
+ } catch {
16718
+ return [];
16719
+ }
16720
+ const map = /* @__PURE__ */ new Map();
16721
+ for (const r of rows) {
16722
+ const sid = String(r.session_id ?? "");
16723
+ if (!sid)
16724
+ continue;
16725
+ if (!map.has(sid)) {
16726
+ map.set(sid, {
16727
+ session_id: sid,
16728
+ title: String(r.title ?? ""),
16729
+ ai_tool: r.ai_tool != null ? String(r.ai_tool) : null,
16730
+ updated_at: String(r.updated_at ?? ""),
16731
+ match_count: 0,
16732
+ excerpts: []
16733
+ });
16734
+ }
16735
+ const entry = map.get(sid);
16736
+ entry.match_count++;
16737
+ if (entry.excerpts.length < 3)
16738
+ entry.excerpts.push(String(r.excerpt ?? ""));
16739
+ if (map.size >= limit && entry.match_count > 1)
16740
+ break;
16741
+ }
16742
+ return Array.from(map.values()).slice(0, limit);
16743
+ }
16744
+ var init_search = __esm({
16745
+ "src/tools/search.ts"() {
16746
+ "use strict";
16747
+ }
16748
+ });
16749
+
16672
16750
  // src/cli/history.ts
16673
16751
  var history_exports = {};
16674
16752
  __export(history_exports, {
@@ -16788,12 +16866,43 @@ ${msg.content}
16788
16866
  }
16789
16867
  }
16790
16868
  }
16869
+ async function printSearch(query, limit) {
16870
+ const db = await initDb();
16871
+ const results = await runSearch(db, query, limit);
16872
+ if (results.length === 0) {
16873
+ process.stdout.write(`No sessions match: ${query}
16874
+ `);
16875
+ return;
16876
+ }
16877
+ process.stdout.write(`
16878
+ ${BOLD}Search: ${query}${RESET} ${DIM}(${results.length} session${results.length === 1 ? "" : "s"})${RESET}
16879
+
16880
+ `);
16881
+ for (const r of results) {
16882
+ const date = fmtDate(r.updated_at);
16883
+ const prefix = r.session_id.slice(0, 8);
16884
+ const hits = `${r.match_count} hit${r.match_count === 1 ? "" : "s"}`;
16885
+ process.stdout.write(
16886
+ ` ${DIM}${date}${RESET} ${CYAN}${prefix}${RESET} ${BOLD}${hits}${RESET} ${r.title}
16887
+ `
16888
+ );
16889
+ for (const excerpt of r.excerpts) {
16890
+ const highlighted = excerpt.replace(/>>>/g, YELLOW).replace(/<<</g, RESET);
16891
+ process.stdout.write(` ${DIM}\u2026${RESET} ${highlighted}
16892
+ `);
16893
+ }
16894
+ process.stdout.write("\n");
16895
+ }
16896
+ }
16791
16897
  async function runHistory(args2) {
16792
16898
  const limitArg = args2.find((a) => a.startsWith("--limit="));
16793
16899
  const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : 20;
16794
16900
  const refArg = args2.find((a) => a.startsWith("--ref="))?.split("=").slice(1).join("=");
16901
+ const searchArg = args2.find((a) => a.startsWith("--search="))?.split("=").slice(1).join("=");
16795
16902
  const positional = args2.filter((a) => !a.startsWith("--"));
16796
- if (positional.length === 0) {
16903
+ if (searchArg) {
16904
+ await printSearch(searchArg, limit);
16905
+ } else if (positional.length === 0) {
16797
16906
  await printList(limit, refArg);
16798
16907
  } else {
16799
16908
  await printDetail(positional[0]);
@@ -16806,6 +16915,7 @@ var init_history = __esm({
16806
16915
  init_drizzle_orm();
16807
16916
  init_db2();
16808
16917
  init_schema();
16918
+ init_search();
16809
16919
  RESET = "\x1B[0m";
16810
16920
  BOLD = "\x1B[1m";
16811
16921
  DIM = "\x1B[2m";
@@ -17376,7 +17486,7 @@ var require_package = __commonJS({
17376
17486
  "package.json"(exports2, module2) {
17377
17487
  module2.exports = {
17378
17488
  name: "chron-mcp",
17379
- version: "0.1.28",
17489
+ version: "0.1.30",
17380
17490
  mcpName: "io.github.sirinivask/chron",
17381
17491
  description: "Audit-grade timestamped logs for every AI conversation",
17382
17492
  repository: {
@@ -19900,6 +20010,511 @@ var init_import = __esm({
19900
20010
  }
19901
20011
  });
19902
20012
 
20013
+ // src/review/rules.ts
20014
+ var SOC2_RULES, FRAMEWORKS;
20015
+ var init_rules = __esm({
20016
+ "src/review/rules.ts"() {
20017
+ "use strict";
20018
+ SOC2_RULES = [
20019
+ {
20020
+ id: "soc2.cc6_1.ai_access_control_change",
20021
+ framework: "soc2",
20022
+ controls: ["CC6.1"],
20023
+ severity: "high",
20024
+ description: "AI touched access-control-related code",
20025
+ finding: "AI modified code in an access-control-sensitive path.",
20026
+ not_claiming: "This is not evidence of a security failure or SOC 2 non-compliance.",
20027
+ suggested_evidence: [
20028
+ "PR approval with human reviewer",
20029
+ "Change ticket linked to the work",
20030
+ "Manager or security team sign-off"
20031
+ ],
20032
+ match: {
20033
+ type: "code_change_path",
20034
+ path_contains: ["auth", "iam", "rbac", "permission", "policy", "acl", "role", "login", "jwt", "oauth", "saml", "sso", "mfa", "access_control"]
20035
+ }
20036
+ },
20037
+ {
20038
+ id: "soc2.cc6_1.cc6_6.secret_detected",
20039
+ framework: "soc2",
20040
+ 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.",
20045
+ suggested_evidence: [
20046
+ "Confirm no plaintext credential was committed to version control",
20047
+ "Rotate the credential if it was a live secret",
20048
+ "Document the detection and resolution in the change log"
20049
+ ],
20050
+ match: { type: "secret_detected" }
20051
+ },
20052
+ {
20053
+ id: "soc2.cc7_2.cc8_1.ai_infra_change",
20054
+ framework: "soc2",
20055
+ controls: ["CC7.2", "CC8.1"],
20056
+ severity: "high",
20057
+ description: "AI executed or modified deployment or infrastructure files",
20058
+ finding: "AI modified infrastructure or deployment configuration.",
20059
+ not_claiming: "This is not evidence of an unauthorized change.",
20060
+ suggested_evidence: [
20061
+ "Change ticket or approval record",
20062
+ "Pipeline approval gate log",
20063
+ "Human review of the infrastructure diff"
20064
+ ],
20065
+ match: {
20066
+ type: "code_change_path",
20067
+ path_contains: [".tf", "terraform", "dockerfile", "docker-compose", "kubernetes", "k8s", "helm", "ansible", ".github/workflow", "cloudformation", "pipeline", "deploy"]
20068
+ }
20069
+ },
20070
+ {
20071
+ id: "soc2.cc7_2.ai_monitoring_change",
20072
+ framework: "soc2",
20073
+ controls: ["CC7.2"],
20074
+ severity: "high",
20075
+ description: "AI changed logging, monitoring, alerting, or security tooling",
20076
+ finding: "AI modified logging, monitoring, or security alerting code or configuration.",
20077
+ not_claiming: "This is not evidence that monitoring was disabled or circumvented.",
20078
+ suggested_evidence: [
20079
+ "Confirm monitoring coverage was not reduced",
20080
+ "Human review of the change",
20081
+ "Change ticket"
20082
+ ],
20083
+ match: {
20084
+ type: "code_change_path",
20085
+ path_contains: ["logging", "monitoring", "alerting", "splunk", "datadog", "prometheus", "grafana", "cloudwatch", "sentry", "pagerduty", "audit_log", "audit-log", "logger"]
20086
+ }
20087
+ },
20088
+ {
20089
+ id: "soc2.cc6_1.cc6_7.ai_data_handling_change",
20090
+ framework: "soc2",
20091
+ controls: ["CC6.1", "CC6.7"],
20092
+ severity: "medium",
20093
+ description: "AI changed data retention, deletion, export, or encryption logic",
20094
+ finding: "AI modified data handling, retention, or privacy-related code.",
20095
+ not_claiming: "This is not evidence of a data handling violation.",
20096
+ suggested_evidence: [
20097
+ "Privacy or data officer review",
20098
+ "Change ticket documenting the business reason",
20099
+ "Confirm compliance with data retention policy"
20100
+ ],
20101
+ match: {
20102
+ type: "code_change_path",
20103
+ path_contains: ["retention", "encrypt", "decrypt", "gdpr", "privacy", "anonymize", "redact", "purge", "archive", "backup"]
20104
+ }
20105
+ }
20106
+ ];
20107
+ FRAMEWORKS = {
20108
+ soc2: SOC2_RULES
20109
+ };
20110
+ }
20111
+ });
20112
+
20113
+ // src/review/engine.ts
20114
+ async function runReview(db, rules, since) {
20115
+ const client = db.$client;
20116
+ const map = /* @__PURE__ */ new Map();
20117
+ for (const rule of rules) {
20118
+ if (rule.match.type === "code_change_path") {
20119
+ await matchCodeChanges(client, rule, map, since);
20120
+ } else if (rule.match.type === "secret_detected") {
20121
+ await matchSecrets(client, rule, map, since);
20122
+ }
20123
+ }
20124
+ const findings = Array.from(map.values());
20125
+ findings.sort(
20126
+ (a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99) || a.last_occurred_at.localeCompare(b.last_occurred_at)
20127
+ );
20128
+ return findings;
20129
+ }
20130
+ async function matchCodeChanges(client, rule, map, since) {
20131
+ if (rule.match.type !== "code_change_path")
20132
+ return;
20133
+ const keywords = rule.match.path_contains;
20134
+ const result = await client.execute(
20135
+ since ? {
20136
+ sql: `SELECT m.id, m.session_id, m.content, m.created_at, s.title, s.ai_tool
20137
+ FROM messages m JOIN sessions s ON s.id = m.session_id
20138
+ WHERE m.event_type = 'code_change' AND m.created_at >= ?
20139
+ ORDER BY m.created_at`,
20140
+ args: [since]
20141
+ } : `SELECT m.id, m.session_id, m.content, m.created_at, s.title, s.ai_tool
20142
+ FROM messages m JOIN sessions s ON s.id = m.session_id
20143
+ WHERE m.event_type = 'code_change'
20144
+ ORDER BY m.created_at`
20145
+ );
20146
+ for (const row of result.rows) {
20147
+ const content = String(row.content ?? "");
20148
+ let filePath = "";
20149
+ try {
20150
+ const parsed = JSON.parse(content);
20151
+ filePath = (parsed.file_path ?? parsed.path ?? "").toLowerCase();
20152
+ } catch {
20153
+ filePath = content.toLowerCase();
20154
+ }
20155
+ if (!keywords.some((kw) => filePath.includes(kw.toLowerCase())))
20156
+ continue;
20157
+ const sessionId = String(row.session_id ?? "");
20158
+ const mapKey = `${rule.id}::${sessionId}`;
20159
+ const occurredAt = String(row.created_at ?? "");
20160
+ const evidenceItem = `code_change: ${filePath || "unknown path"}`;
20161
+ if (!map.has(mapKey)) {
20162
+ map.set(mapKey, {
20163
+ rule_id: rule.id,
20164
+ framework: rule.framework,
20165
+ controls: rule.controls,
20166
+ severity: rule.severity,
20167
+ session_id: sessionId,
20168
+ session_prefix: sessionId.slice(0, 8),
20169
+ session_title: String(row.title ?? ""),
20170
+ ai_tool: row.ai_tool != null ? String(row.ai_tool) : null,
20171
+ evidence_items: [],
20172
+ first_occurred_at: occurredAt,
20173
+ last_occurred_at: occurredAt,
20174
+ finding: rule.finding,
20175
+ not_claiming: rule.not_claiming,
20176
+ suggested_evidence: rule.suggested_evidence
20177
+ });
20178
+ }
20179
+ const finding = map.get(mapKey);
20180
+ if (!finding.evidence_items.includes(evidenceItem)) {
20181
+ finding.evidence_items.push(evidenceItem);
20182
+ }
20183
+ if (occurredAt > finding.last_occurred_at)
20184
+ finding.last_occurred_at = occurredAt;
20185
+ }
20186
+ }
20187
+ async function matchSecrets(client, rule, map, since) {
20188
+ const result = await client.execute(
20189
+ since ? {
20190
+ sql: `SELECT sd.session_id, sd.type, sd.masked_value, sd.detected_at, s.title, s.ai_tool
20191
+ FROM secrets_detected sd JOIN sessions s ON s.id = sd.session_id
20192
+ WHERE sd.detected_at >= ?
20193
+ ORDER BY sd.detected_at`,
20194
+ args: [since]
20195
+ } : `SELECT sd.session_id, sd.type, sd.masked_value, sd.detected_at, s.title, s.ai_tool
20196
+ FROM secrets_detected sd JOIN sessions s ON s.id = sd.session_id
20197
+ ORDER BY sd.detected_at`
20198
+ );
20199
+ for (const row of result.rows) {
20200
+ const sessionId = String(row.session_id ?? "");
20201
+ const mapKey = `${rule.id}::${sessionId}`;
20202
+ const occurredAt = String(row.detected_at ?? "");
20203
+ const evidenceItem = `secret_detected: ${row.type} (${row.masked_value})`;
20204
+ if (!map.has(mapKey)) {
20205
+ map.set(mapKey, {
20206
+ rule_id: rule.id,
20207
+ framework: rule.framework,
20208
+ controls: rule.controls,
20209
+ severity: rule.severity,
20210
+ session_id: sessionId,
20211
+ session_prefix: sessionId.slice(0, 8),
20212
+ session_title: String(row.title ?? ""),
20213
+ ai_tool: row.ai_tool != null ? String(row.ai_tool) : null,
20214
+ evidence_items: [],
20215
+ first_occurred_at: occurredAt,
20216
+ last_occurred_at: occurredAt,
20217
+ finding: rule.finding,
20218
+ not_claiming: rule.not_claiming,
20219
+ suggested_evidence: rule.suggested_evidence
20220
+ });
20221
+ }
20222
+ const finding = map.get(mapKey);
20223
+ if (!finding.evidence_items.includes(evidenceItem)) {
20224
+ finding.evidence_items.push(evidenceItem);
20225
+ }
20226
+ if (occurredAt > finding.last_occurred_at)
20227
+ finding.last_occurred_at = occurredAt;
20228
+ }
20229
+ }
20230
+ var SEVERITY_ORDER;
20231
+ var init_engine = __esm({
20232
+ "src/review/engine.ts"() {
20233
+ "use strict";
20234
+ SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
20235
+ }
20236
+ });
20237
+
20238
+ // src/cli/review.ts
20239
+ var review_exports = {};
20240
+ __export(review_exports, {
20241
+ runReview: () => runReview2
20242
+ });
20243
+ function printFindings(findings, framework, sessionCount) {
20244
+ 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}
20247
+
20248
+ `);
20249
+ if (findings.length === 0) {
20250
+ process.stdout.write(`${DIM9}No findings. No sessions matched any control review criteria.${RESET10}
20251
+
20252
+ `);
20253
+ return;
20254
+ }
20255
+ process.stdout.write(
20256
+ `${BOLD10}${findings.length}${RESET10} finding${findings.length === 1 ? "" : "s"} across ${BOLD10}${controlsHit.size}${RESET10} control${controlsHit.size === 1 ? "" : "s"}
20257
+
20258
+ `
20259
+ );
20260
+ process.stdout.write(
20261
+ `${DIM9}This report identifies AI-session evidence that may require control-owner review.
20262
+ It is not a certification of compliance or evidence of any violation.${RESET10}
20263
+
20264
+ `
20265
+ );
20266
+ for (const f of findings) {
20267
+ const sevColor = SEV_COLOR[f.severity] ?? DIM9;
20268
+ const controls = f.controls.join(", ");
20269
+ const sev = f.severity.toUpperCase().padEnd(8);
20270
+ process.stdout.write(
20271
+ ` ${sevColor}${BOLD10}${sev}${RESET10} ${MAGENTA2}${controls.padEnd(16)}${RESET10} ${CYAN8}${f.session_prefix}${RESET10} ${f.session_title}
20272
+ `
20273
+ );
20274
+ process.stdout.write(` ${f.finding}
20275
+ `);
20276
+ for (const item of f.evidence_items.slice(0, 5)) {
20277
+ process.stdout.write(` ${DIM9}\u2022 ${item}${RESET10}
20278
+ `);
20279
+ }
20280
+ if (f.evidence_items.length > 5) {
20281
+ process.stdout.write(` ${DIM9} \u2026 ${f.evidence_items.length - 5} more event(s)${RESET10}
20282
+ `);
20283
+ }
20284
+ process.stdout.write(` ${DIM9}Suggested: ${f.suggested_evidence[0]}${RESET10}
20285
+ `);
20286
+ process.stdout.write("\n");
20287
+ }
20288
+ }
20289
+ function esc2(s) {
20290
+ return (s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
20291
+ }
20292
+ function badgeHtml(severity) {
20293
+ return `<span class="badge badge-${esc2(severity)}">${esc2(severity.toUpperCase())}</span>`;
20294
+ }
20295
+ function buildReviewHtml(params) {
20296
+ const { framework, host, generatedAt, sessionCount, findings } = params;
20297
+ const controlsHit = new Set(findings.flatMap((f) => f.controls));
20298
+ const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
20299
+ for (const f of findings)
20300
+ 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) => `
20302
+ <div class="finding-card">
20303
+ <div class="finding-header">
20304
+ ${badgeHtml(f.severity)}
20305
+ <span class="finding-controls">${esc2(f.controls.join(", "))}</span>
20306
+ <span style="flex:1"></span>
20307
+ <span class="finding-session">${esc2(f.session_prefix)}</span>
20308
+ <span style="font-size:12px;color:#374151">${esc2(f.session_title)}</span>
20309
+ </div>
20310
+ <div class="finding-body">
20311
+ <div class="finding-text">${esc2(f.finding)}</div>
20312
+ <div class="finding-disclaimer">${esc2(f.not_claiming)}</div>
20313
+ <strong style="font-size:11px;color:#374151">Evidence in session:</strong>
20314
+ <ul class="evidence-list">
20315
+ ${f.evidence_items.map((e) => `<li>${esc2(e)}</li>`).join("")}
20316
+ </ul>
20317
+ <strong style="font-size:11px;color:#374151">Suggested review actions:</strong>
20318
+ <ul class="suggested-list">
20319
+ ${f.suggested_evidence.map((s) => `<li>${esc2(s)}</li>`).join("")}
20320
+ </ul>
20321
+ </div>
20322
+ </div>`).join("");
20323
+ return `<!DOCTYPE html>
20324
+ <html lang="en">
20325
+ <head>
20326
+ <meta charset="UTF-8">
20327
+ <meta name="viewport" content="width=device-width,initial-scale=1">
20328
+ <title>Chron Review \u2014 ${esc2(framework.toUpperCase())} Controls</title>
20329
+ <style>${REPORT_CSS}</style>
20330
+ </head>
20331
+ <body>
20332
+ <div class="page">
20333
+
20334
+ <div style="min-height:50vh;display:flex;flex-direction:column;justify-content:center">
20335
+ <div class="cover-badge">${esc2(framework.toUpperCase())} Control Review</div>
20336
+ <h1>Chron Review Report</h1>
20337
+ <p class="meta">Generated by <strong>chron review</strong> &nbsp;|&nbsp; Host: <strong>${esc2(host)}</strong></p>
20338
+ <table style="width:auto;margin-bottom:0">
20339
+ <tr><th>Framework</th><td>${esc2(framework.toUpperCase())} Trust Services Criteria</td></tr>
20340
+ <tr><th>Generated</th><td>${esc2(generatedAt)}</td></tr>
20341
+ <tr><th>Sessions Reviewed</th><td>${sessionCount}</td></tr>
20342
+ <tr><th>Findings</th><td>${findings.length}</td></tr>
20343
+ <tr><th>Controls Flagged</th><td>${controlsHit.size > 0 ? Array.from(controlsHit).sort().join(", ") : "None"}</td></tr>
20344
+ </table>
20345
+ </div>
20346
+
20347
+ <div class="disclaimer">
20348
+ <strong>Important:</strong> This report identifies AI-session evidence that may require control-owner review.
20349
+ It is not a certification of compliance, a SOC 2 opinion, or evidence of any control failure.
20350
+ Each finding is a potential review item \u2014 not a violation. A licensed CPA conducting a SOC 2 examination
20351
+ must evaluate whether your controls were suitably designed and operated effectively over the audit period.
20352
+ </div>
20353
+
20354
+ <h2>Summary</h2>
20355
+ <div class="stat-grid">
20356
+ <div class="stat"><div class="stat-value">${sessionCount}</div><div class="stat-label">Sessions Reviewed</div></div>
20357
+ <div class="stat"><div class="stat-value">${findings.length}</div><div class="stat-label">Findings</div></div>
20358
+ <div class="stat"><div class="stat-value">${controlsHit.size}</div><div class="stat-label">Controls Flagged</div></div>
20359
+ </div>
20360
+ ${bySeverity.critical > 0 ? `<p><span class="badge badge-critical">CRITICAL</span> &nbsp;${bySeverity.critical} finding(s) require immediate attention.</p>` : ""}
20361
+
20362
+ <h2>Findings</h2>
20363
+ ${findingCards}
20364
+
20365
+ <h2>Methodology</h2>
20366
+ <div class="appendix">
20367
+ <h3>What this report does</h3>
20368
+ <p>Chron Review evaluates AI session logs against a deterministic rule pack aligned to the AICPA Trust Services Criteria.
20369
+ Rules match on structured event types (<code>code_change</code>, <code>secret_detected</code>) recorded by the Chron MCP server
20370
+ during live AI conversations.</p>
20371
+
20372
+ <h3>Rule pack: ${esc2(framework.toUpperCase())} (v1)</h3>
20373
+ <ul>
20374
+ <li><strong>CC6.1</strong> \u2014 AI touched access-control code (auth, IAM, RBAC, permissions, login, JWT, OAuth, SAML, SSO)</li>
20375
+ <li><strong>CC6.1 / CC6.6</strong> \u2014 Sensitive credential detected in AI session (masked at log time, no plaintext stored)</li>
20376
+ <li><strong>CC7.2 / CC8.1</strong> \u2014 AI modified infrastructure or deployment configuration (Terraform, Docker, Kubernetes, pipelines)</li>
20377
+ <li><strong>CC7.2</strong> \u2014 AI modified logging, monitoring, or alerting code</li>
20378
+ <li><strong>CC6.1 / CC6.7</strong> \u2014 AI modified data retention, deletion, export, or encryption logic</li>
20379
+ </ul>
20380
+
20381
+ <h3>What this report does not do</h3>
20382
+ <ul>
20383
+ <li>Does not evaluate whether your controls were suitably designed or operated effectively (that is the auditor's role).</li>
20384
+ <li>Does not access code repositories, CI/CD systems, identity providers, or any environment outside the Chron audit database.</li>
20385
+ <li>Does not make any compliance determination. All findings use the language "potential review item."</li>
20386
+ <li>Does not cover CC1\u2013CC5 (governance, communication, risk assessment) \u2014 these require policy documents, board records, and cannot be evaluated from session logs.</li>
20387
+ </ul>
20388
+
20389
+ <p style="margin-top:12px;font-size:11px;color:#9ca3af">
20390
+ Generated by chron review &nbsp;|&nbsp; ${esc2(generatedAt)} &nbsp;|&nbsp; Host: ${esc2(host)}
20391
+ </p>
20392
+ </div>
20393
+
20394
+ </div>
20395
+ </body>
20396
+ </html>`;
20397
+ }
20398
+ async function countSessions(db) {
20399
+ const client = db.$client;
20400
+ const result = await client.execute("SELECT COUNT(*) AS n FROM sessions");
20401
+ return Number(result.rows[0].n ?? 0);
20402
+ }
20403
+ async function runReview2(args2) {
20404
+ const frameworkArg = args2.find((a) => a.startsWith("--framework="))?.slice("--framework=".length)?.toLowerCase();
20405
+ const outputArg = args2.find((a) => a.startsWith("--output="))?.slice("--output=".length);
20406
+ const sinceArg = args2.find((a) => a.startsWith("--since="))?.slice("--since=".length);
20407
+ if (!frameworkArg) {
20408
+ process.stderr.write("Usage: chron review --framework=soc2 [--since=<range>] [--output=<file>]\n");
20409
+ process.exit(1);
20410
+ }
20411
+ const rules = FRAMEWORKS[frameworkArg];
20412
+ if (!rules) {
20413
+ process.stderr.write(`Unknown framework: "${frameworkArg}". Available: ${Object.keys(FRAMEWORKS).join(", ")}
20414
+ `);
20415
+ process.exit(1);
20416
+ }
20417
+ let since;
20418
+ if (sinceArg) {
20419
+ try {
20420
+ since = parseSince(sinceArg);
20421
+ } catch (err) {
20422
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
20423
+ `);
20424
+ process.exit(1);
20425
+ }
20426
+ }
20427
+ const db = await initDb();
20428
+ const [findings, sessionCount] = await Promise.all([
20429
+ runReview(db, rules, since),
20430
+ countSessions(db)
20431
+ ]);
20432
+ printFindings(findings, frameworkArg, sessionCount);
20433
+ if (outputArg) {
20434
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19) + " UTC";
20435
+ const html = buildReviewHtml({
20436
+ framework: frameworkArg,
20437
+ host: (0, import_os11.hostname)(),
20438
+ generatedAt,
20439
+ sessionCount,
20440
+ findings
20441
+ });
20442
+ (0, import_fs12.writeFileSync)(outputArg, html, "utf8");
20443
+ process.stdout.write(`Review report written to ${outputArg}
20444
+ `);
20445
+ }
20446
+ }
20447
+ var import_fs12, import_os11, RESET10, BOLD10, DIM9, RED5, YELLOW8, CYAN8, MAGENTA2, SEV_COLOR, REPORT_CSS;
20448
+ var init_review = __esm({
20449
+ "src/cli/review.ts"() {
20450
+ "use strict";
20451
+ import_fs12 = require("fs");
20452
+ import_os11 = require("os");
20453
+ init_db2();
20454
+ init_report();
20455
+ init_rules();
20456
+ init_engine();
20457
+ RESET10 = "\x1B[0m";
20458
+ BOLD10 = "\x1B[1m";
20459
+ DIM9 = "\x1B[2m";
20460
+ RED5 = "\x1B[31m";
20461
+ YELLOW8 = "\x1B[33m";
20462
+ CYAN8 = "\x1B[36m";
20463
+ MAGENTA2 = "\x1B[35m";
20464
+ SEV_COLOR = {
20465
+ critical: RED5,
20466
+ high: YELLOW8,
20467
+ medium: CYAN8,
20468
+ low: DIM9
20469
+ };
20470
+ REPORT_CSS = `
20471
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
20472
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; color: #111; background: #fff; line-height: 1.5; }
20473
+ .page { max-width: 900px; margin: 0 auto; padding: 40px 48px; }
20474
+ h1 { font-size: 28px; font-weight: 700; margin-bottom: 4px; }
20475
+ h2 { font-size: 17px; font-weight: 700; margin: 32px 0 10px; padding-bottom: 4px; border-bottom: 2px solid #e5e7eb; color: #1f2937; }
20476
+ h3 { font-size: 14px; font-weight: 600; margin: 20px 0 6px; color: #374151; }
20477
+ p { margin-bottom: 8px; color: #374151; }
20478
+ .meta { color: #6b7280; font-size: 12px; margin-bottom: 32px; }
20479
+ .cover-badge { display: inline-block; background: #1d4ed8; color: #fff; padding: 3px 10px; border-radius: 4px; font-size: 12px; font-weight: 600; margin-bottom: 16px; }
20480
+ table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-size: 12px; }
20481
+ th { background: #f3f4f6; text-align: left; padding: 6px 10px; font-weight: 600; color: #374151; border: 1px solid #e5e7eb; }
20482
+ td { padding: 5px 10px; border: 1px solid #e5e7eb; vertical-align: top; word-break: break-word; }
20483
+ tr:nth-child(even) td { background: #fafafa; }
20484
+ .stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px; }
20485
+ .stat { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 12px 16px; }
20486
+ .stat-value { font-size: 24px; font-weight: 700; color: #1f2937; }
20487
+ .stat-label { font-size: 11px; color: #6b7280; margin-top: 2px; }
20488
+ .badge { display:inline-block; padding:1px 7px; border-radius:3px; font-size:11px; font-weight:600; color:#fff; }
20489
+ .badge-critical { background:#dc2626; }
20490
+ .badge-high { background:#d97706; }
20491
+ .badge-medium { background:#2563eb; }
20492
+ .badge-low { background:#6b7280; }
20493
+ .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
+ .finding-card { border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 14px; overflow: hidden; }
20495
+ .finding-header { display:flex; align-items:center; gap:10px; padding:10px 14px; background:#f9fafb; border-bottom:1px solid #e5e7eb; }
20496
+ .finding-controls { font-size:12px; font-weight:700; color:#1f2937; }
20497
+ .finding-session { font-family:monospace; font-size:11px; color:#6b7280; }
20498
+ .finding-body { padding:10px 14px; }
20499
+ .finding-text { font-size:13px; color:#111; margin-bottom:6px; }
20500
+ .finding-disclaimer { font-size:11px; color:#6b7280; margin-bottom:8px; }
20501
+ .evidence-list { margin:0 0 8px 16px; }
20502
+ .evidence-list li { font-size:11px; font-family:monospace; color:#374151; margin-bottom:2px; }
20503
+ .suggested-list { margin:0 0 0 16px; }
20504
+ .suggested-list li { font-size:11px; color:#374151; margin-bottom:2px; }
20505
+ .appendix { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px 20px; margin-top: 12px; }
20506
+ ul { padding-left: 20px; margin-bottom: 8px; }
20507
+ li { margin-bottom: 4px; }
20508
+ @media print {
20509
+ body { font-size: 11px; }
20510
+ .page { padding: 20px 24px; max-width: 100%; }
20511
+ h1 { font-size: 22px; }
20512
+ h2 { font-size: 14px; }
20513
+ }
20514
+ `;
20515
+ }
20516
+ });
20517
+
19903
20518
  // src/cli/index.ts
19904
20519
  var [, , command, ...args] = process.argv;
19905
20520
  async function main() {
@@ -19964,6 +20579,11 @@ async function main() {
19964
20579
  await runImport2(args);
19965
20580
  break;
19966
20581
  }
20582
+ case "review": {
20583
+ const { runReview: runReview3 } = await Promise.resolve().then(() => (init_review(), review_exports));
20584
+ await runReview3(args);
20585
+ break;
20586
+ }
19967
20587
  default: {
19968
20588
  const name = command ? `Unknown command: ${command}
19969
20589
 
@@ -19984,11 +20604,13 @@ Commands:
19984
20604
  prune Delete sessions older than a retention cutoff
19985
20605
  doctor Check your Chron setup \u2014 Node version, DB, MCP configs, SIEM
19986
20606
  import Import conversations from external AI tools
20607
+ review Review AI sessions against compliance control criteria
19987
20608
 
19988
20609
  Options (history):
19989
- --limit=<n> Max sessions to show (default: 20)
19990
- --ref=<value> Filter by external_ref prefix (e.g. --ref=jira: or --ref=jira:ENG-123)
19991
- <id-prefix> Show full log for the session with this ID prefix
20610
+ --limit=<n> Max sessions to show (default: 20)
20611
+ --search=<query> Full-text search across all sessions (FTS5: phrases, boolean, prefix*)
20612
+ --ref=<value> Filter by external_ref prefix
20613
+ <id-prefix> Show full log for the session with this ID prefix
19992
20614
 
19993
20615
  Options (report):
19994
20616
  --since=<range> Filter by date: 7d, 30d, or YYYY-MM-DD (default: all time)
@@ -20016,6 +20638,11 @@ Options (doctor):
20016
20638
 
20017
20639
  Options (import):
20018
20640
  chatgpt <file> Import from ChatGPT export (.zip or conversations.json)
20641
+
20642
+ Options (review):
20643
+ --framework=<name> Framework to review against: soc2
20644
+ --since=<range> Limit to sessions since: 7d, 30d, or YYYY-MM-DD
20645
+ --output=<file> Write HTML report to file (printable to PDF from browser)
20019
20646
  `
20020
20647
  );
20021
20648
  process.exit(command ? 1 : 0);
package/dist/index.js CHANGED
@@ -22825,6 +22825,14 @@ async function initDb(dbPath) {
22825
22825
  if (!sessCols.includes("metadata")) {
22826
22826
  await client.execute("ALTER TABLE sessions ADD COLUMN metadata TEXT");
22827
22827
  }
22828
+ const ftsCount = await client.execute("SELECT count(*) as n FROM messages_fts");
22829
+ const msgCount = await client.execute("SELECT count(*) as n FROM messages");
22830
+ if (Number(ftsCount.rows[0][0]) < Number(msgCount.rows[0][0])) {
22831
+ await client.execute("DELETE FROM messages_fts");
22832
+ await client.execute(
22833
+ "INSERT INTO messages_fts(rowid, content, session_id) SELECT rowid, content, session_id FROM messages"
22834
+ );
22835
+ }
22828
22836
  return drizzle(client, { schema: schema_exports });
22829
22837
  }
22830
22838
  var import_os, import_fs, import_path, CREATE_SQL;
@@ -22874,7 +22882,21 @@ var init_db2 = __esm({
22874
22882
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title ON sessions(title)`,
22875
22883
  `CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)`,
22876
22884
  `CREATE INDEX IF NOT EXISTS idx_secrets_detected_session_id ON secrets_detected(session_id)`,
22877
- `CREATE INDEX IF NOT EXISTS idx_secrets_detected_message_id ON secrets_detected(message_id)`
22885
+ `CREATE INDEX IF NOT EXISTS idx_secrets_detected_message_id ON secrets_detected(message_id)`,
22886
+ `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
22887
+ content,
22888
+ session_id UNINDEXED,
22889
+ tokenize = 'porter unicode61'
22890
+ )`,
22891
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ai
22892
+ AFTER INSERT ON messages BEGIN
22893
+ INSERT INTO messages_fts(rowid, content, session_id)
22894
+ VALUES (new.rowid, new.content, new.session_id);
22895
+ END`,
22896
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ad
22897
+ AFTER DELETE ON messages BEGIN
22898
+ DELETE FROM messages_fts WHERE rowid = old.rowid;
22899
+ END`
22878
22900
  ];
22879
22901
  }
22880
22902
  });
@@ -38480,7 +38502,7 @@ var init_time = __esm({
38480
38502
  var version4;
38481
38503
  var init_package = __esm({
38482
38504
  "package.json"() {
38483
- version4 = "0.1.28";
38505
+ version4 = "0.1.30";
38484
38506
  }
38485
38507
  });
38486
38508
 
@@ -38839,9 +38861,22 @@ var init_ntp = __esm({
38839
38861
  });
38840
38862
 
38841
38863
  // src/utils/terminal.ts
38842
- function terminalAudit(message) {
38843
- const setting = process.env.CHRON_TERMINAL_AUDIT;
38844
- if (setting && DISABLE_VALUES.has(setting.toLowerCase()))
38864
+ function resolvedLevel() {
38865
+ const raw = (process.env.CHRON_LOG_LEVEL ?? "").toLowerCase();
38866
+ if (raw === "off" || raw === "false" || raw === "0")
38867
+ return "off";
38868
+ if (raw === "summary")
38869
+ return "summary";
38870
+ const legacy = process.env.CHRON_TERMINAL_AUDIT;
38871
+ if (legacy && DISABLE_VALUES.has(legacy.toLowerCase()))
38872
+ return "off";
38873
+ return "verbose";
38874
+ }
38875
+ function terminalAudit(message, level = "message") {
38876
+ const mode = resolvedLevel();
38877
+ if (mode === "off")
38878
+ return;
38879
+ if (mode === "summary" && level === "message")
38845
38880
  return;
38846
38881
  process.stderr.write(`[chron] ${message}
38847
38882
  `);
@@ -38893,7 +38928,7 @@ function startSession(db) {
38893
38928
  });
38894
38929
  attachNtpMetadata(db, id);
38895
38930
  emitEvent({ event_type: "session_started", timestamp: now, session: { id_prefix: id.slice(0, 8), ai_tool: args.ai_tool ?? null } });
38896
- terminalAudit(`session ${id.slice(0, 8)} started at ${now} (${args.ai_tool ?? "unknown"}: ${args.title})`);
38931
+ terminalAudit(`session ${id.slice(0, 8)} started at ${now} (${args.ai_tool ?? "unknown"}: ${args.title})`, "session");
38897
38932
  return {
38898
38933
  content: [{
38899
38934
  type: "text",
@@ -38909,7 +38944,7 @@ function startSession(db) {
38909
38944
  const [countRow] = await db.select({ count: sql`count(*)` }).from(messages).where(eq(messages.session_id, session.id));
38910
38945
  await db.update(sessions).set({ updated_at: now }).where(eq(sessions.id, session.id));
38911
38946
  emitEvent({ event_type: "session_started", timestamp: now, session: { id_prefix: session.id.slice(0, 8), ai_tool: session.ai_tool } });
38912
- terminalAudit(`session ${session.id.slice(0, 8)} resumed at ${now} (${session.ai_tool ?? "unknown"}: ${session.title})`);
38947
+ terminalAudit(`session ${session.id.slice(0, 8)} resumed at ${now} (${session.ai_tool ?? "unknown"}: ${session.title})`, "session");
38913
38948
  return {
38914
38949
  content: [{
38915
38950
  type: "text",
@@ -38946,7 +38981,7 @@ function initSession(db) {
38946
38981
  created = true;
38947
38982
  ai_tool = args.ai_tool ?? null;
38948
38983
  attachNtpMetadata(db, id);
38949
- terminalAudit(`session ${id.slice(0, 8)} started at ${now} (${ai_tool ?? "unknown"}: ${args.title})`);
38984
+ terminalAudit(`session ${id.slice(0, 8)} started at ${now} (${ai_tool ?? "unknown"}: ${args.title})`, "session");
38950
38985
  } catch (e) {
38951
38986
  const isUnique = e?.message?.includes("UNIQUE constraint failed") || e?.code === "SQLITE_CONSTRAINT_UNIQUE" || e?.cause?.message?.includes("UNIQUE constraint failed") || e?.cause?.extendedCode === "SQLITE_CONSTRAINT_UNIQUE";
38952
38987
  if (!isUnique)
@@ -38957,7 +38992,7 @@ function initSession(db) {
38957
38992
  session_id = session.id;
38958
38993
  created = false;
38959
38994
  ai_tool = session.ai_tool;
38960
- terminalAudit(`session ${session.id.slice(0, 8)} resumed at ${now} (${ai_tool ?? "unknown"}: ${session.title})`);
38995
+ terminalAudit(`session ${session.id.slice(0, 8)} resumed at ${now} (${ai_tool ?? "unknown"}: ${session.title})`, "session");
38961
38996
  }
38962
38997
  emitEvent({ event_type: "session_started", timestamp: now, session: { id_prefix: session_id.slice(0, 8), ai_tool } });
38963
38998
  const limit = args.limit ?? 10;
@@ -39872,6 +39907,73 @@ var init_summary2 = __esm({
39872
39907
  }
39873
39908
  });
39874
39909
 
39910
+ // src/tools/search.ts
39911
+ async function runSearch(db, query, limit = 10) {
39912
+ const client = db.$client;
39913
+ let rows;
39914
+ try {
39915
+ const result = await client.execute({
39916
+ sql: `
39917
+ SELECT
39918
+ messages_fts.session_id AS session_id,
39919
+ snippet(messages_fts, 0, '>>>', '<<<', '\u2026', 24) AS excerpt,
39920
+ s.title AS title,
39921
+ s.ai_tool AS ai_tool,
39922
+ s.updated_at AS updated_at
39923
+ FROM messages_fts
39924
+ JOIN sessions s ON s.id = messages_fts.session_id
39925
+ WHERE messages_fts MATCH ?
39926
+ ORDER BY rank
39927
+ LIMIT ?
39928
+ `,
39929
+ args: [query, limit * 5]
39930
+ // over-fetch so we can group into limit distinct sessions
39931
+ });
39932
+ rows = result.rows;
39933
+ } catch {
39934
+ return [];
39935
+ }
39936
+ const map = /* @__PURE__ */ new Map();
39937
+ for (const r of rows) {
39938
+ const sid = String(r.session_id ?? "");
39939
+ if (!sid)
39940
+ continue;
39941
+ if (!map.has(sid)) {
39942
+ map.set(sid, {
39943
+ session_id: sid,
39944
+ title: String(r.title ?? ""),
39945
+ ai_tool: r.ai_tool != null ? String(r.ai_tool) : null,
39946
+ updated_at: String(r.updated_at ?? ""),
39947
+ match_count: 0,
39948
+ excerpts: []
39949
+ });
39950
+ }
39951
+ const entry = map.get(sid);
39952
+ entry.match_count++;
39953
+ if (entry.excerpts.length < 3)
39954
+ entry.excerpts.push(String(r.excerpt ?? ""));
39955
+ if (map.size >= limit && entry.match_count > 1)
39956
+ break;
39957
+ }
39958
+ return Array.from(map.values()).slice(0, limit);
39959
+ }
39960
+ function searchSessions(db) {
39961
+ return async (args) => {
39962
+ const results = await runSearch(db, args.query, args.limit ?? 10);
39963
+ return {
39964
+ content: [{
39965
+ type: "text",
39966
+ text: JSON.stringify({ query: args.query, total: results.length, results })
39967
+ }]
39968
+ };
39969
+ };
39970
+ }
39971
+ var init_search = __esm({
39972
+ "src/tools/search.ts"() {
39973
+ "use strict";
39974
+ }
39975
+ });
39976
+
39875
39977
  // src/server.ts
39876
39978
  function createServer(db) {
39877
39979
  const server = new McpServer({
@@ -39995,6 +40097,15 @@ function createServer(db) {
39995
40097
  },
39996
40098
  deleteSession(db)
39997
40099
  );
40100
+ server.tool(
40101
+ "search_sessions",
40102
+ 'Full-text search across all logged AI conversations. Returns matching sessions with excerpts. Uses SQLite FTS5 with porter stemming \u2014 supports phrases ("exact match"), boolean (term1 OR term2), and prefix (migrat*).',
40103
+ {
40104
+ query: external_exports.string().describe('FTS5 search query, e.g. "database migration" or "auth bug" or "deploy OR release"'),
40105
+ limit: external_exports.number().int().positive().optional().describe("Max sessions to return (default: 10)")
40106
+ },
40107
+ searchSessions(db)
40108
+ );
39998
40109
  server.tool(
39999
40110
  "summarize_session",
40000
40111
  "Return a structured summary of a session: timeline with latencies, mutations detected, secrets touched, and possible prod-account references. Useful for compliance review and change-control evidence.",
@@ -40025,6 +40136,7 @@ var init_server3 = __esm({
40025
40136
  init_verify();
40026
40137
  init_detect2();
40027
40138
  init_summary2();
40139
+ init_search();
40028
40140
  init_package();
40029
40141
  roleEnum = external_exports.enum(["user", "assistant"]);
40030
40142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chron-mcp",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "mcpName": "io.github.sirinivask/chron",
5
5
  "description": "Audit-grade timestamped logs for every AI conversation",
6
6
  "repository": {