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 +139 -10
- package/dashboards/logscale/README.md +3 -3
- package/dashboards/logscale/api-key-alert.lql +1 -1
- package/dashboards/logscale/chron-ai-activity.yaml +1 -1
- package/dashboards/sentinel/README.md +1 -1
- package/dashboards/sentinel/secrets-alert.kql +1 -1
- package/dashboards/sentinel/secrets-detected.kql +1 -1
- package/dashboards/splunk/README.md +1 -1
- package/dashboards/splunk/secrets-alert.spl +1 -1
- package/dashboards/splunk/secrets-detected.spl +1 -1
- package/dist/cli/index.js +381 -26
- package/dist/index.js +4 -4
- package/package.json +1 -1
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>
|
|
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
|
|
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
|
|
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>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
##
|
|
274
|
+
## Integrity and signing
|
|
199
275
|
|
|
200
|
-
Every
|
|
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
|
|
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 ["
|
|
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: `
|
|
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 ["
|
|
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 ["
|
|
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: `
|
|
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 ("
|
|
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 ("
|
|
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: `
|
|
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 ("
|
|
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 ("
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
|
3557
|
-
const rowsAffected =
|
|
3558
|
-
const lastInsertRowid = BigInt(
|
|
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
|
|
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(
|
|
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(
|
|
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((
|
|
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((
|
|
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(
|
|
16579
|
-
const path =
|
|
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
|
|
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
|
-
|
|
16896
|
+
ok3 = false;
|
|
16897
16897
|
break;
|
|
16898
16898
|
}
|
|
16899
16899
|
}
|
|
16900
|
-
if (
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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.
|
|
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: [{
|