chron-mcp 0.1.25 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -102,6 +102,7 @@ Commands:
102
102
  verify Verify a session's hash chain and Ed25519 signature
103
103
  prune Delete sessions older than a retention cutoff
104
104
  doctor Check your Chron setup — Node version, DB, MCP configs, SIEM
105
+ import Import conversations from external AI tools into Chron
105
106
 
106
107
  Options (history):
107
108
  --limit=<n> Max sessions to show (default: 20)
@@ -131,6 +132,9 @@ Options (prune):
131
132
 
132
133
  Options (doctor):
133
134
  --json Machine-readable JSON output
135
+
136
+ Options (import):
137
+ chatgpt <file> Import from ChatGPT export (.zip or conversations.json)
134
138
  ```
135
139
 
136
140
  ### `chron doctor`
@@ -643,6 +647,34 @@ Point your MCP config at the URL:
643
647
 
644
648
  ---
645
649
 
650
+ ## ChatGPT import
651
+
652
+ Bring your existing ChatGPT history into Chron's tamper-evident audit trail.
653
+
654
+ ```bash
655
+ # From a ChatGPT data export ZIP
656
+ chron import chatgpt ~/Downloads/chatgpt-export.zip
657
+
658
+ # Or from an extracted conversations.json
659
+ chron import chatgpt ~/Downloads/conversations.json
660
+ ```
661
+
662
+ **How to get your ChatGPT export:**
663
+ ChatGPT → Settings → Data Controls → Export data → wait for email → download the ZIP.
664
+
665
+ **What gets imported:**
666
+ - One Chron session per conversation, with `ai_tool=chatgpt`
667
+ - Original message timestamps from the export
668
+ - `external_ref=chatgpt:<conversation_id>` on every session (visible in `chron history`)
669
+ - Full SHA-256 hash chain across all imported messages
670
+ - Secret detection runs on all user messages
671
+
672
+ **Re-running is safe** — already-imported conversations are skipped by `external_ref` match.
673
+
674
+ After import, sessions appear in `chron history` and `chron verify` works on them the same as any natively-logged session.
675
+
676
+ ---
677
+
646
678
  ## Your data
647
679
 
648
680
  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 join10(chunks, separator) {
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 = join10;
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 readFileSync7 = (path) => fs.readFileSync(path, "utf-8");
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: readFileSync7,
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: readFileSync7 } = require_filesystem();
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 = readFileSync7(LDD_PATH);
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 = readFileSync7(LDD_PATH);
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
- randomFillSync(randomPool, 0, RANDOM_POOL_SIZE);
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((join10) => join10.alias === tableName)) {
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((join10) => join10.alias === tableName)) {
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") {
@@ -17302,6 +17302,20 @@ function keysDir() {
17302
17302
  function privKeyPath(sessionId) {
17303
17303
  return (0, import_path2.join)(keysDir(), `${sessionId}.key`);
17304
17304
  }
17305
+ function pubKeyPath(sessionId) {
17306
+ return (0, import_path2.join)(keysDir(), `${sessionId}.pub`);
17307
+ }
17308
+ function generateSessionKeypair(sessionId) {
17309
+ const { privateKey, publicKey } = (0, import_crypto2.generateKeyPairSync)("ed25519", {
17310
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
17311
+ publicKeyEncoding: { type: "spki", format: "pem" }
17312
+ });
17313
+ const dir = keysDir();
17314
+ (0, import_fs3.mkdirSync)(dir, { recursive: true });
17315
+ (0, import_fs3.writeFileSync)(privKeyPath(sessionId), privateKey, { mode: 384 });
17316
+ (0, import_fs3.writeFileSync)(pubKeyPath(sessionId), publicKey);
17317
+ return publicKey;
17318
+ }
17305
17319
  function sessionDigest(sessionId, finalContentHash, messageCount, firstCreatedAt) {
17306
17320
  const input = `${sessionId}|${finalContentHash}|${messageCount}|${firstCreatedAt}`;
17307
17321
  return (0, import_crypto2.createHash)("sha256").update(input).digest();
@@ -17356,7 +17370,7 @@ var require_package = __commonJS({
17356
17370
  "package.json"(exports2, module2) {
17357
17371
  module2.exports = {
17358
17372
  name: "chron-mcp",
17359
- version: "0.1.25",
17373
+ version: "0.1.26",
17360
17374
  mcpName: "io.github.sirinivask/chron",
17361
17375
  description: "Audit-grade timestamped logs for every AI conversation",
17362
17376
  repository: {
@@ -18651,9 +18665,9 @@ ${BOLD7}Signature${RESET7}
18651
18665
  warn("No manifest.sig \u2014 bundle was not signed");
18652
18666
  }
18653
18667
  if (sigFile?.session_id && sigFile?.signature) {
18654
- const pubKeyPath = (0, import_path8.join)(tempDir, "pubkeys", `${sigFile.session_id}.pub`);
18668
+ const pubKeyPath2 = (0, import_path8.join)(tempDir, "pubkeys", `${sigFile.session_id}.pub`);
18655
18669
  try {
18656
- const pubKey = (0, import_fs9.readFileSync)(pubKeyPath, "utf8");
18670
+ const pubKey = (0, import_fs9.readFileSync)(pubKeyPath2, "utf8");
18657
18671
  const valid = verifyBufferSignature(pubKey, sigFile.signature, manifestBytes);
18658
18672
  if (valid) {
18659
18673
  ok(`Ed25519 signature valid (session ${sigFile.session_id.slice(0, 8)})`);
@@ -19142,6 +19156,618 @@ var init_doctor = __esm({
19142
19156
  }
19143
19157
  });
19144
19158
 
19159
+ // node_modules/uuid/dist/esm/stringify.js
19160
+ function unsafeStringify(arr, offset = 0) {
19161
+ 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();
19162
+ }
19163
+ var byteToHex;
19164
+ var init_stringify = __esm({
19165
+ "node_modules/uuid/dist/esm/stringify.js"() {
19166
+ byteToHex = [];
19167
+ for (let i = 0; i < 256; ++i) {
19168
+ byteToHex.push((i + 256).toString(16).slice(1));
19169
+ }
19170
+ }
19171
+ });
19172
+
19173
+ // node_modules/uuid/dist/esm/rng.js
19174
+ function rng() {
19175
+ if (poolPtr > rnds8Pool.length - 16) {
19176
+ (0, import_crypto5.randomFillSync)(rnds8Pool);
19177
+ poolPtr = 0;
19178
+ }
19179
+ return rnds8Pool.slice(poolPtr, poolPtr += 16);
19180
+ }
19181
+ var import_crypto5, rnds8Pool, poolPtr;
19182
+ var init_rng = __esm({
19183
+ "node_modules/uuid/dist/esm/rng.js"() {
19184
+ import_crypto5 = require("crypto");
19185
+ rnds8Pool = new Uint8Array(256);
19186
+ poolPtr = rnds8Pool.length;
19187
+ }
19188
+ });
19189
+
19190
+ // node_modules/uuid/dist/esm/native.js
19191
+ var import_crypto6, native_default;
19192
+ var init_native = __esm({
19193
+ "node_modules/uuid/dist/esm/native.js"() {
19194
+ import_crypto6 = require("crypto");
19195
+ native_default = { randomUUID: import_crypto6.randomUUID };
19196
+ }
19197
+ });
19198
+
19199
+ // node_modules/uuid/dist/esm/v4.js
19200
+ function v4(options, buf, offset) {
19201
+ if (native_default.randomUUID && !buf && !options) {
19202
+ return native_default.randomUUID();
19203
+ }
19204
+ options = options || {};
19205
+ const rnds = options.random ?? options.rng?.() ?? rng();
19206
+ if (rnds.length < 16) {
19207
+ throw new Error("Random bytes length must be >= 16");
19208
+ }
19209
+ rnds[6] = rnds[6] & 15 | 64;
19210
+ rnds[8] = rnds[8] & 63 | 128;
19211
+ if (buf) {
19212
+ offset = offset || 0;
19213
+ if (offset < 0 || offset + 16 > buf.length) {
19214
+ throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
19215
+ }
19216
+ for (let i = 0; i < 16; ++i) {
19217
+ buf[offset + i] = rnds[i];
19218
+ }
19219
+ return buf;
19220
+ }
19221
+ return unsafeStringify(rnds);
19222
+ }
19223
+ var v4_default;
19224
+ var init_v4 = __esm({
19225
+ "node_modules/uuid/dist/esm/v4.js"() {
19226
+ init_native();
19227
+ init_rng();
19228
+ init_stringify();
19229
+ v4_default = v4;
19230
+ }
19231
+ });
19232
+
19233
+ // node_modules/uuid/dist/esm/index.js
19234
+ var init_esm = __esm({
19235
+ "node_modules/uuid/dist/esm/index.js"() {
19236
+ init_v4();
19237
+ }
19238
+ });
19239
+
19240
+ // src/utils/detect.ts
19241
+ function luhnCheck(value) {
19242
+ const digits = value.replace(/\D/g, "");
19243
+ let sum = 0;
19244
+ let alt = false;
19245
+ for (let i = digits.length - 1; i >= 0; i--) {
19246
+ let n = parseInt(digits[i], 10);
19247
+ if (alt) {
19248
+ n *= 2;
19249
+ if (n > 9)
19250
+ n -= 9;
19251
+ }
19252
+ sum += n;
19253
+ alt = !alt;
19254
+ }
19255
+ return sum % 10 === 0;
19256
+ }
19257
+ function ibanCheck(value) {
19258
+ const normalized = value.replace(/\s/g, "").toUpperCase();
19259
+ if (normalized.length < 15 || normalized.length > 34)
19260
+ return false;
19261
+ const rearranged = normalized.slice(4) + normalized.slice(0, 4);
19262
+ const numeric2 = rearranged.replace(/[A-Z]/g, (c) => String(c.charCodeAt(0) - 55));
19263
+ let remainder = 0;
19264
+ for (const ch of numeric2) {
19265
+ remainder = (remainder * 10 + parseInt(ch, 10)) % 97;
19266
+ }
19267
+ return remainder === 1;
19268
+ }
19269
+ function contextMatches(kw, input, idx) {
19270
+ const lo = Math.max(0, idx - CONTEXT_WINDOW);
19271
+ const hi = Math.min(input.length, idx + CONTEXT_WINDOW);
19272
+ return kw.test(input.slice(lo, hi));
19273
+ }
19274
+ function annotate(s) {
19275
+ return {
19276
+ ...s,
19277
+ severity: SEVERITY2[s.type] ?? "low",
19278
+ category: CATEGORY[s.type] ?? "other"
19279
+ };
19280
+ }
19281
+ function scanForSecrets(input) {
19282
+ const withPriority = [];
19283
+ for (let i = 0; i < PATTERNS.length; i++) {
19284
+ const { type, regex, validate } = PATTERNS[i];
19285
+ regex.lastIndex = 0;
19286
+ let match;
19287
+ while ((match = regex.exec(input)) !== null) {
19288
+ if (validate && !validate(match[0], input, match.index))
19289
+ continue;
19290
+ withPriority.push({
19291
+ type,
19292
+ value: match[0],
19293
+ start: match.index,
19294
+ end: match.index + match[0].length,
19295
+ priority: i
19296
+ });
19297
+ }
19298
+ }
19299
+ withPriority.sort((a, b) => a.priority - b.priority || a.start - b.start);
19300
+ let deduped = deduplicateOverlaps(withPriority);
19301
+ const pairs = detectCredentialPairs(input, deduped);
19302
+ if (pairs.length > 0) {
19303
+ for (const pair of pairs) {
19304
+ deduped.push({ ...pair, priority: -1 });
19305
+ }
19306
+ deduped.sort((a, b) => a.priority - b.priority || a.start - b.start);
19307
+ deduped = deduplicateOverlaps(deduped);
19308
+ }
19309
+ deduped.sort((a, b) => a.start - b.start);
19310
+ return deduped.map(({ priority: _p, ...s }) => annotate(s));
19311
+ }
19312
+ function looksLikePassword(s) {
19313
+ if (s.length < 8)
19314
+ return false;
19315
+ const hasUpper = /[A-Z]/.test(s);
19316
+ const hasLower = /[a-z]/.test(s);
19317
+ const hasDigit = /[0-9]/.test(s);
19318
+ const hasSpecial = /[@!#$%^&*\-_+=?]/.test(s);
19319
+ const score = [hasUpper, hasLower, hasDigit, hasSpecial].filter(Boolean).length;
19320
+ return score >= 3;
19321
+ }
19322
+ function detectCredentialPairs(input, _existing) {
19323
+ const results = [];
19324
+ EMAIL_RE.lastIndex = 0;
19325
+ let match;
19326
+ while ((match = EMAIL_RE.exec(input)) !== null) {
19327
+ const emailStart = match.index;
19328
+ const emailEnd = match.index + match[0].length;
19329
+ const charBefore = emailStart > 0 ? input[emailStart - 1] : "";
19330
+ if (charBefore === "@" || charBefore === ":")
19331
+ continue;
19332
+ const windowStart = Math.max(0, emailStart - PAIR_WINDOW);
19333
+ const windowEnd = Math.min(input.length, emailEnd + PAIR_WINDOW);
19334
+ const surrounding = input.slice(windowStart, windowEnd);
19335
+ const tokens = surrounding.match(/\S+/g) ?? [];
19336
+ for (const token of tokens) {
19337
+ if (token === match[0])
19338
+ continue;
19339
+ if (!looksLikePassword(token))
19340
+ continue;
19341
+ const tokenAbsoluteStart = windowStart + surrounding.indexOf(token);
19342
+ const pairStart = Math.min(emailStart, tokenAbsoluteStart);
19343
+ const pairEnd = Math.max(emailEnd, tokenAbsoluteStart + token.length);
19344
+ results.push({
19345
+ type: "credential_pair",
19346
+ value: input.slice(pairStart, pairEnd),
19347
+ start: pairStart,
19348
+ end: pairEnd
19349
+ });
19350
+ break;
19351
+ }
19352
+ }
19353
+ return results;
19354
+ }
19355
+ function deduplicateOverlaps(secrets) {
19356
+ const kept = [];
19357
+ for (const candidate of secrets) {
19358
+ const overlaps = kept.some((k) => candidate.start < k.end && candidate.end > k.start);
19359
+ if (!overlaps)
19360
+ kept.push(candidate);
19361
+ }
19362
+ return kept;
19363
+ }
19364
+ var CONTEXT_WINDOW, DOB_KEYWORDS, PASS_KEYWORDS, SSN_KEYWORDS, PATTERNS, SEVERITY2, CATEGORY, EMAIL_RE, PAIR_WINDOW;
19365
+ var init_detect = __esm({
19366
+ "src/utils/detect.ts"() {
19367
+ "use strict";
19368
+ CONTEXT_WINDOW = 120;
19369
+ DOB_KEYWORDS = /\b(?:dob|date\s+of\s+birth|birth\s+date|born\s+on|birthday)\b/i;
19370
+ PASS_KEYWORDS = /\b(?:passport|travel\s+document|document\s+no(?:\.|\b))\b/i;
19371
+ SSN_KEYWORDS = /\b(?:ssn|social\s+security|tin\b)\b/i;
19372
+ PATTERNS = [
19373
+ // Private key blocks (must come first — multiline, unambiguous)
19374
+ { type: "private_key", regex: /-----BEGIN [A-Z ]+ PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+ PRIVATE KEY-----/g },
19375
+ // Cloud / AI provider keys
19376
+ { type: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/g },
19377
+ { type: "anthropic_api_key", regex: /sk-ant-api\d{2}-[a-zA-Z0-9\-_]{40,}/g },
19378
+ { type: "openai_api_key", regex: /sk-proj-[a-zA-Z0-9\-_]{20,}/g },
19379
+ { type: "google_api_key", regex: /AIza[0-9A-Za-z\-_]{35}/g },
19380
+ // SaaS API keys
19381
+ { type: "github_token", regex: /gh[pousr]_[a-zA-Z0-9]{30,}/g },
19382
+ { type: "slack_token", regex: /xox[bpas]-[0-9]{8,13}-[0-9]{8,13}-[a-zA-Z0-9]{20,}/g },
19383
+ { type: "stripe_key", regex: /sk_(live|test)_[a-zA-Z0-9]{24,}/g },
19384
+ { type: "sendgrid_key", regex: /SG\.[a-zA-Z0-9\-_]{22,}\.[a-zA-Z0-9\-_]{22,}/g },
19385
+ { type: "huggingface_token", regex: /hf_[a-zA-Z0-9]{30,}/g },
19386
+ // Auth / session tokens
19387
+ { type: "jwt", regex: /eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/g },
19388
+ // Credentials in URLs (before env_value so it wins on connection strings)
19389
+ { type: "url_credentials", regex: /\w+:\/\/[^:@\s\/]{1,64}:[^@\s]{4,}@[^\s]/g },
19390
+ // Passwords in key=value and JSON
19391
+ { type: "password", regex: /(?:password|passwd|pwd)["']?\s*[=:]\s*["']?([^\s"',]{6,})/gi },
19392
+ // PII — financial
19393
+ {
19394
+ type: "credit_card",
19395
+ regex: /\b\d{4}[\s\-]\d{4}[\s\-]\d{4}[\s\-]\d{4}\b/g,
19396
+ validate: (v) => luhnCheck(v)
19397
+ },
19398
+ {
19399
+ type: "iban",
19400
+ regex: /\b[A-Z]{2}\d{2}[A-Z0-9]{4,30}\b/g,
19401
+ validate: (v) => ibanCheck(v)
19402
+ },
19403
+ // PII — identity (context-aware)
19404
+ {
19405
+ type: "ssn",
19406
+ regex: /\b\d{3}-\d{2}-\d{4}\b/g,
19407
+ validate: (_v, input, idx) => {
19408
+ if (contextMatches(SSN_KEYWORDS, input, idx))
19409
+ return true;
19410
+ const surrounding = input.slice(Math.max(0, idx - 20), idx + 20);
19411
+ if (/(?:19|20)\d{2}/.test(surrounding))
19412
+ return false;
19413
+ return true;
19414
+ }
19415
+ },
19416
+ {
19417
+ type: "dob",
19418
+ regex: /\b(?:0?[1-9]|1[0-2])[-\/](?:0?[1-9]|[12]\d|3[01])[-\/](?:19|20)\d{2}\b/g,
19419
+ validate: (_v, input, idx) => contextMatches(DOB_KEYWORDS, input, idx)
19420
+ },
19421
+ {
19422
+ type: "passport",
19423
+ regex: /\b[A-Z]\d{8}\b/g,
19424
+ validate: (_v, input, idx) => contextMatches(PASS_KEYWORDS, input, idx)
19425
+ },
19426
+ // PII — contact
19427
+ { type: "email", regex: /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g },
19428
+ {
19429
+ type: "phone_us",
19430
+ // Requires area code starting 2-9 (excludes version numbers, dates, plain 7-digit numbers)
19431
+ regex: /\b(?:\+?1[-.\s]?)?\(?([2-9][0-9]{2})\)?[-.\s]([2-9][0-9]{2})[-.\s]([0-9]{4})\b/g
19432
+ },
19433
+ {
19434
+ type: "phone_e164",
19435
+ // E.164 international (non-US, min 8 digits after +)
19436
+ regex: /\+(?!1[^0-9])(?:[2-9][0-9]{1}|[1][^1])[0-9]{6,12}\b/g
19437
+ },
19438
+ // Network — internal RFC-1918 addresses
19439
+ {
19440
+ type: "internal_ip",
19441
+ 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
19442
+ },
19443
+ // Generic env assignment (lowest priority — catches remaining secrets)
19444
+ { type: "env_value", regex: /\b[A-Z][A-Z0-9_]{2,}=[^\s]{8,}/g }
19445
+ ];
19446
+ SEVERITY2 = {
19447
+ private_key: "critical",
19448
+ credit_card: "critical",
19449
+ ssn: "critical",
19450
+ iban: "critical",
19451
+ aws_access_key: "high",
19452
+ anthropic_api_key: "high",
19453
+ openai_api_key: "high",
19454
+ google_api_key: "high",
19455
+ github_token: "high",
19456
+ slack_token: "high",
19457
+ stripe_key: "high",
19458
+ sendgrid_key: "high",
19459
+ huggingface_token: "high",
19460
+ jwt: "high",
19461
+ url_credentials: "high",
19462
+ password: "high",
19463
+ credential_pair: "high",
19464
+ passport: "high",
19465
+ dob: "high",
19466
+ email: "medium",
19467
+ phone_us: "medium",
19468
+ phone_e164: "medium",
19469
+ internal_ip: "low",
19470
+ env_value: "low"
19471
+ };
19472
+ CATEGORY = {
19473
+ private_key: "credential",
19474
+ credit_card: "financial",
19475
+ ssn: "identity",
19476
+ iban: "financial",
19477
+ passport: "identity",
19478
+ dob: "identity",
19479
+ aws_access_key: "credential",
19480
+ anthropic_api_key: "credential",
19481
+ openai_api_key: "credential",
19482
+ google_api_key: "credential",
19483
+ github_token: "credential",
19484
+ slack_token: "credential",
19485
+ stripe_key: "credential",
19486
+ sendgrid_key: "credential",
19487
+ huggingface_token: "credential",
19488
+ jwt: "credential",
19489
+ url_credentials: "credential",
19490
+ password: "credential",
19491
+ credential_pair: "credential",
19492
+ email: "contact",
19493
+ phone_us: "contact",
19494
+ phone_e164: "contact",
19495
+ internal_ip: "network",
19496
+ env_value: "credential"
19497
+ };
19498
+ EMAIL_RE = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g;
19499
+ PAIR_WINDOW = 100;
19500
+ }
19501
+ });
19502
+
19503
+ // src/cli/import.ts
19504
+ var import_exports = {};
19505
+ __export(import_exports, {
19506
+ importGptConversations: () => importGptConversations,
19507
+ runImport: () => runImport
19508
+ });
19509
+ function unixToIso(ts) {
19510
+ if (!ts)
19511
+ return (/* @__PURE__ */ new Date()).toISOString().replace("Z", "+00:00");
19512
+ return new Date(ts * 1e3).toISOString().replace("Z", "+00:00");
19513
+ }
19514
+ function extractText(content) {
19515
+ if (!content)
19516
+ return "";
19517
+ if (!content.parts)
19518
+ return "";
19519
+ return content.parts.filter((p) => typeof p === "string" && p.trim().length > 0).join("\n").trim();
19520
+ }
19521
+ function maskValue(v) {
19522
+ return v.length > 8 ? v.slice(0, 4) + "****" + v.slice(-4) : "****";
19523
+ }
19524
+ function linearize(mapping) {
19525
+ const root = Object.values(mapping).find((n) => n.parent === null || !mapping[n.parent ?? ""]);
19526
+ if (!root)
19527
+ return [];
19528
+ const path = [];
19529
+ let current = root;
19530
+ while (current) {
19531
+ path.push(current);
19532
+ const lastChildId = current.children[current.children.length - 1];
19533
+ current = lastChildId ? mapping[lastChildId] : void 0;
19534
+ }
19535
+ return path;
19536
+ }
19537
+ function messagesFromConversation(conv) {
19538
+ const nodes = linearize(conv.mapping);
19539
+ const result = [];
19540
+ for (const node of nodes) {
19541
+ const msg = node.message;
19542
+ if (!msg)
19543
+ continue;
19544
+ const role = msg.author.role;
19545
+ if (role !== "user" && role !== "assistant")
19546
+ continue;
19547
+ const text2 = extractText(msg.content);
19548
+ if (!text2)
19549
+ continue;
19550
+ result.push({ role, content: text2, ts: unixToIso(msg.create_time) });
19551
+ }
19552
+ return result;
19553
+ }
19554
+ function findFile(dir, name) {
19555
+ for (const entry of (0, import_fs11.readdirSync)(dir)) {
19556
+ const full = (0, import_path10.join)(dir, entry);
19557
+ try {
19558
+ if ((0, import_fs11.statSync)(full).isDirectory()) {
19559
+ const found = findFile(full, name);
19560
+ if (found)
19561
+ return found;
19562
+ } else if (entry === name) {
19563
+ return full;
19564
+ }
19565
+ } catch {
19566
+ }
19567
+ }
19568
+ return null;
19569
+ }
19570
+ async function importGptConversations(db, conversations) {
19571
+ let imported = 0;
19572
+ let skipped = 0;
19573
+ let totalMessages = 0;
19574
+ let totalSecrets = 0;
19575
+ const errors = [];
19576
+ for (const conv of conversations) {
19577
+ const externalRef = `chatgpt:${conv.id}`;
19578
+ const displayTitle = (conv.title ?? "Untitled").slice(0, 60);
19579
+ const [existing] = await db.select({ id: sessions.id }).from(sessions).where(eq(sessions.external_ref, externalRef)).limit(1);
19580
+ if (existing) {
19581
+ skipped++;
19582
+ continue;
19583
+ }
19584
+ const msgs = messagesFromConversation(conv);
19585
+ if (msgs.length === 0) {
19586
+ skipped++;
19587
+ continue;
19588
+ }
19589
+ try {
19590
+ let sessionSecrets = 0;
19591
+ await db.transaction(async (tx) => {
19592
+ let baseTitle = (conv.title ?? "Untitled ChatGPT conversation").slice(0, 500);
19593
+ const [titleConflict] = await tx.select({ id: sessions.id }).from(sessions).where(eq(sessions.title, baseTitle)).limit(1);
19594
+ if (titleConflict) {
19595
+ baseTitle = `${baseTitle} (${conv.id.slice(0, 8)})`.slice(0, 500);
19596
+ }
19597
+ const sessionId = v4_default();
19598
+ let publicKey = null;
19599
+ try {
19600
+ publicKey = generateSessionKeypair(sessionId);
19601
+ } catch {
19602
+ }
19603
+ await tx.insert(sessions).values({
19604
+ id: sessionId,
19605
+ title: baseTitle,
19606
+ ai_tool: "chatgpt",
19607
+ created_at: unixToIso(conv.create_time),
19608
+ updated_at: unixToIso(conv.update_time),
19609
+ external_ref: externalRef,
19610
+ parent_session_id: null,
19611
+ public_key: publicKey
19612
+ });
19613
+ let prevHash = null;
19614
+ for (const m of msgs) {
19615
+ const msgId = v4_default();
19616
+ const hash = computeContentHash(sessionId, m.role, m.content, m.ts, prevHash, "message");
19617
+ await tx.insert(messages).values({
19618
+ id: msgId,
19619
+ session_id: sessionId,
19620
+ role: m.role,
19621
+ content: m.content,
19622
+ created_at: m.ts,
19623
+ prev_hash: prevHash,
19624
+ content_hash: hash,
19625
+ event_type: "message"
19626
+ });
19627
+ if (m.role === "user") {
19628
+ const found = scanForSecrets(m.content);
19629
+ if (found.length > 0) {
19630
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace("Z", "+00:00");
19631
+ await tx.insert(secrets_detected).values(
19632
+ found.map((s) => ({
19633
+ id: v4_default(),
19634
+ session_id: sessionId,
19635
+ message_id: msgId,
19636
+ type: s.type,
19637
+ masked_value: maskValue(s.value),
19638
+ detected_at: now
19639
+ }))
19640
+ );
19641
+ sessionSecrets += found.length;
19642
+ totalSecrets += found.length;
19643
+ }
19644
+ }
19645
+ prevHash = hash;
19646
+ }
19647
+ });
19648
+ totalMessages += msgs.length;
19649
+ imported++;
19650
+ } catch (err) {
19651
+ errors.push({ title: displayTitle, error: err?.message ?? String(err) });
19652
+ }
19653
+ }
19654
+ return { imported, skipped, totalMessages, totalSecrets, errors };
19655
+ }
19656
+ async function importConversations(filePath) {
19657
+ const db = await initDb();
19658
+ let raw;
19659
+ let tempDir = null;
19660
+ const ext = (0, import_path10.extname)(filePath).toLowerCase();
19661
+ if (ext === ".zip") {
19662
+ tempDir = (0, import_path10.join)((0, import_os10.tmpdir)(), `chron-import-${Date.now()}`);
19663
+ (0, import_fs11.mkdirSync)(tempDir, { recursive: true });
19664
+ try {
19665
+ (0, import_child_process4.execFileSync)("unzip", ["-q", "-o", filePath, "conversations.json", "-d", tempDir], { stdio: "pipe" });
19666
+ } catch {
19667
+ try {
19668
+ (0, import_child_process4.execFileSync)("unzip", ["-q", "-o", filePath, "-d", tempDir], { stdio: "pipe" });
19669
+ } catch {
19670
+ process.stderr.write(`Failed to extract ${filePath}. Make sure 'unzip' is installed.
19671
+ `);
19672
+ (0, import_fs11.rmSync)(tempDir, { recursive: true, force: true });
19673
+ process.exit(1);
19674
+ }
19675
+ }
19676
+ const found = findFile(tempDir, "conversations.json");
19677
+ if (!found) {
19678
+ process.stderr.write(`conversations.json not found inside the ZIP.
19679
+ `);
19680
+ (0, import_fs11.rmSync)(tempDir, { recursive: true, force: true });
19681
+ process.exit(1);
19682
+ }
19683
+ raw = (0, import_fs11.readFileSync)(found, "utf8");
19684
+ } else if (ext === ".json") {
19685
+ raw = (0, import_fs11.readFileSync)(filePath, "utf8");
19686
+ } else {
19687
+ process.stderr.write(`Unsupported file type: ${ext}. Pass a .zip export or a conversations.json file.
19688
+ `);
19689
+ process.exit(1);
19690
+ }
19691
+ let conversations;
19692
+ try {
19693
+ conversations = JSON.parse(raw);
19694
+ if (!Array.isArray(conversations))
19695
+ throw new Error("Expected a JSON array");
19696
+ } catch {
19697
+ process.stderr.write(`Failed to parse conversations JSON.
19698
+ `);
19699
+ if (tempDir)
19700
+ (0, import_fs11.rmSync)(tempDir, { recursive: true, force: true });
19701
+ process.exit(1);
19702
+ }
19703
+ process.stdout.write(`
19704
+ ${BOLD9}chron import chatgpt${RESET9} ${DIM8}${filePath}${RESET9}
19705
+ `);
19706
+ process.stdout.write(`${DIM8}${conversations.length} conversation(s) found${RESET9}
19707
+
19708
+ `);
19709
+ const result = await importGptConversations(db, conversations);
19710
+ for (const e of result.errors) {
19711
+ process.stderr.write(` ${YELLOW7}!${RESET9} Failed to import "${e.title}": ${e.error}
19712
+ `);
19713
+ }
19714
+ if (tempDir)
19715
+ (0, import_fs11.rmSync)(tempDir, { recursive: true, force: true });
19716
+ process.stdout.write("\n");
19717
+ process.stdout.write(`${BOLD9}Done.${RESET9} `);
19718
+ process.stdout.write(`${GREEN6}${result.imported} imported${RESET9}`);
19719
+ if (result.skipped > 0)
19720
+ process.stdout.write(` ${DIM8}${result.skipped} skipped (already in DB)${RESET9}`);
19721
+ process.stdout.write(` ${CYAN7}${result.totalMessages} messages total${RESET9}`);
19722
+ if (result.totalSecrets > 0)
19723
+ process.stdout.write(` ${YELLOW7}${result.totalSecrets} secret(s) detected${RESET9}`);
19724
+ if (result.errors.length > 0)
19725
+ process.stdout.write(` ${YELLOW7}${result.errors.length} error(s)${RESET9}`);
19726
+ process.stdout.write("\n\n");
19727
+ process.stdout.write(`${DIM8}Run 'chron history' to browse imported sessions.${RESET9}
19728
+
19729
+ `);
19730
+ }
19731
+ async function runImport(args2) {
19732
+ const [subcommand, filePath] = args2;
19733
+ if (subcommand !== "chatgpt" || !filePath) {
19734
+ process.stderr.write(
19735
+ "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"
19736
+ );
19737
+ process.exit(1);
19738
+ }
19739
+ const resolved = filePath.replace(/^~/, process.env.HOME ?? "");
19740
+ if (!(0, import_fs11.existsSync)(resolved)) {
19741
+ process.stderr.write(`File not found: ${resolved}
19742
+ `);
19743
+ process.exit(1);
19744
+ }
19745
+ await importConversations(resolved);
19746
+ }
19747
+ var import_fs11, import_path10, import_os10, import_child_process4, RESET9, BOLD9, DIM8, GREEN6, YELLOW7, CYAN7;
19748
+ var init_import = __esm({
19749
+ "src/cli/import.ts"() {
19750
+ "use strict";
19751
+ import_fs11 = require("fs");
19752
+ import_path10 = require("path");
19753
+ import_os10 = require("os");
19754
+ import_child_process4 = require("child_process");
19755
+ init_drizzle_orm();
19756
+ init_esm();
19757
+ init_db2();
19758
+ init_schema();
19759
+ init_hash();
19760
+ init_detect();
19761
+ init_signing();
19762
+ RESET9 = "\x1B[0m";
19763
+ BOLD9 = "\x1B[1m";
19764
+ DIM8 = "\x1B[2m";
19765
+ GREEN6 = "\x1B[32m";
19766
+ YELLOW7 = "\x1B[33m";
19767
+ CYAN7 = "\x1B[36m";
19768
+ }
19769
+ });
19770
+
19145
19771
  // src/cli/index.ts
19146
19772
  var [, , command, ...args] = process.argv;
19147
19773
  async function main() {
@@ -19201,6 +19827,11 @@ async function main() {
19201
19827
  await runDoctor2(args);
19202
19828
  break;
19203
19829
  }
19830
+ case "import": {
19831
+ const { runImport: runImport2 } = await Promise.resolve().then(() => (init_import(), import_exports));
19832
+ await runImport2(args);
19833
+ break;
19834
+ }
19204
19835
  default: {
19205
19836
  const name = command ? `Unknown command: ${command}
