chron-mcp 0.1.28 → 0.1.30
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 +633 -6
- package/dist/index.js +121 -9
- package/package.json +1 -1
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 (
|
|
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.
|
|
17489
|
+
version: "0.1.30",
|
|
17380
17490
|
mcpName: "io.github.sirinivask/chron",
|
|
17381
17491
|
description: "Audit-grade timestamped logs for every AI conversation",
|
|
17382
17492
|
repository: {
|
|
@@ -19900,6 +20010,511 @@ var init_import = __esm({
|
|
|
19900
20010
|
}
|
|
19901
20011
|
});
|
|
19902
20012
|
|
|
20013
|
+
// src/review/rules.ts
|
|
20014
|
+
var SOC2_RULES, FRAMEWORKS;
|
|
20015
|
+
var init_rules = __esm({
|
|
20016
|
+
"src/review/rules.ts"() {
|
|
20017
|
+
"use strict";
|
|
20018
|
+
SOC2_RULES = [
|
|
20019
|
+
{
|
|
20020
|
+
id: "soc2.cc6_1.ai_access_control_change",
|
|
20021
|
+
framework: "soc2",
|
|
20022
|
+
controls: ["CC6.1"],
|
|
20023
|
+
severity: "high",
|
|
20024
|
+
description: "AI touched access-control-related code",
|
|
20025
|
+
finding: "AI modified code in an access-control-sensitive path.",
|
|
20026
|
+
not_claiming: "This is not evidence of a security failure or SOC 2 non-compliance.",
|
|
20027
|
+
suggested_evidence: [
|
|
20028
|
+
"PR approval with human reviewer",
|
|
20029
|
+
"Change ticket linked to the work",
|
|
20030
|
+
"Manager or security team sign-off"
|
|
20031
|
+
],
|
|
20032
|
+
match: {
|
|
20033
|
+
type: "code_change_path",
|
|
20034
|
+
path_contains: ["auth", "iam", "rbac", "permission", "policy", "acl", "role", "login", "jwt", "oauth", "saml", "sso", "mfa", "access_control"]
|
|
20035
|
+
}
|
|
20036
|
+
},
|
|
20037
|
+
{
|
|
20038
|
+
id: "soc2.cc6_1.cc6_6.secret_detected",
|
|
20039
|
+
framework: "soc2",
|
|
20040
|
+
controls: ["CC6.1", "CC6.6"],
|
|
20041
|
+
severity: "critical",
|
|
20042
|
+
description: "AI session contained a detected secret or credential",
|
|
20043
|
+
finding: "A sensitive credential or secret was detected in an AI session.",
|
|
20044
|
+
not_claiming: "This is not evidence of a breach. Chron masks all detected values at log time \u2014 no plaintext credentials are stored.",
|
|
20045
|
+
suggested_evidence: [
|
|
20046
|
+
"Confirm no plaintext credential was committed to version control",
|
|
20047
|
+
"Rotate the credential if it was a live secret",
|
|
20048
|
+
"Document the detection and resolution in the change log"
|
|
20049
|
+
],
|
|
20050
|
+
match: { type: "secret_detected" }
|
|
20051
|
+
},
|
|
20052
|
+
{
|
|
20053
|
+
id: "soc2.cc7_2.cc8_1.ai_infra_change",
|
|
20054
|
+
framework: "soc2",
|
|
20055
|
+
controls: ["CC7.2", "CC8.1"],
|
|
20056
|
+
severity: "high",
|
|
20057
|
+
description: "AI executed or modified deployment or infrastructure files",
|
|
20058
|
+
finding: "AI modified infrastructure or deployment configuration.",
|
|
20059
|
+
not_claiming: "This is not evidence of an unauthorized change.",
|
|
20060
|
+
suggested_evidence: [
|
|
20061
|
+
"Change ticket or approval record",
|
|
20062
|
+
"Pipeline approval gate log",
|
|
20063
|
+
"Human review of the infrastructure diff"
|
|
20064
|
+
],
|
|
20065
|
+
match: {
|
|
20066
|
+
type: "code_change_path",
|
|
20067
|
+
path_contains: [".tf", "terraform", "dockerfile", "docker-compose", "kubernetes", "k8s", "helm", "ansible", ".github/workflow", "cloudformation", "pipeline", "deploy"]
|
|
20068
|
+
}
|
|
20069
|
+
},
|
|
20070
|
+
{
|
|
20071
|
+
id: "soc2.cc7_2.ai_monitoring_change",
|
|
20072
|
+
framework: "soc2",
|
|
20073
|
+
controls: ["CC7.2"],
|
|
20074
|
+
severity: "high",
|
|
20075
|
+
description: "AI changed logging, monitoring, alerting, or security tooling",
|
|
20076
|
+
finding: "AI modified logging, monitoring, or security alerting code or configuration.",
|
|
20077
|
+
not_claiming: "This is not evidence that monitoring was disabled or circumvented.",
|
|
20078
|
+
suggested_evidence: [
|
|
20079
|
+
"Confirm monitoring coverage was not reduced",
|
|
20080
|
+
"Human review of the change",
|
|
20081
|
+
"Change ticket"
|
|
20082
|
+
],
|
|
20083
|
+
match: {
|
|
20084
|
+
type: "code_change_path",
|
|
20085
|
+
path_contains: ["logging", "monitoring", "alerting", "splunk", "datadog", "prometheus", "grafana", "cloudwatch", "sentry", "pagerduty", "audit_log", "audit-log", "logger"]
|
|
20086
|
+
}
|
|
20087
|
+
},
|
|
20088
|
+
{
|
|
20089
|
+
id: "soc2.cc6_1.cc6_7.ai_data_handling_change",
|
|
20090
|
+
framework: "soc2",
|
|
20091
|
+
controls: ["CC6.1", "CC6.7"],
|
|
20092
|
+
severity: "medium",
|
|
20093
|
+
description: "AI changed data retention, deletion, export, or encryption logic",
|
|
20094
|
+
finding: "AI modified data handling, retention, or privacy-related code.",
|
|
20095
|
+
not_claiming: "This is not evidence of a data handling violation.",
|
|
20096
|
+
suggested_evidence: [
|
|
20097
|
+
"Privacy or data officer review",
|
|
20098
|
+
"Change ticket documenting the business reason",
|
|
20099
|
+
"Confirm compliance with data retention policy"
|
|
20100
|
+
],
|
|
20101
|
+
match: {
|
|
20102
|
+
type: "code_change_path",
|
|
20103
|
+
path_contains: ["retention", "encrypt", "decrypt", "gdpr", "privacy", "anonymize", "redact", "purge", "archive", "backup"]
|
|
20104
|
+
}
|
|
20105
|
+
}
|
|
20106
|
+
];
|
|
20107
|
+
FRAMEWORKS = {
|
|
20108
|
+
soc2: SOC2_RULES
|
|
20109
|
+
};
|
|
20110
|
+
}
|
|
20111
|
+
});
|
|
20112
|
+
|
|
20113
|
+
// src/review/engine.ts
|
|
20114
|
+
async function runReview(db, rules, since) {
|
|
20115
|
+
const client = db.$client;
|
|
20116
|
+
const map = /* @__PURE__ */ new Map();
|
|
20117
|
+
for (const rule of rules) {
|
|
20118
|
+
if (rule.match.type === "code_change_path") {
|
|
20119
|
+
await matchCodeChanges(client, rule, map, since);
|
|
20120
|
+
} else if (rule.match.type === "secret_detected") {
|
|
20121
|
+
await matchSecrets(client, rule, map, since);
|
|
20122
|
+
}
|
|
20123
|
+
}
|
|
20124
|
+
const findings = Array.from(map.values());
|
|
20125
|
+
findings.sort(
|
|
20126
|
+
(a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99) || a.last_occurred_at.localeCompare(b.last_occurred_at)
|
|
20127
|
+
);
|
|
20128
|
+
return findings;
|
|
20129
|
+
}
|
|
20130
|
+
async function matchCodeChanges(client, rule, map, since) {
|
|
20131
|
+
if (rule.match.type !== "code_change_path")
|
|
20132
|
+
return;
|
|
20133
|
+
const keywords = rule.match.path_contains;
|
|
20134
|
+
const result = await client.execute(
|
|
20135
|
+
since ? {
|
|
20136
|
+
sql: `SELECT m.id, m.session_id, m.content, m.created_at, s.title, s.ai_tool
|
|
20137
|
+
FROM messages m JOIN sessions s ON s.id = m.session_id
|
|
20138
|
+
WHERE m.event_type = 'code_change' AND m.created_at >= ?
|
|
20139
|
+
ORDER BY m.created_at`,
|
|
20140
|
+
args: [since]
|
|
20141
|
+
} : `SELECT m.id, m.session_id, m.content, m.created_at, s.title, s.ai_tool
|
|
20142
|
+
FROM messages m JOIN sessions s ON s.id = m.session_id
|
|
20143
|
+
WHERE m.event_type = 'code_change'
|
|
20144
|
+
ORDER BY m.created_at`
|
|
20145
|
+
);
|
|
20146
|
+
for (const row of result.rows) {
|
|
20147
|
+
const content = String(row.content ?? "");
|
|
20148
|
+
let filePath = "";
|
|
20149
|
+
try {
|
|
20150
|
+
const parsed = JSON.parse(content);
|
|
20151
|
+
filePath = (parsed.file_path ?? parsed.path ?? "").toLowerCase();
|
|
20152
|
+
} catch {
|
|
20153
|
+
filePath = content.toLowerCase();
|
|
20154
|
+
}
|
|
20155
|
+
if (!keywords.some((kw) => filePath.includes(kw.toLowerCase())))
|
|
20156
|
+
continue;
|
|
20157
|
+
const sessionId = String(row.session_id ?? "");
|
|
20158
|
+
const mapKey = `${rule.id}::${sessionId}`;
|
|
20159
|
+
const occurredAt = String(row.created_at ?? "");
|
|
20160
|
+
const evidenceItem = `code_change: ${filePath || "unknown path"}`;
|
|
20161
|
+
if (!map.has(mapKey)) {
|
|
20162
|
+
map.set(mapKey, {
|
|
20163
|
+
rule_id: rule.id,
|
|
20164
|
+
framework: rule.framework,
|
|
20165
|
+
controls: rule.controls,
|
|
20166
|
+
severity: rule.severity,
|
|
20167
|
+
session_id: sessionId,
|
|
20168
|
+
session_prefix: sessionId.slice(0, 8),
|
|
20169
|
+
session_title: String(row.title ?? ""),
|
|
20170
|
+
ai_tool: row.ai_tool != null ? String(row.ai_tool) : null,
|
|
20171
|
+
evidence_items: [],
|
|
20172
|
+
first_occurred_at: occurredAt,
|
|
20173
|
+
last_occurred_at: occurredAt,
|
|
20174
|
+
finding: rule.finding,
|
|
20175
|
+
not_claiming: rule.not_claiming,
|
|
20176
|
+
suggested_evidence: rule.suggested_evidence
|
|
20177
|
+
});
|
|
20178
|
+
}
|
|
20179
|
+
const finding = map.get(mapKey);
|
|
20180
|
+
if (!finding.evidence_items.includes(evidenceItem)) {
|
|
20181
|
+
finding.evidence_items.push(evidenceItem);
|
|
20182
|
+
}
|
|
20183
|
+
if (occurredAt > finding.last_occurred_at)
|
|
20184
|
+
finding.last_occurred_at = occurredAt;
|
|
20185
|
+
}
|
|
20186
|
+
}
|
|
20187
|
+
async function matchSecrets(client, rule, map, since) {
|
|
20188
|
+
const result = await client.execute(
|
|
20189
|
+
since ? {
|
|
20190
|
+
sql: `SELECT sd.session_id, sd.type, sd.masked_value, sd.detected_at, s.title, s.ai_tool
|
|
20191
|
+
FROM secrets_detected sd JOIN sessions s ON s.id = sd.session_id
|
|
20192
|
+
WHERE sd.detected_at >= ?
|
|
20193
|
+
ORDER BY sd.detected_at`,
|
|
20194
|
+
args: [since]
|
|
20195
|
+
} : `SELECT sd.session_id, sd.type, sd.masked_value, sd.detected_at, s.title, s.ai_tool
|
|
20196
|
+
FROM secrets_detected sd JOIN sessions s ON s.id = sd.session_id
|
|
20197
|
+
ORDER BY sd.detected_at`
|
|
20198
|
+
);
|
|
20199
|
+
for (const row of result.rows) {
|
|
20200
|
+
const sessionId = String(row.session_id ?? "");
|
|
20201
|
+
const mapKey = `${rule.id}::${sessionId}`;
|
|
20202
|
+
const occurredAt = String(row.detected_at ?? "");
|
|
20203
|
+
const evidenceItem = `secret_detected: ${row.type} (${row.masked_value})`;
|
|
20204
|
+
if (!map.has(mapKey)) {
|
|
20205
|
+
map.set(mapKey, {
|
|
20206
|
+
rule_id: rule.id,
|
|
20207
|
+
framework: rule.framework,
|
|
20208
|
+
controls: rule.controls,
|
|
20209
|
+
severity: rule.severity,
|
|
20210
|
+
session_id: sessionId,
|
|
20211
|
+
session_prefix: sessionId.slice(0, 8),
|
|
20212
|
+
session_title: String(row.title ?? ""),
|
|
20213
|
+
ai_tool: row.ai_tool != null ? String(row.ai_tool) : null,
|
|
20214
|
+
evidence_items: [],
|
|
20215
|
+
first_occurred_at: occurredAt,
|
|
20216
|
+
last_occurred_at: occurredAt,
|
|
20217
|
+
finding: rule.finding,
|
|
20218
|
+
not_claiming: rule.not_claiming,
|
|
20219
|
+
suggested_evidence: rule.suggested_evidence
|
|
20220
|
+
});
|
|
20221
|
+
}
|
|
20222
|
+
const finding = map.get(mapKey);
|
|
20223
|
+
if (!finding.evidence_items.includes(evidenceItem)) {
|
|
20224
|
+
finding.evidence_items.push(evidenceItem);
|
|
20225
|
+
}
|
|
20226
|
+
if (occurredAt > finding.last_occurred_at)
|
|
20227
|
+
finding.last_occurred_at = occurredAt;
|
|
20228
|
+
}
|
|
20229
|
+
}
|
|
20230
|
+
var SEVERITY_ORDER;
|
|
20231
|
+
var init_engine = __esm({
|
|
20232
|
+
"src/review/engine.ts"() {
|
|
20233
|
+
"use strict";
|
|
20234
|
+
SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
20235
|
+
}
|
|
20236
|
+
});
|
|
20237
|
+
|
|
20238
|
+
// src/cli/review.ts
|
|
20239
|
+
var review_exports = {};
|
|
20240
|
+
__export(review_exports, {
|
|
20241
|
+
runReview: () => runReview2
|
|
20242
|
+
});
|
|
20243
|
+
function printFindings(findings, framework, sessionCount) {
|
|
20244
|
+
const controlsHit = new Set(findings.flatMap((f) => f.controls));
|
|
20245
|
+
process.stdout.write(`
|
|
20246
|
+
${BOLD10}Chron Review${RESET10} ${CYAN8}${framework.toUpperCase()}${RESET10} ${DIM9}${sessionCount} session(s) reviewed${RESET10}
|
|
20247
|
+
|
|
20248
|
+
`);
|
|
20249
|
+
if (findings.length === 0) {
|
|
20250
|
+
process.stdout.write(`${DIM9}No findings. No sessions matched any control review criteria.${RESET10}
|
|
20251
|
+
|
|
20252
|
+
`);
|
|
20253
|
+
return;
|
|
20254
|
+
}
|
|
20255
|
+
process.stdout.write(
|
|
20256
|
+
`${BOLD10}${findings.length}${RESET10} finding${findings.length === 1 ? "" : "s"} across ${BOLD10}${controlsHit.size}${RESET10} control${controlsHit.size === 1 ? "" : "s"}
|
|
20257
|
+
|
|
20258
|
+
`
|
|
20259
|
+
);
|
|
20260
|
+
process.stdout.write(
|
|
20261
|
+
`${DIM9}This report identifies AI-session evidence that may require control-owner review.
|
|
20262
|
+
It is not a certification of compliance or evidence of any violation.${RESET10}
|
|
20263
|
+
|
|
20264
|
+
`
|
|
20265
|
+
);
|
|
20266
|
+
for (const f of findings) {
|
|
20267
|
+
const sevColor = SEV_COLOR[f.severity] ?? DIM9;
|
|
20268
|
+
const controls = f.controls.join(", ");
|
|
20269
|
+
const sev = f.severity.toUpperCase().padEnd(8);
|
|
20270
|
+
process.stdout.write(
|
|
20271
|
+
` ${sevColor}${BOLD10}${sev}${RESET10} ${MAGENTA2}${controls.padEnd(16)}${RESET10} ${CYAN8}${f.session_prefix}${RESET10} ${f.session_title}
|
|
20272
|
+
`
|
|
20273
|
+
);
|
|
20274
|
+
process.stdout.write(` ${f.finding}
|
|
20275
|
+
`);
|
|
20276
|
+
for (const item of f.evidence_items.slice(0, 5)) {
|
|
20277
|
+
process.stdout.write(` ${DIM9}\u2022 ${item}${RESET10}
|
|
20278
|
+
`);
|
|
20279
|
+
}
|
|
20280
|
+
if (f.evidence_items.length > 5) {
|
|
20281
|
+
process.stdout.write(` ${DIM9} \u2026 ${f.evidence_items.length - 5} more event(s)${RESET10}
|
|
20282
|
+
`);
|
|
20283
|
+
}
|
|
20284
|
+
process.stdout.write(` ${DIM9}Suggested: ${f.suggested_evidence[0]}${RESET10}
|
|
20285
|
+
`);
|
|
20286
|
+
process.stdout.write("\n");
|
|
20287
|
+
}
|
|
20288
|
+
}
|
|
20289
|
+
function esc2(s) {
|
|
20290
|
+
return (s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
20291
|
+
}
|
|
20292
|
+
function badgeHtml(severity) {
|
|
20293
|
+
return `<span class="badge badge-${esc2(severity)}">${esc2(severity.toUpperCase())}</span>`;
|
|
20294
|
+
}
|
|
20295
|
+
function buildReviewHtml(params) {
|
|
20296
|
+
const { framework, host, generatedAt, sessionCount, findings } = params;
|
|
20297
|
+
const controlsHit = new Set(findings.flatMap((f) => f.controls));
|
|
20298
|
+
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
20299
|
+
for (const f of findings)
|
|
20300
|
+
bySeverity[f.severity] = (bySeverity[f.severity] ?? 0) + 1;
|
|
20301
|
+
const findingCards = findings.length === 0 ? '<p style="color:#6b7280">No findings. No sessions matched any control review criteria.</p>' : findings.map((f) => `
|
|
20302
|
+
<div class="finding-card">
|
|
20303
|
+
<div class="finding-header">
|
|
20304
|
+
${badgeHtml(f.severity)}
|
|
20305
|
+
<span class="finding-controls">${esc2(f.controls.join(", "))}</span>
|
|
20306
|
+
<span style="flex:1"></span>
|
|
20307
|
+
<span class="finding-session">${esc2(f.session_prefix)}</span>
|
|
20308
|
+
<span style="font-size:12px;color:#374151">${esc2(f.session_title)}</span>
|
|
20309
|
+
</div>
|
|
20310
|
+
<div class="finding-body">
|
|
20311
|
+
<div class="finding-text">${esc2(f.finding)}</div>
|
|
20312
|
+
<div class="finding-disclaimer">${esc2(f.not_claiming)}</div>
|
|
20313
|
+
<strong style="font-size:11px;color:#374151">Evidence in session:</strong>
|
|
20314
|
+
<ul class="evidence-list">
|
|
20315
|
+
${f.evidence_items.map((e) => `<li>${esc2(e)}</li>`).join("")}
|
|
20316
|
+
</ul>
|
|
20317
|
+
<strong style="font-size:11px;color:#374151">Suggested review actions:</strong>
|
|
20318
|
+
<ul class="suggested-list">
|
|
20319
|
+
${f.suggested_evidence.map((s) => `<li>${esc2(s)}</li>`).join("")}
|
|
20320
|
+
</ul>
|
|
20321
|
+
</div>
|
|
20322
|
+
</div>`).join("");
|
|
20323
|
+
return `<!DOCTYPE html>
|
|
20324
|
+
<html lang="en">
|
|
20325
|
+
<head>
|
|
20326
|
+
<meta charset="UTF-8">
|
|
20327
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
20328
|
+
<title>Chron Review \u2014 ${esc2(framework.toUpperCase())} Controls</title>
|
|
20329
|
+
<style>${REPORT_CSS}</style>
|
|
20330
|
+
</head>
|
|
20331
|
+
<body>
|
|
20332
|
+
<div class="page">
|
|
20333
|
+
|
|
20334
|
+
<div style="min-height:50vh;display:flex;flex-direction:column;justify-content:center">
|
|
20335
|
+
<div class="cover-badge">${esc2(framework.toUpperCase())} Control Review</div>
|
|
20336
|
+
<h1>Chron Review Report</h1>
|
|
20337
|
+
<p class="meta">Generated by <strong>chron review</strong> | Host: <strong>${esc2(host)}</strong></p>
|
|
20338
|
+
<table style="width:auto;margin-bottom:0">
|
|
20339
|
+
<tr><th>Framework</th><td>${esc2(framework.toUpperCase())} Trust Services Criteria</td></tr>
|
|
20340
|
+
<tr><th>Generated</th><td>${esc2(generatedAt)}</td></tr>
|
|
20341
|
+
<tr><th>Sessions Reviewed</th><td>${sessionCount}</td></tr>
|
|
20342
|
+
<tr><th>Findings</th><td>${findings.length}</td></tr>
|
|
20343
|
+
<tr><th>Controls Flagged</th><td>${controlsHit.size > 0 ? Array.from(controlsHit).sort().join(", ") : "None"}</td></tr>
|
|
20344
|
+
</table>
|
|
20345
|
+
</div>
|
|
20346
|
+
|
|
20347
|
+
<div class="disclaimer">
|
|
20348
|
+
<strong>Important:</strong> This report identifies AI-session evidence that may require control-owner review.
|
|
20349
|
+
It is not a certification of compliance, a SOC 2 opinion, or evidence of any control failure.
|
|
20350
|
+
Each finding is a potential review item \u2014 not a violation. A licensed CPA conducting a SOC 2 examination
|
|
20351
|
+
must evaluate whether your controls were suitably designed and operated effectively over the audit period.
|
|
20352
|
+
</div>
|
|
20353
|
+
|
|
20354
|
+
<h2>Summary</h2>
|
|
20355
|
+
<div class="stat-grid">
|
|
20356
|
+
<div class="stat"><div class="stat-value">${sessionCount}</div><div class="stat-label">Sessions Reviewed</div></div>
|
|
20357
|
+
<div class="stat"><div class="stat-value">${findings.length}</div><div class="stat-label">Findings</div></div>
|
|
20358
|
+
<div class="stat"><div class="stat-value">${controlsHit.size}</div><div class="stat-label">Controls Flagged</div></div>
|
|
20359
|
+
</div>
|
|
20360
|
+
${bySeverity.critical > 0 ? `<p><span class="badge badge-critical">CRITICAL</span> ${bySeverity.critical} finding(s) require immediate attention.</p>` : ""}
|
|
20361
|
+
|
|
20362
|
+
<h2>Findings</h2>
|
|
20363
|
+
${findingCards}
|
|
20364
|
+
|
|
20365
|
+
<h2>Methodology</h2>
|
|
20366
|
+
<div class="appendix">
|
|
20367
|
+
<h3>What this report does</h3>
|
|
20368
|
+
<p>Chron Review evaluates AI session logs against a deterministic rule pack aligned to the AICPA Trust Services Criteria.
|
|
20369
|
+
Rules match on structured event types (<code>code_change</code>, <code>secret_detected</code>) recorded by the Chron MCP server
|
|
20370
|
+
during live AI conversations.</p>
|
|
20371
|
+
|
|
20372
|
+
<h3>Rule pack: ${esc2(framework.toUpperCase())} (v1)</h3>
|
|
20373
|
+
<ul>
|
|
20374
|
+
<li><strong>CC6.1</strong> \u2014 AI touched access-control code (auth, IAM, RBAC, permissions, login, JWT, OAuth, SAML, SSO)</li>
|
|
20375
|
+
<li><strong>CC6.1 / CC6.6</strong> \u2014 Sensitive credential detected in AI session (masked at log time, no plaintext stored)</li>
|
|
20376
|
+
<li><strong>CC7.2 / CC8.1</strong> \u2014 AI modified infrastructure or deployment configuration (Terraform, Docker, Kubernetes, pipelines)</li>
|
|
20377
|
+
<li><strong>CC7.2</strong> \u2014 AI modified logging, monitoring, or alerting code</li>
|
|
20378
|
+
<li><strong>CC6.1 / CC6.7</strong> \u2014 AI modified data retention, deletion, export, or encryption logic</li>
|
|
20379
|
+
</ul>
|
|
20380
|
+
|
|
20381
|
+
<h3>What this report does not do</h3>
|
|
20382
|
+
<ul>
|
|
20383
|
+
<li>Does not evaluate whether your controls were suitably designed or operated effectively (that is the auditor's role).</li>
|
|
20384
|
+
<li>Does not access code repositories, CI/CD systems, identity providers, or any environment outside the Chron audit database.</li>
|
|
20385
|
+
<li>Does not make any compliance determination. All findings use the language "potential review item."</li>
|
|
20386
|
+
<li>Does not cover CC1\u2013CC5 (governance, communication, risk assessment) \u2014 these require policy documents, board records, and cannot be evaluated from session logs.</li>
|
|
20387
|
+
</ul>
|
|
20388
|
+
|
|
20389
|
+
<p style="margin-top:12px;font-size:11px;color:#9ca3af">
|
|
20390
|
+
Generated by chron review | ${esc2(generatedAt)} | Host: ${esc2(host)}
|
|
20391
|
+
</p>
|
|
20392
|
+
</div>
|
|
20393
|
+
|
|
20394
|
+
</div>
|
|
20395
|
+
</body>
|
|
20396
|
+
</html>`;
|
|
20397
|
+
}
|
|
20398
|
+
async function countSessions(db) {
|
|
20399
|
+
const client = db.$client;
|
|
20400
|
+
const result = await client.execute("SELECT COUNT(*) AS n FROM sessions");
|
|
20401
|
+
return Number(result.rows[0].n ?? 0);
|
|
20402
|
+
}
|
|
20403
|
+
async function runReview2(args2) {
|
|
20404
|
+
const frameworkArg = args2.find((a) => a.startsWith("--framework="))?.slice("--framework=".length)?.toLowerCase();
|
|
20405
|
+
const outputArg = args2.find((a) => a.startsWith("--output="))?.slice("--output=".length);
|
|
20406
|
+
const sinceArg = args2.find((a) => a.startsWith("--since="))?.slice("--since=".length);
|
|
20407
|
+
if (!frameworkArg) {
|
|
20408
|
+
process.stderr.write("Usage: chron review --framework=soc2 [--since=<range>] [--output=<file>]\n");
|
|
20409
|
+
process.exit(1);
|
|
20410
|
+
}
|
|
20411
|
+
const rules = FRAMEWORKS[frameworkArg];
|
|
20412
|
+
if (!rules) {
|
|
20413
|
+
process.stderr.write(`Unknown framework: "${frameworkArg}". Available: ${Object.keys(FRAMEWORKS).join(", ")}
|
|
20414
|
+
`);
|
|
20415
|
+
process.exit(1);
|
|
20416
|
+
}
|
|
20417
|
+
let since;
|
|
20418
|
+
if (sinceArg) {
|
|
20419
|
+
try {
|
|
20420
|
+
since = parseSince(sinceArg);
|
|
20421
|
+
} catch (err) {
|
|
20422
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
20423
|
+
`);
|
|
20424
|
+
process.exit(1);
|
|
20425
|
+
}
|
|
20426
|
+
}
|
|
20427
|
+
const db = await initDb();
|
|
20428
|
+
const [findings, sessionCount] = await Promise.all([
|
|
20429
|
+
runReview(db, rules, since),
|
|
20430
|
+
countSessions(db)
|
|
20431
|
+
]);
|
|
20432
|
+
printFindings(findings, frameworkArg, sessionCount);
|
|
20433
|
+
if (outputArg) {
|
|
20434
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
|
20435
|
+
const html = buildReviewHtml({
|
|
20436
|
+
framework: frameworkArg,
|
|
20437
|
+
host: (0, import_os11.hostname)(),
|
|
20438
|
+
generatedAt,
|
|
20439
|
+
sessionCount,
|
|
20440
|
+
findings
|
|
20441
|
+
});
|
|
20442
|
+
(0, import_fs12.writeFileSync)(outputArg, html, "utf8");
|
|
20443
|
+
process.stdout.write(`Review report written to ${outputArg}
|
|
20444
|
+
`);
|
|
20445
|
+
}
|
|
20446
|
+
}
|
|
20447
|
+
var import_fs12, import_os11, RESET10, BOLD10, DIM9, RED5, YELLOW8, CYAN8, MAGENTA2, SEV_COLOR, REPORT_CSS;
|
|
20448
|
+
var init_review = __esm({
|
|
20449
|
+
"src/cli/review.ts"() {
|
|
20450
|
+
"use strict";
|
|
20451
|
+
import_fs12 = require("fs");
|
|
20452
|
+
import_os11 = require("os");
|
|
20453
|
+
init_db2();
|
|
20454
|
+
init_report();
|
|
20455
|
+
init_rules();
|
|
20456
|
+
init_engine();
|
|
20457
|
+
RESET10 = "\x1B[0m";
|
|
20458
|
+
BOLD10 = "\x1B[1m";
|
|
20459
|
+
DIM9 = "\x1B[2m";
|
|
20460
|
+
RED5 = "\x1B[31m";
|
|
20461
|
+
YELLOW8 = "\x1B[33m";
|
|
20462
|
+
CYAN8 = "\x1B[36m";
|
|
20463
|
+
MAGENTA2 = "\x1B[35m";
|
|
20464
|
+
SEV_COLOR = {
|
|
20465
|
+
critical: RED5,
|
|
20466
|
+
high: YELLOW8,
|
|
20467
|
+
medium: CYAN8,
|
|
20468
|
+
low: DIM9
|
|
20469
|
+
};
|
|
20470
|
+
REPORT_CSS = `
|
|
20471
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
20472
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; color: #111; background: #fff; line-height: 1.5; }
|
|
20473
|
+
.page { max-width: 900px; margin: 0 auto; padding: 40px 48px; }
|
|
20474
|
+
h1 { font-size: 28px; font-weight: 700; margin-bottom: 4px; }
|
|
20475
|
+
h2 { font-size: 17px; font-weight: 700; margin: 32px 0 10px; padding-bottom: 4px; border-bottom: 2px solid #e5e7eb; color: #1f2937; }
|
|
20476
|
+
h3 { font-size: 14px; font-weight: 600; margin: 20px 0 6px; color: #374151; }
|
|
20477
|
+
p { margin-bottom: 8px; color: #374151; }
|
|
20478
|
+
.meta { color: #6b7280; font-size: 12px; margin-bottom: 32px; }
|
|
20479
|
+
.cover-badge { display: inline-block; background: #1d4ed8; color: #fff; padding: 3px 10px; border-radius: 4px; font-size: 12px; font-weight: 600; margin-bottom: 16px; }
|
|
20480
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-size: 12px; }
|
|
20481
|
+
th { background: #f3f4f6; text-align: left; padding: 6px 10px; font-weight: 600; color: #374151; border: 1px solid #e5e7eb; }
|
|
20482
|
+
td { padding: 5px 10px; border: 1px solid #e5e7eb; vertical-align: top; word-break: break-word; }
|
|
20483
|
+
tr:nth-child(even) td { background: #fafafa; }
|
|
20484
|
+
.stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px; }
|
|
20485
|
+
.stat { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 12px 16px; }
|
|
20486
|
+
.stat-value { font-size: 24px; font-weight: 700; color: #1f2937; }
|
|
20487
|
+
.stat-label { font-size: 11px; color: #6b7280; margin-top: 2px; }
|
|
20488
|
+
.badge { display:inline-block; padding:1px 7px; border-radius:3px; font-size:11px; font-weight:600; color:#fff; }
|
|
20489
|
+
.badge-critical { background:#dc2626; }
|
|
20490
|
+
.badge-high { background:#d97706; }
|
|
20491
|
+
.badge-medium { background:#2563eb; }
|
|
20492
|
+
.badge-low { background:#6b7280; }
|
|
20493
|
+
.disclaimer { background: #fffbeb; border: 1px solid #fde68a; border-left: 4px solid #f59e0b; border-radius: 4px; padding: 12px 14px; margin: 14px 0 20px; font-size: 12px; color: #374151; }
|
|
20494
|
+
.finding-card { border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 14px; overflow: hidden; }
|
|
20495
|
+
.finding-header { display:flex; align-items:center; gap:10px; padding:10px 14px; background:#f9fafb; border-bottom:1px solid #e5e7eb; }
|
|
20496
|
+
.finding-controls { font-size:12px; font-weight:700; color:#1f2937; }
|
|
20497
|
+
.finding-session { font-family:monospace; font-size:11px; color:#6b7280; }
|
|
20498
|
+
.finding-body { padding:10px 14px; }
|
|
20499
|
+
.finding-text { font-size:13px; color:#111; margin-bottom:6px; }
|
|
20500
|
+
.finding-disclaimer { font-size:11px; color:#6b7280; margin-bottom:8px; }
|
|
20501
|
+
.evidence-list { margin:0 0 8px 16px; }
|
|
20502
|
+
.evidence-list li { font-size:11px; font-family:monospace; color:#374151; margin-bottom:2px; }
|
|
20503
|
+
.suggested-list { margin:0 0 0 16px; }
|
|
20504
|
+
.suggested-list li { font-size:11px; color:#374151; margin-bottom:2px; }
|
|
20505
|
+
.appendix { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px 20px; margin-top: 12px; }
|
|
20506
|
+
ul { padding-left: 20px; margin-bottom: 8px; }
|
|
20507
|
+
li { margin-bottom: 4px; }
|
|
20508
|
+
@media print {
|
|
20509
|
+
body { font-size: 11px; }
|
|
20510
|
+
.page { padding: 20px 24px; max-width: 100%; }
|
|
20511
|
+
h1 { font-size: 22px; }
|
|
20512
|
+
h2 { font-size: 14px; }
|
|
20513
|
+
}
|
|
20514
|
+
`;
|
|
20515
|
+
}
|
|
20516
|
+
});
|
|
20517
|
+
|
|
19903
20518
|
// src/cli/index.ts
|
|
19904
20519
|
var [, , command, ...args] = process.argv;
|
|
19905
20520
|
async function main() {
|
|
@@ -19964,6 +20579,11 @@ async function main() {
|
|
|
19964
20579
|
await runImport2(args);
|
|
19965
20580
|
break;
|
|
19966
20581
|
}
|
|
20582
|
+
case "review": {
|
|
20583
|
+
const { runReview: runReview3 } = await Promise.resolve().then(() => (init_review(), review_exports));
|
|
20584
|
+
await runReview3(args);
|
|
20585
|
+
break;
|
|
20586
|
+
}
|
|
19967
20587
|
default: {
|
|
19968
20588
|
const name = command ? `Unknown command: ${command}
|
|
19969
20589
|
|
|
@@ -19984,11 +20604,13 @@ Commands:
|
|
|
19984
20604
|
prune Delete sessions older than a retention cutoff
|
|
19985
20605
|
doctor Check your Chron setup \u2014 Node version, DB, MCP configs, SIEM
|
|
19986
20606
|
import Import conversations from external AI tools
|
|
20607
|
+
review Review AI sessions against compliance control criteria
|
|
19987
20608
|
|
|
19988
20609
|
Options (history):
|
|
19989
|
-
--limit=<n>
|
|
19990
|
-
--
|
|
19991
|
-
|
|
20610
|
+
--limit=<n> Max sessions to show (default: 20)
|
|
20611
|
+
--search=<query> Full-text search across all sessions (FTS5: phrases, boolean, prefix*)
|
|
20612
|
+
--ref=<value> Filter by external_ref prefix
|
|
20613
|
+
<id-prefix> Show full log for the session with this ID prefix
|
|
19992
20614
|
|
|
19993
20615
|
Options (report):
|
|
19994
20616
|
--since=<range> Filter by date: 7d, 30d, or YYYY-MM-DD (default: all time)
|
|
@@ -20016,6 +20638,11 @@ Options (doctor):
|
|
|
20016
20638
|
|
|
20017
20639
|
Options (import):
|
|
20018
20640
|
chatgpt <file> Import from ChatGPT export (.zip or conversations.json)
|
|
20641
|
+
|
|
20642
|
+
Options (review):
|
|
20643
|
+
--framework=<name> Framework to review against: soc2
|
|
20644
|
+
--since=<range> Limit to sessions since: 7d, 30d, or YYYY-MM-DD
|
|
20645
|
+
--output=<file> Write HTML report to file (printable to PDF from browser)
|
|
20019
20646
|
`
|
|
20020
20647
|
);
|
|
20021
20648
|
process.exit(command ? 1 : 0);
|
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.
|
|
38505
|
+
version4 = "0.1.30";
|
|
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
|
|
38843
|
-
const
|
|
38844
|
-
if (
|
|
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
|
}
|