chron-mcp 0.1.28 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -16615,6 +16615,14 @@ async function initDb(dbPath3) {
16615
16615
  if (!sessCols.includes("metadata")) {
16616
16616
  await client.execute("ALTER TABLE sessions ADD COLUMN metadata TEXT");
16617
16617
  }
16618
+ const ftsCount = await client.execute("SELECT count(*) as n FROM messages_fts");
16619
+ const msgCount = await client.execute("SELECT count(*) as n FROM messages");
16620
+ if (Number(ftsCount.rows[0][0]) < Number(msgCount.rows[0][0])) {
16621
+ await client.execute("DELETE FROM messages_fts");
16622
+ await client.execute(
16623
+ "INSERT INTO messages_fts(rowid, content, session_id) SELECT rowid, content, session_id FROM messages"
16624
+ );
16625
+ }
16618
16626
  return drizzle(client, { schema: schema_exports });
16619
16627
  }
16620
16628
  var import_os, import_fs, import_path, CREATE_SQL;
@@ -16664,11 +16672,81 @@ var init_db2 = __esm({
16664
16672
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title ON sessions(title)`,
16665
16673
  `CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)`,
16666
16674
  `CREATE INDEX IF NOT EXISTS idx_secrets_detected_session_id ON secrets_detected(session_id)`,
16667
- `CREATE INDEX IF NOT EXISTS idx_secrets_detected_message_id ON secrets_detected(message_id)`
16675
+ `CREATE INDEX IF NOT EXISTS idx_secrets_detected_message_id ON secrets_detected(message_id)`,
16676
+ `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
16677
+ content,
16678
+ session_id UNINDEXED,
16679
+ tokenize = 'porter unicode61'
16680
+ )`,
16681
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ai
16682
+ AFTER INSERT ON messages BEGIN
16683
+ INSERT INTO messages_fts(rowid, content, session_id)
16684
+ VALUES (new.rowid, new.content, new.session_id);
16685
+ END`,
16686
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ad
16687
+ AFTER DELETE ON messages BEGIN
16688
+ DELETE FROM messages_fts WHERE rowid = old.rowid;
16689
+ END`
16668
16690
  ];
16669
16691
  }
16670
16692
  });
16671
16693
 
16694
+ // src/tools/search.ts
16695
+ async function runSearch(db, query, limit = 10) {
16696
+ const client = db.$client;
16697
+ let rows;
16698
+ try {
16699
+ const result = await client.execute({
16700
+ sql: `
16701
+ SELECT
16702
+ messages_fts.session_id AS session_id,
16703
+ snippet(messages_fts, 0, '>>>', '<<<', '\u2026', 24) AS excerpt,
16704
+ s.title AS title,
16705
+ s.ai_tool AS ai_tool,
16706
+ s.updated_at AS updated_at
16707
+ FROM messages_fts
16708
+ JOIN sessions s ON s.id = messages_fts.session_id
16709
+ WHERE messages_fts MATCH ?
16710
+ ORDER BY rank
16711
+ LIMIT ?
16712
+ `,
16713
+ args: [query, limit * 5]
16714
+ // over-fetch so we can group into limit distinct sessions
16715
+ });
16716
+ rows = result.rows;
16717
+ } catch {
16718
+ return [];
16719
+ }
16720
+ const map = /* @__PURE__ */ new Map();
16721
+ for (const r of rows) {
16722
+ const sid = String(r.session_id ?? "");
16723
+ if (!sid)
16724
+ continue;
16725
+ if (!map.has(sid)) {
16726
+ map.set(sid, {
16727
+ session_id: sid,
16728
+ title: String(r.title ?? ""),
16729
+ ai_tool: r.ai_tool != null ? String(r.ai_tool) : null,
16730
+ updated_at: String(r.updated_at ?? ""),
16731
+ match_count: 0,
16732
+ excerpts: []
16733
+ });
16734
+ }
16735
+ const entry = map.get(sid);
16736
+ entry.match_count++;
16737
+ if (entry.excerpts.length < 3)
16738
+ entry.excerpts.push(String(r.excerpt ?? ""));
16739
+ if (map.size >= limit && entry.match_count > 1)
16740
+ break;
16741
+ }
16742
+ return Array.from(map.values()).slice(0, limit);
16743
+ }
16744
+ var init_search = __esm({
16745
+ "src/tools/search.ts"() {
16746
+ "use strict";
16747
+ }
16748
+ });
16749
+
16672
16750
  // src/cli/history.ts
16673
16751
  var history_exports = {};
16674
16752
  __export(history_exports, {
@@ -16788,12 +16866,43 @@ ${msg.content}
16788
16866
  }
16789
16867
  }
16790
16868
  }
16869
+ async function printSearch(query, limit) {
16870
+ const db = await initDb();
16871
+ const results = await runSearch(db, query, limit);
16872
+ if (results.length === 0) {
16873
+ process.stdout.write(`No sessions match: ${query}
16874
+ `);
16875
+ return;
16876
+ }
16877
+ process.stdout.write(`
16878
+ ${BOLD}Search: ${query}${RESET} ${DIM}(${results.length} session${results.length === 1 ? "" : "s"})${RESET}
16879
+
16880
+ `);
16881
+ for (const r of results) {
16882
+ const date = fmtDate(r.updated_at);
16883
+ const prefix = r.session_id.slice(0, 8);
16884
+ const hits = `${r.match_count} hit${r.match_count === 1 ? "" : "s"}`;
16885
+ process.stdout.write(
16886
+ ` ${DIM}${date}${RESET} ${CYAN}${prefix}${RESET} ${BOLD}${hits}${RESET} ${r.title}
16887
+ `
16888
+ );
16889
+ for (const excerpt of r.excerpts) {
16890
+ const highlighted = excerpt.replace(/>>>/g, YELLOW).replace(/<<</g, RESET);
16891
+ process.stdout.write(` ${DIM}\u2026${RESET} ${highlighted}
16892
+ `);
16893
+ }
16894
+ process.stdout.write("\n");
16895
+ }
16896
+ }
16791
16897
  async function runHistory(args2) {
16792
16898
  const limitArg = args2.find((a) => a.startsWith("--limit="));
16793
16899
  const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : 20;
16794
16900
  const refArg = args2.find((a) => a.startsWith("--ref="))?.split("=").slice(1).join("=");
16901
+ const searchArg = args2.find((a) => a.startsWith("--search="))?.split("=").slice(1).join("=");
16795
16902
  const positional = args2.filter((a) => !a.startsWith("--"));
16796
- if (positional.length === 0) {
16903
+ if (searchArg) {
16904
+ await printSearch(searchArg, limit);
16905
+ } else if (positional.length === 0) {
16797
16906
  await printList(limit, refArg);
16798
16907
  } else {
16799
16908
  await printDetail(positional[0]);
@@ -16806,6 +16915,7 @@ var init_history = __esm({
16806
16915
  init_drizzle_orm();
16807
16916
  init_db2();
16808
16917
  init_schema();
16918
+ init_search();
16809
16919
  RESET = "\x1B[0m";
16810
16920
  BOLD = "\x1B[1m";
16811
16921
  DIM = "\x1B[2m";
@@ -17376,7 +17486,7 @@ var require_package = __commonJS({
17376
17486
  "package.json"(exports2, module2) {
17377
17487
  module2.exports = {
17378
17488
  name: "chron-mcp",
17379
- version: "0.1.28",
17489
+ version: "0.1.29",
17380
17490
  mcpName: "io.github.sirinivask/chron",
17381
17491
  description: "Audit-grade timestamped logs for every AI conversation",
17382
17492
  repository: {
@@ -19986,9 +20096,10 @@ Commands:
19986
20096
  import Import conversations from external AI tools
19987
20097
 
19988
20098
  Options (history):
19989
- --limit=<n> Max sessions to show (default: 20)
19990
- --ref=<value> Filter by external_ref prefix (e.g. --ref=jira: or --ref=jira:ENG-123)
19991
- <id-prefix> Show full log for the session with this ID prefix
20099
+ --limit=<n> Max sessions to show (default: 20)
20100
+ --search=<query> Full-text search across all sessions (FTS5: phrases, boolean, prefix*)
20101
+ --ref=<value> Filter by external_ref prefix
20102
+ <id-prefix> Show full log for the session with this ID prefix
19992
20103
 
19993
20104
  Options (report):
19994
20105
  --since=<range> Filter by date: 7d, 30d, or YYYY-MM-DD (default: all time)
package/dist/index.js CHANGED
@@ -22825,6 +22825,14 @@ async function initDb(dbPath) {
22825
22825
  if (!sessCols.includes("metadata")) {
22826
22826
  await client.execute("ALTER TABLE sessions ADD COLUMN metadata TEXT");
22827
22827
  }
22828
+ const ftsCount = await client.execute("SELECT count(*) as n FROM messages_fts");
22829
+ const msgCount = await client.execute("SELECT count(*) as n FROM messages");
22830
+ if (Number(ftsCount.rows[0][0]) < Number(msgCount.rows[0][0])) {
22831
+ await client.execute("DELETE FROM messages_fts");
22832
+ await client.execute(
22833
+ "INSERT INTO messages_fts(rowid, content, session_id) SELECT rowid, content, session_id FROM messages"
22834
+ );
22835
+ }
22828
22836
  return drizzle(client, { schema: schema_exports });
22829
22837
  }
22830
22838
  var import_os, import_fs, import_path, CREATE_SQL;
@@ -22874,7 +22882,21 @@ var init_db2 = __esm({
22874
22882
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title ON sessions(title)`,
22875
22883
  `CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)`,
