chron-mcp 0.1.24 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  <div align="center">
2
2
  <img src="assets/chron-icon-png-transperent.png" alt="Chron" width="96" />
3
3
  <h1>Chron</h1>
4
- <p>A timestamped audit trail for every AI conversation stored locally, owned by you.</p>
4
+ <p>Local-first, tamper-evident audit trails for AI conversations, tool calls, code changes, and secrets.</p>
5
5
  </div>
6
6
 
7
- AI tools show when you sent a message. Chron logs when the AI responded too — and keeps a permanent, queryable record of every exchange across every tool you use.
7
+ AI tools show when you sent a message. Chron logs when the AI responded too — and keeps a permanent, queryable, tamper-evident record of the AI work happening across your tools.
8
8
 
9
9
  Works with Claude Desktop, Claude Code, Cursor, Windsurf, and any MCP-compatible AI tool.
10
10
 
@@ -16,9 +16,29 @@ AI tools produce no audit trail by default. You cannot answer:
16
16
  - What did the AI say, and when exactly?
17
17
  - How long did the AI take to respond?
18
18
  - What was the full conversation that produced this output?
19
+ - Which tool calls, command results, and code diffs happened during the session?
20
+ - Did a secret or credential get pasted into an AI prompt?
21
+ - Has this audit record been edited after the fact?
19
22
  - What did I ask Claude last week about this codebase?
20
23
 
21
- Chron fixes that. Every exchange is logged with a precise local datetime (including timezone offset) to a SQLite file you own. No cloud, no vendor lock-in, no data leaving your machine.
24
+ Chron fixes that. Every exchange is logged with a precise local datetime (including timezone offset) to a SQLite file you own. It can hash-chain every event, sign sessions with Ed25519, detect secrets/PII, produce SOC 2 evidence, and stream metadata-only events to your SIEM. No cloud, no vendor lock-in, no message content leaving your machine.
25
+
26
+ ---
27
+
28
+ ## What Chron captures
29
+
30
+ Chron stores a local audit trail of:
31
+
32
+ - User and assistant messages
33
+ - Tool calls and tool results
34
+ - Code changes with file path, operation, and unified diff
35
+ - Session start/resume metadata, including parent session and external refs
36
+ - Secret/PII detections with masked values
37
+ - Hash-chain integrity data
38
+ - Optional Ed25519 session signatures
39
+ - Optional NTP clock attestation metadata
40
+
41
+ Chron's SIEM integrations send only metadata: session starts, role-only message events, and masked secret detections. Message content and raw secret values stay local.
22
42
 
23
43
  ---
24
44
 
@@ -49,6 +69,18 @@ npm install -g chron-mcp
49
69
 
50
70
  First run creates `~/.chron/chron.db` automatically. No database setup, no env vars, no migrations.
51
71
 
72
+ Check your setup:
73
+
74
+ ```bash
75
+ chron doctor
76
+ ```
77
+
78
+ For CI or automation:
79
+
80
+ ```bash
81
+ chron doctor --json
82
+ ```
83
+
52
84
  ---
53
85
 
54
86
  ## CLI
@@ -65,18 +97,57 @@ Commands:
65
97
  secrets List detected secrets across sessions
66
98
  settings View current configuration
67
99
  connect Connect to a SIEM integration (crowdstrike, sentinel, splunk)
100
+ summary Structured summary of a session (timeline, mutations, secrets)
101
+ sign Sign a session with its Ed25519 key — produces a .chron.sig file
102
+ verify Verify a session's hash chain and Ed25519 signature
103
+ prune Delete sessions older than a retention cutoff
104
+ doctor Check your Chron setup — Node version, DB, MCP configs, SIEM
68
105
 
69
106
  Options (history):
70
107
  --limit=<n> Max sessions to show (default: 20)
108
+ --ref=<value> Filter by external_ref prefix (e.g. --ref=jira:ENG-123)
71
109
  <id-prefix> Show full log for the session with this ID prefix
72
110
 
73
111
  Options (report):
74
- --since=<range> Filter by date: 7d, 30d, or YYYY-MM-DD (default: all time)
75
-
76
- Options (export / secrets):
77
- <id-prefix> Scope to a single session
112
+ --since=<range> Filter by date: 7d, 30d, or YYYY-MM-DD (default: all time)
113
+ --format=soc2 Generate SOC 2 HTML evidence package
114
+ --output=<file> Output file for --format=soc2
115
+
116
+ Options (export):
117
+ <id-prefix> Markdown export for a single session
118
+ --signed Tamper-evident bundle (JSONL + manifest + Ed25519 sig)
119
+ --session=<id> Filter bundle to a single session
120
+ --since=<range> Filter bundle by date: 7d, 30d, or YYYY-MM-DD
121
+ --output=<file> Output path (default: bundle.chron.tar.gz)
122
+
123
+ Options (verify):
124
+ <id-prefix> Verify a session's hash chain + Ed25519 signature
125
+ --bundle=<file> Verify a signed bundle offline (no DB needed)
126
+
127
+ Options (prune):
128
+ --older-than=<n>d Cutoff in days (falls back to retention_days in config)
129
+ --dry-run Show what would be deleted without deleting
130
+ --confirm Required flag to actually delete
131
+
132
+ Options (doctor):
133
+ --json Machine-readable JSON output
78
134
  ```
79
135
 
136
+ ### `chron doctor`
137
+
138
+ `chron doctor` validates the pieces that make Chron useful in real life:
139
+
140
+ - Node.js version (v18+ required)
141
+ - Local Chron version vs npm latest
142
+ - `npx chron-mcp --version`
143
+ - DB directory and key directory write access
144
+ - Claude Desktop, Claude Code, Cursor, and Windsurf MCP config presence
145
+ - Whether Chron is configured in each MCP client
146
+ - Optional HTTP mode health check on `/health`
147
+ - Splunk, Sentinel, and LogScale configuration via env vars or `~/.chron/config.json`
148
+
149
+ Warnings for optional integrations do not fail the command. Real failures exit with code `1`.
150
+
80
151
  ---
81
152
 
82
153
  ## What it logs
@@ -186,21 +257,26 @@ Chron ships with `skills/chron.skill.md` — a plain-text instruction file that
186
257
  | `init_session` | Initialize or resume a session — returns session_id, message count, and recent messages in one call |
187
258
  | `start_session` | Create a new named audit session (legacy — prefer `init_session`) |
188
259
  | `log_message` | Record a single message with the current local datetime |
260
+ | `log_tool_call` | Record an AI tool invocation as an audit event |
261
+ | `log_tool_result` | Record tool output and link it to a tool call |
262
+ | `log_code_change` | Record a file edit with operation and unified diff |
189
263
  | `log_exchange` | Log a user/assistant pair atomically (for batch imports) |
190
264
  | `list_sessions` | List all sessions ordered by most recently active |
191
265
  | `get_session_history` | Retrieve the full timestamped log for a session |
192
266
  | `verify_session` | Verify the tamper-evident hash chain — detects any post-log edits |
193
267
  | `scan_prompt` | Scan text for secrets (API keys, credentials) before logging — returns masked detections |
194
268
  | `rehydrate_response` | Restore redacted placeholders in an assistant response back to their original values |
269
+ | `delete_session` | Delete a session and cascade its messages/secrets |
270
+ | `summarize_session` | Return a timeline summary with latency, mutations, secrets, and prod references |
195
271
 
196
272
  ---
197
273
 
198
- ## Tamper-evident hash chaining
274
+ ## Integrity and signing
199
275
 
200
- Every message is linked to the previous one via a SHA-256 chain:
276
+ Every new audit event is linked to the previous one via a SHA-256 chain:
201
277
 
202
278
  ```
