chron-mcp 0.1.25 → 0.1.28
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 +34 -0
- package/dist/cli/index.js +796 -29
- package/dist/index.js +44 -3
- package/package.json +1 -1
- package/skills/codex.skill.md +73 -0
package/README.md
CHANGED
|
@@ -40,6 +40,8 @@ Chron stores a local audit trail of:
|
|
|
40
40
|
|
|
41
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.
|
|
42
42
|
|
|
43
|
+
Need runtime policy enforcement for AI agents? CLAIIM adds agent identity, approval workflows, and ALLOW/DENY gates on top of Chron proof. Try the public preview: https://claiim.io/preview
|
|
44
|
+
|
|
43
45
|
---
|
|
44
46
|
|
|
45
47
|
## Install
|
|
@@ -102,6 +104,7 @@ Commands:
|
|
|
102
104
|
verify Verify a session's hash chain and Ed25519 signature
|
|
103
105
|
prune Delete sessions older than a retention cutoff
|
|
104
106
|
doctor Check your Chron setup — Node version, DB, MCP configs, SIEM
|
|
107
|
+
import Import conversations from external AI tools into Chron
|
|
105
108
|
|
|
106
109
|
Options (history):
|
|
107
110
|
--limit=<n> Max sessions to show (default: 20)
|
|
@@ -131,6 +134,9 @@ Options (prune):
|
|
|
131
134
|
|
|
132
135
|
Options (doctor):
|
|
133
136
|
--json Machine-readable JSON output
|
|
137
|
+
|
|
138
|
+
Options (import):
|
|
139
|
+
chatgpt <file> Import from ChatGPT export (.zip or conversations.json)
|
|
134
140
|
```
|
|
135
141
|
|
|
136
142
|
### `chron doctor`
|
|
@@ -643,6 +649,34 @@ Point your MCP config at the URL:
|
|
|
643
649
|
|
|
644
650
|
---
|
|
645
651
|
|
|
652
|
+
## ChatGPT import
|
|
653
|
+
|
|
654
|
+
Bring your existing ChatGPT history into Chron's tamper-evident audit trail.
|
|
655
|
+
|
|
656
|
+
```bash
|
|
657
|
+
# From a ChatGPT data export ZIP
|
|
658
|
+
chron import chatgpt ~/Downloads/chatgpt-export.zip
|
|
659
|
+
|
|
660
|
+
# Or from an extracted conversations.json
|
|
661
|
+
chron import chatgpt ~/Downloads/conversations.json
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
**How to get your ChatGPT export:**
|
|
665
|
+
ChatGPT → Settings → Data Controls → Export data → wait for email → download the ZIP.
|
|
666
|
+
|
|
667
|
+
**What gets imported:**
|
|
668
|
+
- One Chron session per conversation, with `ai_tool=chatgpt`
|
|
669
|
+
- Original message timestamps from the export
|
|
670
|
+
- `external_ref=chatgpt:<conversation_id>` on every session (visible in `chron history`)
|
|
671
|
+
- Full SHA-256 hash chain across all imported messages
|
|
672
|
+
- Secret detection runs on all user messages
|
|
673
|
+
|
|
674
|
+
**Re-running is safe** — already-imported conversations are skipped by `external_ref` match.
|
|
675
|
+
|
|
676
|
+
After import, sessions appear in `chron history` and `chron verify` works on them the same as any natively-logged session.
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
646
680
|
## Your data
|
|
647
681
|
|
|
648
682
|
Your audit log lives at `~/.chron/chron.db` — a single SQLite file on your machine. Query it directly with any SQLite tool:
|
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 join11(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 = join11;
|
|
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 readFileSync8 = (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: readFileSync8,
|
|
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: readFileSync8 } = 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 = readFileSync8(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 = readFileSync8(LDD_PATH);
|
|
2972
2972
|
const versionMatch = lddContent.match(RE_GLIBC_VERSION);
|
|
2973
2973
|
if (versionMatch) {
|
|
2974
2974
|
cachedVersionFilesystem = versionMatch[1];
|
|
@@ -5265,7 +5265,7 @@ var require_sender = __commonJS({
|
|
|
5265
5265
|
"node_modules/ws/lib/sender.js"(exports2, module2) {
|
|
5266
5266
|
"use strict";
|
|
5267
5267
|
var { Duplex } = require("stream");
|
|
5268
|
-
var { randomFillSync } = require("crypto");
|
|
5268
|
+
var { randomFillSync: randomFillSync2 } = require("crypto");
|
|
5269
5269
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
5270
5270
|
var { EMPTY_BUFFER, kWebSocket, NOOP } = require_constants();
|
|
5271
5271
|
var { isBlob, isValidStatusCode } = require_validation();
|
|
@@ -5337,7 +5337,7 @@ var require_sender = __commonJS({
|
|
|
5337
5337
|
if (randomPool === void 0) {
|
|
5338
5338
|
randomPool = Buffer.alloc(RANDOM_POOL_SIZE);
|
|
5339
5339
|
}
|
|
5340
|
-
|
|
5340
|
+
randomFillSync2(randomPool, 0, RANDOM_POOL_SIZE);
|
|
5341
5341
|
randomPoolPointer = 0;
|
|
5342
5342
|
}
|
|
5343
5343
|
mask[0] = randomPool[randomPoolPointer++];
|
|
@@ -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((join11) => join11.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((join11) => join11.alias === tableName)) {
|
|
15145
15145
|
throw new Error(`Alias "${tableName}" is already used in this query`);
|
|
15146
15146
|
}
|
|
15147
15147
|
if (typeof on === "function") {
|
|
@@ -16582,6 +16582,7 @@ async function initDb(dbPath3) {
|
|
|
16582
16582
|
}
|
|
16583
16583
|
const client = createClient({ url: path.startsWith(":") ? path : `file:${path}` });
|
|
16584
16584
|
await client.execute("PRAGMA journal_mode = WAL");
|
|
16585
|
+
await client.execute("PRAGMA busy_timeout = 5000");
|
|
16585
16586
|
await client.execute("PRAGMA foreign_keys = ON");
|
|
16586
16587
|
for (const sql2 of CREATE_SQL) {
|
|
16587
16588
|
await client.execute(sql2);
|
|
@@ -17032,12 +17033,16 @@ ${overview.total_detections > 0 ? `<p class="warn">⚠ ${overview.total_dete
|
|
|
17032
17033
|
</tbody>
|
|
17033
17034
|
</table>
|
|
17034
17035
|
${brokenList}
|
|
17035
|
-
<p style="font-size:11px;color:#6b7280">
|
|
17036
|
-
|
|
17037
|
-
|
|
17038
|
-
</p>
|
|
17036
|
+
<p style="font-size:11px;color:#6b7280">
|
|
17037
|
+
Note: This report verifies <em>chain linkage</em> (prev_hash continuity). Full content-hash recomputation
|
|
17038
|
+
(which detects in-place tampering) can be performed with <code>chron verify <session-id></code>.
|
|
17039
|
+
</p>
|
|
17040
|
+
<p class="governance-note">
|
|
17041
|
+
For formal AI governance, CLAIIM extends Chron evidence with agent identity, policy gates,
|
|
17042
|
+
approvals, and runtime ALLOW/DENY enforcement. See <a href="https://claiim.io/preview">https://claiim.io/preview</a>.
|
|
17043
|
+
</p>
|
|
17039
17044
|
|
|
17040
|
-
<!-- \u2466 SESSION INVENTORY -->
|
|
17045
|
+
<!-- \u2466 SESSION INVENTORY -->
|
|
17041
17046
|
<h2 class="section-break">Session Inventory</h2>
|
|
17042
17047
|
<p>All sessions recorded in the audit database (most recent first, capped at 500 rows).</p>
|
|
17043
17048
|
<table>
|
|
@@ -17161,6 +17166,7 @@ tr:nth-child(even) td { background: #fafafa; }
|
|
|
17161
17166
|
.section-break { page-break-before: always; }
|
|
17162
17167
|
.appendix { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px 20px; margin-top: 12px; }
|
|
17163
17168
|
.appendix p, .appendix li { color: #374151; font-size: 12px; }
|
|
17169
|
+
.governance-note { background: #f8fafc; border: 1px solid #dbeafe; border-left: 4px solid #2563eb; border-radius: 4px; padding: 12px 14px; margin: 14px 0 20px; font-size: 12px; color: #374151; }
|
|
17164
17170
|
ul { padding-left: 20px; margin-bottom: 8px; }
|
|
17165
17171
|
li { margin-bottom: 4px; }
|
|
17166
17172
|
@media print {
|
|
@@ -17302,6 +17308,20 @@ function keysDir() {
|
|
|
17302
17308
|
function privKeyPath(sessionId) {
|
|
17303
17309
|
return (0, import_path2.join)(keysDir(), `${sessionId}.key`);
|
|
17304
17310
|
}
|
|
17311
|
+
function pubKeyPath(sessionId) {
|
|
17312
|
+
return (0, import_path2.join)(keysDir(), `${sessionId}.pub`);
|
|
17313
|
+
}
|
|
17314
|
+
function generateSessionKeypair(sessionId) {
|
|
17315
|
+
const { privateKey, publicKey } = (0, import_crypto2.generateKeyPairSync)("ed25519", {
|
|
17316
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
17317
|
+
publicKeyEncoding: { type: "spki", format: "pem" }
|
|
17318
|
+
});
|
|
17319
|
+
const dir = keysDir();
|
|
17320
|
+
(0, import_fs3.mkdirSync)(dir, { recursive: true });
|
|
17321
|
+
(0, import_fs3.writeFileSync)(privKeyPath(sessionId), privateKey, { mode: 384 });
|
|
17322
|
+
(0, import_fs3.writeFileSync)(pubKeyPath(sessionId), publicKey);
|
|
17323
|
+
return publicKey;
|
|
17324
|
+
}
|
|
17305
17325
|
function sessionDigest(sessionId, finalContentHash, messageCount, firstCreatedAt) {
|
|
17306
17326
|
const input = `${sessionId}|${finalContentHash}|${messageCount}|${firstCreatedAt}`;
|
|
17307
17327
|
return (0, import_crypto2.createHash)("sha256").update(input).digest();
|
|
@@ -17356,7 +17376,7 @@ var require_package = __commonJS({
|
|
|
17356
17376
|
"package.json"(exports2, module2) {
|
|
17357
17377
|
module2.exports = {
|
|
17358
17378
|
name: "chron-mcp",
|
|
17359
|
-
version: "0.1.
|
|
17379
|
+
version: "0.1.28",
|
|
17360
17380
|
mcpName: "io.github.sirinivask/chron",
|
|
17361
17381
|
description: "Audit-grade timestamped logs for every AI conversation",
|
|
17362
17382
|
repository: {
|
|
@@ -18163,6 +18183,93 @@ ${DIM4}Authenticating with Azure AD...${RESET5} `);
|
|
|
18163
18183
|
`);
|
|
18164
18184
|
process.stdout.write(`${DIM4}Config saved to ~/.chron/config.json \u2014 events will flow immediately in all running sessions.${RESET5}
|
|
18165
18185
|
|
|
18186
|
+
`);
|
|
18187
|
+
}
|
|
18188
|
+
function codexGlobalConfigPath() {
|
|
18189
|
+
return (0, import_path5.join)((0, import_os6.homedir)(), ".codex", "config.toml");
|
|
18190
|
+
}
|
|
18191
|
+
function isChronInCodexConfig(content) {
|
|
18192
|
+
return content.includes("[mcp_servers.chron]");
|
|
18193
|
+
}
|
|
18194
|
+
function appendChronToToml(content) {
|
|
18195
|
+
return content.trimEnd() + "\n" + CODEX_MCP_BLOCK;
|
|
18196
|
+
}
|
|
18197
|
+
async function connectCodex() {
|
|
18198
|
+
process.stdout.write(`
|
|
18199
|
+
${BOLD5}Connect Chron \u2192 Codex${RESET5}
|
|
18200
|
+
|
|
18201
|
+
`);
|
|
18202
|
+
const globalPath = codexGlobalConfigPath();
|
|
18203
|
+
const projectPath = (0, import_path5.join)(process.cwd(), ".codex", "config.toml");
|
|
18204
|
+
const hasGlobal = (0, import_fs5.existsSync)(globalPath);
|
|
18205
|
+
const hasProject = (0, import_fs5.existsSync)(projectPath);
|
|
18206
|
+
let targetPath;
|
|
18207
|
+
let isProject = false;
|
|
18208
|
+
if (hasGlobal && hasProject) {
|
|
18209
|
+
process.stdout.write(`${DIM4}Found both global and project Codex configs.${RESET5}
|
|
18210
|
+
`);
|
|
18211
|
+
process.stdout.write(`${DIM4} Global: ${globalPath}${RESET5}
|
|
18212
|
+
`);
|
|
18213
|
+
process.stdout.write(`${DIM4} Project: ${projectPath}${RESET5}
|
|
18214
|
+
|
|
18215
|
+
`);
|
|
18216
|
+
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
18217
|
+
const choice = await new Promise(
|
|
18218
|
+
(resolve) => rl.question(` Add Chron to (g)lobal or (p)roject config? [g/p]: `, resolve)
|
|
18219
|
+
);
|
|
18220
|
+
rl.close();
|
|
18221
|
+
isProject = choice.trim().toLowerCase() === "p";
|
|
18222
|
+
targetPath = isProject ? projectPath : globalPath;
|
|
18223
|
+
} else if (hasProject) {
|
|
18224
|
+
targetPath = projectPath;
|
|
18225
|
+
isProject = true;
|
|
18226
|
+
} else if (hasGlobal) {
|
|
18227
|
+
targetPath = globalPath;
|
|
18228
|
+
} else {
|
|
18229
|
+
process.stdout.write(`${YELLOW3}!${RESET5} Codex config not found. Creating ${globalPath}
|
|
18230
|
+
|
|
18231
|
+
`);
|
|
18232
|
+
(0, import_fs5.mkdirSync)((0, import_path5.join)((0, import_os6.homedir)(), ".codex"), { recursive: true });
|
|
18233
|
+
(0, import_fs5.writeFileSync)(globalPath, CODEX_MCP_BLOCK.trimStart());
|
|
18234
|
+
process.stdout.write(`${GREEN2}\u2713${RESET5} Created ${globalPath} with Chron MCP config.
|
|
18235
|
+
`);
|
|
18236
|
+
_printCodexNextSteps();
|
|
18237
|
+
return;
|
|
18238
|
+
}
|
|
18239
|
+
const existing = (0, import_fs5.readFileSync)(targetPath, "utf8");
|
|
18240
|
+
if (isChronInCodexConfig(existing)) {
|
|
18241
|
+
process.stdout.write(`${GREEN2}\u2713${RESET5} Chron MCP is already configured in ${targetPath}
|
|
18242
|
+
|
|
18243
|
+
`);
|
|
18244
|
+
_printCodexNextSteps();
|
|
18245
|
+
return;
|
|
18246
|
+
}
|
|
18247
|
+
const updated = appendChronToToml(existing);
|
|
18248
|
+
(0, import_fs5.writeFileSync)(targetPath, updated, "utf8");
|
|
18249
|
+
process.stdout.write(`${GREEN2}\u2713${RESET5} Added Chron MCP to ${targetPath}
|
|
18250
|
+
|
|
18251
|
+
`);
|
|
18252
|
+
_printCodexNextSteps();
|
|
18253
|
+
}
|
|
18254
|
+
function _printCodexNextSteps() {
|
|
18255
|
+
process.stdout.write(`${BOLD5}Next steps:${RESET5}
|
|
18256
|
+
|
|
18257
|
+
`);
|
|
18258
|
+
process.stdout.write(` 1. Restart Codex to pick up the new MCP server.
|
|
18259
|
+
`);
|
|
18260
|
+
process.stdout.write(` 2. Ask Codex to start a Chron session:
|
|
18261
|
+
`);
|
|
18262
|
+
process.stdout.write(` ${DIM4}"Use init_session to start a Chron audit session for this work."${RESET5}
|
|
18263
|
+
`);
|
|
18264
|
+
process.stdout.write(` 3. Verify setup:
|
|
18265
|
+
`);
|
|
18266
|
+
process.stdout.write(` ${CYAN4}chron doctor${RESET5}
|
|
18267
|
+
|
|
18268
|
+
`);
|
|
18269
|
+
process.stdout.write(`${DIM4}Codex will automatically call log_tool_call, log_tool_result, and
|
|
18270
|
+
`);
|
|
18271
|
+
process.stdout.write(`log_code_change during its work once the session is started.${RESET5}
|
|
18272
|
+
|
|
18166
18273
|
`);
|
|
18167
18274
|
}
|
|
18168
18275
|
async function runConnect(args2) {
|
|
@@ -18177,6 +18284,9 @@ async function runConnect(args2) {
|
|
|
18177
18284
|
case "splunk":
|
|
18178
18285
|
await connectSplunk();
|
|
18179
18286
|
break;
|
|
18287
|
+
case "codex":
|
|
18288
|
+
await connectCodex();
|
|
18289
|
+
break;
|
|
18180
18290
|
default: {
|
|
18181
18291
|
const name = subcommand ? `Unknown integration: ${subcommand}
|
|
18182
18292
|
|
|
@@ -18185,6 +18295,7 @@ async function runConnect(args2) {
|
|
|
18185
18295
|
`${name}Usage: chron connect <integration>
|
|
18186
18296
|
|
|
18187
18297
|
Integrations:
|
|
18298
|
+
codex Add Chron MCP to Codex config (global or project)
|
|
18188
18299
|
crowdstrike Connect to CrowdStrike LogScale (direct ingest)
|
|
18189
18300
|
sentinel Connect to Microsoft Sentinel (Azure Monitor Logs Ingestion API)
|
|
18190
18301
|
splunk Connect to Splunk via HTTP Event Collector (HEC)
|
|
@@ -18194,7 +18305,7 @@ Integrations:
|
|
|
18194
18305
|
}
|
|
18195
18306
|
}
|
|
18196
18307
|
}
|
|
18197
|
-
var import_readline, import_os6, import_path5, import_fs5, RESET5, BOLD5, DIM4, CYAN4, GREEN2, RED, YELLOW3;
|
|
18308
|
+
var import_readline, import_os6, import_path5, import_fs5, RESET5, BOLD5, DIM4, CYAN4, GREEN2, RED, YELLOW3, CODEX_MCP_BLOCK;
|
|
18198
18309
|
var init_connect = __esm({
|
|
18199
18310
|
"src/cli/connect.ts"() {
|
|
18200
18311
|
"use strict";
|
|
@@ -18209,6 +18320,11 @@ var init_connect = __esm({
|
|
|
18209
18320
|
GREEN2 = "\x1B[32m";
|
|
18210
18321
|
RED = "\x1B[31m";
|
|
18211
18322
|
YELLOW3 = "\x1B[33m";
|
|
18323
|
+
CODEX_MCP_BLOCK = `
|
|
18324
|
+
[mcp_servers.chron]
|
|
18325
|
+
command = "npx"
|
|
18326
|
+
args = ["-y", "chron-mcp"]
|
|
18327
|
+
`;
|
|
18212
18328
|
}
|
|
18213
18329
|
});
|
|
18214
18330
|
|
|
@@ -18531,7 +18647,15 @@ async function runSign(args2) {
|
|
|
18531
18647
|
process.exit(1);
|
|
18532
18648
|
}
|
|
18533
18649
|
const signature = signSession(session.id, finalContentHash, messageCount, firstCreatedAt);
|
|
18534
|
-
|
|
18650
|
+
try {
|
|
18651
|
+
await db.update(sessions).set({ signature }).where(eq(sessions.id, session.id));
|
|
18652
|
+
} catch (err) {
|
|
18653
|
+
const cause = err?.cause ?? err;
|
|
18654
|
+
process.stderr.write(`Failed to save signature: ${cause?.message ?? err.message}
|
|
18655
|
+
`);
|
|
18656
|
+
process.stderr.write("Tip: if the Chron MCP server is running, it may hold a write lock. Retry or stop the MCP server first.\n");
|
|
18657
|
+
process.exit(1);
|
|
18658
|
+
}
|
|
18535
18659
|
const sigData = {
|
|
18536
18660
|
chron_signature: "v1",
|
|
18537
18661
|
session_id: session.id,
|
|
@@ -18651,9 +18775,9 @@ ${BOLD7}Signature${RESET7}
|
|
|
18651
18775
|
warn("No manifest.sig \u2014 bundle was not signed");
|
|
18652
18776
|
}
|
|
18653
18777
|
if (sigFile?.session_id && sigFile?.signature) {
|
|
18654
|
-
const
|
|
18778
|
+
const pubKeyPath2 = (0, import_path8.join)(tempDir, "pubkeys", `${sigFile.session_id}.pub`);
|
|
18655
18779
|
try {
|
|
18656
|
-
const pubKey = (0, import_fs9.readFileSync)(
|
|
18780
|
+
const pubKey = (0, import_fs9.readFileSync)(pubKeyPath2, "utf8");
|
|
18657
18781
|
const valid = verifyBufferSignature(pubKey, sigFile.signature, manifestBytes);
|
|
18658
18782
|
if (valid) {
|
|
18659
18783
|
ok(`Ed25519 signature valid (session ${sigFile.session_id.slice(0, 8)})`);
|
|
@@ -18856,7 +18980,7 @@ function loadConfig2() {
|
|
|
18856
18980
|
function mcpConfigs() {
|
|
18857
18981
|
const home = (0, import_os9.homedir)();
|
|
18858
18982
|
const plat = (0, import_os9.platform)();
|
|
18859
|
-
const
|
|
18983
|
+
const jsonCandidates = [
|
|
18860
18984
|
{
|
|
18861
18985
|
name: "Claude Desktop",
|
|
18862
18986
|
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")
|
|
@@ -18865,7 +18989,7 @@ function mcpConfigs() {
|
|
|
18865
18989
|
{ name: "Cursor", path: (0, import_path9.join)(home, ".cursor", "mcp.json") },
|
|
18866
18990
|
{ name: "Windsurf", path: (0, import_path9.join)(home, ".codeium", "windsurf", "mcp_config.json") }
|
|
18867
18991
|
];
|
|
18868
|
-
|
|
18992
|
+
const results = jsonCandidates.map((c) => {
|
|
18869
18993
|
if (!(0, import_fs10.existsSync)(c.path)) {
|
|
18870
18994
|
return { name: c.name, path: c.path, exists: false, chronConfigured: null };
|
|
18871
18995
|
}
|
|
@@ -18880,6 +19004,22 @@ function mcpConfigs() {
|
|
|
18880
19004
|
return { name: c.name, path: c.path, exists: true, chronConfigured: null, note: "Could not parse config" };
|
|
18881
19005
|
}
|
|
18882
19006
|
});
|
|
19007
|
+
const codexGlobal = (0, import_path9.join)(home, ".codex", "config.toml");
|
|
19008
|
+
const codexProject = (0, import_path9.join)(process.cwd(), ".codex", "config.toml");
|
|
19009
|
+
for (const p of [codexGlobal, codexProject]) {
|
|
19010
|
+
if (!(0, import_fs10.existsSync)(p)) {
|
|
19011
|
+
results.push({ name: p === codexGlobal ? "Codex (global)" : "Codex (project)", path: p, exists: false, chronConfigured: null });
|
|
19012
|
+
} else {
|
|
19013
|
+
try {
|
|
19014
|
+
const content = (0, import_fs10.readFileSync)(p, "utf8");
|
|
19015
|
+
const configured = content.includes("[mcp_servers.chron]");
|
|
19016
|
+
results.push({ name: p === codexGlobal ? "Codex (global)" : "Codex (project)", path: p, exists: true, chronConfigured: configured });
|
|
19017
|
+
} catch {
|
|
19018
|
+
results.push({ name: p === codexGlobal ? "Codex (global)" : "Codex (project)", path: p, exists: true, chronConfigured: null, note: "Could not read config" });
|
|
19019
|
+
}
|
|
19020
|
+
}
|
|
19021
|
+
}
|
|
19022
|
+
return results;
|
|
18883
19023
|
}
|
|
18884
19024
|
function npmLatestVersion() {
|
|
18885
19025
|
try {
|
|
@@ -19009,14 +19149,16 @@ ${BOLD8}chron doctor${RESET8} ${DIM7}v${import_package2.version}${RESET8}
|
|
|
19009
19149
|
if (!jsonMode)
|
|
19010
19150
|
ok2(`${t.name}`, "chron configured");
|
|
19011
19151
|
} else {
|
|
19152
|
+
const isCodex = t.name.startsWith("Codex");
|
|
19153
|
+
const fixHint = isCodex ? `chron connect codex` : `Add chron to mcpServers in ${t.path}
|
|
19154
|
+
See: https://github.com/sirinivask/chron#installation`;
|
|
19012
19155
|
results.push({
|
|
19013
19156
|
pass: "warn",
|
|
19014
|
-
label: `${t.name}: chron
|
|
19015
|
-
fix:
|
|
19016
|
-
See: https://github.com/sirinivask/chron#installation`
|
|
19157
|
+
label: `${t.name}: chron not configured`,
|
|
19158
|
+
fix: fixHint
|
|
19017
19159
|
});
|
|
19018
19160
|
if (!jsonMode)
|
|
19019
|
-
warn2(`${t.name}`, `chron not in mcpServers \u2014 add it to ${t.path}`);
|
|
19161
|
+
warn2(`${t.name}`, isCodex ? "chron not in MCP servers \u2014 run: chron connect codex" : `chron not in mcpServers \u2014 add it to ${t.path}`);
|
|
19020
19162
|
}
|
|
19021
19163
|
}
|
|
19022
19164
|
if (!jsonMode)
|
|
@@ -19075,6 +19217,9 @@ ${BOLD8}chron doctor${RESET8} ${DIM7}v${import_package2.version}${RESET8}
|
|
|
19075
19217
|
if (failures.length === 0 && warnings.length === 0) {
|
|
19076
19218
|
process.stdout.write(`${GREEN5}${BOLD8}All checks passed.${RESET8} Chron is correctly set up.
|
|
19077
19219
|
|
|
19220
|
+
`);
|
|
19221
|
+
process.stdout.write(`${DIM7}Team AI governance: CLAIIM adds runtime ALLOW/DENY gates on top of Chron proof \u2014 ${CLAIIM_PREVIEW_URL}${RESET8}
|
|
19222
|
+
|
|
19078
19223
|
`);
|
|
19079
19224
|
} else {
|
|
19080
19225
|
if (failures.length > 0) {
|
|
@@ -19123,7 +19268,7 @@ ${BOLD8}chron doctor${RESET8} ${DIM7}v${import_package2.version}${RESET8}
|
|
|
19123
19268
|
}
|
|
19124
19269
|
process.exit(failures.length > 0 ? 1 : 0);
|
|
19125
19270
|
}
|
|
19126
|
-
var import_os9, import_path9, import_fs10, import_child_process3, import_package2, RESET8, BOLD8, DIM7, GREEN5, RED4, YELLOW6, CYAN6;
|
|
19271
|
+
var import_os9, import_path9, import_fs10, import_child_process3, import_package2, RESET8, BOLD8, DIM7, GREEN5, RED4, YELLOW6, CYAN6, CLAIIM_PREVIEW_URL;
|
|
19127
19272
|
var init_doctor = __esm({
|
|
19128
19273
|
"src/cli/doctor.ts"() {
|
|
19129
19274
|
"use strict";
|
|
@@ -19139,6 +19284,619 @@ var init_doctor = __esm({
|
|
|
19139
19284
|
RED4 = "\x1B[31m";
|
|
19140
19285
|
YELLOW6 = "\x1B[33m";
|
|
19141
19286
|
CYAN6 = "\x1B[36m";
|
|
19287
|
+
CLAIIM_PREVIEW_URL = "https://claiim.io/preview";
|
|
19288
|
+
}
|
|
19289
|
+
});
|
|
19290
|
+
|
|
19291
|
+
// node_modules/uuid/dist/esm/stringify.js
|
|
19292
|
+
function unsafeStringify(arr, offset = 0) {
|
|
19293
|
+
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
19294
|
+
}
|
|
19295
|
+
var byteToHex;
|
|
19296
|
+
var init_stringify = __esm({
|
|
19297
|
+
"node_modules/uuid/dist/esm/stringify.js"() {
|
|
19298
|
+
byteToHex = [];
|
|
19299
|
+
for (let i = 0; i < 256; ++i) {
|
|
19300
|
+
byteToHex.push((i + 256).toString(16).slice(1));
|
|
19301
|
+
}
|
|
19302
|
+
}
|
|
19303
|
+
});
|
|
19304
|
+
|
|
19305
|
+
// node_modules/uuid/dist/esm/rng.js
|
|
19306
|
+
function rng() {
|
|
19307
|
+
if (poolPtr > rnds8Pool.length - 16) {
|
|
19308
|
+
(0, import_crypto5.randomFillSync)(rnds8Pool);
|
|
19309
|
+
poolPtr = 0;
|
|
19310
|
+
}
|
|
19311
|
+
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
19312
|
+
}
|
|
19313
|
+
var import_crypto5, rnds8Pool, poolPtr;
|
|
19314
|
+
var init_rng = __esm({
|
|
19315
|
+
"node_modules/uuid/dist/esm/rng.js"() {
|
|
19316
|
+
import_crypto5 = require("crypto");
|
|
19317
|
+
rnds8Pool = new Uint8Array(256);
|
|
19318
|
+
poolPtr = rnds8Pool.length;
|
|
19319
|
+
}
|
|
19320
|
+
});
|
|
19321
|
+
|
|
19322
|
+
// node_modules/uuid/dist/esm/native.js
|
|
19323
|
+
var import_crypto6, native_default;
|
|
19324
|
+
var init_native = __esm({
|
|
19325
|
+
"node_modules/uuid/dist/esm/native.js"() {
|
|
19326
|
+
import_crypto6 = require("crypto");
|
|
19327
|
+
native_default = { randomUUID: import_crypto6.randomUUID };
|
|
19328
|
+
}
|
|
19329
|
+
});
|
|
19330
|
+
|
|
19331
|
+
// node_modules/uuid/dist/esm/v4.js
|
|
19332
|
+
function v4(options, buf, offset) {
|
|
19333
|
+
if (native_default.randomUUID && !buf && !options) {
|
|
19334
|
+
return native_default.randomUUID();
|
|
19335
|
+
}
|
|
19336
|
+
options = options || {};
|
|
19337
|
+
const rnds = options.random ?? options.rng?.() ?? rng();
|
|
19338
|
+
if (rnds.length < 16) {
|
|
19339
|
+
throw new Error("Random bytes length must be >= 16");
|
|
19340
|
+
}
|
|
19341
|
+
rnds[6] = rnds[6] & 15 | 64;
|
|
19342
|
+
rnds[8] = rnds[8] & 63 | 128;
|
|
19343
|
+
if (buf) {
|
|
19344
|
+
offset = offset || 0;
|
|
19345
|
+
if (offset < 0 || offset + 16 > buf.length) {
|
|
19346
|
+
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
|
|
19347
|
+
}
|
|
19348
|
+
for (let i = 0; i < 16; ++i) {
|
|
19349
|
+
buf[offset + i] = rnds[i];
|
|
19350
|
+
}
|
|
19351
|
+
return buf;
|
|
19352
|
+
}
|
|
19353
|
+
return unsafeStringify(rnds);
|
|
19354
|
+
}
|
|
19355
|
+
var v4_default;
|
|
19356
|
+
var init_v4 = __esm({
|
|
19357
|
+
"node_modules/uuid/dist/esm/v4.js"() {
|
|
19358
|
+
init_native();
|
|
19359
|
+
init_rng();
|
|
19360
|
+
init_stringify();
|
|
19361
|
+
v4_default = v4;
|
|
19362
|
+
}
|
|
19363
|
+
});
|
|
19364
|
+
|
|
19365
|
+
// node_modules/uuid/dist/esm/index.js
|
|
19366
|
+
var init_esm = __esm({
|
|
19367
|
+
"node_modules/uuid/dist/esm/index.js"() {
|
|
19368
|
+
init_v4();
|
|
19369
|
+
}
|
|
19370
|
+
});
|
|
19371
|
+
|
|
19372
|
+
// src/utils/detect.ts
|
|
19373
|
+
function luhnCheck(value) {
|
|
19374
|
+
const digits = value.replace(/\D/g, "");
|
|
19375
|
+
let sum = 0;
|
|
19376
|
+
let alt = false;
|
|
19377
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
19378
|
+
let n = parseInt(digits[i], 10);
|
|
19379
|
+
if (alt) {
|
|
19380
|
+
n *= 2;
|
|
19381
|
+
if (n > 9)
|
|
19382
|
+
n -= 9;
|
|
19383
|
+
}
|
|
19384
|
+
sum += n;
|
|
19385
|
+
alt = !alt;
|
|
19386
|
+
}
|
|
19387
|
+
return sum % 10 === 0;
|
|
19388
|
+
}
|
|
19389
|
+
function ibanCheck(value) {
|
|
19390
|
+
const normalized = value.replace(/\s/g, "").toUpperCase();
|
|
19391
|
+
if (normalized.length < 15 || normalized.length > 34)
|
|
19392
|
+
return false;
|
|
19393
|
+
const rearranged = normalized.slice(4) + normalized.slice(0, 4);
|
|
19394
|
+
const numeric2 = rearranged.replace(/[A-Z]/g, (c) => String(c.charCodeAt(0) - 55));
|
|
19395
|
+
let remainder = 0;
|
|
19396
|
+
for (const ch of numeric2) {
|
|
19397
|
+
remainder = (remainder * 10 + parseInt(ch, 10)) % 97;
|
|
19398
|
+
}
|
|
19399
|
+
return remainder === 1;
|
|
19400
|
+
}
|
|
19401
|
+
function contextMatches(kw, input, idx) {
|
|
19402
|
+
const lo = Math.max(0, idx - CONTEXT_WINDOW);
|
|
19403
|
+
const hi = Math.min(input.length, idx + CONTEXT_WINDOW);
|
|
19404
|
+
return kw.test(input.slice(lo, hi));
|
|
19405
|
+
}
|
|
19406
|
+
function annotate(s) {
|
|
19407
|
+
return {
|
|
19408
|
+
...s,
|
|
19409
|
+
severity: SEVERITY2[s.type] ?? "low",
|
|
19410
|
+
category: CATEGORY[s.type] ?? "other"
|
|
19411
|
+
};
|
|
19412
|
+
}
|
|
19413
|
+
function scanForSecrets(input) {
|
|
19414
|
+
const withPriority = [];
|
|
19415
|
+
for (let i = 0; i < PATTERNS.length; i++) {
|
|
19416
|
+
const { type, regex, validate } = PATTERNS[i];
|
|
19417
|
+
regex.lastIndex = 0;
|
|
19418
|
+
let match;
|
|
19419
|
+
while ((match = regex.exec(input)) !== null) {
|
|
19420
|
+
if (validate && !validate(match[0], input, match.index))
|
|
19421
|
+
continue;
|
|
19422
|
+
withPriority.push({
|
|
19423
|
+
type,
|
|
19424
|
+
value: match[0],
|
|
19425
|
+
start: match.index,
|
|
19426
|
+
end: match.index + match[0].length,
|
|
19427
|
+
priority: i
|
|
19428
|
+
});
|
|
19429
|
+
}
|
|
19430
|
+
}
|
|
19431
|
+
withPriority.sort((a, b) => a.priority - b.priority || a.start - b.start);
|
|
19432
|
+
let deduped = deduplicateOverlaps(withPriority);
|
|
19433
|
+
const pairs = detectCredentialPairs(input, deduped);
|
|
19434
|
+
if (pairs.length > 0) {
|
|
19435
|
+
for (const pair of pairs) {
|
|
19436
|
+
deduped.push({ ...pair, priority: -1 });
|
|
19437
|
+
}
|
|
19438
|
+
deduped.sort((a, b) => a.priority - b.priority || a.start - b.start);
|
|
19439
|
+
deduped = deduplicateOverlaps(deduped);
|
|
19440
|
+
}
|
|
19441
|
+
deduped.sort((a, b) => a.start - b.start);
|
|
19442
|
+
return deduped.map(({ priority: _p, ...s }) => annotate(s));
|
|
19443
|
+
}
|
|
19444
|
+
function looksLikePassword(s) {
|
|
19445
|
+
if (s.length < 8)
|
|
19446
|
+
return false;
|
|
19447
|
+
const hasUpper = /[A-Z]/.test(s);
|
|
19448
|
+
const hasLower = /[a-z]/.test(s);
|
|
19449
|
+
const hasDigit = /[0-9]/.test(s);
|
|
19450
|
+
const hasSpecial = /[@!#$%^&*\-_+=?]/.test(s);
|
|
19451
|
+
const score = [hasUpper, hasLower, hasDigit, hasSpecial].filter(Boolean).length;
|
|
19452
|
+
return score >= 3;
|
|
19453
|
+
}
|
|
19454
|
+
function detectCredentialPairs(input, _existing) {
|
|
19455
|
+
const results = [];
|
|
19456
|
+
EMAIL_RE.lastIndex = 0;
|
|
19457
|
+
let match;
|
|
19458
|
+
while ((match = EMAIL_RE.exec(input)) !== null) {
|
|
19459
|
+
const emailStart = match.index;
|
|
19460
|
+
const emailEnd = match.index + match[0].length;
|
|
19461
|
+
const charBefore = emailStart > 0 ? input[emailStart - 1] : "";
|
|
19462
|
+
if (charBefore === "@" || charBefore === ":")
|
|
19463
|
+
continue;
|
|
19464
|
+
const windowStart = Math.max(0, emailStart - PAIR_WINDOW);
|
|
19465
|
+
const windowEnd = Math.min(input.length, emailEnd + PAIR_WINDOW);
|
|
19466
|
+
const surrounding = input.slice(windowStart, windowEnd);
|
|
19467
|
+
const tokens = surrounding.match(/\S+/g) ?? [];
|
|
19468
|
+
for (const token of tokens) {
|
|
19469
|
+
if (token === match[0])
|
|
19470
|
+
continue;
|
|
19471
|
+
if (!looksLikePassword(token))
|
|
19472
|
+
continue;
|
|
19473
|
+
const tokenAbsoluteStart = windowStart + surrounding.indexOf(token);
|
|
19474
|
+
const pairStart = Math.min(emailStart, tokenAbsoluteStart);
|
|
19475
|
+
const pairEnd = Math.max(emailEnd, tokenAbsoluteStart + token.length);
|
|
19476
|
+
results.push({
|
|
19477
|
+
type: "credential_pair",
|
|
19478
|
+
value: input.slice(pairStart, pairEnd),
|
|
19479
|
+
start: pairStart,
|
|
19480
|
+
end: pairEnd
|
|
19481
|
+
});
|
|
19482
|
+
break;
|
|
19483
|
+
}
|
|
19484
|
+
}
|
|
19485
|
+
return results;
|
|
19486
|
+
}
|
|
19487
|
+
function deduplicateOverlaps(secrets) {
|
|
19488
|
+
const kept = [];
|
|
19489
|
+
for (const candidate of secrets) {
|
|
19490
|
+
const overlaps = kept.some((k) => candidate.start < k.end && candidate.end > k.start);
|
|
19491
|
+
if (!overlaps)
|
|
19492
|
+
kept.push(candidate);
|
|
19493
|
+
}
|
|
19494
|
+
return kept;
|
|
19495
|
+
}
|
|
19496
|
+
var CONTEXT_WINDOW, DOB_KEYWORDS, PASS_KEYWORDS, SSN_KEYWORDS, PATTERNS, SEVERITY2, CATEGORY, EMAIL_RE, PAIR_WINDOW;
|
|
19497
|
+
var init_detect = __esm({
|
|
19498
|
+
"src/utils/detect.ts"() {
|
|
19499
|
+
"use strict";
|
|
19500
|
+
CONTEXT_WINDOW = 120;
|
|
19501
|
+
DOB_KEYWORDS = /\b(?:dob|date\s+of\s+birth|birth\s+date|born\s+on|birthday)\b/i;
|
|
19502
|
+
PASS_KEYWORDS = /\b(?:passport|travel\s+document|document\s+no(?:\.|\b))\b/i;
|
|
19503
|
+
SSN_KEYWORDS = /\b(?:ssn|social\s+security|tin\b)\b/i;
|
|
19504
|
+
PATTERNS = [
|
|
19505
|
+
// Private key blocks (must come first — multiline, unambiguous)
|
|
19506
|
+
{ type: "private_key", regex: /-----BEGIN [A-Z ]+ PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+ PRIVATE KEY-----/g },
|
|
19507
|
+
// Cloud / AI provider keys
|
|
19508
|
+
{ type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
|
|
19509
|
+
{ type: "anthropic_api_key", regex: /sk-ant-api\d{2}-[a-zA-Z0-9\-_]{40,}/g },
|
|
19510
|
+
{ type: "openai_api_key", regex: /sk-proj-[a-zA-Z0-9\-_]{20,}/g },
|
|
19511
|
+
{ type: "google_api_key", regex: /AIza[0-9A-Za-z\-_]{35}/g },
|
|
19512
|
+
// SaaS API keys
|
|
19513
|
+
{ type: "github_token", regex: /gh[pousr]_[a-zA-Z0-9]{30,}/g },
|
|
19514
|
+
{ type: "slack_token", regex: /xox[bpas]-[0-9]{8,13}-[0-9]{8,13}-[a-zA-Z0-9]{20,}/g },
|
|
19515
|
+
{ type: "stripe_key", regex: /sk_(live|test)_[a-zA-Z0-9]{24,}/g },
|
|
19516
|
+
{ type: "sendgrid_key", regex: /SG\.[a-zA-Z0-9\-_]{22,}\.[a-zA-Z0-9\-_]{22,}/g },
|
|
19517
|
+
{ type: "huggingface_token", regex: /hf_[a-zA-Z0-9]{30,}/g },
|
|
19518
|
+
// Auth / session tokens
|
|
19519
|
+
{ type: "jwt", regex: /eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/g },
|
|
19520
|
+
// Credentials in URLs (before env_value so it wins on connection strings)
|
|
19521
|
+
{ type: "url_credentials", regex: /\w+:\/\/[^:@\s\/]{1,64}:[^@\s]{4,}@[^\s]/g },
|
|
19522
|
+
// Passwords in key=value and JSON
|
|
19523
|
+
{ type: "password", regex: /(?:password|passwd|pwd)["']?\s*[=:]\s*["']?([^\s"',]{6,})/gi },
|
|
19524
|
+
// PII — financial
|
|
19525
|
+
{
|
|
19526
|
+
type: "credit_card",
|
|
19527
|
+
regex: /\b\d{4}[\s\-]\d{4}[\s\-]\d{4}[\s\-]\d{4}\b/g,
|
|
19528
|
+
validate: (v) => luhnCheck(v)
|
|
19529
|
+
},
|
|
19530
|
+
{
|
|
19531
|
+
type: "iban",
|
|
19532
|
+
regex: /\b[A-Z]{2}\d{2}[A-Z0-9]{4,30}\b/g,
|
|
19533
|
+
validate: (v) => ibanCheck(v)
|
|
19534
|
+
},
|
|
19535
|
+
// PII — identity (context-aware)
|
|
19536
|
+
{
|
|
19537
|
+
type: "ssn",
|
|
19538
|
+
regex: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
19539
|
+
validate: (_v, input, idx) => {
|
|
19540
|
+
if (contextMatches(SSN_KEYWORDS, input, idx))
|
|
19541
|
+
return true;
|
|
19542
|
+
const surrounding = input.slice(Math.max(0, idx - 20), idx + 20);
|
|
19543
|
+
if (/(?:19|20)\d{2}/.test(surrounding))
|
|
19544
|
+
return false;
|
|
19545
|
+
return true;
|
|
19546
|
+
}
|
|
19547
|
+
},
|
|
19548
|
+
{
|
|
19549
|
+
type: "dob",
|
|
19550
|
+
regex: /\b(?:0?[1-9]|1[0-2])[-\/](?:0?[1-9]|[12]\d|3[01])[-\/](?:19|20)\d{2}\b/g,
|
|
19551
|
+
validate: (_v, input, idx) => contextMatches(DOB_KEYWORDS, input, idx)
|
|
19552
|
+
},
|
|
19553
|
+
{
|
|
19554
|
+
type: "passport",
|
|
19555
|
+
regex: /\b[A-Z]\d{8}\b/g,
|
|
19556
|
+
validate: (_v, input, idx) => contextMatches(PASS_KEYWORDS, input, idx)
|
|
19557
|
+
},
|
|
19558
|
+
// PII — contact
|
|
19559
|
+
{ type: "email", regex: /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g },
|
|
19560
|
+
{
|
|
19561
|
+
type: "phone_us",
|
|
19562
|
+
// Requires area code starting 2-9 (excludes version numbers, dates, plain 7-digit numbers)
|
|
19563
|
+
regex: /\b(?:\+?1[-.\s]?)?\(?([2-9][0-9]{2})\)?[-.\s]([2-9][0-9]{2})[-.\s]([0-9]{4})\b/g
|
|
19564
|
+
},
|
|
19565
|
+
{
|
|
19566
|
+
type: "phone_e164",
|
|
19567
|
+
// E.164 international (non-US, min 8 digits after +)
|
|
19568
|
+
regex: /\+(?!1[^0-9])(?:[2-9][0-9]{1}|[1][^1])[0-9]{6,12}\b/g
|
|
19569
|
+
},
|
|
19570
|
+
// Network — internal RFC-1918 addresses
|
|
19571
|
+
{
|
|
19572
|
+
type: "internal_ip",
|
|
19573
|
+
regex: /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g
|
|
19574
|
+
},
|
|
19575
|
+
// Generic env assignment (lowest priority — catches remaining secrets)
|
|
19576
|
+
{ type: "env_value", regex: /\b[A-Z][A-Z0-9_]{2,}=[^\s]{8,}/g }
|
|
19577
|
+
];
|
|
19578
|
+
SEVERITY2 = {
|
|
19579
|
+
private_key: "critical",
|
|
19580
|
+
credit_card: "critical",
|
|
19581
|
+
ssn: "critical",
|
|
19582
|
+
iban: "critical",
|
|
19583
|
+
aws_access_key: "high",
|
|
19584
|
+
anthropic_api_key: "high",
|
|
19585
|
+
openai_api_key: "high",
|
|
19586
|
+
google_api_key: "high",
|
|
19587
|
+
github_token: "high",
|
|
19588
|
+
slack_token: "high",
|
|
19589
|
+
stripe_key: "high",
|
|
19590
|
+
sendgrid_key: "high",
|
|
19591
|
+
huggingface_token: "high",
|
|
19592
|
+
jwt: "high",
|
|
19593
|
+
url_credentials: "high",
|
|
19594
|
+
password: "high",
|
|
19595
|
+
credential_pair: "high",
|
|
19596
|
+
passport: "high",
|
|
19597
|
+
dob: "high",
|
|
19598
|
+
email: "medium",
|
|
19599
|
+
phone_us: "medium",
|
|
19600
|
+
phone_e164: "medium",
|
|
19601
|
+
internal_ip: "low",
|
|
19602
|
+
env_value: "low"
|
|
19603
|
+
};
|
|
19604
|
+
CATEGORY = {
|
|
19605
|
+
private_key: "credential",
|
|
19606
|
+
credit_card: "financial",
|
|
19607
|
+
ssn: "identity",
|
|
19608
|
+
iban: "financial",
|
|
19609
|
+
passport: "identity",
|
|
19610
|
+
dob: "identity",
|
|
19611
|
+
aws_access_key: "credential",
|
|
19612
|
+
anthropic_api_key: "credential",
|
|
19613
|
+
openai_api_key: "credential",
|
|
19614
|
+
google_api_key: "credential",
|
|
19615
|
+
github_token: "credential",
|
|
19616
|
+
slack_token: "credential",
|
|
19617
|
+
stripe_key: "credential",
|
|
19618
|
+
sendgrid_key: "credential",
|
|
19619
|
+
huggingface_token: "credential",
|
|
19620
|
+
jwt: "credential",
|
|
19621
|
+
url_credentials: "credential",
|
|
19622
|
+
password: "credential",
|
|
19623
|
+
credential_pair: "credential",
|
|
19624
|
+
email: "contact",
|
|
19625
|
+
phone_us: "contact",
|
|
19626
|
+
phone_e164: "contact",
|
|
19627
|
+
internal_ip: "network",
|
|
19628
|
+
env_value: "credential"
|
|
19629
|
+
};
|
|
19630
|
+
EMAIL_RE = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g;
|
|
19631
|
+
PAIR_WINDOW = 100;
|
|
19632
|
+
}
|
|
19633
|
+
});
|
|
19634
|
+
|
|
19635
|
+
// src/cli/import.ts
|
|
19636
|
+
var import_exports = {};
|
|
19637
|
+
__export(import_exports, {
|
|
19638
|
+
importGptConversations: () => importGptConversations,
|
|
19639
|
+
runImport: () => runImport
|
|
19640
|
+
});
|
|
19641
|
+
function unixToIso(ts) {
|
|
19642
|
+
if (!ts)
|
|
19643
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("Z", "+00:00");
|
|
19644
|
+
return new Date(ts * 1e3).toISOString().replace("Z", "+00:00");
|
|
19645
|
+
}
|
|
19646
|
+
function extractText(content) {
|
|
19647
|
+
if (!content)
|
|
19648
|
+
return "";
|
|
19649
|
+
if (!content.parts)
|
|
19650
|
+
return "";
|
|
19651
|
+
return content.parts.filter((p) => typeof p === "string" && p.trim().length > 0).join("\n").trim();
|
|
19652
|
+
}
|
|
19653
|
+
function maskValue(v) {
|
|
19654
|
+
return v.length > 8 ? v.slice(0, 4) + "****" + v.slice(-4) : "****";
|
|
19655
|
+
}
|
|
19656
|
+
function linearize(mapping) {
|
|
19657
|
+
const root = Object.values(mapping).find((n) => n.parent === null || !mapping[n.parent ?? ""]);
|
|
19658
|
+
if (!root)
|
|
19659
|
+
return [];
|
|
19660
|
+
const path = [];
|
|
19661
|
+
let current = root;
|
|
19662
|
+
while (current) {
|
|
19663
|
+
path.push(current);
|
|
19664
|
+
const lastChildId = current.children[current.children.length - 1];
|
|
19665
|
+
current = lastChildId ? mapping[lastChildId] : void 0;
|
|
19666
|
+
}
|
|
19667
|
+
return path;
|
|
19668
|
+
}
|
|
19669
|
+
function messagesFromConversation(conv) {
|
|
19670
|
+
const nodes = linearize(conv.mapping);
|
|
19671
|
+
const result = [];
|
|
19672
|
+
for (const node of nodes) {
|
|
19673
|
+
const msg = node.message;
|
|
19674
|
+
if (!msg)
|
|
19675
|
+
continue;
|
|
19676
|
+
const role = msg.author.role;
|
|
19677
|
+
if (role !== "user" && role !== "assistant")
|
|
19678
|
+
continue;
|
|
19679
|
+
const text2 = extractText(msg.content);
|
|
19680
|
+
if (!text2)
|
|
19681
|
+
continue;
|
|
19682
|
+
result.push({ role, content: text2, ts: unixToIso(msg.create_time) });
|
|
19683
|
+
}
|
|
19684
|
+
return result;
|
|
19685
|
+
}
|
|
19686
|
+
function findFile(dir, name) {
|
|
19687
|
+
for (const entry of (0, import_fs11.readdirSync)(dir)) {
|
|
19688
|
+
const full = (0, import_path10.join)(dir, entry);
|
|
19689
|
+
try {
|
|
19690
|
+
if ((0, import_fs11.statSync)(full).isDirectory()) {
|
|
19691
|
+
const found = findFile(full, name);
|
|
19692
|
+
if (found)
|
|
19693
|
+
return found;
|
|
19694
|
+
} else if (entry === name) {
|
|
19695
|
+
return full;
|
|
19696
|
+
}
|
|
19697
|
+
} catch {
|
|
19698
|
+
}
|
|
19699
|
+
}
|
|
19700
|
+
return null;
|
|
19701
|
+
}
|
|
19702
|
+
async function importGptConversations(db, conversations) {
|
|
19703
|
+
let imported = 0;
|
|
19704
|
+
let skipped = 0;
|
|
19705
|
+
let totalMessages = 0;
|
|
19706
|
+
let totalSecrets = 0;
|
|
19707
|
+
const errors = [];
|
|
19708
|
+
for (const conv of conversations) {
|
|
19709
|
+
const externalRef = `chatgpt:${conv.id}`;
|
|
19710
|
+
const displayTitle = (conv.title ?? "Untitled").slice(0, 60);
|
|
19711
|
+
const [existing] = await db.select({ id: sessions.id }).from(sessions).where(eq(sessions.external_ref, externalRef)).limit(1);
|
|
19712
|
+
if (existing) {
|
|
19713
|
+
skipped++;
|
|
19714
|
+
continue;
|
|
19715
|
+
}
|
|
19716
|
+
const msgs = messagesFromConversation(conv);
|
|
19717
|
+
if (msgs.length === 0) {
|
|
19718
|
+
skipped++;
|
|
19719
|
+
continue;
|
|
19720
|
+
}
|
|
19721
|
+
try {
|
|
19722
|
+
let sessionSecrets = 0;
|
|
19723
|
+
await db.transaction(async (tx) => {
|
|
19724
|
+
let baseTitle = (conv.title ?? "Untitled ChatGPT conversation").slice(0, 500);
|
|
19725
|
+
const [titleConflict] = await tx.select({ id: sessions.id }).from(sessions).where(eq(sessions.title, baseTitle)).limit(1);
|
|
19726
|
+
if (titleConflict) {
|
|
19727
|
+
baseTitle = `${baseTitle} (${conv.id.slice(0, 8)})`.slice(0, 500);
|
|
19728
|
+
}
|
|
19729
|
+
const sessionId = v4_default();
|
|
19730
|
+
let publicKey = null;
|
|
19731
|
+
try {
|
|
19732
|
+
publicKey = generateSessionKeypair(sessionId);
|
|
19733
|
+
} catch {
|
|
19734
|
+
}
|
|
19735
|
+
await tx.insert(sessions).values({
|
|
19736
|
+
id: sessionId,
|
|
19737
|
+
title: baseTitle,
|
|
19738
|
+
ai_tool: "chatgpt",
|
|
19739
|
+
created_at: unixToIso(conv.create_time),
|
|
19740
|
+
updated_at: unixToIso(conv.update_time),
|
|
19741
|
+
external_ref: externalRef,
|
|
19742
|
+
parent_session_id: null,
|
|
19743
|
+
public_key: publicKey
|
|
19744
|
+
});
|
|
19745
|
+
let prevHash = null;
|
|
19746
|
+
for (const m of msgs) {
|
|
19747
|
+
const msgId = v4_default();
|
|
19748
|
+
const hash = computeContentHash(sessionId, m.role, m.content, m.ts, prevHash, "message");
|
|
19749
|
+
await tx.insert(messages).values({
|
|
19750
|
+
id: msgId,
|
|
19751
|
+
session_id: sessionId,
|
|
19752
|
+
role: m.role,
|
|
19753
|
+
content: m.content,
|
|
19754
|
+
created_at: m.ts,
|
|
19755
|
+
prev_hash: prevHash,
|
|
19756
|
+
content_hash: hash,
|
|
19757
|
+
event_type: "message"
|
|
19758
|
+
});
|
|
19759
|
+
if (m.role === "user") {
|
|
19760
|
+
const found = scanForSecrets(m.content);
|
|
19761
|
+
if (found.length > 0) {
|
|
19762
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().replace("Z", "+00:00");
|
|
19763
|
+
await tx.insert(secrets_detected).values(
|
|
19764
|
+
found.map((s) => ({
|
|
19765
|
+
id: v4_default(),
|
|
19766
|
+
session_id: sessionId,
|
|
19767
|
+
message_id: msgId,
|
|
19768
|
+
type: s.type,
|
|
19769
|
+
masked_value: maskValue(s.value),
|
|
19770
|
+
detected_at: now
|
|
19771
|
+
}))
|
|
19772
|
+
);
|
|
19773
|
+
sessionSecrets += found.length;
|
|
19774
|
+
totalSecrets += found.length;
|
|
19775
|
+
}
|
|
19776
|
+
}
|
|
19777
|
+
prevHash = hash;
|
|
19778
|
+
}
|
|
19779
|
+
});
|
|
19780
|
+
totalMessages += msgs.length;
|
|
19781
|
+
imported++;
|
|
19782
|
+
} catch (err) {
|
|
19783
|
+
errors.push({ title: displayTitle, error: err?.message ?? String(err) });
|
|
19784
|
+
}
|
|
19785
|
+
}
|
|
19786
|
+
return { imported, skipped, totalMessages, totalSecrets, errors };
|
|
19787
|
+
}
|
|
19788
|
+
async function importConversations(filePath) {
|
|
19789
|
+
const db = await initDb();
|
|
19790
|
+
let raw;
|
|
19791
|
+
let tempDir = null;
|
|
19792
|
+
const ext = (0, import_path10.extname)(filePath).toLowerCase();
|
|
19793
|
+
if (ext === ".zip") {
|
|
19794
|
+
tempDir = (0, import_path10.join)((0, import_os10.tmpdir)(), `chron-import-${Date.now()}`);
|
|
19795
|
+
(0, import_fs11.mkdirSync)(tempDir, { recursive: true });
|
|
19796
|
+
try {
|
|
19797
|
+
(0, import_child_process4.execFileSync)("unzip", ["-q", "-o", filePath, "conversations.json", "-d", tempDir], { stdio: "pipe" });
|
|
19798
|
+
} catch {
|
|
19799
|
+
try {
|
|
19800
|
+
(0, import_child_process4.execFileSync)("unzip", ["-q", "-o", filePath, "-d", tempDir], { stdio: "pipe" });
|
|
19801
|
+
} catch {
|
|
19802
|
+
process.stderr.write(`Failed to extract ${filePath}. Make sure 'unzip' is installed.
|
|
19803
|
+
`);
|
|
19804
|
+
(0, import_fs11.rmSync)(tempDir, { recursive: true, force: true });
|
|
19805
|
+
process.exit(1);
|
|
19806
|
+
}
|
|
19807
|
+
}
|
|
19808
|
+
const found = findFile(tempDir, "conversations.json");
|
|
19809
|
+
if (!found) {
|
|
19810
|
+
process.stderr.write(`conversations.json not found inside the ZIP.
|
|
19811
|
+
`);
|
|
19812
|
+
(0, import_fs11.rmSync)(tempDir, { recursive: true, force: true });
|
|
19813
|
+
process.exit(1);
|
|
19814
|
+
}
|
|
19815
|
+
raw = (0, import_fs11.readFileSync)(found, "utf8");
|
|
19816
|
+
} else if (ext === ".json") {
|
|
19817
|
+
raw = (0, import_fs11.readFileSync)(filePath, "utf8");
|
|
19818
|
+
} else {
|
|
19819
|
+
process.stderr.write(`Unsupported file type: ${ext}. Pass a .zip export or a conversations.json file.
|
|
19820
|
+
`);
|
|
19821
|
+
process.exit(1);
|
|
19822
|
+
}
|
|
19823
|
+
let conversations;
|
|
19824
|
+
try {
|
|
19825
|
+
conversations = JSON.parse(raw);
|
|
19826
|
+
if (!Array.isArray(conversations))
|
|
19827
|
+
throw new Error("Expected a JSON array");
|
|
19828
|
+
} catch {
|
|
19829
|
+
process.stderr.write(`Failed to parse conversations JSON.
|
|
19830
|
+
`);
|
|
19831
|
+
if (tempDir)
|
|
19832
|
+
(0, import_fs11.rmSync)(tempDir, { recursive: true, force: true });
|
|
19833
|
+
process.exit(1);
|
|
19834
|
+
}
|
|
19835
|
+
process.stdout.write(`
|
|
19836
|
+
${BOLD9}chron import chatgpt${RESET9} ${DIM8}${filePath}${RESET9}
|
|
19837
|
+
`);
|
|
19838
|
+
process.stdout.write(`${DIM8}${conversations.length} conversation(s) found${RESET9}
|
|
19839
|
+
|
|
19840
|
+
`);
|
|
19841
|
+
const result = await importGptConversations(db, conversations);
|
|
19842
|
+
for (const e of result.errors) {
|
|
19843
|
+
process.stderr.write(` ${YELLOW7}!${RESET9} Failed to import "${e.title}": ${e.error}
|
|
19844
|
+
`);
|
|
19845
|
+
}
|
|
19846
|
+
if (tempDir)
|
|
19847
|
+
(0, import_fs11.rmSync)(tempDir, { recursive: true, force: true });
|
|
19848
|
+
process.stdout.write("\n");
|
|
19849
|
+
process.stdout.write(`${BOLD9}Done.${RESET9} `);
|
|
19850
|
+
process.stdout.write(`${GREEN6}${result.imported} imported${RESET9}`);
|
|
19851
|
+
if (result.skipped > 0)
|
|
19852
|
+
process.stdout.write(` ${DIM8}${result.skipped} skipped (already in DB)${RESET9}`);
|
|
19853
|
+
process.stdout.write(` ${CYAN7}${result.totalMessages} messages total${RESET9}`);
|
|
19854
|
+
if (result.totalSecrets > 0)
|
|
19855
|
+
process.stdout.write(` ${YELLOW7}${result.totalSecrets} secret(s) detected${RESET9}`);
|
|
19856
|
+
if (result.errors.length > 0)
|
|
19857
|
+
process.stdout.write(` ${YELLOW7}${result.errors.length} error(s)${RESET9}`);
|
|
19858
|
+
process.stdout.write("\n\n");
|
|
19859
|
+
process.stdout.write(`${DIM8}Run 'chron history' to browse imported sessions.${RESET9}
|
|
19860
|
+
|
|
19861
|
+
`);
|
|
19862
|
+
}
|
|
19863
|
+
async function runImport(args2) {
|
|
19864
|
+
const [subcommand, filePath] = args2;
|
|
19865
|
+
if (subcommand !== "chatgpt" || !filePath) {
|
|
19866
|
+
process.stderr.write(
|
|
19867
|
+
"Usage: chron import chatgpt <file>\n\n <file> Path to ChatGPT export ZIP (chatgpt-export-*.zip)\n or extracted conversations.json\n\nExample:\n chron import chatgpt ~/Downloads/chatgpt-export.zip\n chron import chatgpt ~/Downloads/conversations.json\n"
|
|
19868
|
+
);
|
|
19869
|
+
process.exit(1);
|
|
19870
|
+
}
|
|
19871
|
+
const resolved = filePath.replace(/^~/, process.env.HOME ?? "");
|
|
19872
|
+
if (!(0, import_fs11.existsSync)(resolved)) {
|
|
19873
|
+
process.stderr.write(`File not found: ${resolved}
|
|
19874
|
+
`);
|
|
19875
|
+
process.exit(1);
|
|
19876
|
+
}
|
|
19877
|
+
await importConversations(resolved);
|
|
19878
|
+
}
|
|
19879
|
+
var import_fs11, import_path10, import_os10, import_child_process4, RESET9, BOLD9, DIM8, GREEN6, YELLOW7, CYAN7;
|
|
19880
|
+
var init_import = __esm({
|
|
19881
|
+
"src/cli/import.ts"() {
|
|
19882
|
+
"use strict";
|
|
19883
|
+
import_fs11 = require("fs");
|
|
19884
|
+
import_path10 = require("path");
|
|
19885
|
+
import_os10 = require("os");
|
|
19886
|
+
import_child_process4 = require("child_process");
|
|
19887
|
+
init_drizzle_orm();
|
|
19888
|
+
init_esm();
|
|
19889
|
+
init_db2();
|
|
19890
|
+
init_schema();
|
|
19891
|
+
init_hash();
|
|
19892
|
+
init_detect();
|
|
19893
|
+
init_signing();
|
|
19894
|
+
RESET9 = "\x1B[0m";
|
|
19895
|
+
BOLD9 = "\x1B[1m";
|
|
19896
|
+
DIM8 = "\x1B[2m";
|
|
19897
|
+
GREEN6 = "\x1B[32m";
|
|
19898
|
+
YELLOW7 = "\x1B[33m";
|
|
19899
|
+
CYAN7 = "\x1B[36m";
|
|
19142
19900
|
}
|
|
19143
19901
|
});
|
|
19144
19902
|
|
|
@@ -19201,6 +19959,11 @@ async function main() {
|
|
|
19201
19959
|
await runDoctor2(args);
|
|
19202
19960
|
break;
|
|
19203
19961
|
}
|
|
19962
|
+
case "import": {
|
|
19963
|
+
const { runImport: runImport2 } = await Promise.resolve().then(() => (init_import(), import_exports));
|
|
19964
|
+
await runImport2(args);
|
|
19965
|
+
break;
|
|
19966
|
+
}
|
|
19204
19967
|
default: {
|
|
19205
19968
|
const name = command ? `Unknown command: ${command}
|
|
19206
19969
|
|
|
@@ -19214,12 +19977,13 @@ Commands:
|
|
|
19214
19977
|
export Export a session as markdown
|
|
19215
19978
|
secrets List detected secrets across sessions
|
|
19216
19979
|
settings View current configuration
|
|
19217
|
-
connect Connect to a SIEM
|
|
19980
|
+
connect Connect to a SIEM or AI tool (codex, crowdstrike, sentinel, splunk)
|
|
19218
19981
|
summary Structured summary of a session (timeline, mutations, secrets)
|
|
19219
19982
|
sign Sign a session with its Ed25519 key \u2014 produces a .chron.sig file
|
|
19220
19983
|
verify Verify a session's hash chain and Ed25519 signature
|
|
19221
19984
|
prune Delete sessions older than a retention cutoff
|
|
19222
19985
|
doctor Check your Chron setup \u2014 Node version, DB, MCP configs, SIEM
|
|
19986
|
+
import Import conversations from external AI tools
|
|
19223
19987
|
|
|
19224
19988
|
Options (history):
|
|
19225
19989
|
--limit=<n> Max sessions to show (default: 20)
|
|
@@ -19249,6 +20013,9 @@ Options (prune):
|
|
|
19249
20013
|
|
|
19250
20014
|
Options (doctor):
|
|
19251
20015
|
--json Machine-readable JSON output
|
|
20016
|
+
|
|
20017
|
+
Options (import):
|
|
20018
|
+
chatgpt <file> Import from ChatGPT export (.zip or conversations.json)
|
|
19252
20019
|
`
|
|
19253
20020
|
);
|
|
19254
20021
|
process.exit(command ? 1 : 0);
|
package/dist/index.js
CHANGED
|
@@ -22792,6 +22792,7 @@ async function initDb(dbPath) {
|
|
|
22792
22792
|
}
|
|
22793
22793
|
const client = createClient({ url: path.startsWith(":") ? path : `file:${path}` });
|
|
22794
22794
|
await client.execute("PRAGMA journal_mode = WAL");
|
|
22795
|
+
await client.execute("PRAGMA busy_timeout = 5000");
|
|
22795
22796
|
await client.execute("PRAGMA foreign_keys = ON");
|
|
22796
22797
|
for (const sql2 of CREATE_SQL) {
|
|
22797
22798
|
await client.execute(sql2);
|
|
@@ -38479,7 +38480,7 @@ var init_time = __esm({
|
|
|
38479
38480
|
var version4;
|
|
38480
38481
|
var init_package = __esm({
|
|
38481
38482
|
"package.json"() {
|
|
38482
|
-
version4 = "0.1.
|
|
38483
|
+
version4 = "0.1.28";
|
|
38483
38484
|
}
|
|
38484
38485
|
});
|
|
38485
38486
|
|
|
@@ -38837,6 +38838,31 @@ var init_ntp = __esm({
|
|
|
38837
38838
|
}
|
|
38838
38839
|
});
|
|
38839
38840
|
|
|
38841
|
+
// src/utils/terminal.ts
|
|
38842
|
+
function terminalAudit(message) {
|
|
38843
|
+
const setting = process.env.CHRON_TERMINAL_AUDIT;
|
|
38844
|
+
if (setting && DISABLE_VALUES.has(setting.toLowerCase()))
|
|
38845
|
+
return;
|
|
38846
|
+
process.stderr.write(`[chron] ${message}
|
|
38847
|
+
`);
|
|
38848
|
+
}
|
|
38849
|
+
function formatDuration(ms) {
|
|
38850
|
+
if (ms < 1e3)
|
|
38851
|
+
return `${ms}ms`;
|
|
38852
|
+
if (ms < 6e4)
|
|
38853
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
38854
|
+
const minutes = Math.floor(ms / 6e4);
|
|
38855
|
+
const seconds = Math.round(ms % 6e4 / 1e3);
|
|
38856
|
+
return `${minutes}m ${seconds}s`;
|
|
38857
|
+
}
|
|
38858
|
+
var DISABLE_VALUES;
|
|
38859
|
+
var init_terminal = __esm({
|
|
38860
|
+
"src/utils/terminal.ts"() {
|
|
38861
|
+
"use strict";
|
|
38862
|
+
DISABLE_VALUES = /* @__PURE__ */ new Set(["0", "false", "off", "no"]);
|
|
38863
|
+
}
|
|
38864
|
+
});
|
|
38865
|
+
|
|
38840
38866
|
// src/tools/sessions.ts
|
|
38841
38867
|
function attachNtpMetadata(db, sessionId) {
|
|
38842
38868
|
setImmediate(() => {
|
|
@@ -38867,6 +38893,7 @@ function startSession(db) {
|
|
|
38867
38893
|
});
|
|
38868
38894
|
attachNtpMetadata(db, id);
|
|
38869
38895
|
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})`);
|
|
38870
38897
|
return {
|
|
38871
38898
|
content: [{
|
|
38872
38899
|
type: "text",
|
|
@@ -38882,6 +38909,7 @@ function startSession(db) {
|
|
|
38882
38909
|
const [countRow] = await db.select({ count: sql`count(*)` }).from(messages).where(eq(messages.session_id, session.id));
|
|
38883
38910
|
await db.update(sessions).set({ updated_at: now }).where(eq(sessions.id, session.id));
|
|
38884
38911
|
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})`);
|
|
38885
38913
|
return {
|
|
38886
38914
|
content: [{
|
|
38887
38915
|
type: "text",
|
|
@@ -38918,6 +38946,7 @@ function initSession(db) {
|
|
|
38918
38946
|
created = true;
|
|
38919
38947
|
ai_tool = args.ai_tool ?? null;
|
|
38920
38948
|
attachNtpMetadata(db, id);
|
|
38949
|
+
terminalAudit(`session ${id.slice(0, 8)} started at ${now} (${ai_tool ?? "unknown"}: ${args.title})`);
|
|
38921
38950
|
} catch (e) {
|
|
38922
38951
|
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";
|
|
38923
38952
|
if (!isUnique)
|
|
@@ -38928,6 +38957,7 @@ function initSession(db) {
|
|
|
38928
38957
|
session_id = session.id;
|
|
38929
38958
|
created = false;
|
|
38930
38959
|
ai_tool = session.ai_tool;
|
|
38960
|
+
terminalAudit(`session ${session.id.slice(0, 8)} resumed at ${now} (${ai_tool ?? "unknown"}: ${session.title})`);
|
|
38931
38961
|
}
|
|
38932
38962
|
emitEvent({ event_type: "session_started", timestamp: now, session: { id_prefix: session_id.slice(0, 8), ai_tool } });
|
|
38933
38963
|
const limit = args.limit ?? 10;
|
|
@@ -39016,6 +39046,7 @@ var init_sessions = __esm({
|
|
|
39016
39046
|
init_relay();
|
|
39017
39047
|
init_signing();
|
|
39018
39048
|
init_ntp();
|
|
39049
|
+
init_terminal();
|
|
39019
39050
|
}
|
|
39020
39051
|
});
|
|
39021
39052
|
|
|
@@ -39328,7 +39359,7 @@ function isFkError(e) {
|
|
|
39328
39359
|
}
|
|
39329
39360
|
async function insertMessage(db, session_id, role, content, event_type) {
|
|
39330
39361
|
return db.transaction(async (tx) => {
|
|
39331
|
-
const last = await tx.select({ content_hash: messages.content_hash }).from(messages).where(eq(messages.session_id, session_id)).orderBy(desc(messages.created_at), desc(sql`rowid`)).limit(1);
|
|
39362
|
+
const last = await tx.select({ content_hash: messages.content_hash, created_at: messages.created_at }).from(messages).where(eq(messages.session_id, session_id)).orderBy(desc(messages.created_at), desc(sql`rowid`)).limit(1);
|
|
39332
39363
|
const ts = localISOString();
|
|
39333
39364
|
const msgId = v4_default();
|
|
39334
39365
|
const prevHash = last[0]?.content_hash ?? null;
|
|
@@ -39345,7 +39376,13 @@ async function insertMessage(db, session_id, role, content, event_type) {
|
|
|
39345
39376
|
});
|
|
39346
39377
|
await tx.update(sessions).set({ updated_at: ts }).where(eq(sessions.id, session_id));
|
|
39347
39378
|
const [sessionRow] = await tx.select({ ai_tool: sessions.ai_tool }).from(sessions).where(eq(sessions.id, session_id)).limit(1);
|
|
39348
|
-
return {
|
|
39379
|
+
return {
|
|
39380
|
+
id: msgId,
|
|
39381
|
+
now: ts,
|
|
39382
|
+
contentHash: hash,
|
|
39383
|
+
ai_tool: sessionRow?.ai_tool ?? null,
|
|
39384
|
+
previous_created_at: last[0]?.created_at ?? null
|
|
39385
|
+
};
|
|
39349
39386
|
});
|
|
39350
39387
|
}
|
|
39351
39388
|
function logMessage(db) {
|
|
@@ -39361,6 +39398,9 @@ function logMessage(db) {
|
|
|
39361
39398
|
now = result.now;
|
|
39362
39399
|
contentHash = result.contentHash;
|
|
39363
39400
|
sessionAiTool = result.ai_tool;
|
|
39401
|
+
const elapsed = result.previous_created_at ? ` (+${formatDuration(new Date(now).getTime() - new Date(result.previous_created_at).getTime())})` : "";
|
|
39402
|
+
const marker = args.role === "user" ? "user start" : "assistant end";
|
|
39403
|
+
terminalAudit(`${marker} ${now}${elapsed} session ${args.session_id.slice(0, 8)}`);
|
|
39364
39404
|
} catch (e) {
|
|
39365
39405
|
if (isFkError(e)) {
|
|
39366
39406
|
return { content: [{ type: "text", text: `Session not found: ${args.session_id}` }], isError: true };
|
|
@@ -39508,6 +39548,7 @@ var init_messages = __esm({
|
|
|
39508
39548
|
init_hash();
|
|
39509
39549
|
init_detect();
|
|
39510
39550
|
init_relay();
|
|
39551
|
+
init_terminal();
|
|
39511
39552
|
}
|
|
39512
39553
|
});
|
|
39513
39554
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Chron — Codex Audit Skill
|
|
2
|
+
|
|
3
|
+
Chron creates a tamper-evident audit trail of every Codex session — messages, tool calls, tool results, and code changes — stored in a local SQLite database the user owns. No data leaves the machine.
|
|
4
|
+
|
|
5
|
+
## Session Start
|
|
6
|
+
|
|
7
|
+
At the beginning of every Codex thread, call:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
init_session(title="<short description of the task>", ai_tool="codex")
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This creates or resumes a session and returns `session_id`, `message_count`, and recent `messages`. Save `session_id` — you will use it for every subsequent call.
|
|
14
|
+
|
|
15
|
+
If `message_count > 0`, you are resuming an existing session. Acknowledge this to the user.
|
|
16
|
+
|
|
17
|
+
## Every Exchange
|
|
18
|
+
|
|
19
|
+
For every user message and your response, log both ends before outputting:
|
|
20
|
+
|
|
21
|
+
1. `log_message(session_id, role="user", content=<exact user message>)` — log the user's message first
|
|
22
|
+
2. Compose your response internally
|
|
23
|
+
3. `log_message(session_id, role="assistant", content=<your response>)` — log before outputting
|
|
24
|
+
4. Output the response
|
|
25
|
+
|
|
26
|
+
## Tool Calls and Results
|
|
27
|
+
|
|
28
|
+
Before executing any tool (shell command, file read, browser action, code execution):
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
log_tool_call(session_id, tool_name="<tool>", tool_input={<args>})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
After receiving the tool output:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
log_tool_result(session_id, tool_call_id=<id from log_tool_call>, output="<result>", exit_code=<n>)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Code Changes
|
|
41
|
+
|
|
42
|
+
After writing or editing any file:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
log_code_change(
|
|
46
|
+
session_id,
|
|
47
|
+
file_path="<absolute or repo-relative path>",
|
|
48
|
+
operation="create" | "edit" | "delete",
|
|
49
|
+
diff="<unified diff of the change>"
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This creates a cryptographically linked record of every file edit, tied to the conversation that caused it.
|
|
54
|
+
|
|
55
|
+
## Audit Order (per turn)
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
log_message(role="user") ← before doing anything
|
|
59
|
+
log_tool_call(...) ← before each tool
|
|
60
|
+
log_tool_result(...) ← after each tool result
|
|
61
|
+
log_code_change(...) ← after each file edit
|
|
62
|
+
log_message(role="assistant") ← before final response output
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Verify Integrity
|
|
66
|
+
|
|
67
|
+
At any point, the user can run:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
chron verify <session-id-prefix>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This walks the SHA-256 hash chain and verifies the Ed25519 session signature, confirming the audit record has not been modified since it was written.
|