chron-mcp 0.1.18 → 0.1.20

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.
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "chron",
3
3
  "description": "Audit-grade timestamped logs for every AI conversation — stored locally, owned by you.",
4
- "version": "0.1.12",
4
+ "version": "0.1.20",
5
5
  "author": {
6
- "name": "Nivaya",
7
- "email": "ksrinivas07530@gmail.com"
6
+ "name": "Nivaya"
8
7
  },
9
8
  "homepage": "https://github.com/sirinivask/chron",
10
9
  "repository": "https://github.com/sirinivask/chron",
package/dist/cli/index.js CHANGED
@@ -1184,7 +1184,7 @@ var init_sql = __esm({
1184
1184
  return new SQL([new StringChunk(str)]);
1185
1185
  }
1186
1186
  sql2.raw = raw;
1187
- function join4(chunks, separator) {
1187
+ function join6(chunks, separator) {
1188
1188
  const result = [];
1189
1189
  for (const [i, chunk] of chunks.entries()) {
1190
1190
  if (i > 0 && separator !== void 0) {
@@ -1194,7 +1194,7 @@ var init_sql = __esm({
1194
1194
  }
1195
1195
  return new SQL(result);
1196
1196
  }
1197
- sql2.join = join4;
1197
+ sql2.join = join6;
1198
1198
  function identifier(value) {
1199
1199
  return new Name(value);
1200
1200
  }
@@ -2806,7 +2806,7 @@ var require_filesystem = __commonJS({
2806
2806
  "use strict";
2807
2807
  var fs = require("fs");
2808
2808
  var LDD_PATH = "/usr/bin/ldd";
2809
- var readFileSync2 = (path) => fs.readFileSync(path, "utf-8");
2809
+ var readFileSync3 = (path) => fs.readFileSync(path, "utf-8");
2810
2810
  var readFile = (path) => new Promise((resolve, reject) => {
2811
2811
  fs.readFile(path, "utf-8", (err, data) => {
2812
2812
  if (err) {
@@ -2818,7 +2818,7 @@ var require_filesystem = __commonJS({
2818
2818
  });
2819
2819
  module2.exports = {
2820
2820
  LDD_PATH,
2821
- readFileSync: readFileSync2,
2821
+ readFileSync: readFileSync3,
2822
2822
  readFile
2823
2823
  };
2824
2824
  }
@@ -2830,7 +2830,7 @@ var require_detect_libc = __commonJS({
2830
2830
  "use strict";
2831
2831
  var childProcess = require("child_process");
2832
2832
  var { isLinux, getReport } = require_process();
2833
- var { LDD_PATH, readFile, readFileSync: readFileSync2 } = require_filesystem();
2833
+ var { LDD_PATH, readFile, readFileSync: readFileSync3 } = require_filesystem();
2834
2834
  var cachedFamilyFilesystem;
2835
2835
  var cachedVersionFilesystem;
2836
2836
  var command2 = "getconf GNU_LIBC_VERSION 2>&1 || true; ldd --version 2>&1 || true";
@@ -2911,7 +2911,7 @@ var require_detect_libc = __commonJS({
2911
2911
  }
2912
2912
  cachedFamilyFilesystem = null;
2913
2913
  try {
2914
- const lddContent = readFileSync2(LDD_PATH);
2914
+ const lddContent = readFileSync3(LDD_PATH);
2915
2915
  cachedFamilyFilesystem = getFamilyFromLddContent(lddContent);
2916
2916
  } catch (e) {
2917
2917
  }
@@ -2968,7 +2968,7 @@ var require_detect_libc = __commonJS({
2968
2968
  }
2969
2969
  cachedVersionFilesystem = null;
2970
2970
  try {
2971
- const lddContent = readFileSync2(LDD_PATH);
2971
+ const lddContent = readFileSync3(LDD_PATH);
2972
2972
  const versionMatch = lddContent.match(RE_GLIBC_VERSION);
2973
2973
  if (versionMatch) {
2974
2974
  cachedVersionFilesystem = versionMatch[1];
@@ -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: createHash2 } = require("crypto");
6165
+ var { randomBytes, createHash: createHash4 } = 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 = createHash2("sha1").update(key + GUID).digest("base64");
6845
+ const digest = createHash4("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: createHash2 } = require("crypto");
7232
+ var { createHash: createHash4 } = 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 = createHash2("sha1").update(key + GUID).digest("base64");
7541
+ const digest = createHash4("sha1").update(key + GUID).digest("base64");
7542
7542
  const headers = [
7543
7543
  "HTTP/1.1 101 Switching Protocols",
7544
7544
  "Upgrade: websocket",
@@ -14252,7 +14252,7 @@ var init_select2 = __esm({
14252
14252
  const tableName = getTableLikeName(table);
14253
14253
  for (const item of extractUsedTable(table))
14254
14254
  this.usedTables.add(item);
14255
- if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
14255
+ if (typeof tableName === "string" && this.config.joins?.some((join6) => join6.alias === tableName)) {
14256
14256
  throw new Error(`Alias "${tableName}" is already used in this query`);
14257
14257
  }
14258
14258
  if (!this.isPartialSelect) {
@@ -15141,7 +15141,7 @@ var init_update = __esm({
15141
15141
  createJoin(joinType) {
15142
15142
  return (table, on) => {
15143
15143
  const tableName = getTableLikeName(table);
15144
- if (typeof tableName === "string" && this.config.joins.some((join4) => join4.alias === tableName)) {
15144
+ if (typeof tableName === "string" && this.config.joins.some((join6) => join6.alias === tableName)) {
15145
15145
  throw new Error(`Alias "${tableName}" is already used in this query`);
15146
15146
  }
15147
15147
  if (typeof on === "function") {
@@ -16539,7 +16539,11 @@ var init_schema = __esm({
16539
16539
  title: text("title").notNull().unique(),
16540
16540
  ai_tool: text("ai_tool"),
16541
16541
  created_at: text("created_at").notNull(),
16542
- updated_at: text("updated_at").notNull()
16542
+ updated_at: text("updated_at").notNull(),
16543
+ parent_session_id: text("parent_session_id"),
16544
+ external_ref: text("external_ref"),
16545
+ public_key: text("public_key"),
16546
+ signature: text("signature")
16543
16547
  });
16544
16548
  messages = sqliteTable("messages", {
16545
16549
  id: text("id").primaryKey(),
@@ -16548,7 +16552,9 @@ var init_schema = __esm({
16548
16552
  content: text("content").notNull(),
16549
16553
  created_at: text("created_at").notNull(),
16550
16554
  prev_hash: text("prev_hash"),
16551
- content_hash: text("content_hash")
16555
+ content_hash: text("content_hash"),
16556
+ // NULL = pre-migration row (treated as 'message'); set for all new rows
16557
+ event_type: text("event_type")
16552
16558
  });
16553
16559
  secrets_detected = sqliteTable("secrets_detected", {
16554
16560
  id: text("id").primaryKey(),
@@ -16579,14 +16585,31 @@ async function initDb(dbPath2) {
16579
16585
  for (const sql2 of CREATE_SQL) {
16580
16586
  await client.execute(sql2);
16581
16587
  }
16582
- const tableInfo = await client.execute("PRAGMA table_info(messages)");
16583
- const columns = tableInfo.rows.map((r) => r[1]);
16584
- if (!columns.includes("prev_hash")) {
16588
+ const msgInfo = await client.execute("PRAGMA table_info(messages)");
16589
+ const msgCols = msgInfo.rows.map((r) => r[1]);
16590
+ if (!msgCols.includes("prev_hash")) {
16585
16591
  await client.execute("ALTER TABLE messages ADD COLUMN prev_hash TEXT");
16586
16592
  }
16587
- if (!columns.includes("content_hash")) {
16593
+ if (!msgCols.includes("content_hash")) {
16588
16594
  await client.execute("ALTER TABLE messages ADD COLUMN content_hash TEXT");
16589
16595
  }
16596
+ if (!msgCols.includes("event_type")) {
16597
+ await client.execute("ALTER TABLE messages ADD COLUMN event_type TEXT");
16598
+ }
16599
+ const sessInfo = await client.execute("PRAGMA table_info(sessions)");
16600
+ const sessCols = sessInfo.rows.map((r) => r[1]);
16601
+ if (!sessCols.includes("parent_session_id")) {
16602
+ await client.execute("ALTER TABLE sessions ADD COLUMN parent_session_id TEXT");
16603
+ }
16604
+ if (!sessCols.includes("external_ref")) {
16605
+ await client.execute("ALTER TABLE sessions ADD COLUMN external_ref TEXT");
16606
+ }
16607
+ if (!sessCols.includes("public_key")) {
16608
+ await client.execute("ALTER TABLE sessions ADD COLUMN public_key TEXT");
16609
+ }
16610
+ if (!sessCols.includes("signature")) {
16611
+ await client.execute("ALTER TABLE sessions ADD COLUMN signature TEXT");
16612
+ }
16590
16613
  return drizzle(client, { schema: schema_exports });
16591
16614
  }
16592
16615
  var import_os, import_fs, import_path, CREATE_SQL;
@@ -16605,7 +16628,11 @@ var init_db2 = __esm({
16605
16628
  title TEXT NOT NULL UNIQUE,
16606
16629
  ai_tool TEXT,
16607
16630
  created_at TEXT NOT NULL,
16608
- updated_at TEXT NOT NULL
16631
+ updated_at TEXT NOT NULL,
16632
+ parent_session_id TEXT,
16633
+ external_ref TEXT,
16634
+ public_key TEXT,
16635
+ signature TEXT
16609
16636
  )`,
16610
16637
  `CREATE TABLE IF NOT EXISTS messages (
16611
16638
  id TEXT PRIMARY KEY,
@@ -16615,6 +16642,7 @@ var init_db2 = __esm({
16615
16642
  created_at TEXT NOT NULL,
16616
16643
  prev_hash TEXT,
16617
16644
  content_hash TEXT,
16645
+ event_type TEXT,
16618
16646
  FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
16619
16647
  )`,
16620
16648
  `CREATE TABLE IF NOT EXISTS secrets_detected (
@@ -16806,14 +16834,14 @@ async function queryIntegrity(db) {
16806
16834
  unchained++;
16807
16835
  continue;
16808
16836
  }
16809
- let ok = true;
16837
+ let ok2 = true;
16810
16838
  for (let i = 1; i < chained.length; i++) {
16811
16839
  if (chained[i].prev_hash !== chained[i - 1].content_hash) {
16812
- ok = false;
16840
+ ok2 = false;
16813
16841
  break;
16814
16842
  }
16815
16843
  }
16816
- if (ok)
16844
+ if (ok2)
16817
16845
  intact++;
16818
16846
  else {
16819
16847
  broken++;
@@ -17401,7 +17429,7 @@ var require_package = __commonJS({
17401
17429
  "package.json"(exports2, module2) {
17402
17430
  module2.exports = {
17403
17431
  name: "chron-mcp",
17404
- version: "0.1.18",
17432
+ version: "0.1.20",
17405
17433
  mcpName: "io.github.sirinivask/chron",
17406
17434
  description: "Audit-grade timestamped logs for every AI conversation",
17407
17435
  repository: {
@@ -17560,7 +17588,7 @@ ${DIM4}Sending test event...${RESET5} `);
17560
17588
  }
17561
17589
  }]
17562
17590
  }]);
17563
- let ok = false;
17591
+ let ok2 = false;
17564
17592
  let statusCode = 0;
17565
17593
  try {
17566
17594
  const res = await fetch(url, {
@@ -17572,7 +17600,7 @@ ${DIM4}Sending test event...${RESET5} `);
17572
17600
  body: testPayload
17573
17601
  });
17574
17602
  statusCode = res.status;
17575
- ok = res.ok;
17603
+ ok2 = res.ok;
17576
17604
  } catch (e) {
17577
17605
  process.stdout.write(`${RED}failed${RESET5}
17578
17606
  `);
@@ -17581,7 +17609,7 @@ ${DIM4}Sending test event...${RESET5} `);
17581
17609
  `);
17582
17610
  process.exit(1);
17583
17611
  }
17584
- if (!ok) {
17612
+ if (!ok2) {
17585
17613
  process.stdout.write(`${RED}failed (HTTP ${statusCode})${RESET5}
17586
17614
  `);
17587
17615
  process.stderr.write(` ${RED}Check your URL and token, then try again.${RESET5}
@@ -18100,6 +18128,254 @@ var init_summary = __esm({
18100
18128
  }
18101
18129
  });
18102
18130
 
18131
+ // src/utils/signing.ts
18132
+ function keysDir() {
18133
+ return (0, import_path4.join)((0, import_os5.homedir)(), ".chron", "keys");
18134
+ }
18135
+ function privKeyPath(sessionId) {
18136
+ return (0, import_path4.join)(keysDir(), `${sessionId}.key`);
18137
+ }
18138
+ function sessionDigest(sessionId, finalContentHash, messageCount, firstCreatedAt) {
18139
+ const input = `${sessionId}|${finalContentHash}|${messageCount}|${firstCreatedAt}`;
18140
+ return (0, import_crypto2.createHash)("sha256").update(input).digest();
18141
+ }
18142
+ function signSession(sessionId, finalContentHash, messageCount, firstCreatedAt) {
18143
+ const privPath = privKeyPath(sessionId);
18144
+ if (!(0, import_fs4.existsSync)(privPath)) {
18145
+ throw new Error(`Private key not found: ${privPath}`);
18146
+ }
18147
+ const privateKey = (0, import_fs4.readFileSync)(privPath, "utf8");
18148
+ const digest = sessionDigest(sessionId, finalContentHash, messageCount, firstCreatedAt);
18149
+ const sig = (0, import_crypto2.sign)(null, digest, privateKey);
18150
+ return sig.toString("base64");
18151
+ }
18152
+ function verifySignature(publicKeyPem, signatureB64, sessionId, finalContentHash, messageCount, firstCreatedAt) {
18153
+ try {
18154
+ const digest = sessionDigest(sessionId, finalContentHash, messageCount, firstCreatedAt);
18155
+ const sigBuf = Buffer.from(signatureB64, "base64");
18156
+ return (0, import_crypto2.verify)(null, digest, publicKeyPem, sigBuf);
18157
+ } catch {
18158
+ return false;
18159
+ }
18160
+ }
18161
+ var import_crypto2, import_fs4, import_path4, import_os5;
18162
+ var init_signing = __esm({
18163
+ "src/utils/signing.ts"() {
18164
+ "use strict";
18165
+ import_crypto2 = require("crypto");
18166
+ import_fs4 = require("fs");
18167
+ import_path4 = require("path");
18168
+ import_os5 = require("os");
18169
+ }
18170
+ });
18171
+
18172
+ // src/cli/sign.ts
18173
+ var sign_exports = {};
18174
+ __export(sign_exports, {
18175
+ runSign: () => runSign
18176
+ });
18177
+ async function runSign(args2) {
18178
+ const prefix = args2[0];
18179
+ if (!prefix) {
18180
+ process.stderr.write("Usage: chron sign <session-id-prefix>\n");
18181
+ process.exit(1);
18182
+ }
18183
+ const db = await initDb();
18184
+ const allSessions = await db.select().from(sessions);
18185
+ const session = allSessions.find((s) => s.id.startsWith(prefix));
18186
+ if (!session) {
18187
+ process.stderr.write(`Session not found: ${prefix}
18188
+ `);
18189
+ process.exit(1);
18190
+ }
18191
+ if (!session.public_key) {
18192
+ process.stderr.write(`Session ${session.id.slice(0, 8)} has no public key \u2014 was it created before v0.1.19?
18193
+ `);
18194
+ process.exit(1);
18195
+ }
18196
+ if (!(0, import_fs6.existsSync)(privKeyPath(session.id))) {
18197
+ process.stderr.write(`Private key not found: ${privKeyPath(session.id)}
18198
+ `);
18199
+ process.stderr.write("The key is stored on the machine where the session was created.\n");
18200
+ process.exit(1);
18201
+ }
18202
+ const [countRow] = await db.select({
18203
+ count: sql`count(*)`,
18204
+ first_created_at: sql`min(${messages.created_at})`
18205
+ }).from(messages).where(eq(messages.session_id, session.id));
18206
+ const messageCount = countRow?.count ?? 0;
18207
+ const firstCreatedAt = countRow?.first_created_at ?? "";
18208
+ if (messageCount === 0) {
18209
+ process.stderr.write("Session has no messages \u2014 nothing to sign.\n");
18210
+ process.exit(1);
18211
+ }
18212
+ const lastMsg = await db.select({ content_hash: messages.content_hash }).from(messages).where(eq(messages.session_id, session.id)).orderBy(asc(sql`rowid`)).limit(1e3);
18213
+ const finalContentHash = lastMsg[lastMsg.length - 1]?.content_hash ?? "";
18214
+ if (!finalContentHash) {
18215
+ process.stderr.write("Session has no hash chain \u2014 cannot sign.\n");
18216
+ process.exit(1);
18217
+ }
18218
+ const signature = signSession(session.id, finalContentHash, messageCount, firstCreatedAt);
18219
+ await db.update(sessions).set({ signature }).where(eq(sessions.id, session.id));
18220
+ const sigData = {
18221
+ chron_signature: "v1",
18222
+ session_id: session.id,
18223
+ session_title: session.title,
18224
+ public_key_pem: session.public_key,
18225
+ message_count: messageCount,
18226
+ first_message_at: firstCreatedAt,
18227
+ final_content_hash: finalContentHash,
18228
+ signature,
18229
+ signed_at: (/* @__PURE__ */ new Date()).toISOString()
18230
+ };
18231
+ const sigFile = (0, import_path5.join)(process.cwd(), `${session.id.slice(0, 8)}.chron.sig`);
18232
+ (0, import_fs5.writeFileSync)(sigFile, JSON.stringify(sigData, null, 2));
18233
+ process.stdout.write(`Signed session ${session.id.slice(0, 8)}
18234
+ `);
18235
+ process.stdout.write(` messages : ${messageCount}
18236
+ `);
18237
+ process.stdout.write(` final hash: ${finalContentHash.slice(0, 16)}\u2026
18238
+ `);
18239
+ process.stdout.write(` sig file : ${sigFile}
18240
+ `);
18241
+ }
18242
+ var import_fs5, import_path5, import_fs6;
18243
+ var init_sign = __esm({
18244
+ "src/cli/sign.ts"() {
18245
+ "use strict";
18246
+ init_drizzle_orm();
18247
+ import_fs5 = require("fs");
18248
+ import_path5 = require("path");
18249
+ init_db2();
18250
+ init_schema();
18251
+ init_signing();
18252
+ import_fs6 = require("fs");
18253
+ }
18254
+ });
18255
+
18256
+ // src/utils/hash.ts
18257
+ function computeContentHash(sessionId, role, content, createdAt, prevHash, eventType) {
18258
+ const base = `${sessionId}|${role}|${content}|${createdAt}|${prevHash ?? ""}`;
18259
+ const input = eventType != null ? `${base}|${eventType}` : base;
18260
+ return (0, import_crypto3.createHash)("sha256").update(input).digest("hex");
18261
+ }
18262
+ var import_crypto3;
18263
+ var init_hash = __esm({
18264
+ "src/utils/hash.ts"() {
18265
+ "use strict";
18266
+ import_crypto3 = require("crypto");
18267
+ }
18268
+ });
18269
+
18270
+ // src/cli/verify.ts
18271
+ var verify_exports = {};
18272
+ __export(verify_exports, {
18273
+ runVerify: () => runVerify
18274
+ });
18275
+ function ok(msg) {
18276
+ process.stdout.write(` ${GREEN3}\u2713${RESET7} ${msg}
18277
+ `);
18278
+ }
18279
+ function fail(msg) {
18280
+ process.stdout.write(` ${RED3}\u2717${RESET7} ${msg}
18281
+ `);
18282
+ }
18283
+ function warn(msg) {
18284
+ process.stdout.write(` ${YELLOW5}!${RESET7} ${msg}
18285
+ `);
18286
+ }
18287
+ async function runVerify(args2) {
18288
+ const prefix = args2[0];
18289
+ if (!prefix) {
18290
+ process.stderr.write("Usage: chron verify <session-id-prefix>\n");
18291
+ process.exit(1);
18292
+ }
18293
+ const db = await initDb();
18294
+ const allSessions = await db.select().from(sessions);
18295
+ const session = allSessions.find((s) => s.id.startsWith(prefix));
18296
+ if (!session) {
18297
+ process.stderr.write(`Session not found: ${prefix}
18298
+ `);
18299
+ process.exit(1);
18300
+ }
18301
+ process.stdout.write(`
18302
+ ${BOLD7}Verifying session ${session.id.slice(0, 8)}${RESET7} \u2014 ${session.title}
18303
+
18304
+ `);
18305
+ const rows = await db.select().from(messages).where(eq(messages.session_id, session.id)).orderBy(asc(messages.created_at), asc(sql`rowid`));
18306
+ process.stdout.write(`${BOLD7}Hash chain${RESET7} (${rows.length} messages)
18307
+ `);
18308
+ const chained = rows.filter((r) => r.content_hash !== null);
18309
+ let chainOk = true;
18310
+ if (chained.length === 0) {
18311
+ warn("No chained messages \u2014 session pre-dates hash chaining");
18312
+ } else {
18313
+ for (let i = 0; i < chained.length; i++) {
18314
+ const row = chained[i];
18315
+ const expectedPrev = i === 0 ? null : chained[i - 1].content_hash;
18316
+ if (row.prev_hash !== expectedPrev) {
18317
+ fail(`Row ${i + 1} (${row.id.slice(0, 8)}): prev_hash mismatch \u2014 chain broken`);
18318
+ chainOk = false;
18319
+ break;
18320
+ }
18321
+ const expected = computeContentHash(row.session_id, row.role, row.content, row.created_at, row.prev_hash, row.event_type ?? void 0);
18322
+ if (row.content_hash !== expected) {
18323
+ fail(`Row ${i + 1} (${row.id.slice(0, 8)}): content_hash mismatch \u2014 row tampered`);
18324
+ chainOk = false;
18325
+ break;
18326
+ }
18327
+ }
18328
+ if (chainOk)
18329
+ ok(`All ${chained.length} hashes valid`);
18330
+ }
18331
+ process.stdout.write(`
18332
+ ${BOLD7}Signature${RESET7}
18333
+ `);
18334
+ if (!session.public_key) {
18335
+ warn("No public key \u2014 session was created before signing support (v0.1.19)");
18336
+ } else if (!session.signature) {
18337
+ warn("Not yet signed \u2014 run: chron sign " + session.id.slice(0, 8));
18338
+ } else {
18339
+ const finalHash = chained[chained.length - 1]?.content_hash ?? "";
18340
+ const firstCreatedAt = rows[0]?.created_at ?? "";
18341
+ const sigValid = verifySignature(
18342
+ session.public_key,
18343
+ session.signature,
18344
+ session.id,
18345
+ finalHash,
18346
+ rows.length,
18347
+ firstCreatedAt
18348
+ );
18349
+ if (sigValid) {
18350
+ ok("Ed25519 signature valid");
18351
+ process.stdout.write(` ${DIM6}public key: ${session.public_key.split("\n")[1]?.slice(0, 40)}\u2026${RESET7}
18352
+ `);
18353
+ } else {
18354
+ fail("Ed25519 signature INVALID \u2014 session data may have been altered after signing");
18355
+ }
18356
+ }
18357
+ process.stdout.write("\n");
18358
+ const allOk = chainOk && (session.signature ? true : true);
18359
+ process.exit(allOk ? 0 : 1);
18360
+ }
18361
+ var RESET7, BOLD7, GREEN3, RED3, DIM6, YELLOW5;
18362
+ var init_verify = __esm({
18363
+ "src/cli/verify.ts"() {
18364
+ "use strict";
18365
+ init_drizzle_orm();
18366
+ init_db2();
18367
+ init_schema();
18368
+ init_hash();
18369
+ init_signing();
18370
+ RESET7 = "\x1B[0m";
18371
+ BOLD7 = "\x1B[1m";
18372
+ GREEN3 = "\x1B[32m";
18373
+ RED3 = "\x1B[31m";
18374
+ DIM6 = "\x1B[2m";
18375
+ YELLOW5 = "\x1B[33m";
18376
+ }
18377
+ });
18378
+
18103
18379
  // src/cli/index.ts
18104
18380
  var [, , command, ...args] = process.argv;
18105
18381
  async function main() {
@@ -18139,6 +18415,16 @@ async function main() {
18139
18415
  await runSummary2(args);
18140
18416
  break;
18141
18417
  }
18418
+ case "sign": {
18419
+ const { runSign: runSign2 } = await Promise.resolve().then(() => (init_sign(), sign_exports));
18420
+ await runSign2(args);
18421
+ break;
18422
+ }
18423
+ case "verify": {
18424
+ const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
18425
+ await runVerify2(args);
18426
+ break;
18427
+ }
18142
18428
  default: {
18143
18429
  const name = command ? `Unknown command: ${command}
18144
18430
 
@@ -18154,6 +18440,8 @@ Commands:
18154
18440
  settings View current configuration
18155
18441
  connect Connect to a SIEM integration (crowdstrike, sentinel, splunk)
18156
18442
  summary Structured summary of a session (timeline, mutations, secrets)
18443
+ sign Sign a session with its Ed25519 key \u2014 produces a .chron.sig file
18444
+ verify Verify a session's hash chain and Ed25519 signature
18157
18445
 
18158
18446
  Options (history):
18159
18447
  --limit=<n> Max sessions to show (default: 20)
@@ -18164,7 +18452,7 @@ Options (report):
18164
18452
  --format=soc2 Generate SOC 2 HTML evidence package
18165
18453
  --output=<file> Output file for --format=soc2 (default: soc2-report.html)
18166
18454
 
18167
- Options (export / secrets):
18455
+ Options (export / secrets / sign / verify):
18168
18456
  <id-prefix> Scope to a single session
18169
18457
  `
18170
18458
  );
package/dist/index.js CHANGED
@@ -6890,7 +6890,7 @@ var require_filesystem = __commonJS({
6890
6890
  "use strict";
6891
6891
  var fs = require("fs");
6892
6892
  var LDD_PATH = "/usr/bin/ldd";
6893
- var readFileSync3 = (path) => fs.readFileSync(path, "utf-8");
6893
+ var readFileSync4 = (path) => fs.readFileSync(path, "utf-8");
6894
6894
  var readFile = (path) => new Promise((resolve, reject) => {
6895
6895
  fs.readFile(path, "utf-8", (err, data) => {
6896
6896
  if (err) {
@@ -6902,7 +6902,7 @@ var require_filesystem = __commonJS({
6902
6902
  });
6903
6903
  module2.exports = {
6904
6904
  LDD_PATH,
6905
- readFileSync: readFileSync3,
6905
+ readFileSync: readFileSync4,
6906
6906
  readFile
6907
6907
  };
6908
6908
  }
@@ -6914,7 +6914,7 @@ var require_detect_libc = __commonJS({
6914
6914
  "use strict";
6915
6915
  var childProcess = require("child_process");
6916
6916
  var { isLinux, getReport } = require_process();
6917
- var { LDD_PATH, readFile, readFileSync: readFileSync3 } = require_filesystem();
6917
+ var { LDD_PATH, readFile, readFileSync: readFileSync4 } = require_filesystem();
6918
6918
  var cachedFamilyFilesystem;
6919
6919
  var cachedVersionFilesystem;
6920
6920
  var command = "getconf GNU_LIBC_VERSION 2>&1 || true; ldd --version 2>&1 || true";
@@ -6995,7 +6995,7 @@ var require_detect_libc = __commonJS({
6995
6995
  }
6996
6996
  cachedFamilyFilesystem = null;
6997
6997
  try {
6998
- const lddContent = readFileSync3(LDD_PATH);
6998
+ const lddContent = readFileSync4(LDD_PATH);
6999
6999
  cachedFamilyFilesystem = getFamilyFromLddContent(lddContent);
7000
7000
  } catch (e) {
7001
7001
  }
@@ -7052,7 +7052,7 @@ var require_detect_libc = __commonJS({
7052
7052
  }
7053
7053
  cachedVersionFilesystem = null;
7054
7054
  try {
7055
- const lddContent = readFileSync3(LDD_PATH);
7055
+ const lddContent = readFileSync4(LDD_PATH);
7056
7056
  const versionMatch = lddContent.match(RE_GLIBC_VERSION);
7057
7057
  if (versionMatch) {
7058
7058
  cachedVersionFilesystem = versionMatch[1];
@@ -10246,7 +10246,7 @@ var require_websocket = __commonJS({
10246
10246
  var http = require("http");
10247
10247
  var net = require("net");
10248
10248
  var tls = require("tls");
10249
- var { randomBytes, createHash: createHash3 } = require("crypto");
10249
+ var { randomBytes, createHash: createHash4 } = require("crypto");
10250
10250
  var { Duplex, Readable } = require("stream");
10251
10251
  var { URL: URL3 } = require("url");
10252
10252
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -10926,7 +10926,7 @@ var require_websocket = __commonJS({
10926
10926
  abortHandshake(websocket, socket, "Invalid Upgrade header");
10927
10927
  return;
10928
10928
  }
10929
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
10929
+ const digest = createHash4("sha1").update(key + GUID).digest("base64");
10930
10930
  if (res.headers["sec-websocket-accept"] !== digest) {
10931
10931
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
10932
10932
  return;
@@ -11313,7 +11313,7 @@ var require_websocket_server = __commonJS({
11313
11313
  var EventEmitter = require("events");
11314
11314
  var http = require("http");
11315
11315
  var { Duplex } = require("stream");
11316
- var { createHash: createHash3 } = require("crypto");
11316
+ var { createHash: createHash4 } = require("crypto");
11317
11317
  var extension2 = require_extension();
11318
11318
  var PerMessageDeflate2 = require_permessage_deflate();
11319
11319
  var subprotocol2 = require_subprotocol();
@@ -11622,7 +11622,7 @@ var require_websocket_server = __commonJS({
11622
11622
  }
11623
11623
  if (this._state > RUNNING)
11624
11624
  return abortHandshake(socket, 503);
11625
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
11625
+ const digest = createHash4("sha1").update(key + GUID).digest("base64");
11626
11626
  const headers = [
11627
11627
  "HTTP/1.1 101 Switching Protocols",
11628
11628
  "Upgrade: websocket",
@@ -17521,7 +17521,7 @@ var init_sql2 = __esm({
17521
17521
  return new SQL([new StringChunk(str)]);
17522
17522
  }
17523
17523
  sql2.raw = raw;
17524
- function join5(chunks, separator) {
17524
+ function join6(chunks, separator) {
17525
17525
  const result = [];
17526
17526
  for (const [i, chunk] of chunks.entries()) {
17527
17527
  if (i > 0 && separator !== void 0) {
@@ -17531,7 +17531,7 @@ var init_sql2 = __esm({
17531
17531
  }
17532
17532
  return new SQL(result);
17533
17533
  }
17534
- sql2.join = join5;
17534
+ sql2.join = join6;
17535
17535
  function identifier(value) {
17536
17536
  return new Name(value);
17537
17537
  }
@@ -20462,7 +20462,7 @@ var init_select2 = __esm({
20462
20462
  const tableName = getTableLikeName(table);
20463
20463
  for (const item of extractUsedTable(table))
20464
20464
  this.usedTables.add(item);
20465
- if (typeof tableName === "string" && this.config.joins?.some((join5) => join5.alias === tableName)) {
20465
+ if (typeof tableName === "string" && this.config.joins?.some((join6) => join6.alias === tableName)) {
20466
20466
  throw new Error(`Alias "${tableName}" is already used in this query`);
20467
20467
  }
20468
20468
  if (!this.isPartialSelect) {
@@ -21351,7 +21351,7 @@ var init_update = __esm({
21351
21351
  createJoin(joinType) {
21352
21352
  return (table, on) => {
21353
21353
  const tableName = getTableLikeName(table);
21354
- if (typeof tableName === "string" && this.config.joins.some((join5) => join5.alias === tableName)) {
21354
+ if (typeof tableName === "string" && this.config.joins.some((join6) => join6.alias === tableName)) {
21355
21355
  throw new Error(`Alias "${tableName}" is already used in this query`);
21356
21356
  }
21357
21357
  if (typeof on === "function") {
@@ -22749,7 +22749,11 @@ var init_schema = __esm({
22749
22749
  title: text("title").notNull().unique(),
22750
22750
  ai_tool: text("ai_tool"),
22751
22751
  created_at: text("created_at").notNull(),
22752
- updated_at: text("updated_at").notNull()
22752
+ updated_at: text("updated_at").notNull(),
22753
+ parent_session_id: text("parent_session_id"),
22754
+ external_ref: text("external_ref"),
22755
+ public_key: text("public_key"),
22756
+ signature: text("signature")
22753
22757
  });
22754
22758
  messages = sqliteTable("messages", {
22755
22759
  id: text("id").primaryKey(),
@@ -22758,7 +22762,9 @@ var init_schema = __esm({
22758
22762
  content: text("content").notNull(),
22759
22763
  created_at: text("created_at").notNull(),
22760
22764
  prev_hash: text("prev_hash"),
22761
- content_hash: text("content_hash")
22765
+ content_hash: text("content_hash"),
22766
+ // NULL = pre-migration row (treated as 'message'); set for all new rows
22767
+ event_type: text("event_type")
22762
22768
  });
22763
22769
  secrets_detected = sqliteTable("secrets_detected", {
22764
22770
  id: text("id").primaryKey(),
@@ -22789,14 +22795,31 @@ async function initDb(dbPath) {
22789
22795
  for (const sql2 of CREATE_SQL) {
22790
22796
  await client.execute(sql2);
22791
22797
  }
22792
- const tableInfo = await client.execute("PRAGMA table_info(messages)");
22793
- const columns = tableInfo.rows.map((r) => r[1]);
22794
- if (!columns.includes("prev_hash")) {
22798
+ const msgInfo = await client.execute("PRAGMA table_info(messages)");
22799
+ const msgCols = msgInfo.rows.map((r) => r[1]);
22800
+ if (!msgCols.includes("prev_hash")) {
22795
22801
  await client.execute("ALTER TABLE messages ADD COLUMN prev_hash TEXT");
22796
22802
  }
22797
- if (!columns.includes("content_hash")) {
22803
+ if (!msgCols.includes("content_hash")) {
22798
22804
  await client.execute("ALTER TABLE messages ADD COLUMN content_hash TEXT");
22799
22805
  }
22806
+ if (!msgCols.includes("event_type")) {
22807
+ await client.execute("ALTER TABLE messages ADD COLUMN event_type TEXT");
22808
+ }
22809
+ const sessInfo = await client.execute("PRAGMA table_info(sessions)");
22810
+ const sessCols = sessInfo.rows.map((r) => r[1]);
22811
+ if (!sessCols.includes("parent_session_id")) {
22812
+ await client.execute("ALTER TABLE sessions ADD COLUMN parent_session_id TEXT");
22813
+ }
22814
+ if (!sessCols.includes("external_ref")) {
22815
+ await client.execute("ALTER TABLE sessions ADD COLUMN external_ref TEXT");
22816
+ }
22817
+ if (!sessCols.includes("public_key")) {
22818
+ await client.execute("ALTER TABLE sessions ADD COLUMN public_key TEXT");
22819
+ }
22820
+ if (!sessCols.includes("signature")) {
22821
+ await client.execute("ALTER TABLE sessions ADD COLUMN signature TEXT");
22822
+ }
22800
22823
  return drizzle(client, { schema: schema_exports });
22801
22824
  }
22802
22825
  var import_os, import_fs, import_path, CREATE_SQL;
@@ -22815,7 +22838,11 @@ var init_db2 = __esm({
22815
22838
  title TEXT NOT NULL UNIQUE,
22816
22839
  ai_tool TEXT,
22817
22840
  created_at TEXT NOT NULL,
22818
- updated_at TEXT NOT NULL
22841
+ updated_at TEXT NOT NULL,
22842
+ parent_session_id TEXT,
22843
+ external_ref TEXT,
22844
+ public_key TEXT,
22845
+ signature TEXT
22819
22846
  )`,
22820
22847
  `CREATE TABLE IF NOT EXISTS messages (
22821
22848
  id TEXT PRIMARY KEY,
@@ -22825,6 +22852,7 @@ var init_db2 = __esm({
22825
22852
  created_at TEXT NOT NULL,
22826
22853
  prev_hash TEXT,
22827
22854
  content_hash TEXT,
22855
+ event_type TEXT,
22828
22856
  FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
22829
22857
  )`,
22830
22858
  `CREATE TABLE IF NOT EXISTS secrets_detected (
@@ -38446,7 +38474,7 @@ var init_time = __esm({
38446
38474
  var version4;
38447
38475
  var init_package = __esm({
38448
38476
  "package.json"() {
38449
- version4 = "0.1.18";
38477
+ version4 = "0.1.20";
38450
38478
  }
38451
38479
  });
38452
38480
 
@@ -38690,18 +38718,71 @@ var init_relay = __esm({
38690
38718
  }
38691
38719
  });
38692
38720
 
38721
+ // src/utils/signing.ts
38722
+ function keysDir() {
38723
+ return (0, import_path3.join)((0, import_os3.homedir)(), ".chron", "keys");
38724
+ }
38725
+ function privKeyPath(sessionId) {
38726
+ return (0, import_path3.join)(keysDir(), `${sessionId}.key`);
38727
+ }
38728
+ function pubKeyPath(sessionId) {
38729
+ return (0, import_path3.join)(keysDir(), `${sessionId}.pub`);
38730
+ }
38731
+ function generateSessionKeypair(sessionId) {
38732
+ const { privateKey, publicKey } = (0, import_crypto4.generateKeyPairSync)("ed25519", {
38733
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
38734
+ publicKeyEncoding: { type: "spki", format: "pem" }
38735
+ });
38736
+ const dir = keysDir();
38737
+ (0, import_fs3.mkdirSync)(dir, { recursive: true });
38738
+ (0, import_fs3.writeFileSync)(privKeyPath(sessionId), privateKey, { mode: 384 });
38739
+ (0, import_fs3.writeFileSync)(pubKeyPath(sessionId), publicKey);
38740
+ return publicKey;
38741
+ }
38742
+ function sessionDigest(sessionId, finalContentHash, messageCount, firstCreatedAt) {
38743
+ const input = `${sessionId}|${finalContentHash}|${messageCount}|${firstCreatedAt}`;
38744
+ return (0, import_crypto4.createHash)("sha256").update(input).digest();
38745
+ }
38746
+ function verifySignature(publicKeyPem, signatureB64, sessionId, finalContentHash, messageCount, firstCreatedAt) {
38747
+ try {
38748
+ const digest = sessionDigest(sessionId, finalContentHash, messageCount, firstCreatedAt);
38749
+ const sigBuf = Buffer.from(signatureB64, "base64");
38750
+ return (0, import_crypto4.verify)(null, digest, publicKeyPem, sigBuf);
38751
+ } catch {
38752
+ return false;
38753
+ }
38754
+ }
38755
+ var import_crypto4, import_fs3, import_path3, import_os3;
38756
+ var init_signing = __esm({
38757
+ "src/utils/signing.ts"() {
38758
+ "use strict";
38759
+ import_crypto4 = require("crypto");
38760
+ import_fs3 = require("fs");
38761
+ import_path3 = require("path");
38762
+ import_os3 = require("os");
38763
+ }
38764
+ });
38765
+
38693
38766
  // src/tools/sessions.ts
38694
38767
  function startSession(db) {
38695
38768
  return async (args) => {
38696
38769
  const id = v4_default();
38697
38770
  const now = localISOString();
38698
38771
  try {
38772
+ let publicKey = null;
38773
+ try {
38774
+ publicKey = generateSessionKeypair(id);
38775
+ } catch {
38776
+ }
38699
38777
  await db.insert(sessions).values({
38700
38778
  id,
38701
38779
  title: args.title,
38702
38780
  ai_tool: args.ai_tool ?? null,
38703
38781
  created_at: now,
38704
- updated_at: now
38782
+ updated_at: now,
38783
+ parent_session_id: args.parent_session_id ?? null,
38784
+ external_ref: args.external_ref ?? null,
38785
+ public_key: publicKey
38705
38786
  });
38706
38787
  emitEvent({ event_type: "session_started", timestamp: now, session: { id_prefix: id.slice(0, 8), ai_tool: args.ai_tool ?? null } });
38707
38788
  return {
@@ -38736,12 +38817,20 @@ function initSession(db) {
38736
38817
  let created;
38737
38818
  let ai_tool;
38738
38819
  try {
38820
+ let publicKey = null;
38821
+ try {
38822
+ publicKey = generateSessionKeypair(id);
38823
+ } catch {
38824
+ }
38739
38825
  await db.insert(sessions).values({
38740
38826
  id,
38741
38827
  title: args.title,
38742
38828
  ai_tool: args.ai_tool ?? null,
38743
38829
  created_at: now,
38744
- updated_at: now
38830
+ updated_at: now,
38831
+ parent_session_id: args.parent_session_id ?? null,
38832
+ external_ref: args.external_ref ?? null,
38833
+ public_key: publicKey
38745
38834
  });
38746
38835
  session_id = id;
38747
38836
  created = true;
@@ -38842,18 +38931,21 @@ var init_sessions = __esm({
38842
38931
  init_schema();
38843
38932
  init_time();
38844
38933
  init_relay();
38934
+ init_signing();
38845
38935
  }
38846
38936
  });
38847
38937
 
38848
38938
  // src/utils/hash.ts
38849
- function computeContentHash(sessionId, role, content, createdAt, prevHash) {
38850
- return (0, import_crypto4.createHash)("sha256").update(`${sessionId}|${role}|${content}|${createdAt}|${prevHash ?? ""}`).digest("hex");
38939
+ function computeContentHash(sessionId, role, content, createdAt, prevHash, eventType) {
38940
+ const base = `${sessionId}|${role}|${content}|${createdAt}|${prevHash ?? ""}`;
38941
+ const input = eventType != null ? `${base}|${eventType}` : base;
38942
+ return (0, import_crypto5.createHash)("sha256").update(input).digest("hex");
38851
38943
  }
38852
- var import_crypto4;
38944
+ var import_crypto5;
38853
38945
  var init_hash = __esm({
38854
38946
  "src/utils/hash.ts"() {
38855
38947
  "use strict";
38856
- import_crypto4 = require("crypto");
38948
+ import_crypto5 = require("crypto");
38857
38949
  }
38858
38950
  });
38859
38951
 
@@ -39150,32 +39242,37 @@ async function writeDetections(db, session_id, message_id, content) {
39150
39242
  function isFkError(e) {
39151
39243
  return e?.message?.includes("FOREIGN KEY constraint failed") || e?.code === "SQLITE_CONSTRAINT_FOREIGNKEY" || e?.cause?.message?.includes("FOREIGN KEY constraint failed") || e?.cause?.extendedCode === "SQLITE_CONSTRAINT_FOREIGNKEY";
39152
39244
  }
39245
+ async function insertMessage(db, session_id, role, content, event_type) {
39246
+ return db.transaction(async (tx) => {
39247
+ 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);
39248
+ const ts = localISOString();
39249
+ const msgId = v4_default();
39250
+ const prevHash = last[0]?.content_hash ?? null;
39251
+ const hash = computeContentHash(session_id, role, content, ts, prevHash, event_type);
39252
+ await tx.insert(messages).values({
39253
+ id: msgId,
39254
+ session_id,
39255
+ role,
39256
+ content,
39257
+ created_at: ts,
39258
+ prev_hash: prevHash,
39259
+ content_hash: hash,
39260
+ event_type
39261
+ });
39262
+ await tx.update(sessions).set({ updated_at: ts }).where(eq(sessions.id, session_id));
39263
+ const [sessionRow] = await tx.select({ ai_tool: sessions.ai_tool }).from(sessions).where(eq(sessions.id, session_id)).limit(1);
39264
+ return { id: msgId, now: ts, contentHash: hash, ai_tool: sessionRow?.ai_tool ?? null };
39265
+ });
39266
+ }
39153
39267
  function logMessage(db) {
39154
39268
  return async (args) => {
39269
+ const eventType = args.event_type ?? "message";
39155
39270
  let id;
39156
39271
  let now;
39157
39272
  let contentHash;
39158
39273
  let sessionAiTool;
39159
39274
  try {
39160
- const result = await db.transaction(async (tx) => {
39161
- const last = await tx.select({ content_hash: messages.content_hash }).from(messages).where(eq(messages.session_id, args.session_id)).orderBy(desc(messages.created_at), desc(sql`rowid`)).limit(1);
39162
- const ts = localISOString();
39163
- const msgId = v4_default();
39164
- const prevHash = last[0]?.content_hash ?? null;
39165
- const hash = computeContentHash(args.session_id, args.role, args.content, ts, prevHash);
39166
- await tx.insert(messages).values({
39167
- id: msgId,
39168
- session_id: args.session_id,
39169
- role: args.role,
39170
- content: args.content,
39171
- created_at: ts,
39172
- prev_hash: prevHash,
39173
- content_hash: hash
39174
- });
39175
- await tx.update(sessions).set({ updated_at: ts }).where(eq(sessions.id, args.session_id));
39176
- const [sessionRow] = await tx.select({ ai_tool: sessions.ai_tool }).from(sessions).where(eq(sessions.id, args.session_id)).limit(1);
39177
- return { id: msgId, now: ts, contentHash: hash, ai_tool: sessionRow?.ai_tool ?? null };
39178
- });
39275
+ const result = await insertMessage(db, args.session_id, args.role, args.content, eventType);
39179
39276
  id = result.id;
39180
39277
  now = result.now;
39181
39278
  contentHash = result.contentHash;
@@ -39187,7 +39284,7 @@ function logMessage(db) {
39187
39284
  throw e;
39188
39285
  }
39189
39286
  emitEvent({ event_type: "message_logged", timestamp: now, session: { id_prefix: args.session_id.slice(0, 8), ai_tool: sessionAiTool }, message: { role: args.role } });
39190
- if (args.role === "user") {
39287
+ if (args.role === "user" && eventType === "message") {
39191
39288
  setImmediate(() => writeDetections(db, args.session_id, id, args.content).catch(() => void 0));
39192
39289
  }
39193
39290
  return {
@@ -39195,15 +39292,45 @@ function logMessage(db) {
39195
39292
  };
39196
39293
  };
39197
39294
  }
39295
+ function logToolCall(db) {
39296
+ return async (args) => {
39297
+ const toolCallId = args.tool_call_id ?? v4_default();
39298
+ const content = JSON.stringify({ tool_name: args.tool_name, input: args.tool_input, tool_call_id: toolCallId });
39299
+ let result;
39300
+ try {
39301
+ result = await insertMessage(db, args.session_id, "assistant", content, "tool_call");
39302
+ } catch (e) {
39303
+ if (isFkError(e)) {
39304
+ return { content: [{ type: "text", text: `Session not found: ${args.session_id}` }], isError: true };
39305
+ }
39306
+ throw e;
39307
+ }
39308
+ return {
39309
+ content: [{ type: "text", text: JSON.stringify({ message_id: result.id, tool_call_id: toolCallId, created_at: result.now, content_hash: result.contentHash }) }]
39310
+ };
39311
+ };
39312
+ }
39313
+ function logToolResult(db) {
39314
+ return async (args) => {
39315
+ const content = JSON.stringify({ tool_call_id: args.tool_call_id, output: args.output, exit_code: args.exit_code ?? null });
39316
+ let result;
39317
+ try {
39318
+ result = await insertMessage(db, args.session_id, "user", content, "tool_result");
39319
+ } catch (e) {
39320
+ if (isFkError(e)) {
39321
+ return { content: [{ type: "text", text: `Session not found: ${args.session_id}` }], isError: true };
39322
+ }
39323
+ throw e;
39324
+ }
39325
+ return {
39326
+ content: [{ type: "text", text: JSON.stringify({ message_id: result.id, tool_call_id: args.tool_call_id, created_at: result.now, content_hash: result.contentHash }) }]
39327
+ };
39328
+ };
39329
+ }
39198
39330
  function logExchange(db) {
39199
39331
  return async (args) => {
39200
- let userId;
39201
- let assistantId;
39202
- let userNow;
39203
- let assistantNow;
39204
- let userHash;
39205
- let assistantHash;
39206
- let sessionAiTool;
39332
+ let userId, assistantId, userNow, assistantNow;
39333
+ let userHash, assistantHash, sessionAiTool;
39207
39334
  try {
39208
39335
  const result = await db.transaction(async (tx) => {
39209
39336
  const last = await tx.select({ content_hash: messages.content_hash }).from(messages).where(eq(messages.session_id, args.session_id)).orderBy(desc(messages.created_at), desc(sql`rowid`)).limit(1);
@@ -39212,8 +39339,8 @@ function logExchange(db) {
39212
39339
  const aNow = localISOString();
39213
39340
  const aId = v4_default();
39214
39341
  const prevHash = last[0]?.content_hash ?? null;
39215
- const uHash = computeContentHash(args.session_id, "user", args.user_content, uNow, prevHash);
39216
- const aHash = computeContentHash(args.session_id, "assistant", args.assistant_content, aNow, uHash);
39342
+ const uHash = computeContentHash(args.session_id, "user", args.user_content, uNow, prevHash, "message");
39343
+ const aHash = computeContentHash(args.session_id, "assistant", args.assistant_content, aNow, uHash, "message");
39217
39344
  await tx.insert(messages).values({
39218
39345
  id: uId,
39219
39346
  session_id: args.session_id,
@@ -39221,7 +39348,8 @@ function logExchange(db) {
39221
39348
  content: args.user_content,
39222
39349
  created_at: uNow,
39223
39350
  prev_hash: prevHash,
39224
- content_hash: uHash
39351
+ content_hash: uHash,
39352
+ event_type: "message"
39225
39353
  });
39226
39354
  await tx.insert(messages).values({
39227
39355
  id: aId,
@@ -39230,7 +39358,8 @@ function logExchange(db) {
39230
39358
  content: args.assistant_content,
39231
39359
  created_at: aNow,
39232
39360
  prev_hash: uHash,
39233
- content_hash: aHash
39361
+ content_hash: aHash,
39362
+ event_type: "message"
39234
39363
  });
39235
39364
  await tx.update(sessions).set({ updated_at: aNow }).where(eq(sessions.id, args.session_id));
39236
39365
  const [sessionRow] = await tx.select({ ai_tool: sessions.ai_tool }).from(sessions).where(eq(sessions.id, args.session_id)).limit(1);
@@ -39284,6 +39413,7 @@ var init_messages = __esm({
39284
39413
  // src/tools/verify.ts
39285
39414
  function verifySession(db) {
39286
39415
  return async (args) => {
39416
+ const [sessionRow] = await db.select().from(sessions).where(eq(sessions.id, args.session_id)).limit(1);
39287
39417
  const rows = await db.select().from(messages).where(eq(messages.session_id, args.session_id)).orderBy(asc(messages.created_at), asc(sql`rowid`));
39288
39418
  const chained = rows.filter((r) => r.content_hash !== null);
39289
39419
  if (chained.length === 0) {
@@ -39305,7 +39435,7 @@ function verifySession(db) {
39305
39435
  }]
39306
39436
  };
39307
39437
  }
39308
- const expected = computeContentHash(row.session_id, row.role, row.content, row.created_at, row.prev_hash);
39438
+ const expected = computeContentHash(row.session_id, row.role, row.content, row.created_at, row.prev_hash, row.event_type ?? void 0);
39309
39439
  if (row.content_hash !== expected) {
39310
39440
  return {
39311
39441
  content: [{
@@ -39315,10 +39445,23 @@ function verifySession(db) {
39315
39445
  };
39316
39446
  }
39317
39447
  }
39448
+ let signature_valid = "unsigned";
39449
+ if (sessionRow?.public_key && sessionRow?.signature) {
39450
+ const finalHash = chained[chained.length - 1]?.content_hash ?? "";
39451
+ const firstCreatedAt = rows[0]?.created_at ?? "";
39452
+ signature_valid = verifySignature(
39453
+ sessionRow.public_key,
39454
+ sessionRow.signature,
39455
+ args.session_id,
39456
+ finalHash,
39457
+ rows.length,
39458
+ firstCreatedAt
39459
+ );
39460
+ }
39318
39461
  return {
39319
39462
  content: [{
39320
39463
  type: "text",
39321
- text: JSON.stringify({ valid: true, messages: rows.length, chained: chained.length })
39464
+ text: JSON.stringify({ valid: true, messages: rows.length, chained: chained.length, signature_valid })
39322
39465
  }]
39323
39466
  };
39324
39467
  };
@@ -39329,6 +39472,7 @@ var init_verify = __esm({
39329
39472
  init_drizzle_orm();
39330
39473
  init_schema();
39331
39474
  init_hash();
39475
+ init_signing();
39332
39476
  }
39333
39477
  });
39334
39478
 
@@ -39591,7 +39735,9 @@ function createServer(db) {
39591
39735
  {
39592
39736
  title: external_exports.string().describe('Descriptive session title, e.g. "Contract review \u2014 2026-05-08"'),
39593
39737
  ai_tool: external_exports.string().optional().describe('AI tool name: "claude", "cursor", "windsurf", etc.'),
39594
- limit: external_exports.number().int().positive().optional().describe("Number of recent messages to return (default: 10)")
39738
+ limit: external_exports.number().int().positive().optional().describe("Number of recent messages to return (default: 10)"),
39739
+ parent_session_id: external_exports.string().optional().describe("Parent session ID if this is a subagent session spawned by another session"),
39740
+ external_ref: external_exports.string().optional().describe('External ticket/PR reference, e.g. "jira:ENG-123" or "github:org/repo#456"')
39595
39741
  },
39596
39742
  initSession(db)
39597
39743
  );
@@ -39600,7 +39746,9 @@ function createServer(db) {
39600
39746
  "Create a new audit session or resume an existing one by title. Call this at the start of every conversation.",
39601
39747
  {
39602
39748
  title: external_exports.string().describe('Descriptive session title, e.g. "Contract review \u2014 2026-05-08"'),
39603
- ai_tool: external_exports.string().optional().describe('AI tool name: "claude", "cursor", "windsurf", etc.')
39749
+ ai_tool: external_exports.string().optional().describe('AI tool name: "claude", "cursor", "windsurf", etc.'),
39750
+ parent_session_id: external_exports.string().optional().describe("Parent session ID if this is a subagent session"),
39751
+ external_ref: external_exports.string().optional().describe('External ticket/PR reference, e.g. "jira:ENG-123"')
39604
39752
  },
39605
39753
  startSession(db)
39606
39754
  );
@@ -39614,6 +39762,28 @@ function createServer(db) {
39614
39762
  },
39615
39763
  logMessage(db)
39616
39764
  );
39765
+ server.tool(
39766
+ "log_tool_call",
39767
+ "Record an AI tool invocation as a first-class audit event (event_type=tool_call). Call immediately before executing a tool. Returns tool_call_id to pass to log_tool_result.",
39768
+ {
39769
+ session_id: external_exports.string().describe("Session ID"),
39770
+ tool_name: external_exports.string().describe('Name of the tool being called (e.g. "Edit", "Bash", "WebFetch")'),
39771
+ tool_input: external_exports.record(external_exports.unknown()).describe("Tool input parameters as an object"),
39772
+ tool_call_id: external_exports.string().optional().describe("Optional ID to link this call to its result. Generated automatically if omitted.")
39773
+ },
39774
+ logToolCall(db)
39775
+ );
39776
+ server.tool(
39777
+ "log_tool_result",
39778
+ "Record the result of a tool call as a first-class audit event (event_type=tool_result). Call immediately after receiving tool output. Links back to log_tool_call via tool_call_id.",
39779
+ {
39780
+ session_id: external_exports.string().describe("Session ID"),
39781
+ tool_call_id: external_exports.string().describe("tool_call_id returned by log_tool_call"),
39782
+ output: external_exports.string().describe("Tool output (stdout, file content, API response, etc.)"),
39783
+ exit_code: external_exports.number().int().optional().describe("Exit code for shell tools (0 = success)")
39784
+ },
39785
+ logToolResult(db)
39786
+ );
39617
39787
  server.tool(
39618
39788
  "log_exchange",
39619
39789
  "Record a user+assistant exchange from historical or batch imports only. Do NOT use for live conversations \u2014 both timestamps are captured at the same instant with no real gap. For live sessions always call log_message twice: once for the user message, once for the assistant response.",
@@ -39706,24 +39876,24 @@ __export(setup_exports, {
39706
39876
  runSetup: () => runSetup
39707
39877
  });
39708
39878
  function configPath(...parts) {
39709
- return (0, import_path3.join)((0, import_os3.homedir)(), ...parts);
39879
+ return (0, import_path4.join)((0, import_os4.homedir)(), ...parts);
39710
39880
  }
39711
39881
  function readJson(filePath) {
39712
- if (!(0, import_fs3.existsSync)(filePath))
39882
+ if (!(0, import_fs4.existsSync)(filePath))
39713
39883
  return {};
39714
39884
  try {
39715
- return JSON.parse((0, import_fs3.readFileSync)(filePath, "utf8"));
39885
+ return JSON.parse((0, import_fs4.readFileSync)(filePath, "utf8"));
39716
39886
  } catch {
39717
39887
  return {};
39718
39888
  }
39719
39889
  }
39720
39890
  function writeJson(filePath, data) {
39721
- (0, import_fs3.mkdirSync)((0, import_path3.dirname)(filePath), { recursive: true });
39722
- (0, import_fs3.writeFileSync)(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
39891
+ (0, import_fs4.mkdirSync)((0, import_path4.dirname)(filePath), { recursive: true });
39892
+ (0, import_fs4.writeFileSync)(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
39723
39893
  }
39724
39894
  function configureTool(name, filePath) {
39725
- const dir = (0, import_path3.dirname)(filePath);
39726
- if (!(0, import_fs3.existsSync)(dir) && name !== "Claude Code") {
39895
+ const dir = (0, import_path4.dirname)(filePath);
39896
+ if (!(0, import_fs4.existsSync)(dir) && name !== "Claude Code") {
39727
39897
  return { tool: name, status: "skipped" };
39728
39898
  }
39729
39899
  try {
@@ -39744,7 +39914,7 @@ function configureClaudeCode() {
39744
39914
  try {
39745
39915
  (0, import_child_process.execSync)("claude mcp add chron -- npx -y chron-mcp", { stdio: "pipe" });
39746
39916
  } catch {
39747
- const result = configureTool("Claude Code", (0, import_path3.join)((0, import_os3.homedir)(), ".claude", "settings.json"));
39917
+ const result = configureTool("Claude Code", (0, import_path4.join)((0, import_os4.homedir)(), ".claude", "settings.json"));
39748
39918
  if (result.status === "error")
39749
39919
  return result;
39750
39920
  }
@@ -39752,13 +39922,13 @@ function configureClaudeCode() {
39752
39922
  return { tool: "Claude Code", status: "added" };
39753
39923
  }
39754
39924
  function installClaudeCodeHook() {
39755
- const skillSrc = (0, import_path3.join)(__dirname, "..", "skills", "chron.skill.md");
39756
- const skillDst = (0, import_path3.join)((0, import_os3.homedir)(), ".chron", "chron.skill.md");
39757
- if ((0, import_fs3.existsSync)(skillSrc)) {
39758
- (0, import_fs3.mkdirSync)((0, import_path3.dirname)(skillDst), { recursive: true });
39759
- (0, import_fs3.copyFileSync)(skillSrc, skillDst);
39925
+ const skillSrc = (0, import_path4.join)(__dirname, "..", "skills", "chron.skill.md");
39926
+ const skillDst = (0, import_path4.join)((0, import_os4.homedir)(), ".chron", "chron.skill.md");
39927
+ if ((0, import_fs4.existsSync)(skillSrc)) {
39928
+ (0, import_fs4.mkdirSync)((0, import_path4.dirname)(skillDst), { recursive: true });
39929
+ (0, import_fs4.copyFileSync)(skillSrc, skillDst);
39760
39930
  }
39761
- const settingsPath = (0, import_path3.join)((0, import_os3.homedir)(), ".claude", "settings.json");
39931
+ const settingsPath = (0, import_path4.join)((0, import_os4.homedir)(), ".claude", "settings.json");
39762
39932
  const settings = readJson(settingsPath);
39763
39933
  if (!settings.hooks)
39764
39934
  settings.hooks = {};
@@ -39782,13 +39952,13 @@ async function runSetup() {
39782
39952
  }
39783
39953
  return results.filter((r) => r.status !== "skipped");
39784
39954
  }
39785
- var import_fs3, import_os3, import_path3, import_child_process, CHRON_ENTRY, TOOLS;
39955
+ var import_fs4, import_os4, import_path4, import_child_process, CHRON_ENTRY, TOOLS;
39786
39956
  var init_setup = __esm({
39787
39957
  "src/setup.ts"() {
39788
39958
  "use strict";
39789
- import_fs3 = require("fs");
39790
- import_os3 = require("os");
39791
- import_path3 = require("path");
39959
+ import_fs4 = require("fs");
39960
+ import_os4 = require("os");
39961
+ import_path4 = require("path");
39792
39962
  import_child_process = require("child_process");
39793
39963
  CHRON_ENTRY = {
39794
39964
  command: "npx",
@@ -39797,11 +39967,11 @@ var init_setup = __esm({
39797
39967
  TOOLS = [
39798
39968
  {
39799
39969
  name: "Claude Desktop",
39800
- path: process.platform === "win32" ? (0, import_path3.join)(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json") : configPath("Library", "Application Support", "Claude", "claude_desktop_config.json")
39970
+ path: process.platform === "win32" ? (0, import_path4.join)(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json") : configPath("Library", "Application Support", "Claude", "claude_desktop_config.json")
39801
39971
  },
39802
39972
  {
39803
39973
  name: "Cursor",
39804
- path: process.platform === "win32" ? (0, import_path3.join)(process.env.APPDATA ?? "", "Cursor", "User", "globalStorage", "cursor.mcp", "mcp.json") : configPath(".cursor", "mcp.json")
39974
+ path: process.platform === "win32" ? (0, import_path4.join)(process.env.APPDATA ?? "", "Cursor", "User", "globalStorage", "cursor.mcp", "mcp.json") : configPath(".cursor", "mcp.json")
39805
39975
  },
39806
39976
  {
39807
39977
  name: "Windsurf",
@@ -59989,7 +60159,7 @@ var require_view = __commonJS({
59989
60159
  var dirname3 = path.dirname;
59990
60160
  var basename = path.basename;
59991
60161
  var extname = path.extname;
59992
- var join5 = path.join;
60162
+ var join6 = path.join;
59993
60163
  var resolve = path.resolve;
59994
60164
  module2.exports = View2;
59995
60165
  function View2(name, options) {
@@ -60037,12 +60207,12 @@ var require_view = __commonJS({
60037
60207
  };
60038
60208
  View2.prototype.resolve = function resolve2(dir, file) {
60039
60209
  var ext = this.ext;
60040
- var path2 = join5(dir, file);
60210
+ var path2 = join6(dir, file);
60041
60211
  var stat = tryStat(path2);
60042
60212
  if (stat && stat.isFile()) {
60043
60213
  return path2;
60044
60214
  }
60045
- path2 = join5(dir, basename(file, ext), "index" + ext);
60215
+ path2 = join6(dir, basename(file, ext), "index" + ext);
60046
60216
  stat = tryStat(path2);
60047
60217
  if (stat && stat.isFile()) {
60048
60218
  return path2;
@@ -61109,7 +61279,7 @@ var require_send = __commonJS({
61109
61279
  var Stream2 = require("stream");
61110
61280
  var util2 = require("util");
61111
61281
  var extname = path.extname;
61112
- var join5 = path.join;
61282
+ var join6 = path.join;
61113
61283
  var normalize = path.normalize;
61114
61284
  var resolve = path.resolve;
61115
61285
  var sep = path.sep;
@@ -61328,7 +61498,7 @@ var require_send = __commonJS({
61328
61498
  return res;
61329
61499
  }
61330
61500
  parts = path2.split(sep);
61331
- path2 = normalize(join5(root, path2));
61501
+ path2 = normalize(join6(root, path2));
61332
61502
  } else {
61333
61503
  if (UP_PATH_REGEXP.test(path2)) {
61334
61504
  debug('malicious path "%s"', path2);
@@ -61469,7 +61639,7 @@ var require_send = __commonJS({
61469
61639
  return self.onStatError(err);
61470
61640
  return self.error(404);
61471
61641
  }
61472
- var p = join5(path2, self._index[i]);
61642
+ var p = join6(path2, self._index[i]);
61473
61643
  debug('stat "%s"', p);
61474
61644
  fs.stat(p, function(err2, stat) {
61475
61645
  if (err2)
@@ -68917,8 +69087,8 @@ var StdioServerTransport = class {
68917
69087
  };
68918
69088
 
68919
69089
  // src/index.ts
68920
- var import_os4 = require("os");
68921
- var import_path4 = require("path");
69090
+ var import_os5 = require("os");
69091
+ var import_path5 = require("path");
68922
69092
  init_db2();
68923
69093
  init_server3();
68924
69094
  init_package();
@@ -68928,7 +69098,7 @@ if (process.argv[2] === "--version" || process.argv[2] === "-v") {
68928
69098
  process.exit(0);
68929
69099
  }
68930
69100
  async function main() {
68931
- const dbPath = process.env.CHRON_DB_PATH ?? (0, import_path4.join)((0, import_os4.homedir)(), ".chron", "chron.db");
69101
+ const dbPath = process.env.CHRON_DB_PATH ?? (0, import_path5.join)((0, import_os5.homedir)(), ".chron", "chron.db");
68932
69102
  if (process.stdin.isTTY) {
68933
69103
  process.stdout.write(`chron-mcp ${version4}
68934
69104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chron-mcp",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "mcpName": "io.github.sirinivask/chron",
5
5
  "description": "Audit-grade timestamped logs for every AI conversation",
6
6
  "repository": {