22876
22884
  `CREATE INDEX IF NOT EXISTS idx_secrets_detected_session_id ON secrets_detected(session_id)`,
22877
- `CREATE INDEX IF NOT EXISTS idx_secrets_detected_message_id ON secrets_detected(message_id)`
22885
+ `CREATE INDEX IF NOT EXISTS idx_secrets_detected_message_id ON secrets_detected(message_id)`,
22886
+ `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
22887
+ content,
22888
+ session_id UNINDEXED,
22889
+ tokenize = 'porter unicode61'
22890
+ )`,
22891
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ai
22892
+ AFTER INSERT ON messages BEGIN
22893
+ INSERT INTO messages_fts(rowid, content, session_id)
22894
+ VALUES (new.rowid, new.content, new.session_id);
22895
+ END`,
22896
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ad
22897
+ AFTER DELETE ON messages BEGIN
22898
+ DELETE FROM messages_fts WHERE rowid = old.rowid;
22899
+ END`
22878
22900
  ];
22879
22901
  }
22880
22902
  });
@@ -38480,7 +38502,7 @@ var init_time = __esm({
38480
38502
  var version4;
38481
38503
  var init_package = __esm({
38482
38504
  "package.json"() {
38483
- version4 = "0.1.28";
38505
+ version4 = "0.1.29";
38484
38506
  }
38485
38507
  });
38486
38508
 
@@ -38839,9 +38861,22 @@ var init_ntp = __esm({
38839
38861
  });
38840
38862
 
38841
38863
  // src/utils/terminal.ts
38842
- function terminalAudit(message) {
38843
- const setting = process.env.CHRON_TERMINAL_AUDIT;
38844
- if (setting && DISABLE_VALUES.has(setting.toLowerCase()))
38864
+ function resolvedLevel() {
38865
+ const raw = (process.env.CHRON_LOG_LEVEL ?? "").toLowerCase();
38866
+ if (raw === "off" || raw === "false" || raw === "0")
38867
+ return "off";
38868
+ if (raw === "summary")
38869
+ return "summary";
38870
+ const legacy = process.env.CHRON_TERMINAL_AUDIT;
38871
+ if (legacy && DISABLE_VALUES.has(legacy.toLowerCase()))
38872
+ return "off";
38873
+ return "verbose";
38874
+ }
38875
+ function terminalAudit(message, level = "message") {
38876
+ const mode = resolvedLevel();
38877
+ if (mode === "off")
38878
+ return;
38879
+ if (mode === "summary" && level === "message")
38845
38880
  return;
38846
38881
  process.stderr.write(`[chron] ${message}
38847
38882
  `);
@@ -38893,7 +38928,7 @@ function startSession(db) {
38893
38928
  });
38894
38929
  attachNtpMetadata(db, id);
38895
38930
  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})`);
38931
+ terminalAudit(`session ${id.slice(0, 8)} started at ${now} (${args.ai_tool ?? "unknown"}: ${args.title})`, "session");
38897
38932
  return {
38898
38933
  content: [{
38899
38934
  type: "text",
@@ -38909,7 +38944,7 @@ function startSession(db) {
38909
38944
  const [countRow] = await db.select({ count: sql`count(*)` }).from(messages).where(eq(messages.session_id, session.id));
38910
38945
  await db.update(sessions).set({ updated_at: now }).where(eq(sessions.id, session.id));
38911
38946
  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})`);
