chron-mcp 0.1.17 → 0.1.18

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.
Files changed (3) hide show
  1. package/dist/cli/index.js +585 -44
  2. package/dist/index.js +53611 -52817
  3. package/package.json +1 -1
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 } = require("crypto");
6165
+ var { randomBytes, createHash: createHash2 } = 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 = createHash("sha1").update(key + GUID).digest("base64");
6845
+ const digest = createHash2("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 } = require("crypto");
7232
+ var { createHash: createHash2 } = 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 = createHash("sha1").update(key + GUID).digest("base64");
7541
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
7542
7542
  const headers = [
7543
7543
  "HTTP/1.1 101 Switching Protocols",
7544
7544
  "Upgrade: websocket",
@@ -16731,6 +16731,366 @@ var init_history = __esm({
16731
16731
  }
16732
16732
  });
16733
16733
 
16734
+ // src/cli/soc2.ts
16735
+ var soc2_exports = {};
16736
+ __export(soc2_exports, {
16737
+ buildSoc2Report: () => buildSoc2Report
16738
+ });
16739
+ async function q(db, sql2) {
16740
+ const res = await db.$client.execute(sql2);
16741
+ return res.rows;
16742
+ }
16743
+ async function queryOverview(db) {
16744
+ const rows = await q(db, `
16745
+ SELECT
16746
+ (SELECT COUNT(*) FROM sessions) AS total_sessions,
16747
+ (SELECT COUNT(*) FROM messages) AS total_messages,
16748
+ (SELECT COUNT(*) FROM messages WHERE role = 'user') AS user_messages,
16749
+ (SELECT COUNT(*) FROM messages WHERE role = 'assistant') AS ai_messages,
16750
+ (SELECT COUNT(*) FROM secrets_detected) AS total_detections,
16751
+ (SELECT MIN(created_at) FROM sessions) AS period_start,
16752
+ (SELECT MAX(updated_at) FROM sessions) AS period_end
16753
+ `);
16754
+ return rows[0];
16755
+ }
16756
+ async function queryToolUsage(db) {
16757
+ return await q(db, `
16758
+ SELECT
16759
+ COALESCE(s.ai_tool, 'unknown') AS tool,
16760
+ COUNT(DISTINCT s.id) AS sessions,
16761
+ COUNT(m.id) AS messages
16762
+ FROM sessions s
16763
+ LEFT JOIN messages m ON m.session_id = s.id
16764
+ GROUP BY s.ai_tool
16765
+ ORDER BY sessions DESC
16766
+ `);
16767
+ }
16768
+ async function queryDetectionsByType(db) {
16769
+ return await q(db, `
16770
+ SELECT type, COUNT(*) AS cnt, GROUP_CONCAT(masked_value, ', ') AS samples
16771
+ FROM secrets_detected
16772
+ GROUP BY type
16773
+ ORDER BY cnt DESC
16774
+ `);
16775
+ }
16776
+ async function querySessionInventory(db) {
16777
+ return await q(db, `
16778
+ SELECT
16779
+ s.id, s.title, s.ai_tool,
16780
+ s.created_at, s.updated_at,
16781
+ COUNT(m.id) AS message_count
16782
+ FROM sessions s
16783
+ LEFT JOIN messages m ON m.session_id = s.id
16784
+ GROUP BY s.id
16785
+ ORDER BY s.created_at DESC
16786
+ LIMIT 500
16787
+ `);
16788
+ }
16789
+ async function queryIntegrity(db) {
16790
+ const rows = await q(db, `
16791
+ SELECT session_id, content_hash, prev_hash
16792
+ FROM messages
16793
+ ORDER BY session_id, created_at, rowid
16794
+ `);
16795
+ const bySession = /* @__PURE__ */ new Map();
16796
+ for (const r of rows) {
16797
+ const list = bySession.get(r.session_id) ?? [];
16798
+ list.push({ content_hash: r.content_hash, prev_hash: r.prev_hash });
16799
+ bySession.set(r.session_id, list);
16800
+ }
16801
+ let intact = 0, broken = 0, unchained = 0;
16802
+ const brokenSessions = [];
16803
+ for (const [sid, chain] of bySession) {
16804
+ const chained = chain.filter((r) => r.content_hash !== null);
16805
+ if (chained.length === 0) {
16806
+ unchained++;
16807
+ continue;
16808
+ }
16809
+ let ok = true;
16810
+ for (let i = 1; i < chained.length; i++) {
16811
+ if (chained[i].prev_hash !== chained[i - 1].content_hash) {
16812
+ ok = false;
16813
+ break;
16814
+ }
16815
+ }
16816
+ if (ok)
16817
+ intact++;
16818
+ else {
16819
+ broken++;
16820
+ brokenSessions.push(sid.slice(0, 8));
16821
+ }
16822
+ }
16823
+ return { intact, broken, unchained, brokenSessions };
16824
+ }
16825
+ async function dbFingerprint(db) {
16826
+ const rows = await q(db, `SELECT COUNT(*) AS n, MAX(created_at) AS latest FROM messages`);
16827
+ const row = rows[0];
16828
+ const h = (0, import_crypto.createHash)("sha256").update(`${row?.n ?? 0}:${row?.latest ?? ""}`).digest("hex");
16829
+ return h.slice(0, 16);
16830
+ }
16831
+ function esc(s) {
16832
+ return (s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
16833
+ }
16834
+ function fmtDate2(iso) {
16835
+ if (!iso)
16836
+ return "\u2014";
16837
+ return iso.slice(0, 10);
16838
+ }
16839
+ function severityBadge(type) {
16840
+ const level = SEVERITY[type] ?? "Low";
16841
+ const color = level === "Critical" ? "#dc2626" : level === "High" ? "#d97706" : level === "Medium" ? "#2563eb" : "#6b7280";
16842
+ return `<span style="background:${color};color:#fff;padding:1px 6px;border-radius:3px;font-size:11px;font-weight:600">${level}</span>`;
16843
+ }
16844
+ function buildHtml(params) {
16845
+ const { host, fingerprint, overview, toolUsage, detections, inventory, integrity, generatedAt } = params;
16846
+ const periodStart = fmtDate2(overview.period_start) || "\u2014";
16847
+ const periodEnd = fmtDate2(overview.period_end) || "\u2014";
16848
+ const integrityStatus = integrity.broken === 0 ? `<span class="ok">&#10003; All ${integrity.intact} chained session(s) intact</span>` : `<span class="fail">&#9888; ${integrity.broken} session(s) with broken chain</span>`;
16849
+ const toolRows = toolUsage.map(
16850
+ (r) => `<tr><td>${esc(r.tool)}</td><td>${r.sessions}</td><td>${r.messages}</td></tr>`
16851
+ ).join("");
16852
+ const detectionRows = detections.map((r) => {
16853
+ const samples = (r.samples ?? "").split(", ").slice(0, 3).map(esc).join(", ");
16854
+ return `<tr><td>${esc(r.type)}</td><td>${severityBadge(r.type)}</td><td>${r.cnt}</td><td>${samples}</td></tr>`;
16855
+ }).join("") || '<tr><td colspan="4" style="color:#6b7280">No detections recorded</td></tr>';
16856
+ const inventoryRows = inventory.map(
16857
+ (r) => `<tr><td><code style="font-size:11px">${esc(r.id.slice(0, 8))}</code></td><td>${esc(r.title)}</td><td>${esc(r.ai_tool ?? "unknown")}</td><td>${fmtDate2(r.created_at)}</td><td>${fmtDate2(r.updated_at)}</td><td>${r.message_count}</td></tr>`
16858
+ ).join("") || '<tr><td colspan="6" style="color:#6b7280">No sessions</td></tr>';
16859
+ const brokenList = integrity.brokenSessions.length > 0 ? `<p class="fail">Sessions with broken chains: ${integrity.brokenSessions.map(esc).join(", ")}</p>` : "";
16860
+ return `<!DOCTYPE html>
16861
+ <html lang="en">
16862
+ <head>
16863
+ <meta charset="UTF-8">
16864
+ <meta name="viewport" content="width=device-width,initial-scale=1">
16865
+ <title>Chron AI Audit \u2014 SOC 2 Evidence Package</title>
16866
+ <style>${CSS}</style>
16867
+ </head>
16868
+ <body>
16869
+ <div class="page">
16870
+
16871
+ <!-- \u2460 COVER PAGE -->
16872
+ <div style="min-height:60vh;display:flex;flex-direction:column;justify-content:center">
16873
+ <div class="cover-badge">SOC 2 Evidence Package</div>
16874
+ <h1>AI Conversation Audit Report</h1>
16875
+ <p class="meta">Prepared by <strong>chron-mcp</strong> &nbsp;|&nbsp; Organisation: <strong>${esc(host)}</strong></p>
16876
+ <table style="width:auto;margin-bottom:0">
16877
+ <tr><th>Reporting Period</th><td>${periodStart} \u2013 ${periodEnd}</td></tr>
16878
+ <tr><th>Generated</th><td>${generatedAt}</td></tr>
16879
+ <tr><th>Evidence DB Fingerprint</th><td><span class="fingerprint">${fingerprint}</span></td></tr>
16880
+ <tr><th>Total Sessions</th><td>${overview.total_sessions}</td></tr>
16881
+ <tr><th>Total Messages</th><td>${overview.total_messages}</td></tr>
16882
+ </table>
16883
+ <p style="margin-top:20px;font-size:11px;color:#9ca3af">
16884
+ This report is generated from a locally-stored, tamper-evident SQLite database. All AI conversation
16885
+ activity has been recorded with cryptographic hash chaining. No conversation content is transmitted
16886
+ externally. This document is intended as supporting evidence for SOC 2 Type II audits under the
16887
+ Availability and Confidentiality trust service criteria.
16888
+ </p>
16889
+ </div>
16890
+
16891
+ <!-- \u2461 EXECUTIVE SUMMARY -->
16892
+ <h2 class="section-break">Executive Summary</h2>
16893
+ <div class="stat-grid">
16894
+ <div class="stat"><div class="stat-value">${overview.total_sessions}</div><div class="stat-label">Total Sessions</div></div>
16895
+ <div class="stat"><div class="stat-value">${overview.total_messages}</div><div class="stat-label">Total Messages</div></div>
16896
+ <div class="stat"><div class="stat-value">${overview.total_detections}</div><div class="stat-label">PII / Secret Detections</div></div>
16897
+ <div class="stat"><div class="stat-value">${integrity.broken === 0 ? "\u2713" : "\u2717"}</div><div class="stat-label">Chain Integrity</div></div>
16898
+ </div>
16899
+ <p>Messages: <strong>${overview.user_messages}</strong> user turns, <strong>${overview.ai_messages}</strong> AI turns. Reporting period: <strong>${periodStart}</strong> to <strong>${periodEnd}</strong>.</p>
16900
+ <p>Chain integrity: ${integrityStatus}${integrity.unchained > 0 ? ` (${integrity.unchained} session(s) pre-date hash chaining)` : ""}.</p>
16901
+ ${overview.total_detections > 0 ? `<p class="warn">&#9888; ${overview.total_detections} sensitive data detection(s) recorded. See Section 5 for details.</p>` : `<p class="ok">&#10003; No sensitive data detections recorded in this period.</p>`}
16902
+
16903
+ <!-- \u2462 AI TOOL USAGE -->
16904
+ <h2>AI Tool Usage</h2>
16905
+ <table>
16906
+ <thead><tr><th>AI Tool / Provider</th><th>Sessions</th><th>Messages</th></tr></thead>
16907
+ <tbody>${toolRows || '<tr><td colspan="3" style="color:#6b7280">No data</td></tr>'}</tbody>
16908
+ </table>
16909
+
16910
+ <!-- \u2463 ACTIVITY SUMMARY -->
16911
+ <h2>Activity Summary</h2>
16912
+ <p>All sessions are listed in the Session Inventory (Section 7). The table below shows per-tool message distribution.</p>
16913
+ <table>
16914
+ <thead><tr><th>AI Tool</th><th>User Messages</th><th>AI Messages</th><th>Ratio</th></tr></thead>
16915
+ <tbody>
16916
+ ${toolUsage.map((r) => {
16917
+ const ratio = r.messages > 0 ? (r.messages / 2 / r.sessions).toFixed(1) : "0";
16918
+ return `<tr><td>${esc(r.tool)}</td><td>~${Math.round(r.messages / 2)}</td><td>~${Math.round(r.messages / 2)}</td><td>${ratio} turns/session avg</td></tr>`;
16919
+ }).join("") || '<tr><td colspan="4" style="color:#6b7280">No data</td></tr>'}
16920
+ </tbody>
16921
+ </table>
16922
+ <p style="font-size:11px;color:#6b7280">Note: User/AI message split shown as estimate (\xF72) because exact per-session breakdown requires individual session queries.</p>
16923
+
16924
+ <!-- \u2464 SENSITIVE DATA CONTROLS -->
16925
+ <h2 class="section-break">Sensitive Data Controls</h2>
16926
+ <p>The following sensitive data types were detected during the reporting period. All values are masked \u2014 no plaintext credentials are stored in this report.</p>
16927
+ <table>
16928
+ <thead><tr><th>Detection Type</th><th>Severity</th><th>Count</th><th>Sample Masked Values</th></tr></thead>
16929
+ <tbody>${detectionRows}</tbody>
16930
+ </table>
16931
+ <h3>Control Measures</h3>
16932
+ <ul>
16933
+ <li>All detected values are masked at log time using token substitution (e.g. <code>$CHRON_CREDIT_CARD_1</code>).</li>
16934
+ <li>Original values are never stored in plaintext in the audit database.</li>
16935
+ <li>Detections trigger a <code>secret_detected</code> relay event to any connected SIEM (Splunk, Sentinel, LogScale).</li>
16936
+ <li>Luhn validation is applied to credit card numbers; IBAN mod-97 validation applied to bank account numbers.</li>
16937
+ </ul>
16938
+
16939
+ <!-- \u2465 AUDIT TRAIL INTEGRITY -->
16940
+ <h2>Audit Trail Integrity</h2>
16941
+ <p>Each message is linked to the previous message in its session via SHA-256 hash chaining. The chain is verified by confirming that each message's <code>prev_hash</code> matches the <code>content_hash</code> of the preceding message.</p>
16942
+ <table>
16943
+ <thead><tr><th>Metric</th><th>Value</th></tr></thead>
16944
+ <tbody>
16945
+ <tr><td>Sessions with intact chain</td><td class="ok">${integrity.intact}</td></tr>
16946
+ <tr><td>Sessions with broken chain</td><td class="${integrity.broken > 0 ? "fail" : "ok"}">${integrity.broken}</td></tr>
16947
+ <tr><td>Sessions pre-dating hash chaining</td><td>${integrity.unchained}</td></tr>
16948
+ </tbody>
16949
+ </table>
16950
+ ${brokenList}
16951
+ <p style="font-size:11px;color:#6b7280">
16952
+ Note: This report verifies <em>chain linkage</em> (prev_hash continuity). Full content-hash recomputation
16953
+ (which detects in-place tampering) can be performed with <code>chron verify &lt;session-id&gt;</code>.
16954
+ </p>
16955
+
16956
+ <!-- \u2466 SESSION INVENTORY -->
16957
+ <h2 class="section-break">Session Inventory</h2>
16958
+ <p>All sessions recorded in the audit database (most recent first, capped at 500 rows).</p>
16959
+ <table>
16960
+ <thead><tr><th>ID (prefix)</th><th>Title</th><th>AI Tool</th><th>Started</th><th>Last Active</th><th>Messages</th></tr></thead>
16961
+ <tbody>${inventoryRows}</tbody>
16962
+ </table>
16963
+
16964
+ <!-- \u2467 METHODOLOGY APPENDIX -->
16965
+ <h2>Methodology Appendix</h2>
16966
+ <div class="appendix">
16967
+ <h3>Data Collection</h3>
16968
+ <p>Chron MCP intercepts every AI conversation turn via the Model Context Protocol. Messages are written to a local SQLite database at <code>~/.chron/chron.db</code> (or the path specified by <code>CHRON_DB_PATH</code>).</p>
16969
+
16970
+ <h3>Hash Chain Construction</h3>
16971
+ <ul>
16972
+ <li>Each message record contains a <code>content_hash</code> field: <code>SHA-256(session_id || role || content || created_at || prev_hash)</code></li>
16973
+ <li>The <code>prev_hash</code> field links to the <code>content_hash</code> of the preceding message in the session.</li>
16974
+ <li>Any insertion, deletion, or modification of a message breaks the chain at that point.</li>
16975
+ </ul>
16976
+
16977
+ <h3>PII / Secret Detection</h3>
16978
+ <ul>
16979
+ <li>Detection uses regex patterns with algorithmic validation (Luhn for credit cards, mod-97 for IBAN).</li>
16980
+ <li>Context-aware patterns (DOB, passport) require relevant keywords within 120 characters.</li>
16981
+ <li>Severity levels: Critical (SSN, credit card, private key, IBAN), High (API keys, passwords, passport), Medium (email, phone), Low (internal IP, env vars).</li>
16982
+ </ul>
16983
+
16984
+ <h3>Limitations</h3>
16985
+ <ul>
16986
+ <li>This report covers conversation metadata and detection events. Message content is not included.</li>
16987
+ <li>Developer identity is derived from session titles and AI tool labels, not from authenticated principals.</li>
16988
+ <li>The chain integrity check in this report is a linkage check. Re-run <code>chron verify</code> per session for full content-hash verification.</li>
16989
+ </ul>
16990
+
16991
+ <p style="margin-top:12px;font-size:11px;color:#9ca3af">Generated by chron-mcp &nbsp;|&nbsp; Evidence DB fingerprint: <span class="fingerprint">${fingerprint}</span> &nbsp;|&nbsp; ${generatedAt}</p>
16992
+ </div>
16993
+
16994
+ </div>
16995
+ </body>
16996
+ </html>`;
16997
+ }
16998
+ async function buildSoc2Report(outputPath) {
16999
+ const db = await initDb();
17000
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19) + " UTC";
17001
+ const [overview, toolUsage, detections, inventory, integrity, fingerprint] = await Promise.all([
17002
+ queryOverview(db),
17003
+ queryToolUsage(db),
17004
+ queryDetectionsByType(db),
17005
+ querySessionInventory(db),
17006
+ queryIntegrity(db),
17007
+ dbFingerprint(db)
17008
+ ]);
17009
+ const html = buildHtml({
17010
+ host: (0, import_os2.hostname)(),
17011
+ fingerprint,
17012
+ overview,
17013
+ toolUsage,
17014
+ detections,
17015
+ inventory,
17016
+ integrity,
17017
+ generatedAt
17018
+ });
17019
+ (0, import_fs2.writeFileSync)(outputPath, html, "utf8");
17020
+ }
17021
+ var import_fs2, import_os2, import_crypto, SEVERITY, CSS;
17022
+ var init_soc2 = __esm({
17023
+ "src/cli/soc2.ts"() {
17024
+ "use strict";
17025
+ import_fs2 = require("fs");
17026
+ import_os2 = require("os");
17027
+ import_crypto = require("crypto");
17028
+ init_db2();
17029
+ SEVERITY = {
17030
+ private_key: "Critical",
17031
+ credit_card: "Critical",
17032
+ ssn: "Critical",
17033
+ iban: "Critical",
17034
+ passport: "High",
17035
+ dob: "High",
17036
+ aws_access_key: "High",
17037
+ anthropic_api_key: "High",
17038
+ openai_api_key: "High",
17039
+ google_api_key: "High",
17040
+ github_token: "High",
17041
+ slack_token: "High",
17042
+ stripe_key: "High",
17043
+ sendgrid_key: "High",
17044
+ huggingface_token: "High",
17045
+ jwt: "High",
17046
+ url_credentials: "High",
17047
+ password: "High",
17048
+ credential_pair: "High",
17049
+ email: "Medium",
17050
+ phone_us: "Medium",
17051
+ phone_e164: "Medium",
17052
+ internal_ip: "Low",
17053
+ env_value: "Low"
17054
+ };
17055
+ CSS = `
17056
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
17057
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; color: #111; background: #fff; line-height: 1.5; }
17058
+ .page { max-width: 900px; margin: 0 auto; padding: 40px 48px; }
17059
+ h1 { font-size: 28px; font-weight: 700; margin-bottom: 4px; }
17060
+ h2 { font-size: 17px; font-weight: 700; margin: 32px 0 10px; padding-bottom: 4px; border-bottom: 2px solid #e5e7eb; color: #1f2937; }
17061
+ h3 { font-size: 14px; font-weight: 600; margin: 20px 0 6px; color: #374151; }
17062
+ p { margin-bottom: 8px; color: #374151; }
17063
+ .meta { color: #6b7280; font-size: 12px; margin-bottom: 32px; }
17064
+ .cover-badge { display: inline-block; background: #1d4ed8; color: #fff; padding: 3px 10px; border-radius: 4px; font-size: 12px; font-weight: 600; margin-bottom: 16px; }
17065
+ table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-size: 12px; }
17066
+ th { background: #f3f4f6; text-align: left; padding: 6px 10px; font-weight: 600; color: #374151; border: 1px solid #e5e7eb; }
17067
+ td { padding: 5px 10px; border: 1px solid #e5e7eb; vertical-align: top; word-break: break-word; max-width: 300px; }
17068
+ tr:nth-child(even) td { background: #fafafa; }
17069
+ .stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
17070
+ .stat { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 12px 16px; }
17071
+ .stat-value { font-size: 24px; font-weight: 700; color: #1f2937; }
17072
+ .stat-label { font-size: 11px; color: #6b7280; margin-top: 2px; }
17073
+ .ok { color: #16a34a; font-weight: 600; }
17074
+ .warn { color: #d97706; font-weight: 600; }
17075
+ .fail { color: #dc2626; font-weight: 600; }
17076
+ .fingerprint { font-family: monospace; background: #f3f4f6; padding: 2px 6px; border-radius: 3px; font-size: 11px; }
17077
+ .section-break { page-break-before: always; }
17078
+ .appendix { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px 20px; margin-top: 12px; }
17079
+ .appendix p, .appendix li { color: #374151; font-size: 12px; }
17080
+ ul { padding-left: 20px; margin-bottom: 8px; }
17081
+ li { margin-bottom: 4px; }
17082
+ @media print {
17083
+ body { font-size: 11px; }
17084
+ .page { padding: 20px 24px; max-width: 100%; }
17085
+ h1 { font-size: 22px; }
17086
+ h2 { font-size: 14px; }
17087
+ .stat-value { font-size: 20px; }
17088
+ .section-break { page-break-before: always; }
17089
+ }
17090
+ `;
17091
+ }
17092
+ });
17093
+
16734
17094
  // src/cli/report.ts
16735
17095
  var report_exports = {};
16736
17096
  __export(report_exports, {
@@ -16814,6 +17174,16 @@ ${BOLD2}Chron Report${RESET2} ${dateFrom} \u2192 ${dateTo}
16814
17174
  process.stdout.write("\n");
16815
17175
  }
16816
17176
  async function runReport(args2) {
17177
+ const format = args2.find((a) => a.startsWith("--format="))?.slice("--format=".length);
17178
+ const outputArg = args2.find((a) => a.startsWith("--output="))?.slice("--output=".length);
17179
+ if (format === "soc2") {
17180
+ const output = outputArg ?? "soc2-report.html";
17181
+ const { buildSoc2Report: buildSoc2Report2 } = await Promise.resolve().then(() => (init_soc2(), soc2_exports));
17182
+ await buildSoc2Report2(output);
17183
+ process.stdout.write(`SOC 2 evidence package written to ${output}
17184
+ `);
17185
+ return;
17186
+ }
16817
17187
  const sinceArg = args2.find((a) => a.startsWith("--since="))?.slice("--since=".length);
16818
17188
  let cutoffDate = null;
16819
17189
  if (sinceArg) {
@@ -16914,7 +17284,7 @@ __export(settings_exports, {
16914
17284
  runSettings: () => runSettings
16915
17285
  });
16916
17286
  function dbPath() {
16917
- return process.env.CHRON_DB_PATH ?? (0, import_path2.join)((0, import_os2.homedir)(), ".chron", "chron.db");
17287
+ return process.env.CHRON_DB_PATH ?? (0, import_path2.join)((0, import_os3.homedir)(), ".chron", "chron.db");
16918
17288
  }
16919
17289
  async function runSettings(_args) {
16920
17290
  process.stdout.write(`
@@ -16928,11 +17298,11 @@ ${BOLD3}Chron Settings${RESET3}
16928
17298
 
16929
17299
  `);
16930
17300
  }
16931
- var import_os2, import_path2, RESET3, BOLD3, DIM2, CYAN2;
17301
+ var import_os3, import_path2, RESET3, BOLD3, DIM2, CYAN2;
16932
17302
  var init_settings = __esm({
16933
17303
  "src/cli/settings.ts"() {
16934
17304
  "use strict";
16935
- import_os2 = require("os");
17305
+ import_os3 = require("os");
16936
17306
  import_path2 = require("path");
16937
17307
  RESET3 = "\x1B[0m";
16938
17308
  BOLD3 = "\x1B[1m";
@@ -16946,7 +17316,7 @@ var secrets_exports = {};
16946
17316
  __export(secrets_exports, {
16947
17317
  runSecrets: () => runSecrets
16948
17318
  });
16949
- function fmtDate2(iso) {
17319
+ function fmtDate3(iso) {
16950
17320
  return iso.slice(0, 16).replace("T", " ");
16951
17321
  }
16952
17322
  async function runSecrets(args2) {
@@ -16983,7 +17353,7 @@ ${BOLD4}${session.title}${RESET4}
16983
17353
  `);
16984
17354
  for (const row of rows) {
16985
17355
  process.stdout.write(
16986
- ` ${DIM3}${fmtDate2(row.detected_at)}${RESET4} ${YELLOW2}${row.type.padEnd(maxType)}${RESET4} ${row.masked_value}
17356
+ ` ${DIM3}${fmtDate3(row.detected_at)}${RESET4} ${YELLOW2}${row.type.padEnd(maxType)}${RESET4} ${row.masked_value}
16987
17357
  `
16988
17358
  );
16989
17359
  }
@@ -17004,7 +17374,7 @@ ${BOLD4}${session.title}${RESET4}
17004
17374
  process.stdout.write("\n");
17005
17375
  for (const row of rows) {
17006
17376
  process.stdout.write(
17007
- ` ${DIM3}${fmtDate2(row.detected_at)}${RESET4} ${CYAN3}${row.session_id.slice(0, 8)}${RESET4} ${YELLOW2}${row.type.padEnd(maxType)}${RESET4} ${row.masked_value.padEnd(20)} ${DIM3}${row.title}${RESET4}
17377
+ ` ${DIM3}${fmtDate3(row.detected_at)}${RESET4} ${CYAN3}${row.session_id.slice(0, 8)}${RESET4} ${YELLOW2}${row.type.padEnd(maxType)}${RESET4} ${row.masked_value.padEnd(20)} ${DIM3}${row.title}${RESET4}
17008
17378
  `
17009
17379
  );
17010
17380
  }
@@ -17031,7 +17401,7 @@ var require_package = __commonJS({
17031
17401
  "package.json"(exports2, module2) {
17032
17402
  module2.exports = {
17033
17403
  name: "chron-mcp",
17034
- version: "0.1.17",
17404
+ version: "0.1.18",
17035
17405
  mcpName: "io.github.sirinivask/chron",
17036
17406
  description: "Audit-grade timestamped logs for every AI conversation",
17037
17407
  repository: {
@@ -17108,33 +17478,35 @@ function prompt(rl, question) {
17108
17478
  return new Promise((resolve) => rl.question(question, resolve));
17109
17479
  }
17110
17480
  function configPath() {
17111
- return (0, import_path3.join)((0, import_os3.homedir)(), ".chron", "config.json");
17481
+ return (0, import_path3.join)((0, import_os4.homedir)(), ".chron", "config.json");
17112
17482
  }
17113
17483
  function loadConfig() {
17114
17484
  try {
17115
- return JSON.parse((0, import_fs2.readFileSync)(configPath(), "utf8"));
17485
+ return JSON.parse((0, import_fs3.readFileSync)(configPath(), "utf8"));
17116
17486
  } catch {
17117
17487
  return {};
17118
17488
  }
17119
17489
  }
17120
17490
  function saveConfig(data) {
17121
- const dir = (0, import_path3.join)((0, import_os3.homedir)(), ".chron");
17122
- (0, import_fs2.mkdirSync)(dir, { recursive: true });
17123
- (0, import_fs2.writeFileSync)(configPath(), JSON.stringify(data, null, 2), "utf8");
17491
+ const dir = (0, import_path3.join)((0, import_os4.homedir)(), ".chron");
17492
+ (0, import_fs3.mkdirSync)(dir, { recursive: true });
17493
+ (0, import_fs3.writeFileSync)(configPath(), JSON.stringify(data, null, 2), "utf8");
17124
17494
  }
17125
17495
  function patchClaudeJson(vars) {
17126
- const path = (0, import_path3.join)((0, import_os3.homedir)(), ".claude.json");
17127
- if (!(0, import_fs2.existsSync)(path))
17496
+ const path = (0, import_path3.join)((0, import_os4.homedir)(), ".claude.json");
17497
+ if (!(0, import_fs3.existsSync)(path))
17128
17498
  return false;
17129
17499
  try {
17130
- const raw = (0, import_fs2.readFileSync)(path, "utf8");
17500
+ const raw = (0, import_fs3.readFileSync)(path, "utf8");
17131
17501
  const doc = JSON.parse(raw);
17132
17502
  const servers = doc.mcpServers ?? {};
17133
17503
  const chron = servers.chron ?? {};
17504
+ if (!chron.command)
17505
+ return false;
17134
17506
  chron.env = { ...chron.env ?? {}, ...vars };
17135
17507
  servers.chron = chron;
17136
17508
  doc.mcpServers = servers;
17137
- (0, import_fs2.writeFileSync)(path, JSON.stringify(doc, null, 2), "utf8");
17509
+ (0, import_fs3.writeFileSync)(path, JSON.stringify(doc, null, 2), "utf8");
17138
17510
  return true;
17139
17511
  } catch {
17140
17512
  return false;
@@ -17493,30 +17865,20 @@ ${DIM4}Authenticating with Azure AD...${RESET5} `);
17493
17865
  process.exit(1);
17494
17866
  }
17495
17867
  const config = loadConfig();
17496
- config.sentinel = { dce, dcrId, stream, tenantId, clientId, connected_at: (/* @__PURE__ */ new Date()).toISOString() };
17868
+ config.sentinel = { dce, dcrId, stream, tenantId, clientId, clientSecret, connected_at: (/* @__PURE__ */ new Date()).toISOString() };
17497
17869
  saveConfig(config);
17870
+ patchClaudeJson({
17871
+ CHRON_SENTINEL_TENANT_ID: tenantId,
17872
+ CHRON_SENTINEL_CLIENT_ID: clientId,
17873
+ CHRON_SENTINEL_CLIENT_SECRET: clientSecret,
17874
+ CHRON_SENTINEL_DCE: dce,
17875
+ CHRON_SENTINEL_DCR_ID: dcrId,
17876
+ CHRON_SENTINEL_STREAM: stream
17877
+ });
17498
17878
  process.stdout.write(`${GREEN}${BOLD5}Connected!${RESET5} Test event sent to Sentinel workspace.
17499
17879
 
17500
17880
  `);
17501
- process.stdout.write(`${BOLD5}Add these env vars to your MCP client config:${RESET5}
17502
-
17503
- `);
17504
- process.stdout.write(` ${CYAN4}CHRON_SENTINEL_TENANT_ID${RESET5} ${tenantId}
17505
- `);
17506
- process.stdout.write(` ${CYAN4}CHRON_SENTINEL_CLIENT_ID${RESET5} ${clientId}
17507
- `);
17508
- process.stdout.write(` ${CYAN4}CHRON_SENTINEL_CLIENT_SECRET${RESET5} ${DIM4}<your-client-secret>${RESET5}
17509
- `);
17510
- process.stdout.write(` ${CYAN4}CHRON_SENTINEL_DCE${RESET5} ${dce}
17511
- `);
17512
- process.stdout.write(` ${CYAN4}CHRON_SENTINEL_DCR_ID${RESET5} ${dcrId}
17513
- `);
17514
- process.stdout.write(` ${CYAN4}CHRON_SENTINEL_STREAM${RESET5} ${stream}
17515
-
17516
- `);
17517
- process.stdout.write(`${DIM4}For Claude Code \u2014 add to the "env" block of the chron entry in ~/.claude.json.
17518
- `);
17519
- process.stdout.write(`Connection saved to ~/.chron/config.json${RESET5}
17881
+ process.stdout.write(`${DIM4}Config saved to ~/.chron/config.json \u2014 events will flow immediately in all running sessions.${RESET5}
17520
17882
 
17521
17883
  `);
17522
17884
  }
@@ -17549,14 +17911,14 @@ Integrations:
17549
17911
  }
17550
17912
  }
17551
17913
  }
17552
- var import_readline, import_os3, import_path3, import_fs2, RESET5, BOLD5, DIM4, CYAN4, GREEN, RED, YELLOW3;
17914
+ var import_readline, import_os4, import_path3, import_fs3, RESET5, BOLD5, DIM4, CYAN4, GREEN, RED, YELLOW3;
17553
17915
  var init_connect = __esm({
17554
17916
  "src/cli/connect.ts"() {
17555
17917
  "use strict";
17556
17918
  import_readline = require("readline");
17557
- import_os3 = require("os");
17919
+ import_os4 = require("os");
17558
17920
  import_path3 = require("path");
17559
- import_fs2 = require("fs");
17921
+ import_fs3 = require("fs");
17560
17922
  RESET5 = "\x1B[0m";
17561
17923
  BOLD5 = "\x1B[1m";
17562
17924
  DIM4 = "\x1B[2m";
@@ -17567,6 +17929,177 @@ var init_connect = __esm({
17567
17929
  }
17568
17930
  });
17569
17931
 
17932
+ // src/cli/summary.ts
17933
+ var summary_exports = {};
17934
+ __export(summary_exports, {
17935
+ buildSummary: () => buildSummary,
17936
+ runSummary: () => runSummary
17937
+ });
17938
+ function detectMutations(content) {
17939
+ const hits = [];
17940
+ for (const { label, re } of MUTATION_PATTERNS) {
17941
+ const m = re.exec(content);
17942
+ if (m)
17943
+ hits.push(`${label}: ${m[0].trim().slice(0, 80)}`);
17944
+ }
17945
+ return hits;
17946
+ }
17947
+ function formatLatency(ms) {
17948
+ if (ms < 1e3)
17949
+ return `${ms}ms`;
17950
+ return `${(ms / 1e3).toFixed(1)}s`;
17951
+ }
17952
+ function formatTs(iso) {
17953
+ return iso.replace("T", " ").replace(/\.\d+.*$/, "");
17954
+ }
17955
+ async function buildSummary(sessionPrefix) {
17956
+ const db = await initDb();
17957
+ const allSessions = await db.select().from(sessions);
17958
+ const session = allSessions.find((s) => s.id.startsWith(sessionPrefix));
17959
+ if (!session)
17960
+ return null;
17961
+ const msgs = await db.select().from(messages).where(eq(messages.session_id, session.id)).orderBy(asc(messages.created_at), asc(sql`rowid`));
17962
+ const secs = await db.select().from(secrets_detected).where(eq(secrets_detected.session_id, session.id)).orderBy(asc(secrets_detected.detected_at));
17963
+ const timeline = [];
17964
+ const mutationsList = [];
17965
+ const prodWrites = [];
17966
+ let userTurns = 0, aiTurns = 0;
17967
+ for (let i = 0; i < msgs.length; i++) {
17968
+ const m = msgs[i];
17969
+ const prev = msgs[i - 1];
17970
+ const latency_ms = prev ? new Date(m.created_at).getTime() - new Date(prev.created_at).getTime() : null;
17971
+ if (m.role === "user")
17972
+ userTurns++;
17973
+ else
17974
+ aiTurns++;
17975
+ const preview = m.content.replace(/\n/g, " ").slice(0, 120) + (m.content.length > 120 ? "\u2026" : "");
17976
+ timeline.push({ turn: i + 1, role: m.role, timestamp: m.created_at, latency_ms, preview });
17977
+ if (m.role === "assistant") {
17978
+ const mutations = detectMutations(m.content);
17979
+ for (const label of mutations) {
17980
+ mutationsList.push({ turn: i + 1, role: "assistant", snippet: label, label: label.split(":")[0] });
17981
+ }
17982
+ if (PROD_PATTERNS.test(m.content)) {
17983
+ prodWrites.push({ turn: i + 1, snippet: m.content.slice(0, 120) });
17984
+ }
17985
+ }
17986
+ }
17987
+ const firstTs = msgs[0]?.created_at;
17988
+ const lastTs = msgs[msgs.length - 1]?.created_at;
17989
+ const duration_minutes = firstTs && lastTs ? Math.round((new Date(lastTs).getTime() - new Date(firstTs).getTime()) / 6e4) : 0;
17990
+ return {
17991
+ session: {
17992
+ id: session.id,
17993
+ title: session.title,
17994
+ ai_tool: session.ai_tool,
17995
+ started: session.created_at,
17996
+ last_active: session.updated_at
17997
+ },
17998
+ stats: { total_messages: msgs.length, user_turns: userTurns, ai_turns: aiTurns, duration_minutes },
17999
+ secrets: secs.map((s) => ({ type: s.type, masked_value: s.masked_value, detected_at: s.detected_at })),
18000
+ mutations: mutationsList,
18001
+ prod_writes: prodWrites,
18002
+ timeline
18003
+ };
18004
+ }
18005
+ function printSummary(data) {
18006
+ const { session, stats, secrets, mutations, prod_writes, timeline } = data;
18007
+ process.stdout.write(`
18008
+ ${BOLD6}Session Summary${RESET6}
18009
+ `);
18010
+ process.stdout.write(`${DIM5}${session.title}${RESET6}
18011
+ `);
18012
+ process.stdout.write(`${DIM5}ID: ${session.id.slice(0, 8)} | Tool: ${session.ai_tool ?? "unknown"} | Started: ${formatTs(session.started)}${RESET6}
18013
+
18014
+ `);
18015
+ process.stdout.write(`${BOLD6}Stats${RESET6}
18016
+ `);
18017
+ process.stdout.write(` Messages ${stats.total_messages} (you: ${stats.user_turns}, ai: ${stats.ai_turns})
18018
+ `);
18019
+ process.stdout.write(` Duration ${stats.duration_minutes}m
18020
+
18021
+ `);
18022
+ process.stdout.write(`${BOLD6}Secrets touched${RESET6} ${secrets.length === 0 ? `${GREEN2}none${RESET6}` : `${RED2}${secrets.length} detection(s)${RESET6}`}
18023
+ `);
18024
+ for (const s of secrets) {
18025
+ process.stdout.write(` ${RED2}[${s.type}]${RESET6} ${DIM5}${s.masked_value}${RESET6}
18026
+ `);
18027
+ }
18028
+ if (secrets.length)
18029
+ process.stdout.write("\n");
18030
+ process.stdout.write(`${BOLD6}Mutations detected${RESET6} ${mutations.length === 0 ? `${DIM5}none${RESET6}` : `${mutations.length}`}
18031
+ `);
18032
+ for (const m of mutations) {
18033
+ process.stdout.write(` ${CYAN5}[turn ${m.turn}]${RESET6} ${m.snippet}
18034
+ `);
18035
+ }
18036
+ if (mutations.length)
18037
+ process.stdout.write("\n");
18038
+ if (prod_writes.length > 0) {
18039
+ process.stdout.write(`${BOLD6}${YELLOW4}\u26A0 Possible prod references${RESET6} ${prod_writes.length}
18040
+ `);
18041
+ for (const p of prod_writes) {
18042
+ process.stdout.write(` ${YELLOW4}[turn ${p.turn}]${RESET6} ${DIM5}${p.snippet}\u2026${RESET6}
18043
+ `);
18044
+ }
18045
+ process.stdout.write("\n");
18046
+ }
18047
+ process.stdout.write(`${BOLD6}Timeline${RESET6}
18048
+ `);
18049
+ for (const t of timeline) {
18050
+ const role = t.role === "user" ? `${BOLD6}you${RESET6}` : `${CYAN5}ai ${RESET6}`;
18051
+ const lat = t.latency_ms !== null ? ` ${DIM5}+${formatLatency(t.latency_ms)}${RESET6}` : "";
18052
+ process.stdout.write(` ${DIM5}[${t.turn.toString().padStart(3)}]${RESET6} ${role}${lat} ${DIM5}${t.preview}${RESET6}
18053
+ `);
18054
+ }
18055
+ process.stdout.write("\n");
18056
+ }
18057
+ async function runSummary(args2) {
18058
+ const [prefix, ...flags] = args2;
18059
+ const jsonMode = flags.includes("--json");
18060
+ if (!prefix) {
18061
+ process.stdout.write("Usage: chron summary <session-id-prefix> [--json]\n");
18062
+ process.exit(1);
18063
+ }
18064
+ const data = await buildSummary(prefix);
18065
+ if (!data) {
18066
+ process.stderr.write(`No session found with prefix: ${prefix}
18067
+ `);
18068
+ process.exit(1);
18069
+ }
18070
+ if (jsonMode) {
18071
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
18072
+ } else {
18073
+ printSummary(data);
18074
+ }
18075
+ }
18076
+ var RESET6, BOLD6, DIM5, CYAN5, GREEN2, YELLOW4, RED2, MUTATION_PATTERNS, PROD_PATTERNS;
18077
+ var init_summary = __esm({
18078
+ "src/cli/summary.ts"() {
18079
+ "use strict";
18080
+ init_drizzle_orm();
18081
+ init_db2();
18082
+ init_schema();
18083
+ RESET6 = "\x1B[0m";
18084
+ BOLD6 = "\x1B[1m";
18085
+ DIM5 = "\x1B[2m";
18086
+ CYAN5 = "\x1B[36m";
18087
+ GREEN2 = "\x1B[32m";
18088
+ YELLOW4 = "\x1B[33m";
18089
+ RED2 = "\x1B[31m";
18090
+ MUTATION_PATTERNS = [
18091
+ { label: "file write", re: /\b(?:wrote?|created?|saved?|updated?) (?:file |the file )?["']?([^\s"']{1,80})/i },
18092
+ { label: "git", re: /\bgit (?:commit|push|merge|rebase|reset|checkout)\b/i },
18093
+ { label: "package", re: /\b(?:npm|pip|yarn|pnpm|cargo|go get) (?:install|add|update|remove)\b/i },
18094
+ { label: "infra", re: /\b(?:kubectl|terraform|helm|pulumi|ansible|docker) (?:apply|deploy|run|push|create|delete)\b/i },
18095
+ { label: "db write", re: /\b(?:INSERT|UPDATE|DELETE|DROP|ALTER|CREATE TABLE)\b/i },
18096
+ { label: "API call", re: /\b(?:POST|PUT|PATCH|DELETE) (?:request|call|to )?\/?(?:https?:\/\/)?[a-z0-9\-._]+\/[^\s]{1,60}/i },
18097
+ { label: "deploy", re: /\b(?:deploy(?:ed|ing)?|ship(?:ped|ping)?|release(?:d|ing)?)\b/i }
18098
+ ];
18099
+ PROD_PATTERNS = /\b(?:production|prod[^a-z]|live\s+server|live\s+env|live\s+database)\b/i;
18100
+ }
18101
+ });
18102
+
17570
18103
  // src/cli/index.ts
17571
18104
  var [, , command, ...args] = process.argv;
17572
18105
  async function main() {
@@ -17601,6 +18134,11 @@ async function main() {
17601
18134
  await runConnect2(args);
17602
18135
  break;
17603
18136
  }
18137
+ case "summary": {
18138
+ const { runSummary: runSummary2 } = await Promise.resolve().then(() => (init_summary(), summary_exports));
18139
+ await runSummary2(args);
18140
+ break;
18141
+ }
17604
18142
  default: {
17605
18143
  const name = command ? `Unknown command: ${command}
17606
18144
 
@@ -17615,13 +18153,16 @@ Commands:
17615
18153
  secrets List detected secrets across sessions
17616
18154
  settings View current configuration
17617
18155
  connect Connect to a SIEM integration (crowdstrike, sentinel, splunk)
18156
+ summary Structured summary of a session (timeline, mutations, secrets)
17618
18157
 
17619
18158
  Options (history):
17620
18159
  --limit=<n> Max sessions to show (default: 20)
17621
18160
  <id-prefix> Show full log for the session with this ID prefix
17622
18161
 
17623
18162
  Options (report):
17624
- --since=<range> Filter by date: 7d, 30d, or YYYY-MM-DD (default: all time)
18163
+ --since=<range> Filter by date: 7d, 30d, or YYYY-MM-DD (default: all time)
18164
+ --format=soc2 Generate SOC 2 HTML evidence package
18165
+ --output=<file> Output file for --format=soc2 (default: soc2-report.html)
17625
18166
 
17626
18167
  Options (export / secrets):
17627
18168
  <id-prefix> Scope to a single session