203
- content_hash = SHA256(session_id | role | content | created_at | prev_hash)
279
+ content_hash = SHA256(session_id | role | content | created_at | prev_hash | event_type)
204
280
  ```
205
281
 
206
282
  `verify_session` walks the chain and returns:
@@ -209,6 +285,58 @@ content_hash = SHA256(session_id | role | content | created_at | prev_hash)
209
285
 
210
286
  This turns your local log into a verifiable audit artifact. Any edit to a stored message — content, timestamp, or role — breaks the chain and is detected immediately.
211
287
 
288
+ Chron also generates an Ed25519 keypair per new session when possible. The private key stays under `~/.chron/keys`. Use:
289
+
290
+ ```bash
291
+ chron sign <session-id-prefix>
292
+ chron verify <session-id-prefix>
293
+ ```
294
+
295
+ For portable evidence:
296
+
297
+ ```bash
298
+ chron export --signed --session=<session-id-prefix> --output=evidence.chron.tar.gz
299
+ chron verify --bundle=evidence.chron.tar.gz
300
+ ```
301
+
302
+ `chron verify` also reports NTP clock attestation metadata for sessions created with clock-check support.
303
+
304
+ ---
305
+
306
+ ## Secrets and PII detection
307
+
308
+ Chron can detect and mask:
309
+
310
+ - API keys: OpenAI, Anthropic, AWS, Google, GitHub, Slack, Stripe, SendGrid, HuggingFace
311
+ - Private keys, JWTs, URL credentials, password assignments, env-style secrets
312
+ - Credit cards, IBANs, SSNs, DOBs, passports
313
+ - Email addresses, US/E.164 phone numbers, internal RFC-1918 IPs
314
+
315
+ Use:
316
+
317
+ ```bash
318
+ chron secrets
319
+ chron secrets <session-id-prefix>
320
+ ```
321
+
322
+ MCP tools can also call `scan_prompt` before sending content to an AI tool. Redaction uses `$CHRON_*` placeholders and `rehydrate_response` can restore values from the returned token map.
323
+
324
+ ---
325
+
326
+ ## Reports and evidence
327
+
328
+ ```bash
329
+ chron history
330
+ chron summary <session-id-prefix>
331
+ chron report --since=30d
332
+ chron report --format=soc2 --output=soc2-report.html
333
+ chron export <session-id-prefix>
334
+ chron export --signed --since=30d --output=bundle.chron.tar.gz
335
+ chron prune --older-than=90d --dry-run
336
+ ```
337
+
338
+ The SOC 2 report and signed bundles are designed for audit review, legal hold, and incident reconstruction without sending conversation content to Chron infrastructure.
339
+
212
340
  ---
213
341
 
214
342
  ## CrowdStrike LogScale integration
@@ -484,6 +612,7 @@ All six must be set for events to flow. Token refresh is automatic (1-hour Azure
484
612
  | `CHRON_SENTINEL_STREAM` | _(none)_ | Stream name (e.g. `Custom-ChronEvents_CL`) |
485
613
  | `CHRON_SPLUNK_URL` | _(none)_ | Splunk HEC base URL (enables Splunk integration) |
486
614
  | `CHRON_SPLUNK_TOKEN` | _(none)_ | Splunk HEC ingest token |
615
+ | `CHRON_SPLUNK_INSECURE` | _(none)_ | Set to `1` to skip TLS verification for local Splunk/self-signed certs |
487
616
  | `CHRON_RELAY_URL` | _(none)_ | Generic relay endpoint (any SIEM or webhook) |
488
617
  | `CHRON_RELAY_TOKEN` | _(none)_ | Bearer token for generic relay |
489
618
 
@@ -73,7 +73,7 @@ The dashboard includes five widgets:
73
73
  - **AI Tool Usage** — breakdown by tool (Claude, Cursor, Windsurf, etc.)
74
74
  - **Secrets Detected by Type** — credential exposures over time
75
75
  - **Sessions by Developer** — per-machine session count with AI tool
76
- - **Credential Exposures** — full table of api_key, jwt, aws_key detections for SOC review
76
+ - **Credential Exposures** — full table of API keys, tokens, passwords, and private-key detections for SOC review
77
77
 
78
78
  ## Step 6 — Set up the API key alert (optional)
79
79
 
@@ -81,7 +81,7 @@ In LogScale, create a **Scheduled Search** using `api-key-alert.lql`:
81
81
 
82
82
  ```
