chron-mcp 0.1.26 → 0.1.28

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);
@@ -17032,12 +17033,16 @@ ${overview.total_detections > 0 ? `<p class="warn">&#9888; ${overview.total_dete
17032
17033
  </tbody>
17033
17034
  </table>
17034
17035
  ${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>
17036
+ <p style="font-size:11px;color:#6b7280">
17037
+ Note: This report verifies <em>chain linkage</em> (prev_hash continuity). Full content-hash recomputation
17038
+ (which detects in-place tampering) can be performed with <code>chron verify &lt;session-id&gt;</code>.
17039
+ </p>
17040
+ <p class="governance-note">
17041
+ For formal AI governance, CLAIIM extends Chron evidence with agent identity, policy gates,
17042
+ approvals, and runtime ALLOW/DENY enforcement. See <a href="https://claiim.io/preview">https://claiim.io/preview</a>.
17043
+ </p>
17039
17044
 
17040
- <!-- \u2466 SESSION INVENTORY -->
17045
+ <!-- \u2466 SESSION INVENTORY -->
17041
17046
  <h2 class="section-break">Session Inventory</h2>
17042
17047
  <p>All sessions recorded in the audit database (most recent first, capped at 500 rows).</p>
17043
17048
  <table>
@@ -17161,6 +17166,7 @@ tr:nth-child(even) td { background: #fafafa; }
17161
17166
  .section-break { page-break-before: always; }
17162
17167
  .appendix { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px 20px; margin-top: 12px; }
17163
17168
  .appendix p, .appendix li { color: #374151; font-size: 12px; }
17169
+ .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
17170
  ul { padding-left: 20px; margin-bottom: 8px; }
17165
17171
  li { margin-bottom: 4px; }
17166
17172
  @media print {
@@ -17370,7 +17376,7 @@ var require_package = __commonJS({
17370
17376
  "package.json"(exports2, module2) {
17371
17377
  module2.exports = {
17372
17378
  name: "chron-mcp",
17373
- version: "0.1.26",
17379
+ version: "0.1.28",
17374
17380
  mcpName: "io.github.sirinivask/chron",
17375
17381
  description: "Audit-grade timestamped logs for every AI conversation",
17376
17382
  repository: {
@@ -18177,6 +18183,93 @@ ${DIM4}Authenticating with Azure AD...${RESET5} `);
18177
18183
  `);
18178
18184
  process.stdout.write(`${DIM4}Config saved to ~/.chron/config.json \u2014 events will flow immediately in all running sessions.${RESET5}
18179
18185
 
