chron-mcp 0.1.18 → 0.1.19

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.19",
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
@@ -16539,7 +16539,9 @@ 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")
16543
16545
  });
16544
16546
  messages = sqliteTable("messages", {
16545
16547
  id: text("id").primaryKey(),
@@ -16548,7 +16550,9 @@ var init_schema = __esm({
16548
16550
  content: text("content").notNull(),
16549
16551
  created_at: text("created_at").notNull(),
16550
16552
  prev_hash: text("prev_hash"),
16551
- content_hash: text("content_hash")
16553
+ content_hash: text("content_hash"),
16554
+ // NULL = pre-migration row (treated as 'message'); set for all new rows
16555
+ event_type: text("event_type")
16552
16556
  });
16553
16557
  secrets_detected = sqliteTable("secrets_detected", {
16554
16558
  id: text("id").primaryKey(),
@@ -16579,14 +16583,25 @@ async function initDb(dbPath2) {
16579
16583
  for (const sql2 of CREATE_SQL) {
16580
16584
  await client.execute(sql2);
16581
16585
  }
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")) {
16586
+ const msgInfo = await client.execute("PRAGMA table_info(messages)");
16587
+ const msgCols = msgInfo.rows.map((r) => r[1]);
16588
+ if (!msgCols.includes("prev_hash")) {
16585
16589
  await client.execute("ALTER TABLE messages ADD COLUMN prev_hash TEXT");
16586
16590
  }
16587
- if (!columns.includes("content_hash")) {
16591
+ if (!msgCols.includes("content_hash")) {
16588
16592
  await client.execute("ALTER TABLE messages ADD COLUMN content_hash TEXT");
16589
16593
  }
16594
+ if (!msgCols.includes("event_type")) {
16595
+ await client.execute("ALTER TABLE messages ADD COLUMN event_type TEXT");
16596
+ }
16597
+ const sessInfo = await client.execute("PRAGMA table_info(sessions)");
16598
+ const sessCols = sessInfo.rows.map((r) => r[1]);
16599
+ if (!sessCols.includes("parent_session_id")) {
16600
+ await client.execute("ALTER TABLE sessions ADD COLUMN parent_session_id TEXT");
16601
+ }
16602
+ if (!sessCols.includes("external_ref")) {
16603
+ await client.execute("ALTER TABLE sessions ADD COLUMN external_ref TEXT");
16604
+ }
16590
16605
  return drizzle(client, { schema: schema_exports });
16591
16606
  }
16592
16607
  var import_os, import_fs, import_path, CREATE_SQL;
@@ -16605,7 +16620,9 @@ var init_db2 = __esm({
16605
16620
  title TEXT NOT NULL UNIQUE,
16606
16621
  ai_tool TEXT,
16607
16622
  created_at TEXT NOT NULL,
16608
- updated_at TEXT NOT NULL
16623
+ updated_at TEXT NOT NULL,
16624
+ parent_session_id TEXT,
16625
+ external_ref TEXT
16609
16626
  )`,
16610
16627
  `CREATE TABLE IF NOT EXISTS messages (
16611
16628
  id TEXT PRIMARY KEY,
@@ -16615,6 +16632,7 @@ var init_db2 = __esm({
16615
16632
  created_at TEXT NOT NULL,
16616
16633
  prev_hash TEXT,
16617
16634
  content_hash TEXT,
16635
+ event_type TEXT,
16618
16636
  FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
16619
16637
  )`,
16620
16638
  `CREATE TABLE IF NOT EXISTS secrets_detected (
@@ -17401,7 +17419,7 @@ var require_package = __commonJS({
17401
17419
  "package.json"(exports2, module2) {
17402
17420
  module2.exports = {
17403
17421
  name: "chron-mcp",
17404
- version: "0.1.18",
17422
+ version: "0.1.19",
17405
17423
  mcpName: "io.github.sirinivask/chron",
17406
17424
  description: "Audit-grade timestamped logs for every AI conversation",
17407
17425
  repository: {
package/dist/index.js CHANGED
@@ -22749,7 +22749,9 @@ 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")
22753
22755
  });
22754
22756
  messages = sqliteTable("messages", {
22755
22757
  id: text("id").primaryKey(),
@@ -22758,7 +22760,9 @@ var init_schema = __esm({
22758
22760
  content: text("content").notNull(),
22759
22761
  created_at: text("created_at").notNull(),
22760
22762
  prev_hash: text("prev_hash"),
22761
- content_hash: text("content_hash")
22763
+ content_hash: text("content_hash"),
22764
+ // NULL = pre-migration row (treated as 'message'); set for all new rows
22765
+ event_type: text("event_type")
22762
22766
  });
22763
22767
  secrets_detected = sqliteTable("secrets_detected", {
22764
22768
  id: text("id").primaryKey(),
@@ -22789,14 +22793,25 @@ async function initDb(dbPath) {
22789
22793
  for (const sql2 of CREATE_SQL) {
22790
22794
  await client.execute(sql2);
22791
22795
  }
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")) {
22796
+ const msgInfo = await client.execute("PRAGMA table_info(messages)");
22797
+ const msgCols = msgInfo.rows.map((r) => r[1]);
22798
+ if (!msgCols.includes("prev_hash")) {
22795
22799
  await client.execute("ALTER TABLE messages ADD COLUMN prev_hash TEXT");
22796
22800
  }
22797
- if (!columns.includes("content_hash")) {
22801
+ if (!msgCols.includes("content_hash")) {
22798
22802
  await client.execute("ALTER TABLE messages ADD COLUMN content_hash TEXT");
22799
22803
  }
22804
+ if (!msgCols.includes("event_type")) {
22805
+ await client.execute("ALTER TABLE messages ADD COLUMN event_type TEXT");
22806
+ }
22807
+ const sessInfo = await client.execute("PRAGMA table_info(sessions)");
22808
+ const sessCols = sessInfo.rows.map((r) => r[1]);
22809
+ if (!sessCols.includes("parent_session_id")) {
22810
+ await client.execute("ALTER TABLE sessions ADD COLUMN parent_session_id TEXT");
22811
+ }
22812
+ if (!sessCols.includes("external_ref")) {
22813
+ await client.execute("ALTER TABLE sessions ADD COLUMN external_ref TEXT");
22814
+ }
22800
22815
  return drizzle(client, { schema: schema_exports });
22801
22816
  }
22802
22817
  var import_os, import_fs, import_path, CREATE_SQL;
@@ -22815,7 +22830,9 @@ var init_db2 = __esm({
22815
22830
  title TEXT NOT NULL UNIQUE,
22816
22831
  ai_tool TEXT,
22817
22832
  created_at TEXT NOT NULL,
22818
- updated_at TEXT NOT NULL
22833
+ updated_at TEXT NOT NULL,
22834
+ parent_session_id TEXT,
22835
+ external_ref TEXT
22819
22836
  )`,
22820
22837
  `CREATE TABLE IF NOT EXISTS messages (
22821
22838
  id TEXT PRIMARY KEY,
@@ -22825,6 +22842,7 @@ var init_db2 = __esm({
22825
22842
  created_at TEXT NOT NULL,
22826
22843
  prev_hash TEXT,
22827
22844
  content_hash TEXT,
22845
+ event_type TEXT,
22828
22846
  FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
22829
22847
  )`,
22830
22848
  `CREATE TABLE IF NOT EXISTS secrets_detected (
@@ -38446,7 +38464,7 @@ var init_time = __esm({
38446
38464
  var version4;
38447
38465
  var init_package = __esm({
38448
38466
  "package.json"() {
38449
- version4 = "0.1.18";
38467
+ version4 = "0.1.19";
38450
38468
  }
38451
38469
  });
38452
38470
 
@@ -38701,7 +38719,9 @@ function startSession(db) {
38701
38719
  title: args.title,
38702
38720
  ai_tool: args.ai_tool ?? null,
38703
38721
  created_at: now,
38704
- updated_at: now
38722
+ updated_at: now,
38723
+ parent_session_id: args.parent_session_id ?? null,
38724
+ external_ref: args.external_ref ?? null
38705
38725
  });
38706
38726
  emitEvent({ event_type: "session_started", timestamp: now, session: { id_prefix: id.slice(0, 8), ai_tool: args.ai_tool ?? null } });
38707
38727
  return {
@@ -38741,7 +38761,9 @@ function initSession(db) {
38741
38761
  title: args.title,
38742
38762
  ai_tool: args.ai_tool ?? null,
38743
38763
  created_at: now,
38744
- updated_at: now
38764
+ updated_at: now,
38765
+ parent_session_id: args.parent_session_id ?? null,
38766
+ external_ref: args.external_ref ?? null
38745
38767
  });
38746
38768
  session_id = id;
38747
38769
  created = true;
@@ -38846,8 +38868,10 @@ var init_sessions = __esm({
38846
38868
  });
38847
38869
 
38848
38870
  // 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");
38871
+ function computeContentHash(sessionId, role, content, createdAt, prevHash, eventType) {
38872
+ const base = `${sessionId}|${role}|${content}|${createdAt}|${prevHash ?? ""}`;
38873
+ const input = eventType != null ? `${base}|${eventType}` : base;
38874
+ return (0, import_crypto4.createHash)("sha256").update(input).digest("hex");
38851
38875
  }
38852
38876
  var import_crypto4;
38853
38877
  var init_hash = __esm({
@@ -39150,32 +39174,37 @@ async function writeDetections(db, session_id, message_id, content) {
39150
39174
  function isFkError(e) {
39151
39175
  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
39176
  }
39177
+ async function insertMessage(db, session_id, role, content, event_type) {
39178
+ return db.transaction(async (tx) => {
39179
+ 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);
39180
+ const ts = localISOString();
39181
+ const msgId = v4_default();
39182
+ const prevHash = last[0]?.content_hash ?? null;
39183
+ const hash = computeContentHash(session_id, role, content, ts, prevHash, event_type);
39184
+ await tx.insert(messages).values({
39185
+ id: msgId,
39186
+ session_id,
39187
+ role,
39188
+ content,
39189
+ created_at: ts,
39190
+ prev_hash: prevHash,
39191
+ content_hash: hash,
39192
+ event_type
39193
+ });
39194
+ await tx.update(sessions).set({ updated_at: ts }).where(eq(sessions.id, session_id));
39195
+ const [sessionRow] = await tx.select({ ai_tool: sessions.ai_tool }).from(sessions).where(eq(sessions.id, session_id)).limit(1);
39196
+ return { id: msgId, now: ts, contentHash: hash, ai_tool: sessionRow?.ai_tool ?? null };
39197
+ });
39198
+ }
39153
39199
  function logMessage(db) {
39154
39200
  return async (args) => {
39201
+ const eventType = args.event_type ?? "message";
39155
39202
  let id;
39156
39203
  let now;
39157
39204
  let contentHash;
39158
39205
  let sessionAiTool;
39159
39206
  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
- });
39207
+ const result = await insertMessage(db, args.session_id, args.role, args.content, eventType);
39179
39208
  id = result.id;
39180
39209
  now = result.now;
39181
39210
  contentHash = result.contentHash;
@@ -39187,7 +39216,7 @@ function logMessage(db) {
39187
39216
  throw e;
39188
39217
  }
39189
39218
  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") {
39219
+ if (args.role === "user" && eventType === "message") {
39191
39220
  setImmediate(() => writeDetections(db, args.session_id, id, args.content).catch(() => void 0));
39192
39221
  }
39193
39222
  return {
@@ -39195,15 +39224,45 @@ function logMessage(db) {
39195
39224
  };
39196
39225
  };
39197
39226
  }
39227
+ function logToolCall(db) {
39228
+ return async (args) => {
39229
+ const toolCallId = args.tool_call_id ?? v4_default();
39230
+ const content = JSON.stringify({ tool_name: args.tool_name, input: args.tool_input, tool_call_id: toolCallId });
39231
+ let result;
39232
+ try {
39233
+ result = await insertMessage(db, args.session_id, "assistant", content, "tool_call");
39234
+ } catch (e) {
39235
+ if (isFkError(e)) {
39236
+ return { content: [{ type: "text", text: `Session not found: ${args.session_id}` }], isError: true };
39237
+ }
39238
+ throw e;
39239
+ }
39240
+ return {
39241
+ content: [{ type: "text", text: JSON.stringify({ message_id: result.id, tool_call_id: toolCallId, created_at: result.now, content_hash: result.contentHash }) }]
39242
+ };
39243
+ };
39244
+ }
39245
+ function logToolResult(db) {
39246
+ return async (args) => {
39247
+ const content = JSON.stringify({ tool_call_id: args.tool_call_id, output: args.output, exit_code: args.exit_code ?? null });
39248
+ let result;
39249
+ try {
39250
+ result = await insertMessage(db, args.session_id, "user", content, "tool_result");
39251
+ } catch (e) {
39252
+ if (isFkError(e)) {
39253
+ return { content: [{ type: "text", text: `Session not found: ${args.session_id}` }], isError: true };
39254
+ }
39255
+ throw e;
39256
+ }
39257
+ return {
39258
+ 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 }) }]
39259
+ };
39260
+ };
39261
+ }
39198
39262
  function logExchange(db) {
39199
39263
  return async (args) => {
39200
- let userId;
39201
- let assistantId;
39202
- let userNow;
39203
- let assistantNow;
39204
- let userHash;
39205
- let assistantHash;
39206
- let sessionAiTool;
39264
+ let userId, assistantId, userNow, assistantNow;
39265
+ let userHash, assistantHash, sessionAiTool;
39207
39266
  try {
39208
39267
  const result = await db.transaction(async (tx) => {
39209
39268
  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 +39271,8 @@ function logExchange(db) {
39212
39271
  const aNow = localISOString();
39213
39272
  const aId = v4_default();
39214
39273
  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);
39274
+ const uHash = computeContentHash(args.session_id, "user", args.user_content, uNow, prevHash, "message");
39275
+ const aHash = computeContentHash(args.session_id, "assistant", args.assistant_content, aNow, uHash, "message");
39217
39276
  await tx.insert(messages).values({
39218
39277
  id: uId,
39219
39278
  session_id: args.session_id,
@@ -39221,7 +39280,8 @@ function logExchange(db) {
39221
39280
  content: args.user_content,
39222
39281
  created_at: uNow,
39223
39282
  prev_hash: prevHash,
39224
- content_hash: uHash
39283
+ content_hash: uHash,
39284
+ event_type: "message"
39225
39285
  });
39226
39286
  await tx.insert(messages).values({
39227
39287
  id: aId,
@@ -39230,7 +39290,8 @@ function logExchange(db) {
39230
39290
  content: args.assistant_content,
39231
39291
  created_at: aNow,
39232
39292
  prev_hash: uHash,
39233
- content_hash: aHash
39293
+ content_hash: aHash,
39294
+ event_type: "message"
39234
39295
  });
39235
39296
  await tx.update(sessions).set({ updated_at: aNow }).where(eq(sessions.id, args.session_id));
39236
39297
  const [sessionRow] = await tx.select({ ai_tool: sessions.ai_tool }).from(sessions).where(eq(sessions.id, args.session_id)).limit(1);
@@ -39305,7 +39366,7 @@ function verifySession(db) {
39305
39366
  }]
39306
39367
  };
39307
39368
  }
39308
- const expected = computeContentHash(row.session_id, row.role, row.content, row.created_at, row.prev_hash);
39369
+ const expected = computeContentHash(row.session_id, row.role, row.content, row.created_at, row.prev_hash, row.event_type ?? void 0);
39309
39370
  if (row.content_hash !== expected) {
39310
39371
  return {
39311
39372
  content: [{
@@ -39591,7 +39652,9 @@ function createServer(db) {
39591
39652
  {
39592
39653
  title: external_exports.string().describe('Descriptive session title, e.g. "Contract review \u2014 2026-05-08"'),
39593
39654
  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)")
39655
+ limit: external_exports.number().int().positive().optional().describe("Number of recent messages to return (default: 10)"),
39656
+ parent_session_id: external_exports.string().optional().describe("Parent session ID if this is a subagent session spawned by another session"),
39657
+ external_ref: external_exports.string().optional().describe('External ticket/PR reference, e.g. "jira:ENG-123" or "github:org/repo#456"')
39595
39658
  },
39596
39659
  initSession(db)
39597
39660
  );
@@ -39600,7 +39663,9 @@ function createServer(db) {
39600
39663
  "Create a new audit session or resume an existing one by title. Call this at the start of every conversation.",
39601
39664
  {
39602
39665
  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.')
39666
+ ai_tool: external_exports.string().optional().describe('AI tool name: "claude", "cursor", "windsurf", etc.'),
39667
+ parent_session_id: external_exports.string().optional().describe("Parent session ID if this is a subagent session"),
39668
+ external_ref: external_exports.string().optional().describe('External ticket/PR reference, e.g. "jira:ENG-123"')
39604
39669
  },
39605
39670
  startSession(db)
39606
39671
  );
@@ -39614,6 +39679,28 @@ function createServer(db) {
39614
39679
  },
39615
39680
  logMessage(db)
39616
39681
  );
39682
+ server.tool(
39683
+ "log_tool_call",
39684
+ "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.",
39685
+ {
39686
+ session_id: external_exports.string().describe("Session ID"),
39687
+ tool_name: external_exports.string().describe('Name of the tool being called (e.g. "Edit", "Bash", "WebFetch")'),
39688
+ tool_input: external_exports.record(external_exports.unknown()).describe("Tool input parameters as an object"),
39689
+ tool_call_id: external_exports.string().optional().describe("Optional ID to link this call to its result. Generated automatically if omitted.")
39690
+ },
39691
+ logToolCall(db)
39692
+ );
39693
+ server.tool(
39694
+ "log_tool_result",
39695
+ "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.",
39696
+ {
39697
+ session_id: external_exports.string().describe("Session ID"),
39698
+ tool_call_id: external_exports.string().describe("tool_call_id returned by log_tool_call"),
39699
+ output: external_exports.string().describe("Tool output (stdout, file content, API response, etc.)"),
39700
+ exit_code: external_exports.number().int().optional().describe("Exit code for shell tools (0 = success)")
39701
+ },
39702
+ logToolResult(db)
39703
+ );
39617
39704
  server.tool(
39618
39705
  "log_exchange",
39619
39706
  "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.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chron-mcp",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "mcpName": "io.github.sirinivask/chron",
5
5
  "description": "Audit-grade timestamped logs for every AI conversation",
6
6
  "repository": {