83
83
  event_type = "secret_detected"
84
- | detection_type in ["api_key", "aws_key", "gcp_key", "azure_key", "private_key", "jwt"]
84
+ | detection_type in ["private_key", "aws_access_key", "anthropic_api_key", "openai_api_key", "google_api_key", "github_token", "slack_token", "stripe_key", "sendgrid_key", "huggingface_token", "jwt", "url_credentials", "password", "credential_pair"]
85
85
  | table([#host, ai_tool, detection_type, masked_value, session_id_prefix, @timestamp])
86
86
  ```
87
87
 
@@ -104,7 +104,7 @@ All events arrive in LogScale with:
104
104
  | `os` | All events: `darwin`, `linux`, `win32` |
105
105
  | `chron_version` | All events |
106
106
  | `role` | `message_logged` only: `user` or `assistant` |
107
- | `detection_type` | `secret_detected` only: `api_key`, `aws_key`, `jwt`, etc. |
107
+ | `detection_type` | `secret_detected` only: `openai_api_key`, `aws_access_key`, `github_token`, `jwt`, etc. |
108
108
  | `masked_value` | `secret_detected` only: e.g. `AKIA****F3K2` |
109
109
 
110
110
  **What is never transmitted:** message content, actual secret values, file paths, or any user-typed text.
@@ -1,5 +1,5 @@
1
1
  // Alert: API key or credential sent to AI tool
2
2
  // Use as a Scheduled Search alert — notify SOC on any match
3
3
  event_type = "secret_detected"
4
- | detection_type in ["api_key", "aws_key", "gcp_key", "azure_key", "private_key", "jwt"]
4
+ | detection_type in ["private_key", "aws_access_key", "anthropic_api_key", "openai_api_key", "google_api_key", "github_token", "slack_token", "stripe_key", "sendgrid_key", "huggingface_token", "jwt", "url_credentials", "password", "credential_pair"]
5
5
  | table([#host, ai_tool, detection_type, masked_value, session_id_prefix, @timestamp])
@@ -62,5 +62,5 @@ widgets:
62
62
  height: 4
63
63
  query: |
64
64
  event_type = "secret_detected"
65
- | detection_type in ["api_key", "aws_key", "gcp_key", "azure_key", "private_key", "jwt"]
65
+ | detection_type in ["private_key", "aws_access_key", "anthropic_api_key", "openai_api_key", "google_api_key", "github_token", "slack_token", "stripe_key", "sendgrid_key", "huggingface_token", "jwt", "url_credentials", "password", "credential_pair"]
66
66
  | table([#host, ai_tool, detection_type, masked_value, session_id_prefix, @timestamp])
@@ -113,7 +113,7 @@ All events arrive in `ChronEvents_CL` with:
113
113
  | `ChronVersion` | All events |
114
114
  | `Computer` | All events: developer's machine hostname |
115
115
  | `Role` | `message_logged` only: `user` or `assistant` |
116
- | `DetectionType` | `secret_detected` only: `api_key`, `aws_key`, `jwt`, etc. |
116
+ | `DetectionType` | `secret_detected` only: `openai_api_key`, `aws_access_key`, `github_token`, `jwt`, etc. |
117
117
  | `MaskedValue` | `secret_detected` only: e.g. `AKIA****F3K2` |
118
118
 
119
119
  **What is never transmitted:** message content, actual secret values, file paths, or any user-typed text.
@@ -3,5 +3,5 @@
3
3
  ChronEvents_CL
4
4
  | where TimeGenerated > ago(1h)
5
5
  | where EventType == "secret_detected"
6
- | where DetectionType in ("api_key", "aws_key", "gcp_key", "azure_key", "private_key", "jwt")
6
+ | where DetectionType in ("private_key", "aws_access_key", "anthropic_api_key", "openai_api_key", "google_api_key", "github_token", "slack_token", "stripe_key", "sendgrid_key", "huggingface_token", "jwt", "url_credentials", "password", "credential_pair")
7
7
  | project TimeGenerated, Computer, AiTool, DetectionType, MaskedValue, SessionIdPrefix
@@ -2,6 +2,6 @@
2
2
  ChronEvents_CL
3
3
  | where TimeGenerated > ago(30d)
4
4
  | where EventType == "secret_detected"
5
- | where DetectionType in ("api_key", "aws_key", "gcp_key", "azure_key", "private_key", "jwt")
5
+ | where DetectionType in ("private_key", "aws_access_key", "anthropic_api_key", "openai_api_key", "google_api_key", "github_token", "slack_token", "stripe_key", "sendgrid_key", "huggingface_token", "jwt", "url_credentials", "password", "credential_pair")
6
6
  | project TimeGenerated, Computer, AiTool, DetectionType, MaskedValue, SessionIdPrefix
7
7
  | sort by TimeGenerated desc
@@ -122,7 +122,7 @@ All events arrive with `sourcetype=chron:event` and these fields:
122
122
  | `os` | All events: `darwin`, `linux`, `win32` |
123
123
  | `chron_version` | All events |
124
124
  | `role` | `message_logged` only: `user` or `assistant` |
125
- | `detection_type` | `secret_detected` only: `api_key`, `aws_key`, `jwt`, etc. |
125
+ | `detection_type` | `secret_detected` only: `openai_api_key`, `aws_access_key`, `github_token`, `jwt`, etc. |
126
126
  | `masked_value` | `secret_detected` only: e.g. `AKIA****F3K2` |
127
127
 
128
128
  **What is never transmitted:** message content, actual secret values, file paths, or any user-typed text.
@@ -1,3 +1,3 @@
1
1
  sourcetype="chron:event" event_type="secret_detected" earliest=-1h
2
- | where detection_type IN ("api_key","aws_key","gcp_key","azure_key","private_key","jwt")
2
+ | where detection_type IN ("private_key","aws_access_key","anthropic_api_key","openai_api_key","google_api_key","github_token","slack_token","stripe_key","sendgrid_key","huggingface_token","jwt","url_credentials","password","credential_pair")
3
3
  | table _time, host, ai_tool, detection_type, masked_value, session_id_prefix
@@ -1,4 +1,4 @@
1
1
  sourcetype="chron:event" event_type="secret_detected" earliest=-30d
2
- | where detection_type IN ("api_key","aws_key","gcp_key","azure_key","private_key","jwt")
2
+ | where detection_type IN ("private_key","aws_access_key","anthropic_api_key","openai_api_key","google_api_key","github_token","slack_token","stripe_key","sendgrid_key","huggingface_token","jwt","url_credentials","password","credential_pair")
3
3
  | table _time, host, ai_tool, detection_type, masked_value, session_id_prefix
4
4
  | sort -_time
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 join9(chunks, separator) {
1187
+ function join10(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 = join9;
1197
+ sql2.join = join10;
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 readFileSync6 = (path) => fs.readFileSync(path, "utf-8");
2809
+ var readFileSync7 = (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: readFileSync6,
2821
+ readFileSync: readFileSync7,
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: readFileSync6 } = require_filesystem();
2833
+ var { LDD_PATH, readFile, readFileSync: readFileSync7 } = 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 = readFileSync6(LDD_PATH);
2914
+ const lddContent = readFileSync7(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 = readFileSync6(LDD_PATH);
2971
+ const lddContent = readFileSync7(LDD_PATH);
2972
2972
  const versionMatch = lddContent.match(RE_GLIBC_VERSION);
2973
2973
  if (versionMatch) {
2974
2974
  cachedVersionFilesystem = versionMatch[1];
@@ -3553,9 +3553,9 @@ function executeStmt(db, stmt, intMode) {
3553
3553
  const lastInsertRowid = void 0;
3554
3554
  return new ResultSetImpl(columns, columnTypes, rows, rowsAffected, lastInsertRowid);
3555
3555
  } else {
3556
- const info = sqlStmt.run(args2);
3557
- const rowsAffected = info.changes;
3558
- const lastInsertRowid = BigInt(info.lastInsertRowid);
3556
+ const info2 = sqlStmt.run(args2);
3557
+ const rowsAffected = info2.changes;
3558
+ const lastInsertRowid = BigInt(info2.lastInsertRowid);
3559
3559
  return new ResultSetImpl([], [], [], rowsAffected, lastInsertRowid);
3560
3560
  }
3561
3561
  } catch (e) {
@@ -7488,13 +7488,13 @@ var require_websocket_server = __commonJS({
7488
7488
  }
7489
7489
  }
7490
7490
  if (this.options.verifyClient) {
7491
- const info = {
7491
+ const info2 = {
7492
7492
  origin: req.headers[`${version4 === 8 ? "sec-websocket-origin" : "origin"}`],
7493
7493
  secure: !!(req.socket.authorized || req.socket.encrypted),
7494
7494
  req
7495
7495
  };
7496
7496
  if (this.options.verifyClient.length === 2) {
7497
- this.options.verifyClient(info, (verified, code, message, headers) => {
7497
+ this.options.verifyClient(info2, (verified, code, message, headers) => {
7498
7498
  if (!verified) {
7499
7499
  return abortHandshake(socket, code || 401, message, headers);
7500
7500
  }
@@ -7510,7 +7510,7 @@ var require_websocket_server = __commonJS({
7510
7510
  });
7511
7511
  return;
7512
7512
  }
7513
- if (!this.options.verifyClient(info))
7513
+ if (!this.options.verifyClient(info2))
7514
7514
  return abortHandshake(socket, 401);
7515
7515
  }
7516
7516
  this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
@@ -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((join9) => join9.alias === tableName)) {
14255
+ if (typeof tableName === "string" && this.config.joins?.some((join10) => join10.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((join9) => join9.alias === tableName)) {
15144
+ if (typeof tableName === "string" && this.config.joins.some((join10) => join10.alias === tableName)) {
15145
15145
  throw new Error(`Alias "${tableName}" is already used in this query`);
15146
15146
  }
15147
15147
  if (typeof on === "function") {
@@ -16575,8 +16575,8 @@ function getDbPath() {
16575
16575
  return envPath;
16576
16576
  return (0, import_path.join)((0, import_os.homedir)(), ".chron", "chron.db");
16577
16577
  }
16578
- async function initDb(dbPath2) {
16579
- const path = dbPath2 ?? getDbPath();
16578
+ async function initDb(dbPath3) {
16579
+ const path = dbPath3 ?? getDbPath();
16580
16580
  if (!path.startsWith(":")) {
16581
16581
  (0, import_fs.mkdirSync)((0, import_path.dirname)(path), { recursive: true });
16582
16582
  }
@@ -16890,14 +16890,14 @@ async function queryIntegrity(db) {
16890
16890
  unchained++;
16891
16891
  continue;
16892
16892
  }
16893
- let ok2 = true;
16893
+ let ok3 = true;
16894
16894
  for (let i = 1; i < chained.length; i++) {
16895
16895
  if (chained[i].prev_hash !== chained[i - 1].content_hash) {
16896
- ok2 = false;
16896
+ ok3 = false;
16897
16897
  break;
16898
16898
  }
16899
16899
  }
16900
- if (ok2)
16900
+ if (ok3)
16901
16901
  intact++;
16902
16902
  else {
16903
16903
  broken++;
@@ -17356,7 +17356,7 @@ var require_package = __commonJS({
17356
17356
  "package.json"(exports2, module2) {
17357
17357
  module2.exports = {
17358
17358
  name: "chron-mcp",
17359
- version: "0.1.24",
17359
+ version: "0.1.25",
17360
17360
  mcpName: "io.github.sirinivask/chron",
17361
17361
  description: "Audit-grade timestamped logs for every AI conversation",
17362
17362
  repository: {
@@ -17843,7 +17843,7 @@ ${DIM4}Sending test event...${RESET5} `);
17843
17843
  }