18186
+ `);
18187
+ }
18188
+ function codexGlobalConfigPath() {
18189
+ return (0, import_path5.join)((0, import_os6.homedir)(), ".codex", "config.toml");
18190
+ }
18191
+ function isChronInCodexConfig(content) {
18192
+ return content.includes("[mcp_servers.chron]");
18193
+ }
18194
+ function appendChronToToml(content) {
18195
+ return content.trimEnd() + "\n" + CODEX_MCP_BLOCK;
18196
+ }
18197
+ async function connectCodex() {
18198
+ process.stdout.write(`
18199
+ ${BOLD5}Connect Chron \u2192 Codex${RESET5}
18200
+
18201
+ `);
18202
+ const globalPath = codexGlobalConfigPath();
18203
+ const projectPath = (0, import_path5.join)(process.cwd(), ".codex", "config.toml");
18204
+ const hasGlobal = (0, import_fs5.existsSync)(globalPath);
18205
+ const hasProject = (0, import_fs5.existsSync)(projectPath);
18206
+ let targetPath;
18207
+ let isProject = false;
18208
+ if (hasGlobal && hasProject) {
18209
+ process.stdout.write(`${DIM4}Found both global and project Codex configs.${RESET5}
18210
+ `);
18211
+ process.stdout.write(`${DIM4} Global: ${globalPath}${RESET5}
18212
+ `);
18213
+ process.stdout.write(`${DIM4} Project: ${projectPath}${RESET5}
18214
+
18215
+ `);
18216
+ const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
18217
+ const choice = await new Promise(
18218
+ (resolve) => rl.question(` Add Chron to (g)lobal or (p)roject config? [g/p]: `, resolve)
18219
+ );
18220
+ rl.close();
18221
+ isProject = choice.trim().toLowerCase() === "p";
18222
+ targetPath = isProject ? projectPath : globalPath;
18223
+ } else if (hasProject) {
18224
+ targetPath = projectPath;
18225
+ isProject = true;
18226
+ } else if (hasGlobal) {
18227
+ targetPath = globalPath;
18228
+ } else {
18229
+ process.stdout.write(`${YELLOW3}!${RESET5} Codex config not found. Creating ${globalPath}
18230
+
18231
+ `);
18232
+ (0, import_fs5.mkdirSync)((0, import_path5.join)((0, import_os6.homedir)(), ".codex"), { recursive: true });
18233
+ (0, import_fs5.writeFileSync)(globalPath, CODEX_MCP_BLOCK.trimStart());
18234
+ process.stdout.write(`${GREEN2}\u2713${RESET5} Created ${globalPath} with Chron MCP config.
18235
+ `);
18236
+ _printCodexNextSteps();
18237
+ return;
18238
+ }
18239
+ const existing = (0, import_fs5.readFileSync)(targetPath, "utf8");
18240
+ if (isChronInCodexConfig(existing)) {
18241
+ process.stdout.write(`${GREEN2}\u2713${RESET5} Chron MCP is already configured in ${targetPath}
18242
+
18243
+ `);
18244
+ _printCodexNextSteps();
18245
+ return;
18246
+ }
18247
+ const updated = appendChronToToml(existing);
18248
+ (0, import_fs5.writeFileSync)(targetPath, updated, "utf8");
18249
+ process.stdout.write(`${GREEN2}\u2713${RESET5} Added Chron MCP to ${targetPath}
18250
+
18251
+ `);
18252
+ _printCodexNextSteps();
18253
+ }
18254
+ function _printCodexNextSteps() {
18255
+ process.stdout.write(`${BOLD5}Next steps:${RESET5}
18256
+
18257
+ `);
18258
+ process.stdout.write(` 1. Restart Codex to pick up the new MCP server.
18259
+ `);
18260
+ process.stdout.write(` 2. Ask Codex to start a Chron session:
18261
+ `);
18262
+ process.stdout.write(` ${DIM4}"Use init_session to start a Chron audit session for this work."${RESET5}
18263
+ `);
18264
+ process.stdout.write(` 3. Verify setup:
18265
+ `);
18266
+ process.stdout.write(` ${CYAN4}chron doctor${RESET5}
18267
+
18268
+ `);
18269
+ process.stdout.write(`${DIM4}Codex will automatically call log_tool_call, log_tool_result, and
18270
+ `);
18271
+ process.stdout.write(`log_code_change during its work once the session is started.${RESET5}
18272
+
18180
18273
  `);
18181
18274
  }
18182
18275
  async function runConnect(args2) {
@@ -18191,6 +18284,9 @@ async function runConnect(args2) {
18191
18284
  case "splunk":
18192
18285
  await connectSplunk();
18193
18286
  break;
18287
+ case "codex":
18288
+ await connectCodex();
18289
+ break;
18194
18290
  default: {
18195
18291
  const name = subcommand ? `Unknown integration: ${subcommand}
18196
18292
 