19206
19837
 
@@ -19220,6 +19851,7 @@ Commands:
19220
19851
  verify Verify a session's hash chain and Ed25519 signature
19221
19852
  prune Delete sessions older than a retention cutoff
19222
19853
  doctor Check your Chron setup \u2014 Node version, DB, MCP configs, SIEM
19854
+ import Import conversations from external AI tools
19223
19855
 
19224
19856
  Options (history):
19225
19857
  --limit=<n> Max sessions to show (default: 20)
@@ -19249,6 +19881,9 @@ Options (prune):
19249
19881
 
19250
19882
  Options (doctor):
19251
19883
  --json Machine-readable JSON output
19884
+
19885
+ Options (import):
19886
+ chatgpt <file> Import from ChatGPT export (.zip or conversations.json)
19252
19887
  `
19253
19888
  );
19254
19889
  process.exit(command ? 1 : 0);
package/dist/index.js CHANGED
@@ -38479,7 +38479,7 @@ var init_time = __esm({
38479
38479
  var version4;
38480
38480
  var init_package = __esm({
38481
38481
  "package.json"() {
38482
- version4 = "0.1.25";
38482
+ version4 = "0.1.26";
38483
38483
  }
38484
38484
  });
38485
38485
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chron-mcp",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "mcpName": "io.github.sirinivask/chron",
5
5
  "description": "Audit-grade timestamped logs for every AI conversation",
6
6
  "repository": {