17844
17844
  }]
17845
17845
  }]);
17846
- let ok2 = false;
17846
+ let ok3 = false;
17847
17847
  let statusCode = 0;
17848
17848
  try {
17849
17849
  const res = await fetch(url, {
@@ -17855,7 +17855,7 @@ ${DIM4}Sending test event...${RESET5} `);
17855
17855
  body: testPayload
17856
17856
  });
17857
17857
  statusCode = res.status;
17858
- ok2 = res.ok;
17858
+ ok3 = res.ok;
17859
17859
  } catch (e) {
17860
17860
  process.stdout.write(`${RED}failed${RESET5}
17861
17861
  `);
@@ -17864,7 +17864,7 @@ ${DIM4}Sending test event...${RESET5} `);
17864
17864
  `);
17865
17865
  process.exit(1);
17866
17866
  }
17867
- if (!ok2) {
17867
+ if (!ok3) {
17868
17868
  process.stdout.write(`${RED}failed (HTTP ${statusCode})${RESET5}
17869
17869
  `);
17870
17870
  process.stderr.write(` ${RED}Check your URL and token, then try again.${RESET5}
@@ -18235,8 +18235,8 @@ function formatLatency(ms) {
18235
18235
  function formatTs(iso) {
18236
18236
  return iso.replace("T", " ").replace(/\.\d+.*$/, "");
18237
18237
  }
18238
- async function buildSummary(sessionPrefix) {
18239
- const db = await initDb();
18238
+ async function buildSummary(sessionPrefix, existingDb) {
18239
+ const db = existingDb ?? await initDb();
18240
18240
  const allSessions = await db.select().from(sessions);
18241
18241
  const session = allSessions.find((s) => s.id.startsWith(sessionPrefix));
18242
18242
  if (!session)
@@ -18796,6 +18796,352 @@ var init_verify = __esm({
18796
18796
  }
18797
18797
  });
18798
18798
 
18799
+ // src/cli/doctor.ts
18800
+ var doctor_exports = {};
18801
+ __export(doctor_exports, {
18802
+ runDoctor: () => runDoctor
18803
+ });
18804
+ function ok2(label, detail = "") {
18805
+ process.stdout.write(` ${GREEN5}\u2713${RESET8} ${label}${detail ? ` ${DIM7}${detail}${RESET8}` : ""}
18806
+ `);
18807
+ }
18808
+ function fail2(label, detail = "") {
18809
+ process.stdout.write(` ${RED4}\u2717${RESET8} ${label}${detail ? ` ${DIM7}${detail}${RESET8}` : ""}
18810
+ `);
18811
+ }
18812
+ function warn2(label, detail = "") {
18813
+ process.stdout.write(` ${YELLOW6}!${RESET8} ${label}${detail ? ` ${DIM7}${detail}${RESET8}` : ""}
18814
+ `);
18815
+ }
18816
+ function info(label, detail = "") {
18817
+ process.stdout.write(` ${CYAN6}\xB7${RESET8} ${label}${detail ? ` ${DIM7}${detail}${RESET8}` : ""}
18818
+ `);
18819
+ }
18820
+ function section(title) {
18821
+ process.stdout.write(`
18822
+ ${BOLD8}${title}${RESET8}
18823
+ `);
18824
+ }
18825
+ function canWrite(dir) {
18826
+ try {
18827
+ (0, import_fs10.accessSync)(dir, import_fs10.constants.W_OK);
18828
+ return true;
18829
+ } catch {
18830
+ return false;
18831
+ }
18832
+ }
18833
+ function testWriteFile(path) {
18834
+ const probe = path + ".chron-probe";
18835
+ try {
18836
+ (0, import_fs10.writeFileSync)(probe, "ok");
18837
+ (0, import_fs10.unlinkSync)(probe);
18838
+ return true;
18839
+ } catch {
18840
+ return false;
18841
+ }
18842
+ }
18843
+ function dbPath2() {
18844
+ return process.env.CHRON_DB_PATH ?? (0, import_path9.join)((0, import_os9.homedir)(), ".chron", "chron.db");
18845
+ }
18846
+ function keysDir2() {
18847
+ return (0, import_path9.join)((0, import_os9.homedir)(), ".chron", "keys");
18848
+ }
18849
+ function loadConfig2() {
18850
+ try {
18851
+ return JSON.parse((0, import_fs10.readFileSync)((0, import_path9.join)((0, import_os9.homedir)(), ".chron", "config.json"), "utf8"));
18852
+ } catch {
18853
+ return {};
18854
+ }
18855
+ }
18856
+ function mcpConfigs() {
18857
+ const home = (0, import_os9.homedir)();
18858
+ const plat = (0, import_os9.platform)();
18859
+ const candidates = [
18860
+ {
18861
+ name: "Claude Desktop",
18862
+ path: plat === "win32" ? (0, import_path9.join)(process.env.APPDATA ?? (0, import_path9.join)(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json") : (0, import_path9.join)(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")
18863
+ },
18864
+ { name: "Claude Code", path: (0, import_path9.join)(home, ".claude.json") },
18865
+ { name: "Cursor", path: (0, import_path9.join)(home, ".cursor", "mcp.json") },
18866
+ { name: "Windsurf", path: (0, import_path9.join)(home, ".codeium", "windsurf", "mcp_config.json") }
18867
+ ];
18868
+ return candidates.map((c) => {
18869
+ if (!(0, import_fs10.existsSync)(c.path)) {
18870
+ return { name: c.name, path: c.path, exists: false, chronConfigured: null };
18871
+ }
18872
+ try {
18873
+ const raw = JSON.parse((0, import_fs10.readFileSync)(c.path, "utf8"));
18874
+ const servers = raw.mcpServers ?? {};
18875
+ const configured = Object.keys(servers).some(
18876
+ (k) => k === "chron" || k === "chron-mcp" || String(servers[k]?.command ?? "").includes("chron")
18877
+ );
18878
+ return { name: c.name, path: c.path, exists: true, chronConfigured: configured };
18879
+ } catch {
18880
+ return { name: c.name, path: c.path, exists: true, chronConfigured: null, note: "Could not parse config" };
18881
+ }
18882
+ });
18883
+ }
18884
+ function npmLatestVersion() {
18885
+ try {
18886
+ return (0, import_child_process3.execSync)("npm view chron-mcp version --json 2>/dev/null", { timeout: 8e3 }).toString().trim().replace(/"/g, "");
18887
+ } catch {
18888
+ return null;
18889
+ }
18890
+ }
18891
+ function npxWorks() {
18892
+ try {
18893
+ (0, import_child_process3.execSync)("npx chron-mcp --version 2>/dev/null", { timeout: 8e3 });
18894
+ return true;
18895
+ } catch {
18896
+ return false;
18897
+ }
18898
+ }
18899
+ async function httpHealthCheck(port = 3001) {
18900
+ try {
18901
+ const controller = new AbortController();
18902
+ const timer = setTimeout(() => controller.abort(), 2e3);
18903
+ const res = await fetch(`http://localhost:${port}/health`, { signal: controller.signal });
18904
+ clearTimeout(timer);
18905
+ if (!res.ok)
18906
+ return { running: false };
18907
+ const body = await res.json();
18908
+ return { running: true, version: body.version };
18909
+ } catch {
18910
+ return { running: false };
18911
+ }
18912
+ }
18913
+ async function runDoctor(args2) {
18914
+ const jsonMode = args2.includes("--json");
18915
+ const port = parseInt(process.env.PORT ?? "3001", 10);
18916
+ const results = [];
18917
+ if (!jsonMode)
18918
+ process.stdout.write(`
18919
+ ${BOLD8}chron doctor${RESET8} ${DIM7}v${import_package2.version}${RESET8}
18920
+ `);
18921
+ if (!jsonMode)
18922
+ section("Runtime");
18923
+ const nodeVer = process.version.replace("v", "");
18924
+ const [nodeMajor] = nodeVer.split(".").map(Number);
18925
+ const nodeOk = (nodeMajor ?? 0) >= 18;
18926
+ results.push({ pass: nodeOk, label: `Node.js ${process.version}`, fix: nodeOk ? void 0 : "Upgrade Node.js to v18 or newer: https://nodejs.org" });
18927
+ if (!jsonMode)
18928
+ (nodeOk ? ok2 : fail2)(`Node.js ${process.version}`, nodeOk ? "" : "requires \u2265 18");
18929
+ const latestVersion = npmLatestVersion();
18930
+ if (latestVersion) {
18931
+ const upToDate = import_package2.version === latestVersion;
18932
+ results.push({
18933
+ pass: upToDate,
18934
+ label: `chron-mcp ${import_package2.version}${upToDate ? "" : ` (latest: ${latestVersion})`}`,
18935
+ fix: upToDate ? void 0 : `npm install -g chron-mcp@latest`
18936
+ });
18937
+ if (!jsonMode)
18938
+ (upToDate ? ok2 : warn2)(`chron-mcp ${import_package2.version}`, upToDate ? "up to date" : `latest is ${latestVersion} \u2014 run: npm install -g chron-mcp@latest`);
18939
+ } else {
18940
+ results.push({ pass: "warn", label: `chron-mcp ${import_package2.version}`, detail: "Could not reach npm to check for updates" });
18941
+ if (!jsonMode)
18942
+ warn2(`chron-mcp ${import_package2.version}`, "could not check npm for updates");
18943
+ }
18944
+ const npxOk = npxWorks();
18945
+ results.push({ pass: npxOk, label: "npx chron-mcp --version", fix: npxOk ? void 0 : "Run: npm install -g chron-mcp" });
18946
+ if (!jsonMode)
18947
+ (npxOk ? ok2 : fail2)("npx chron-mcp --version", npxOk ? "works" : "failed \u2014 run: npm install -g chron-mcp");
18948
+ if (!jsonMode)
18949
+ section("Storage");
18950
+ const db = dbPath2();
18951
+ const dbDir = (0, import_path9.join)(db, "..");
18952
+ const dbDirExists = (0, import_fs10.existsSync)(dbDir);
18953
+ if (!dbDirExists) {
18954
+ try {
18955
+ (0, import_fs10.mkdirSync)(dbDir, { recursive: true });
18956
+ } catch {
18957
+ }
18958
+ }
18959
+ const dbWritable = testWriteFile(dbDir);
18960
+ const dbExists = (0, import_fs10.existsSync)(db);
18961
+ results.push({
18962
+ pass: dbWritable,
18963
+ label: `DB directory: ${dbDir}`,
18964
+ detail: dbExists ? "database found" : "no database yet (will be created on first use)",
18965
+ fix: dbWritable ? void 0 : `mkdir -p "${dbDir}" && chmod 700 "${dbDir}"`
18966
+ });
18967
+ if (!jsonMode)
18968
+ (dbWritable ? ok2 : fail2)(`DB: ${db}`, dbExists ? "found" : "not yet created \u2014 will init on first use");
18969
+ if (!dbWritable && !jsonMode)
18970
+ info("fix", `mkdir -p "${dbDir}" && chmod 700 "${dbDir}"`);
18971
+ const keys = keysDir2();
18972
+ const keysExist = (0, import_fs10.existsSync)(keys);
18973
+ if (!keysExist) {
18974
+ try {
18975
+ (0, import_fs10.mkdirSync)(keys, { recursive: true });
18976
+ } catch {
18977
+ }
18978
+ }
18979
+ const keysWritable = canWrite(keys);
18980
+ results.push({
18981
+ pass: keysWritable,
18982
+ label: `Keys directory: ${keys}`,
18983
+ fix: keysWritable ? void 0 : `mkdir -p "${keys}" && chmod 700 "${keys}"`
18984
+ });
18985
+ if (!jsonMode)
18986
+ (keysWritable ? ok2 : fail2)(`Keys: ${keys}`, keysWritable ? "" : "not writable \u2014 Ed25519 signing will fail");
18987
+ if (!jsonMode)
18988
+ section("MCP Tool Configurations");
18989
+ const tools = mcpConfigs();
18990
+ const foundTools = tools.filter((t) => t.exists);
18991
+ if (foundTools.length === 0) {
18992
+ results.push({ pass: "warn", label: "No MCP tool config files found", fix: "Install Claude Desktop or Claude Code and add chron to mcpServers" });
18993
+ if (!jsonMode)
18994
+ warn2("No MCP tool config files detected (Claude Desktop, Claude Code, Cursor, Windsurf)");
18995
+ }
18996
+ for (const t of tools) {
18997
+ if (!t.exists) {
18998
+ results.push({ pass: "skip", label: `${t.name}: not installed` });
18999
+ if (!jsonMode)
19000
+ info(`${t.name}`, "not installed");
19001
+ continue;
19002
+ }
19003
+ if (t.chronConfigured === null) {
19004
+ results.push({ pass: "warn", label: `${t.name}: config parse error`, detail: t.note });
19005
+ if (!jsonMode)
19006
+ warn2(`${t.name}`, t.note ?? "config parse error");
19007
+ } else if (t.chronConfigured) {
19008
+ results.push({ pass: true, label: `${t.name}: chron configured` });
19009
+ if (!jsonMode)
19010
+ ok2(`${t.name}`, "chron configured");
19011
+ } else {
19012
+ results.push({
19013
+ pass: "warn",
19014
+ label: `${t.name}: chron NOT in mcpServers`,
19015
+ fix: `Add chron to mcpServers in ${t.path}
19016
+ See: https://github.com/sirinivask/chron#installation`
19017
+ });
19018
+ if (!jsonMode)
19019
+ warn2(`${t.name}`, `chron not in mcpServers \u2014 add it to ${t.path}`);
19020
+ }
19021
+ }
19022
+ if (!jsonMode)
19023
+ section("HTTP Mode");
19024
+ const health = await httpHealthCheck(port);
19025
+ if (health.running) {
19026
+ results.push({ pass: true, label: `HTTP server responding on port ${port}`, detail: health.version ? `v${health.version}` : void 0 });
19027
+ if (!jsonMode)
19028
+ ok2(`HTTP /health on port ${port}`, health.version ? `v${health.version}` : "");
19029
+ } else {
19030
+ results.push({ pass: "skip", label: `HTTP server not running on port ${port}`, detail: "optional \u2014 only needed for ChatGPT / non-stdio MCP clients" });
19031
+ if (!jsonMode)
19032
+ info(`HTTP server not running on port ${port}`, "optional \u2014 start with CHRON_TRANSPORT=http npx chron-mcp");
19033
+ }
19034
+ if (!jsonMode)
19035
+ section("SIEM Integrations");
19036
+ const config = loadConfig2();
19037
+ const siems = [
19038
+ {
19039
+ name: "Splunk",
19040
+ envOk: !!(process.env.CHRON_SPLUNK_URL && process.env.CHRON_SPLUNK_TOKEN),
19041
+ cfgOk: !!(config.splunk && config.splunk.url)
19042
+ },
19043
+ {
19044
+ name: "Microsoft Sentinel",
19045
+ envOk: !!(process.env.CHRON_SENTINEL_DCE && process.env.CHRON_SENTINEL_CLIENT_ID),
19046
+ cfgOk: !!(config.sentinel && config.sentinel.dce)
19047
+ },
19048
+ {
19049
+ name: "CrowdStrike LogScale",
19050
+ envOk: !!(process.env.CHRON_LOGSCALE_URL && process.env.CHRON_LOGSCALE_TOKEN),
19051
+ cfgOk: !!(config.logscale && config.logscale.url)
19052
+ }
19053
+ ];
19054
+ let anySiem = false;
19055
+ for (const s of siems) {
19056
+ if (s.envOk || s.cfgOk) {
19057
+ anySiem = true;
19058
+ results.push({ pass: true, label: `${s.name} connected`, detail: s.envOk ? "via env" : "via ~/.chron/config.json" });
19059
+ if (!jsonMode)
19060
+ ok2(`${s.name}`, s.envOk ? "via env vars" : "via ~/.chron/config.json");
19061
+ } else {
19062
+ results.push({ pass: "skip", label: `${s.name}: not configured`, detail: "optional" });
19063
+ if (!jsonMode)
19064
+ info(`${s.name}`, "not configured \u2014 run: chron connect splunk / sentinel / crowdstrike");
19065
+ }
19066
+ }
19067
+ if (!anySiem && !jsonMode) {
19068
+ process.stdout.write(` ${DIM7}No SIEM configured. Events will only be stored locally.${RESET8}
19069
+ `);
19070
+ }
19071
+ const failures = results.filter((r) => r.pass === false);
19072
+ const warnings = results.filter((r) => r.pass === "warn");
19073
+ if (!jsonMode) {
19074
+ process.stdout.write("\n");
19075
+ if (failures.length === 0 && warnings.length === 0) {
19076
+ process.stdout.write(`${GREEN5}${BOLD8}All checks passed.${RESET8} Chron is correctly set up.
19077
+
19078
+ `);
19079
+ } else {
19080
+ if (failures.length > 0) {
19081
+ process.stdout.write(`${RED4}${BOLD8}${failures.length} issue(s) need attention:${RESET8}
19082
+ `);
19083
+ for (const f of failures) {
19084
+ process.stdout.write(` ${RED4}\u2717${RESET8} ${f.label}
19085
+ `);
19086
+ if (f.fix)
19087
+ process.stdout.write(` ${DIM7}\u2192 ${f.fix}${RESET8}
19088
+ `);
19089
+ }
19090
+ process.stdout.write("\n");
19091
+ }
19092
+ if (warnings.length > 0) {
19093
+ process.stdout.write(`${YELLOW6}${BOLD8}${warnings.length} warning(s):${RESET8}
19094
+ `);
19095
+ for (const w of warnings) {
19096
+ process.stdout.write(` ${YELLOW6}!${RESET8} ${w.label}
19097
+ `);
19098
+ if (w.fix)
19099
+ process.stdout.write(` ${DIM7}\u2192 ${w.fix}${RESET8}
19100
+ `);
19101
+ }
19102
+ process.stdout.write("\n");
19103
+ }
19104
+ }
19105
+ }
19106
+ if (jsonMode) {
19107
+ process.stdout.write(JSON.stringify({
19108
+ chron_version: import_package2.version,
19109
+ node_version: process.version,
19110
+ checks: results.map((r) => ({
19111
+ label: r.label,
19112
+ pass: r.pass,
19113
+ detail: r.detail,
19114
+ fix: r.fix
19115
+ })),
19116
+ summary: {
19117
+ failures: failures.length,
19118
+ warnings: warnings.length,
19119
+ ok: results.filter((r) => r.pass === true).length,
19120
+ skipped: results.filter((r) => r.pass === "skip").length
19121
+ }
19122
+ }, null, 2) + "\n");
19123
+ }
19124
+ process.exit(failures.length > 0 ? 1 : 0);
19125
+ }
19126
+ var import_os9, import_path9, import_fs10, import_child_process3, import_package2, RESET8, BOLD8, DIM7, GREEN5, RED4, YELLOW6, CYAN6;
19127
+ var init_doctor = __esm({
19128
+ "src/cli/doctor.ts"() {
19129
+ "use strict";
19130
+ import_os9 = require("os");
19131
+ import_path9 = require("path");
19132
+ import_fs10 = require("fs");
19133
+ import_child_process3 = require("child_process");
19134
+ import_package2 = __toESM(require_package());
19135
+ RESET8 = "\x1B[0m";
19136
+ BOLD8 = "\x1B[1m";
19137
+ DIM7 = "\x1B[2m";
19138
+ GREEN5 = "\x1B[32m";
19139
+ RED4 = "\x1B[31m";
19140
+ YELLOW6 = "\x1B[33m";
19141
+ CYAN6 = "\x1B[36m";
19142
+ }
19143
+ });
19144
+
18799
19145
  // src/cli/index.ts
18800
19146
  var [, , command, ...args] = process.argv;
18801
19147
  async function main() {
@@ -18850,6 +19196,11 @@ async function main() {
18850
19196
  await runVerify2(args);
18851
19197
  break;
18852
19198
  }
19199
+ case "doctor": {
19200
+ const { runDoctor: runDoctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
19201
+ await runDoctor2(args);
19202
+ break;
19203
+ }
18853
19204
  default: {
18854
19205
  const name = command ? `Unknown command: ${command}
18855
19206
 
@@ -18868,6 +19219,7 @@ Commands:
18868
19219
  sign Sign a session with its Ed25519 key \u2014 produces a .chron.sig file
18869
19220
  verify Verify a session's hash chain and Ed25519 signature
18870
19221
  prune Delete sessions older than a retention cutoff
19222
+ doctor Check your Chron setup \u2014 Node version, DB, MCP configs, SIEM
18871
19223
 
18872
19224
  Options (history):
18873
19225
  --limit=<n> Max sessions to show (default: 20)
@@ -18894,6 +19246,9 @@ Options (prune):
18894
19246
  --older-than=<n>d Cutoff in days (falls back to retention_days in config)
18895
19247
  --dry-run Show what would be deleted without deleting
18896
19248
  --confirm Required flag to actually delete
19249
+
19250
+ Options (doctor):
19251
+ --json Machine-readable JSON output
18897
19252
  `
18898
19253
  );
18899
19254
  process.exit(command ? 1 : 0);
package/dist/index.js CHANGED
@@ -38479,7 +38479,7 @@ var init_time = __esm({
38479
38479
  var version4;
38480
38480
  var init_package = __esm({
38481
38481
  "package.json"() {
38482
- version4 = "0.1.24";
38482
+ version4 = "0.1.25";
38483
38483
  }
38484
38484
  });
38485
38485
 
@@ -39698,8 +39698,8 @@ function detectMutations(content) {
39698
39698
  }
39699
39699
  return hits;
39700
39700
  }
39701
- async function buildSummary(sessionPrefix) {
39702
- const db = await initDb();
39701
+ async function buildSummary(sessionPrefix, existingDb) {
39702
+ const db = existingDb ?? await initDb();
39703
39703
  const allSessions = await db.select().from(sessions);
39704
39704
  const session = allSessions.find((s) => s.id.startsWith(sessionPrefix));
39705
39705
  if (!session)
@@ -39771,7 +39771,7 @@ var init_summary = __esm({
39771
39771
  // src/tools/summary.ts
39772
39772
  function summarizeSession(_db) {
39773
39773
  return async (args) => {
39774
- const data = await buildSummary(args.session_id);
39774
+ const data = await buildSummary(args.session_id, _db);
39775
39775
  if (!data) {
39776
39776
  return {
39777
39777
  content: [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chron-mcp",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "mcpName": "io.github.sirinivask/chron",
5
5
  "description": "Audit-grade timestamped logs for every AI conversation",
6
6
  "repository": {