chron-mcp 0.1.26 → 0.1.29

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/README.md CHANGED
@@ -40,6 +40,8 @@ Chron stores a local audit trail of:
40
40
 
41
41
  Chron's SIEM integrations send only metadata: session starts, role-only message events, and masked secret detections. Message content and raw secret values stay local.
42
42
 
43
+ Need runtime policy enforcement for AI agents? CLAIIM adds agent identity, approval workflows, and ALLOW/DENY gates on top of Chron proof. Try the public preview: https://claiim.io/preview
44
+
43
45
  ---
44
46
 
45
47
  ## Install
package/dist/cli/index.js CHANGED
@@ -16582,6 +16582,7 @@ async function initDb(dbPath3) {
16582
16582
  }
16583
16583
  const client = createClient({ url: path.startsWith(":") ? path : `file:${path}` });
16584
16584
  await client.execute("PRAGMA journal_mode = WAL");
16585
+ await client.execute("PRAGMA busy_timeout = 5000");
16585
16586
  await client.execute("PRAGMA foreign_keys = ON");
16586
16587
  for (const sql2 of CREATE_SQL) {
16587
16588
  await client.execute(sql2);
@@ -16614,6 +16615,14 @@ async function initDb(dbPath3) {
16614
16615
  if (!sessCols.includes("metadata")) {
16615
16616
  await client.execute("ALTER TABLE sessions ADD COLUMN metadata TEXT");
16616
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
+ }
16617
16626
  return drizzle(client, { schema: schema_exports });
16618
16627
  }
16619
16628
  var import_os, import_fs, import_path, CREATE_SQL;
@@ -16663,11 +16672,81 @@ var init_db2 = __esm({
16663
16672
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title ON sessions(title)`,
16664
16673
  `CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)`,
16665
16674
  `CREATE INDEX IF NOT EXISTS idx_secrets_detected_session_id ON secrets_detected(session_id)`,
16666
- `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`
16667
16690
  ];
16668
16691
  }
16669
16692
  });
16670
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
+
16671
16750
  // src/cli/history.ts
16672
16751
  var history_exports = {};
16673
16752
  __export(history_exports, {
@@ -16787,12 +16866,43 @@ ${msg.content}
16787
16866
  }
16788
16867
  }
16789
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
+ }
16790
16897
  async function runHistory(args2) {
16791
16898
  const limitArg = args2.find((a) => a.startsWith("--limit="));
16792
16899
  const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : 20;
16793
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("=");
16794
16902
  const positional = args2.filter((a) => !a.startsWith("--"));
16795
- if (positional.length === 0) {
16903
+ if (searchArg) {
16904
+ await printSearch(searchArg, limit);
16905
+ } else if (positional.length === 0) {
16796
16906
  await printList(limit, refArg);
16797
16907
  } else {
16798
16908
  await printDetail(positional[0]);
@@ -16805,6 +16915,7 @@ var init_history = __esm({
16805
16915
  init_drizzle_orm();
16806
16916
  init_db2();
16807
16917
  init_schema();
16918
+ init_search();
16808
16919
  RESET = "\x1B[0m";
16809
16920
  BOLD = "\x1B[1m";
16810
16921
  DIM = "\x1B[2m";
@@ -17032,12 +17143,16 @@ ${overview.total_detections > 0 ? `<p class="warn">&#9888; ${overview.total_dete
17032
17143
  </tbody>
17033
17144
  </table>
17034
17145
  ${brokenList}
17035
- <p style="font-size:11px;color:#6b7280">
17036
- Note: This report verifies <em>chain linkage</em> (prev_hash continuity). Full content-hash recomputation
17037
- (which detects in-place tampering) can be performed with <code>chron verify &lt;session-id&gt;</code>.
17038
- </p>
17146
+ <p style="font-size:11px;color:#6b7280">
17147
+ Note: This report verifies <em>chain linkage</em> (prev_hash continuity). Full content-hash recomputation
17148
+ (which detects in-place tampering) can be performed with <code>chron verify &lt;session-id&gt;</code>.
17149
+ </p>
17150
+ <p class="governance-note">
17151
+ For formal AI governance, CLAIIM extends Chron evidence with agent identity, policy gates,
17152
+ approvals, and runtime ALLOW/DENY enforcement. See <a href="https://claiim.io/preview">https://claiim.io/preview</a>.
17153
+ </p>
17039
17154
 
17040
- <!-- \u2466 SESSION INVENTORY -->
17155
+ <!-- \u2466 SESSION INVENTORY -->
17041
17156
  <h2 class="section-break">Session Inventory</h2>
17042
17157
  <p>All sessions recorded in the audit database (most recent first, capped at 500 rows).</p>
17043
17158
  <table>
@@ -17161,6 +17276,7 @@ tr:nth-child(even) td { background: #fafafa; }
17161
17276
  .section-break { page-break-before: always; }
17162
17277
  .appendix { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px 20px; margin-top: 12px; }
17163
17278
  .appendix p, .appendix li { color: #374151; font-size: 12px; }
17279
+ .governance-note { background: #f8fafc; border: 1px solid #dbeafe; border-left: 4px solid #2563eb; border-radius: 4px; padding: 12px 14px; margin: 14px 0 20px; font-size: 12px; color: #374151; }
17164
17280
  ul { padding-left: 20px; margin-bottom: 8px; }
17165
17281
  li { margin-bottom: 4px; }
17166
17282
  @media print {
@@ -17370,7 +17486,7 @@ var require_package = __commonJS({
17370
17486
  "package.json"(exports2, module2) {
17371
17487
  module2.exports = {
17372
17488
  name: "chron-mcp",
17373
- version: "0.1.26",
17489
+ version: "0.1.29",
17374
17490
  mcpName: "io.github.sirinivask/chron",
17375
17491
  description: "Audit-grade timestamped logs for every AI conversation",
17376
17492
  repository: {
@@ -18177,6 +18293,93 @@ ${DIM4}Authenticating with Azure AD...${RESET5} `);
18177
18293
  `);
18178
18294
  process.stdout.write(`${DIM4}Config saved to ~/.chron/config.json \u2014 events will flow immediately in all running sessions.${RESET5}
18179
18295
 
18296
+ `);
18297
+ }
18298
+ function codexGlobalConfigPath() {
18299
+ return (0, import_path5.join)((0, import_os6.homedir)(), ".codex", "config.toml");
18300
+ }
18301
+ function isChronInCodexConfig(content) {
18302
+ return content.includes("[mcp_servers.chron]");
18303
+ }
18304
+ function appendChronToToml(content) {
18305
+ return content.trimEnd() + "\n" + CODEX_MCP_BLOCK;
18306
+ }
18307
+ async function connectCodex() {
18308
+ process.stdout.write(`
18309
+ ${BOLD5}Connect Chron \u2192 Codex${RESET5}
18310
+
18311
+ `);
18312
+ const globalPath = codexGlobalConfigPath();
18313
+ const projectPath = (0, import_path5.join)(process.cwd(), ".codex", "config.toml");
18314
+ const hasGlobal = (0, import_fs5.existsSync)(globalPath);
18315
+ const hasProject = (0, import_fs5.existsSync)(projectPath);
18316
+ let targetPath;
18317
+ let isProject = false;
18318
+ if (hasGlobal && hasProject) {
18319
+ process.stdout.write(`${DIM4}Found both global and project Codex configs.${RESET5}
18320
+ `);
18321
+ process.stdout.write(`${DIM4} Global: ${globalPath}${RESET5}
18322
+ `);
18323
+ process.stdout.write(`${DIM4} Project: ${projectPath}${RESET5}
18324
+
18325
+ `);
18326
+ const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
18327
+ const choice = await new Promise(
18328
+ (resolve) => rl.question(` Add Chron to (g)lobal or (p)roject config? [g/p]: `, resolve)
18329
+ );
18330
+ rl.close();
18331
+ isProject = choice.trim().toLowerCase() === "p";
18332
+ targetPath = isProject ? projectPath : globalPath;
18333
+ } else if (hasProject) {
18334
+ targetPath = projectPath;
18335
+ isProject = true;
18336
+ } else if (hasGlobal) {
18337
+ targetPath = globalPath;
18338
+ } else {
18339
+ process.stdout.write(`${YELLOW3}!${RESET5} Codex config not found. Creating ${globalPath}
18340
+
18341
+ `);
18342
+ (0, import_fs5.mkdirSync)((0, import_path5.join)((0, import_os6.homedir)(), ".codex"), { recursive: true });
18343
+ (0, import_fs5.writeFileSync)(globalPath, CODEX_MCP_BLOCK.trimStart());
18344
+ process.stdout.write(`${GREEN2}\u2713${RESET5} Created ${globalPath} with Chron MCP config.
18345
+ `);
18346
+ _printCodexNextSteps();
18347
+ return;
18348
+ }
18349
+ const existing = (0, import_fs5.readFileSync)(targetPath, "utf8");
18350
+ if (isChronInCodexConfig(existing)) {
18351
+ process.stdout.write(`${GREEN2}\u2713${RESET5} Chron MCP is already configured in ${targetPath}
18352
+
18353
+ `);
18354
+ _printCodexNextSteps();
18355
+ return;
18356
+ }
18357
+ const updated = appendChronToToml(existing);
18358
+ (0, import_fs5.writeFileSync)(targetPath, updated, "utf8");
18359
+ process.stdout.write(`${GREEN2}\u2713${RESET5} Added Chron MCP to ${targetPath}
18360
+
18361
+ `);
18362
+ _printCodexNextSteps();
18363
+ }
18364
+ function _printCodexNextSteps() {
18365
+ process.stdout.write(`${BOLD5}Next steps:${RESET5}
18366
+
18367
+ `);
18368
+ process.stdout.write(` 1. Restart Codex to pick up the new MCP server.
18369
+ `);
18370
+ process.stdout.write(` 2. Ask Codex to start a Chron session:
18371
+ `);
18372
+ process.stdout.write(` ${DIM4}"Use init_session to start a Chron audit session for this work."${RESET5}
18373
+ `);
18374
+ process.stdout.write(` 3. Verify setup:
18375
+ `);
18376
+ process.stdout.write(` ${CYAN4}chron doctor${RESET5}
18377
+
18378
+ `);
18379
+ process.stdout.write(`${DIM4}Codex will automatically call log_tool_call, log_tool_result, and
18380
+ `);
18381
+ process.stdout.write(`log_code_change during its work once the session is started.${RESET5}
18382
+
18180
18383
  `);
18181
18384
  }
18182
18385
  async function runConnect(args2) {
@@ -18191,6 +18394,9 @@ async function runConnect(args2) {
18191
18394
  case "splunk":
18192
18395
  await connectSplunk();
18193
18396
  break;
18397
+ case "codex":
18398
+ await connectCodex();
18399
+ break;
18194
18400
  default: {
18195
18401
  const name = subcommand ? `Unknown integration: ${subcommand}
18196
18402
 
@@ -18199,6 +18405,7 @@ async function runConnect(args2) {
18199
18405
  `${name}Usage: chron connect <integration>
18200
18406
 
18201
18407
  Integrations:
18408
+ codex Add Chron MCP to Codex config (global or project)
18202
18409
  crowdstrike Connect to CrowdStrike LogScale (direct ingest)
18203
18410
  sentinel Connect to Microsoft Sentinel (Azure Monitor Logs Ingestion API)
18204
18411
  splunk Connect to Splunk via HTTP Event Collector (HEC)
@@ -18208,7 +18415,7 @@ Integrations:
18208
18415
  }
18209
18416
  }
18210
18417
  }
18211
- var import_readline, import_os6, import_path5, import_fs5, RESET5, BOLD5, DIM4, CYAN4, GREEN2, RED, YELLOW3;
18418
+ var import_readline, import_os6, import_path5, import_fs5, RESET5, BOLD5, DIM4, CYAN4, GREEN2, RED, YELLOW3, CODEX_MCP_BLOCK;
18212
18419
  var init_connect = __esm({
18213
18420
  "src/cli/connect.ts"() {
18214
18421
  "use strict";
@@ -18223,6 +18430,11 @@ var init_connect = __esm({
18223
18430
  GREEN2 = "\x1B[32m";
18224
18431
  RED = "\x1B[31m";
18225
18432
  YELLOW3 = "\x1B[33m";
18433
+ CODEX_MCP_BLOCK = `
18434
+ [mcp_servers.chron]
18435
+ command = "npx"
18436
+ args = ["-y", "chron-mcp"]
18437
+ `;
18226
18438
  }
18227
18439
  });
18228
18440
 
@@ -18545,7 +18757,15 @@ async function runSign(args2) {
18545
18757
  process.exit(1);
18546
18758
  }
18547
18759
  const signature = signSession(session.id, finalContentHash, messageCount, firstCreatedAt);
18548
- await db.update(sessions).set({ signature }).where(eq(sessions.id, session.id));
18760
+ try {
18761
+ await db.update(sessions).set({ signature }).where(eq(sessions.id, session.id));
18762
+ } catch (err) {
18763
+ const cause = err?.cause ?? err;
18764
+ process.stderr.write(`Failed to save signature: ${cause?.message ?? err.message}
18765
+ `);
18766
+ process.stderr.write("Tip: if the Chron MCP server is running, it may hold a write lock. Retry or stop the MCP server first.\n");
18767
+ process.exit(1);
18768
+ }
18549
18769
  const sigData = {
18550
18770
  chron_signature: "v1",
18551
18771
  session_id: session.id,
@@ -18870,7 +19090,7 @@ function loadConfig2() {
18870
19090
  function mcpConfigs() {
18871
19091
  const home = (0, import_os9.homedir)();
18872
19092
  const plat = (0, import_os9.platform)();
18873
- const candidates = [
19093
+ const jsonCandidates = [
18874
19094
  {
18875
19095
  name: "Claude Desktop",
18876
19096
  path: plat === "win32" ? (0, import_path9.join)(process.env.APPDATA ?? (0, import_path9.join)(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json") : (0, import_path9.join)(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")
@@ -18879,7 +19099,7 @@ function mcpConfigs() {
18879
19099
  { name: "Cursor", path: (0, import_path9.join)(home, ".cursor", "mcp.json") },
18880
19100
  { name: "Windsurf", path: (0, import_path9.join)(home, ".codeium", "windsurf", "mcp_config.json") }
18881
19101
  ];
18882
- return candidates.map((c) => {
19102
+ const results = jsonCandidates.map((c) => {
18883
19103
  if (!(0, import_fs10.existsSync)(c.path)) {
18884
19104
  return { name: c.name, path: c.path, exists: false, chronConfigured: null };
18885
19105
  }
@@ -18894,6 +19114,22 @@ function mcpConfigs() {
18894
19114
  return { name: c.name, path: c.path, exists: true, chronConfigured: null, note: "Could not parse config" };
18895
19115
  }
18896
19116
  });
19117
+ const codexGlobal = (0, import_path9.join)(home, ".codex", "config.toml");
19118
+ const codexProject = (0, import_path9.join)(process.cwd(), ".codex", "config.toml");
19119
+ for (const p of [codexGlobal, codexProject]) {
19120
+ if (!(0, import_fs10.existsSync)(p)) {
19121
+ results.push({ name: p === codexGlobal ? "Codex (global)" : "Codex (project)", path: p, exists: false, chronConfigured: null });
19122
+ } else {
19123
+ try {
19124
+ const content = (0, import_fs10.readFileSync)(p, "utf8");
19125
+ const configured = content.includes("[mcp_servers.chron]");
19126
+ results.push({ name: p === codexGlobal ? "Codex (global)" : "Codex (project)", path: p, exists: true, chronConfigured: configured });
19127
+ } catch {
19128
+ results.push({ name: p === codexGlobal ? "Codex (global)" : "Codex (project)", path: p, exists: true, chronConfigured: null, note: "Could not read config" });
19129
+ }
19130
+ }
19131
+ }
19132
+ return results;
18897
19133
  }
18898
19134
  function npmLatestVersion() {
18899
19135
  try {
@@ -19023,14 +19259,16 @@ ${BOLD8}chron doctor${RESET8} ${DIM7}v${import_package2.version}${RESET8}
19023
19259
  if (!jsonMode)
19024
19260
  ok2(`${t.name}`, "chron configured");
19025
19261
  } else {
19262
+ const isCodex = t.name.startsWith("Codex");
19263
+ const fixHint = isCodex ? `chron connect codex` : `Add chron to mcpServers in ${t.path}
19264
+ See: https://github.com/sirinivask/chron#installation`;
19026
19265
  results.push({
19027
19266
  pass: "warn",
19028
- label: `${t.name}: chron NOT in mcpServers`,
19029
- fix: `Add chron to mcpServers in ${t.path}
19030
- See: https://github.com/sirinivask/chron#installation`
19267
+ label: `${t.name}: chron not configured`,
19268
+ fix: fixHint
19031
19269
  });
19032
19270
  if (!jsonMode)
19033
- warn2(`${t.name}`, `chron not in mcpServers \u2014 add it to ${t.path}`);
19271
+ warn2(`${t.name}`, isCodex ? "chron not in MCP servers \u2014 run: chron connect codex" : `chron not in mcpServers \u2014 add it to ${t.path}`);
19034
19272
  }
19035
19273
  }
19036
19274
  if (!jsonMode)
@@ -19089,6 +19327,9 @@ ${BOLD8}chron doctor${RESET8} ${DIM7}v${import_package2.version}${RESET8}
19089
19327
  if (failures.length === 0 && warnings.length === 0) {
19090
19328
  process.stdout.write(`${GREEN5}${BOLD8}All checks passed.${RESET8} Chron is correctly set up.
19091
19329
 
19330
+ `);
19331
+ process.stdout.write(`${DIM7}Team AI governance: CLAIIM adds runtime ALLOW/DENY gates on top of Chron proof \u2014 ${CLAIIM_PREVIEW_URL}${RESET8}
19332
+
19092
19333
  `);
19093
19334
  } else {
19094
19335
  if (failures.length > 0) {
@@ -19137,7 +19378,7 @@ ${BOLD8}chron doctor${RESET8} ${DIM7}v${import_package2.version}${RESET8}
19137
19378
  }
19138
19379
  process.exit(failures.length > 0 ? 1 : 0);
19139
19380
  }
19140
- var import_os9, import_path9, import_fs10, import_child_process3, import_package2, RESET8, BOLD8, DIM7, GREEN5, RED4, YELLOW6, CYAN6;
19381
+ var import_os9, import_path9, import_fs10, import_child_process3, import_package2, RESET8, BOLD8, DIM7, GREEN5, RED4, YELLOW6, CYAN6, CLAIIM_PREVIEW_URL;
19141
19382
  var init_doctor = __esm({
19142
19383
  "src/cli/doctor.ts"() {
19143
19384
  "use strict";
@@ -19153,6 +19394,7 @@ var init_doctor = __esm({
19153
19394
  RED4 = "\x1B[31m";
19154
19395
  YELLOW6 = "\x1B[33m";
19155
19396
  CYAN6 = "\x1B[36m";
19397
+ CLAIIM_PREVIEW_URL = "https://claiim.io/preview";
19156
19398
  }
19157
19399
  });
19158
19400
 
@@ -19845,7 +20087,7 @@ Commands:
19845
20087
  export Export a session as markdown
19846
20088
  secrets List detected secrets across sessions
19847
20089
  settings View current configuration
19848
- connect Connect to a SIEM integration (crowdstrike, sentinel, splunk)
20090
+ connect Connect to a SIEM or AI tool (codex, crowdstrike, sentinel, splunk)
19849
20091
  summary Structured summary of a session (timeline, mutations, secrets)
19850
20092
  sign Sign a session with its Ed25519 key \u2014 produces a .chron.sig file
19851
20093
  verify Verify a session's hash chain and Ed25519 signature
@@ -19854,9 +20096,10 @@ Commands:
19854
20096
  import Import conversations from external AI tools
19855
20097
 
19856
20098
  Options (history):
19857
- --limit=<n> Max sessions to show (default: 20)
19858
- --ref=<value> Filter by external_ref prefix (e.g. --ref=jira: or --ref=jira:ENG-123)
19859
- <id-prefix> Show full log for the session with this ID prefix
20099
+ --limit=<n> Max sessions to show (default: 20)
20100
+ --search=<query> Full-text search across all sessions (FTS5: phrases, boolean, prefix*)
20101
+ --ref=<value> Filter by external_ref prefix
20102
+ <id-prefix> Show full log for the session with this ID prefix
19860
20103
 
19861
20104
  Options (report):
19862
20105
  --since=<range> Filter by date: 7d, 30d, or YYYY-MM-DD (default: all time)
package/dist/index.js CHANGED
@@ -22792,6 +22792,7 @@ async function initDb(dbPath) {
22792
22792
  }
22793
22793
  const client = createClient({ url: path.startsWith(":") ? path : `file:${path}` });
22794
22794
  await client.execute("PRAGMA journal_mode = WAL");
22795
+ await client.execute("PRAGMA busy_timeout = 5000");
22795
22796
  await client.execute("PRAGMA foreign_keys = ON");
22796
22797
  for (const sql2 of CREATE_SQL) {
22797
22798
  await client.execute(sql2);
@@ -22824,6 +22825,14 @@ async function initDb(dbPath) {
22824
22825
  if (!sessCols.includes("metadata")) {
22825
22826
  await client.execute("ALTER TABLE sessions ADD COLUMN metadata TEXT");
22826
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
+ }
22827
22836
  return drizzle(client, { schema: schema_exports });
22828
22837
  }
22829
22838
  var import_os, import_fs, import_path, CREATE_SQL;
@@ -22873,7 +22882,21 @@ var init_db2 = __esm({
22873
22882
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title ON sessions(title)`,
22874
22883
  `CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)`,
22875
22884
  `CREATE INDEX IF NOT EXISTS idx_secrets_detected_session_id ON secrets_detected(session_id)`,
22876
- `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`
22877
22900
  ];
22878
22901
  }
22879
22902
  });
@@ -38479,7 +38502,7 @@ var init_time = __esm({
38479
38502
  var version4;
38480
38503
  var init_package = __esm({
38481
38504
  "package.json"() {
38482
- version4 = "0.1.26";
38505
+ version4 = "0.1.29";
38483
38506
  }
38484
38507
  });
38485
38508
 
@@ -38837,6 +38860,44 @@ var init_ntp = __esm({
38837
38860
  }
38838
38861
  });
38839
38862
 
38863
+ // src/utils/terminal.ts
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")
38880
+ return;
38881
+ process.stderr.write(`[chron] ${message}
38882
+ `);
38883
+ }
38884
+ function formatDuration(ms) {
38885
+ if (ms < 1e3)
38886
+ return `${ms}ms`;
38887
+ if (ms < 6e4)
38888
+ return `${(ms / 1e3).toFixed(1)}s`;
38889
+ const minutes = Math.floor(ms / 6e4);
38890
+ const seconds = Math.round(ms % 6e4 / 1e3);
38891
+ return `${minutes}m ${seconds}s`;
38892
+ }
38893
+ var DISABLE_VALUES;
38894
+ var init_terminal = __esm({
38895
+ "src/utils/terminal.ts"() {
38896
+ "use strict";
38897
+ DISABLE_VALUES = /* @__PURE__ */ new Set(["0", "false", "off", "no"]);
38898
+ }
38899
+ });
38900
+
38840
38901
  // src/tools/sessions.ts
38841
38902
  function attachNtpMetadata(db, sessionId) {
38842
38903
  setImmediate(() => {
@@ -38867,6 +38928,7 @@ function startSession(db) {
38867
38928
  });
38868
38929
  attachNtpMetadata(db, id);
38869
38930
  emitEvent({ event_type: "session_started", timestamp: now, session: { id_prefix: id.slice(0, 8), ai_tool: args.ai_tool ?? null } });
38931
+ terminalAudit(`session ${id.slice(0, 8)} started at ${now} (${args.ai_tool ?? "unknown"}: ${args.title})`, "session");
38870
38932
  return {
38871
38933
  content: [{
38872
38934
  type: "text",
@@ -38882,6 +38944,7 @@ function startSession(db) {
38882
38944
  const [countRow] = await db.select({ count: sql`count(*)` }).from(messages).where(eq(messages.session_id, session.id));
38883
38945
  await db.update(sessions).set({ updated_at: now }).where(eq(sessions.id, session.id));
38884
38946
  emitEvent({ event_type: "session_started", timestamp: now, session: { id_prefix: session.id.slice(0, 8), ai_tool: session.ai_tool } });
38947
+ terminalAudit(`session ${session.id.slice(0, 8)} resumed at ${now} (${session.ai_tool ?? "unknown"}: ${session.title})`, "session");
38885
38948
  return {
38886
38949
  content: [{
38887
38950
  type: "text",
@@ -38918,6 +38981,7 @@ function initSession(db) {
38918
38981
  created = true;
38919
38982
  ai_tool = args.ai_tool ?? null;
38920
38983
  attachNtpMetadata(db, id);
38984
+ terminalAudit(`session ${id.slice(0, 8)} started at ${now} (${ai_tool ?? "unknown"}: ${args.title})`, "session");
38921
38985
  } catch (e) {
38922
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";
38923
38987
  if (!isUnique)
@@ -38928,6 +38992,7 @@ function initSession(db) {
38928
38992
  session_id = session.id;
38929
38993
  created = false;
38930
38994
  ai_tool = session.ai_tool;
38995
+ terminalAudit(`session ${session.id.slice(0, 8)} resumed at ${now} (${ai_tool ?? "unknown"}: ${session.title})`, "session");
38931
38996
  }
38932
38997
  emitEvent({ event_type: "session_started", timestamp: now, session: { id_prefix: session_id.slice(0, 8), ai_tool } });
38933
38998
  const limit = args.limit ?? 10;
@@ -39016,6 +39081,7 @@ var init_sessions = __esm({
39016
39081
  init_relay();
39017
39082
  init_signing();
39018
39083
  init_ntp();
39084
+ init_terminal();
39019
39085
  }
39020
39086
  });
39021
39087
 
@@ -39328,7 +39394,7 @@ function isFkError(e) {
39328
39394
  }
39329
39395
  async function insertMessage(db, session_id, role, content, event_type) {
39330
39396
  return db.transaction(async (tx) => {
39331
- const last = await tx.select({ content_hash: messages.content_hash }).from(messages).where(eq(messages.session_id, session_id)).orderBy(desc(messages.created_at), desc(sql`rowid`)).limit(1);
39397
+ const last = await tx.select({ content_hash: messages.content_hash, created_at: messages.created_at }).from(messages).where(eq(messages.session_id, session_id)).orderBy(desc(messages.created_at), desc(sql`rowid`)).limit(1);
39332
39398
  const ts = localISOString();
39333
39399
  const msgId = v4_default();
39334
39400
  const prevHash = last[0]?.content_hash ?? null;
@@ -39345,7 +39411,13 @@ async function insertMessage(db, session_id, role, content, event_type) {
39345
39411
  });
39346
39412
  await tx.update(sessions).set({ updated_at: ts }).where(eq(sessions.id, session_id));
39347
39413
  const [sessionRow] = await tx.select({ ai_tool: sessions.ai_tool }).from(sessions).where(eq(sessions.id, session_id)).limit(1);
39348
- return { id: msgId, now: ts, contentHash: hash, ai_tool: sessionRow?.ai_tool ?? null };
39414
+ return {
39415
+ id: msgId,
39416
+ now: ts,
39417
+ contentHash: hash,
39418
+ ai_tool: sessionRow?.ai_tool ?? null,
39419
+ previous_created_at: last[0]?.created_at ?? null
39420
+ };
39349
39421
  });
39350
39422
  }
39351
39423
  function logMessage(db) {
@@ -39361,6 +39433,9 @@ function logMessage(db) {
39361
39433
  now = result.now;
39362
39434
  contentHash = result.contentHash;
39363
39435
  sessionAiTool = result.ai_tool;
39436
+ const elapsed = result.previous_created_at ? ` (+${formatDuration(new Date(now).getTime() - new Date(result.previous_created_at).getTime())})` : "";
39437
+ const marker = args.role === "user" ? "user start" : "assistant end";
39438
+ terminalAudit(`${marker} ${now}${elapsed} session ${args.session_id.slice(0, 8)}`);
39364
39439
  } catch (e) {
39365
39440
  if (isFkError(e)) {
39366
39441
  return { content: [{ type: "text", text: `Session not found: ${args.session_id}` }], isError: true };
@@ -39508,6 +39583,7 @@ var init_messages = __esm({
39508
39583
  init_hash();
39509
39584
  init_detect();
39510
39585
  init_relay();
39586
+ init_terminal();
39511
39587
  }
39512
39588
  });
39513
39589
 
@@ -39831,6 +39907,73 @@ var init_summary2 = __esm({
39831
39907
  }
39832
39908
  });
39833
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
+
39834
39977
  // src/server.ts
39835
39978
  function createServer(db) {
39836
39979
  const server = new McpServer({
@@ -39954,6 +40097,15 @@ function createServer(db) {
39954
40097
  },
39955
40098
  deleteSession(db)
39956
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
+ );
39957
40109
  server.tool(
39958
40110
  "summarize_session",
39959
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.",
@@ -39984,6 +40136,7 @@ var init_server3 = __esm({
39984
40136
  init_verify();
39985
40137
  init_detect2();
39986
40138
  init_summary2();
40139
+ init_search();
39987
40140
  init_package();
39988
40141
  roleEnum = external_exports.enum(["user", "assistant"]);
39989
40142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chron-mcp",
3
- "version": "0.1.26",
3
+ "version": "0.1.29",
4
4
  "mcpName": "io.github.sirinivask/chron",
5
5
  "description": "Audit-grade timestamped logs for every AI conversation",
6
6
  "repository": {
@@ -0,0 +1,73 @@
1
+ # Chron — Codex Audit Skill
2
+
3
+ Chron creates a tamper-evident audit trail of every Codex session — messages, tool calls, tool results, and code changes — stored in a local SQLite database the user owns. No data leaves the machine.
4
+
5
+ ## Session Start
6
+
7
+ At the beginning of every Codex thread, call:
8
+
9
+ ```
10
+ init_session(title="<short description of the task>", ai_tool="codex")
11
+ ```
12
+
13
+ This creates or resumes a session and returns `session_id`, `message_count`, and recent `messages`. Save `session_id` — you will use it for every subsequent call.
14
+
15
+ If `message_count > 0`, you are resuming an existing session. Acknowledge this to the user.
16
+
17
+ ## Every Exchange
18
+
19
+ For every user message and your response, log both ends before outputting:
20
+
21
+ 1. `log_message(session_id, role="user", content=<exact user message>)` — log the user's message first
22
+ 2. Compose your response internally
23
+ 3. `log_message(session_id, role="assistant", content=<your response>)` — log before outputting
24
+ 4. Output the response
25
+
26
+ ## Tool Calls and Results
27
+
28
+ Before executing any tool (shell command, file read, browser action, code execution):
29
+
30
+ ```
31
+ log_tool_call(session_id, tool_name="<tool>", tool_input={<args>})
32
+ ```
33
+
34
+ After receiving the tool output:
35
+
36
+ ```
37
+ log_tool_result(session_id, tool_call_id=<id from log_tool_call>, output="<result>", exit_code=<n>)
38
+ ```
39
+
40
+ ## Code Changes
41
+
42
+ After writing or editing any file:
43
+
44
+ ```
45
+ log_code_change(
46
+ session_id,
47
+ file_path="<absolute or repo-relative path>",
48
+ operation="create" | "edit" | "delete",
49
+ diff="<unified diff of the change>"
50
+ )
51
+ ```
52
+
53
+ This creates a cryptographically linked record of every file edit, tied to the conversation that caused it.
54
+
55
+ ## Audit Order (per turn)
56
+
57
+ ```
58
+ log_message(role="user") ← before doing anything
59
+ log_tool_call(...) ← before each tool
60
+ log_tool_result(...) ← after each tool result
61
+ log_code_change(...) ← after each file edit
62
+ log_message(role="assistant") ← before final response output
63
+ ```
64
+
65
+ ## Verify Integrity
66
+
67
+ At any point, the user can run:
68
+
69
+ ```bash
70
+ chron verify <session-id-prefix>
71
+ ```
72
+
73
+ This walks the SHA-256 hash chain and verifies the Ed25519 session signature, confirming the audit record has not been modified since it was written.