38947
+ terminalAudit(`session ${session.id.slice(0, 8)} resumed at ${now} (${session.ai_tool ?? "unknown"}: ${session.title})`, "session");
38913
38948
  return {
38914
38949
  content: [{
38915
38950
  type: "text",
@@ -38946,7 +38981,7 @@ function initSession(db) {
38946
38981
  created = true;
38947
38982
  ai_tool = args.ai_tool ?? null;
38948
38983
  attachNtpMetadata(db, id);
38949
- terminalAudit(`session ${id.slice(0, 8)} started at ${now} (${ai_tool ?? "unknown"}: ${args.title})`);
38984
+ terminalAudit(`session ${id.slice(0, 8)} started at ${now} (${ai_tool ?? "unknown"}: ${args.title})`, "session");
38950
38985
  } catch (e) {
38951
38986
  const isUnique = e?.message?.includes("UNIQUE constraint failed") || e?.code === "SQLITE_CONSTRAINT_UNIQUE" || e?.cause?.message?.includes("UNIQUE constraint failed") || e?.cause?.extendedCode === "SQLITE_CONSTRAINT_UNIQUE";
38952
38987
  if (!isUnique)
@@ -38957,7 +38992,7 @@ function initSession(db) {
38957
38992
  session_id = session.id;
38958
38993
  created = false;
38959
38994
  ai_tool = session.ai_tool;
38960
- terminalAudit(`session ${session.id.slice(0, 8)} resumed at ${now} (${ai_tool ?? "unknown"}: ${session.title})`);
38995
+ terminalAudit(`session ${session.id.slice(0, 8)} resumed at ${now} (${ai_tool ?? "unknown"}: ${session.title})`, "session");
38961
38996
  }
38962
38997
  emitEvent({ event_type: "session_started", timestamp: now, session: { id_prefix: session_id.slice(0, 8), ai_tool } });
38963
38998
  const limit = args.limit ?? 10;
@@ -39872,6 +39907,73 @@ var init_summary2 = __esm({
39872
39907
  }
39873
39908
  });
39874
39909
 
39910
+ // src/tools/search.ts
39911
+ async function runSearch(db, query, limit = 10) {
39912
+ const client = db.$client;
39913
+ let rows;
39914
+ try {
39915
+ const result = await client.execute({
39916
+ sql: `
39917
+ SELECT
39918
+ messages_fts.session_id AS session_id,
39919
+ snippet(messages_fts, 0, '>>>', '<<<', '\u2026', 24) AS excerpt,
39920
+ s.title AS title,
39921
+ s.ai_tool AS ai_tool,
39922
+ s.updated_at AS updated_at
39923
+ FROM messages_fts
39924
+ JOIN sessions s ON s.id = messages_fts.session_id
39925
+ WHERE messages_fts MATCH ?
39926
+ ORDER BY rank
39927
+ LIMIT ?
39928
+ `,
39929
+ args: [query, limit * 5]
39930
+ // over-fetch so we can group into limit distinct sessions
39931
+ });
39932
+ rows = result.rows;
39933
+ } catch {
39934
+ return [];
39935
+ }
39936
+ const map = /* @__PURE__ */ new Map();
39937
+ for (const r of rows) {
39938
+ const sid = String(r.session_id ?? "");
39939
+ if (!sid)
39940
+ continue;
39941
+ if (!map.has(sid)) {
39942
+ map.set(sid, {
39943
+ session_id: sid,
39944
+ title: String(r.title ?? ""),
39945
+ ai_tool: r.ai_tool != null ? String(r.ai_tool) : null,
39946
+ updated_at: String(r.updated_at ?? ""),
39947
+ match_count: 0,
39948
+ excerpts: []
39949
+ });
39950
+ }
39951
+ const entry = map.get(sid);
39952
+ entry.match_count++;
39953
+ if (entry.excerpts.length < 3)
39954
+ entry.excerpts.push(String(r.excerpt ?? ""));
39955
+ if (map.size >= limit && entry.match_count > 1)
39956
+ break;
39957
+ }
39958
+ return Array.from(map.values()).slice(0, limit);
39959
+ }
39960
+ function searchSessions(db) {
39961
+ return async (args) => {
39962
+ const results = await runSearch(db, args.query, args.limit ?? 10);
39963
+ return {
39964
+ content: [{
39965
+ type: "text",
39966
+ text: JSON.stringify({ query: args.query, total: results.length, results })
39967
+ }]
39968
+ };
39969
+ };
39970
+ }
39971
+ var init_search = __esm({
39972
+ "src/tools/search.ts"() {
39973
+ "use strict";
39974
+ }
39975
+ });
39976
+
39875
39977
  // src/server.ts
39876
39978
  function createServer(db) {
39877
39979
  const server = new McpServer({
@@ -39995,6 +40097,15 @@ function createServer(db) {
39995
40097
  },
39996
40098
  deleteSession(db)
39997
40099
  );
40100
+ server.tool(
40101
+ "search_sessions",
40102
+ 'Full-text search across all logged AI conversations. Returns matching sessions with excerpts. Uses SQLite FTS5 with porter stemming \u2014 supports phrases ("exact match"), boolean (term1 OR term2), and prefix (migrat*).',
40103
+ {
40104
+ query: external_exports.string().describe('FTS5 search query, e.g. "database migration" or "auth bug" or "deploy OR release"'),
40105
+ limit: external_exports.number().int().positive().optional().describe("Max sessions to return (default: 10)")
40106
+ },
40107
+ searchSessions(db)
40108
+ );
39998
40109
  server.tool(
39999
40110
  "summarize_session",
40000
40111
  "Return a structured summary of a session: timeline with latencies, mutations detected, secrets touched, and possible prod-account references. Useful for compliance review and change-control evidence.",
@@ -40025,6 +40136,7 @@ var init_server3 = __esm({
40025
40136
  init_verify();
40026
40137
  init_detect2();
40027
40138
  init_summary2();
40139
+ init_search();
40028
40140
  init_package();
40029
40141
  roleEnum = external_exports.enum(["user", "assistant"]);
40030
40142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chron-mcp",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
4
4
  "mcpName": "io.github.sirinivask/chron",
5
5
  "description": "Audit-grade timestamped logs for every AI conversation",
6
6
  "repository": {