@@ -18199,6 +18295,7 @@ async function runConnect(args2) {
18199
18295
  `${name}Usage: chron connect <integration>
18200
18296
 
18201
18297
  Integrations:
18298
+ codex Add Chron MCP to Codex config (global or project)
18202
18299
  crowdstrike Connect to CrowdStrike LogScale (direct ingest)
18203
18300
  sentinel Connect to Microsoft Sentinel (Azure Monitor Logs Ingestion API)
18204
18301
  splunk Connect to Splunk via HTTP Event Collector (HEC)
@@ -18208,7 +18305,7 @@ Integrations:
18208
18305
  }
18209
18306
  }
18210
18307
  }
18211
- var import_readline, import_os6, import_path5, import_fs5, RESET5, BOLD5, DIM4, CYAN4, GREEN2, RED, YELLOW3;
18308
+ var import_readline, import_os6, import_path5, import_fs5, RESET5, BOLD5, DIM4, CYAN4, GREEN2, RED, YELLOW3, CODEX_MCP_BLOCK;
18212
18309
  var init_connect = __esm({
18213
18310
  "src/cli/connect.ts"() {
18214
18311
  "use strict";
@@ -18223,6 +18320,11 @@ var init_connect = __esm({
18223
18320
  GREEN2 = "\x1B[32m";
18224
18321
  RED = "\x1B[31m";
18225
18322
  YELLOW3 = "\x1B[33m";
18323
+ CODEX_MCP_BLOCK = `
18324
+ [mcp_servers.chron]
18325
+ command = "npx"
18326
+ args = ["-y", "chron-mcp"]
18327
+ `;
18226
18328
  }
18227
18329
  });
18228
18330
 
@@ -18545,7 +18647,15 @@ async function runSign(args2) {
18545
18647
  process.exit(1);
18546
18648
  }
18547
18649
  const signature = signSession(session.id, finalContentHash, messageCount, firstCreatedAt);
18548
- await db.update(sessions).set({ signature }).where(eq(sessions.id, session.id));
18650
+ try {
18651
+ await db.update(sessions).set({ signature }).where(eq(sessions.id, session.id));
18652
+ } catch (err) {
18653
+ const cause = err?.cause ?? err;
18654
+ process.stderr.write(`Failed to save signature: ${cause?.message ?? err.message}
18655
+ `);
18656
+ 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");
18657
+ process.exit(1);
18658
+ }
18549
18659
  const sigData = {
18550
18660
  chron_signature: "v1",
18551
18661
  session_id: session.id,
@@ -18870,7 +18980,7 @@ function loadConfig2() {
18870
18980
  function mcpConfigs() {
18871
18981
  const home = (0, import_os9.homedir)();
18872
18982
  const plat = (0, import_os9.platform)();
18873
- const candidates = [
18983
+ const jsonCandidates = [
18874
18984
  {
18875
18985
  name: "Claude Desktop",
18876
18986
  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 +18989,7 @@ function mcpConfigs() {
18879
18989
  { name: "Cursor", path: (0, import_path9.join)(home, ".cursor", "mcp.json") },
18880
18990
  { name: "Windsurf", path: (0, import_path9.join)(home, ".codeium", "windsurf", "mcp_config.json") }
18881
18991
  ];
18882
- return candidates.map((c) => {
18992
+ const results = jsonCandidates.map((c) => {
18883
18993
  if (!(0, import_fs10.existsSync)(c.path)) {
18884
18994
  return { name: c.name, path: c.path, exists: false, chronConfigured: null };
18885
18995
  }
@@ -18894,6 +19004,22 @@ function mcpConfigs() {
18894
19004
  return { name: c.name, path: c.path, exists: true, chronConfigured: null, note: "Could not parse config" };
18895
19005
  }
18896
19006
  });
19007
+ const codexGlobal = (0, import_path9.join)(home, ".codex", "config.toml");
19008
+ const codexProject = (0, import_path9.join)(process.cwd(), ".codex", "config.toml");
19009
+ for (const p of [codexGlobal, codexProject]) {
19010
+ if (!(0, import_fs10.existsSync)(p)) {
19011
+ results.push({ name: p === codexGlobal ? "Codex (global)" : "Codex (project)", path: p, exists: false, chronConfigured: null });
19012
+ } else {
19013
+ try {
19014
+ const content = (0, import_fs10.readFileSync)(p, "utf8");
19015
+ const configured = content.includes("[mcp_servers.chron]");
19016
+ results.push({ name: p === codexGlobal ? "Codex (global)" : "Codex (project)", path: p, exists: true, chronConfigured: configured });
19017
+ } catch {
19018
+ results.push({ name: p === codexGlobal ? "Codex (global)" : "Codex (project)", path: p, exists: true, chronConfigured: null, note: "Could not read config" });
19019
+ }
19020
+ }
19021
+ }
19022
+ return results;
18897
19023
  }
18898
19024
  function npmLatestVersion() {
18899
19025
  try {
@@ -19023,14 +19149,16 @@ ${BOLD8}chron doctor${RESET8} ${DIM7}v${import_package2.version}${RESET8}
19023
19149
  if (!jsonMode)
19024
19150
  ok2(`${t.name}`, "chron configured");
19025
19151
  } else {
19152
+ const isCodex = t.name.startsWith("Codex");
19153
+ const fixHint = isCodex ? `chron connect codex` : `Add chron to mcpServers in ${t.path}
19154
+ See: https://github.com/sirinivask/chron#installation`;
19026
19155
  results.push({
19027
19156
  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`
19157
+ label: `${t.name}: chron not configured`,
19158
+ fix: fixHint
19031
19159
  });
19032
19160
  if (!jsonMode)
19033
- warn2(`${t.name}`, `chron not in mcpServers \u2014 add it to ${t.path}`);
19161
+ 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
19162
  }
19035
19163
  }
19036
19164
  if (!jsonMode)
@@ -19089,6 +19217,9 @@ ${BOLD8}chron doctor${RESET8} ${DIM7}v${import_package2.version}${RESET8}
19089
19217
  if (failures.length === 0 && warnings.length === 0) {
19090
19218
  process.stdout.write(`${GREEN5}${BOLD8}All checks passed.${RESET8} Chron is correctly set up.
19091
19219
 
19220
+ `);
19221
+ process.stdout.write(`${DIM7}Team AI governance: CLAIIM adds runtime ALLOW/DENY gates on top of Chron proof \u2014 ${CLAIIM_PREVIEW_URL}${RESET8}
19222
+
19092
19223
  `);
19093
19224
  } else {
19094
19225
  if (failures.length > 0) {
@@ -19137,7 +19268,7 @@ ${BOLD8}chron doctor${RESET8} ${DIM7}v${import_package2.version}${RESET8}
19137
19268
  }
19138
19269
  process.exit(failures.length > 0 ? 1 : 0);
19139
19270
  }
19140
- var import_os9, import_path9, import_fs10, import_child_process3, import_package2, RESET8, BOLD8, DIM7, GREEN5, RED4, YELLOW6, CYAN6;
19271
+ var import_os9, import_path9, import_fs10, import_child_process3, import_package2, RESET8, BOLD8, DIM7, GREEN5, RED4, YELLOW6, CYAN6, CLAIIM_PREVIEW_URL;
19141
19272
  var init_doctor = __esm({
19142
19273
  "src/cli/doctor.ts"() {
19143
19274
  "use strict";
@@ -19153,6 +19284,7 @@ var init_doctor = __esm({
19153
19284
  RED4 = "\x1B[31m";
19154
19285
  YELLOW6 = "\x1B[33m";
19155
19286
  CYAN6 = "\x1B[36m";
19287
+ CLAIIM_PREVIEW_URL = "https://claiim.io/preview";
19156
19288
  }
19157
19289
  });
19158
19290
 
@@ -19845,7 +19977,7 @@ Commands:
19845
19977
  export Export a session as markdown
19846
19978
  secrets List detected secrets across sessions
19847
19979
  settings View current configuration
19848
- connect Connect to a SIEM integration (crowdstrike, sentinel, splunk)
19980
+ connect Connect to a SIEM or AI tool (codex, crowdstrike, sentinel, splunk)
19849
19981
  summary Structured summary of a session (timeline, mutations, secrets)
19850
19982
  sign Sign a session with its Ed25519 key \u2014 produces a .chron.sig file
19851
19983
  verify Verify a session's hash chain and Ed25519 signature
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);
@@ -38479,7 +38480,7 @@ var init_time = __esm({
38479
38480
  var version4;
38480
38481
  var init_package = __esm({
38481
38482
  "package.json"() {
38482
- version4 = "0.1.26";
38483
+ version4 = "0.1.28";
38483
38484
  }
38484
38485
  });
38485
38486
 
@@ -38837,6 +38838,31 @@ var init_ntp = __esm({
38837
38838
  }
38838
38839
  });
38839
38840
 
38841
+ // src/utils/terminal.ts
38842
+ function terminalAudit(message) {
38843
+ const setting = process.env.CHRON_TERMINAL_AUDIT;
38844
+ if (setting && DISABLE_VALUES.has(setting.toLowerCase()))
38845
+ return;
38846
+ process.stderr.write(`[chron] ${message}
38847
+ `);
38848
+ }
38849
+ function formatDuration(ms) {
38850
+ if (ms < 1e3)
38851
+ return `${ms}ms`;
38852
+ if (ms < 6e4)
38853
+ return `${(ms / 1e3).toFixed(1)}s`;
38854
+ const minutes = Math.floor(ms / 6e4);
38855
+ const seconds = Math.round(ms % 6e4 / 1e3);
38856
+ return `${minutes}m ${seconds}s`;
38857
+ }
38858
+ var DISABLE_VALUES;
38859
+ var init_terminal = __esm({
38860
+ "src/utils/terminal.ts"() {
38861
+ "use strict";
38862
+ DISABLE_VALUES = /* @__PURE__ */ new Set(["0", "false", "off", "no"]);
38863
+ }
38864
+ });
38865
+
38840
38866
  // src/tools/sessions.ts
38841
38867
  function attachNtpMetadata(db, sessionId) {
38842
38868
  setImmediate(() => {
@@ -38867,6 +38893,7 @@ function startSession(db) {
38867
38893
  });
38868
38894
  attachNtpMetadata(db, id);
38869
38895
  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})`);
38870
38897
  return {
38871
38898
  content: [{
38872
38899
  type: "text",
@@ -38882,6 +38909,7 @@ function startSession(db) {
38882
38909
  const [countRow] = await db.select({ count: sql`count(*)` }).from(messages).where(eq(messages.session_id, session.id));
38883
38910
  await db.update(sessions).set({ updated_at: now }).where(eq(sessions.id, session.id));
38884
38911
  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})`);
38885
38913
  return {
38886
38914
  content: [{
38887
38915
  type: "text",
@@ -38918,6 +38946,7 @@ function initSession(db) {
38918
38946
  created = true;
38919
38947
  ai_tool = args.ai_tool ?? null;
38920
38948
  attachNtpMetadata(db, id);
38949
+ terminalAudit(`session ${id.slice(0, 8)} started at ${now} (${ai_tool ?? "unknown"}: ${args.title})`);
38921
38950
  } catch (e) {
38922
38951
  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
38952
  if (!isUnique)
@@ -38928,6 +38957,7 @@ function initSession(db) {
38928
38957
  session_id = session.id;
38929
38958
  created = false;
38930
38959
  ai_tool = session.ai_tool;
38960
+ terminalAudit(`session ${session.id.slice(0, 8)} resumed at ${now} (${ai_tool ?? "unknown"}: ${session.title})`);
38931
38961
  }
38932
38962
  emitEvent({ event_type: "session_started", timestamp: now, session: { id_prefix: session_id.slice(0, 8), ai_tool } });
38933
38963
  const limit = args.limit ?? 10;
@@ -39016,6 +39046,7 @@ var init_sessions = __esm({
39016
39046
  init_relay();
39017
39047
  init_signing();
39018
39048
  init_ntp();
39049
+ init_terminal();
39019
39050
  }
39020
39051
  });
39021
39052
 
@@ -39328,7 +39359,7 @@ function isFkError(e) {
39328
39359
  }
39329
39360
  async function insertMessage(db, session_id, role, content, event_type) {
39330
39361
  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);
39362
+ 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
39363
  const ts = localISOString();
39333
39364
  const msgId = v4_default();
39334
39365
  const prevHash = last[0]?.content_hash ?? null;
@@ -39345,7 +39376,13 @@ async function insertMessage(db, session_id, role, content, event_type) {
39345
39376
  });
39346
39377
  await tx.update(sessions).set({ updated_at: ts }).where(eq(sessions.id, session_id));
39347
39378
  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 };
39379
+ return {
39380
+ id: msgId,
39381
+ now: ts,
39382
+ contentHash: hash,
39383
+ ai_tool: sessionRow?.ai_tool ?? null,
39384
+ previous_created_at: last[0]?.created_at ?? null
39385
+ };
39349
39386
  });
39350
39387
  }
39351
39388
  function logMessage(db) {
@@ -39361,6 +39398,9 @@ function logMessage(db) {
39361
39398
  now = result.now;
39362
39399
  contentHash = result.contentHash;
39363
39400
  sessionAiTool = result.ai_tool;
39401
+ const elapsed = result.previous_created_at ? ` (+${formatDuration(new Date(now).getTime() - new Date(result.previous_created_at).getTime())})` : "";
39402
+ const marker = args.role === "user" ? "user start" : "assistant end";
39403
+ terminalAudit(`${marker} ${now}${elapsed} session ${args.session_id.slice(0, 8)}`);
39364
39404
  } catch (e) {
39365
39405
  if (isFkError(e)) {
39366
39406
  return { content: [{ type: "text", text: `Session not found: ${args.session_id}` }], isError: true };
@@ -39508,6 +39548,7 @@ var init_messages = __esm({
39508
39548
  init_hash();
39509
39549
  init_detect();
39510
39550
  init_relay();
39551
+ init_terminal();
39511
39552
  }
39512
39553
  });
39513
39554
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chron-mcp",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
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.