chat-logbook 0.10.0 → 0.12.0
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 +35 -108
- package/api/dist/drizzle/archive/0007_drop_session_scan_state.sql +2 -0
- package/api/dist/drizzle/archive/meta/_journal.json +7 -0
- package/api/dist/drizzle/checkpoint/0000_sour_zeigeist.sql +11 -0
- package/api/dist/drizzle/checkpoint/meta/0000_snapshot.json +83 -0
- package/api/dist/drizzle/checkpoint/meta/_journal.json +13 -0
- package/api/dist/index.js +699 -532
- package/package.json +4 -2
- package/web/dist/assets/index-BJlGZyK5.js +57 -0
- package/web/dist/assets/index-DSNHfjXl.css +2 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-Bt6Wu3MG.js +0 -57
- package/web/dist/assets/index-Dy8ynnur.css +0 -2
package/api/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import fs6 from "fs";
|
|
|
9
9
|
import os from "os";
|
|
10
10
|
import path4 from "path";
|
|
11
11
|
import { exec } from "child_process";
|
|
12
|
-
import { fileURLToPath as
|
|
12
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
13
13
|
|
|
14
14
|
// ../node_modules/.pnpm/@hono+node-server@1.19.11_hono@4.12.9/node_modules/@hono/node-server/dist/index.mjs
|
|
15
15
|
import { createServer as createServerHTTP } from "http";
|
|
@@ -2860,6 +2860,207 @@ var serveStatic = (options = { root: "" }) => {
|
|
|
2860
2860
|
};
|
|
2861
2861
|
};
|
|
2862
2862
|
|
|
2863
|
+
// src/archive/chat-id.ts
|
|
2864
|
+
import crypto3 from "crypto";
|
|
2865
|
+
var CROCKFORD_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
2866
|
+
var CHAT_ID_LENGTH = 6;
|
|
2867
|
+
var MAX_RETRIES = 5;
|
|
2868
|
+
var CHAT_ID_PREFIX = "clog_";
|
|
2869
|
+
var WIRE_FORM_RE = new RegExp(
|
|
2870
|
+
`^${CHAT_ID_PREFIX}[${CROCKFORD_ALPHABET}]{${CHAT_ID_LENGTH}}$`
|
|
2871
|
+
);
|
|
2872
|
+
function formatChatId(code) {
|
|
2873
|
+
return `${CHAT_ID_PREFIX}${code}`;
|
|
2874
|
+
}
|
|
2875
|
+
function parseChatId(wire) {
|
|
2876
|
+
if (!WIRE_FORM_RE.test(wire)) return null;
|
|
2877
|
+
return wire.slice(CHAT_ID_PREFIX.length);
|
|
2878
|
+
}
|
|
2879
|
+
function generateChatId({
|
|
2880
|
+
isTaken,
|
|
2881
|
+
randomIndex = () => crypto3.randomInt(CROCKFORD_ALPHABET.length)
|
|
2882
|
+
}) {
|
|
2883
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
2884
|
+
let code = "";
|
|
2885
|
+
for (let i = 0; i < CHAT_ID_LENGTH; i++) {
|
|
2886
|
+
code += CROCKFORD_ALPHABET[randomIndex()];
|
|
2887
|
+
}
|
|
2888
|
+
if (!isTaken(code)) {
|
|
2889
|
+
return code;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
throw new Error(
|
|
2893
|
+
`Failed to generate unique chat_id after ${MAX_RETRIES + 1} attempts`
|
|
2894
|
+
);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
// src/visibility.ts
|
|
2898
|
+
function loadChatVisibility(metadata2, opts) {
|
|
2899
|
+
const deleted = metadata2.listDeleted();
|
|
2900
|
+
const trashed = new Set(deleted.map((r) => r.id));
|
|
2901
|
+
const deletedAtById = new Map(
|
|
2902
|
+
deleted.map((r) => [r.id, r.deletedAt?.getTime() ?? null])
|
|
2903
|
+
);
|
|
2904
|
+
const showTrashed = opts.includeTrashed === true;
|
|
2905
|
+
return {
|
|
2906
|
+
isTrashed: (internalId) => trashed.has(internalId),
|
|
2907
|
+
isVisible: (internalId) => showTrashed || !trashed.has(internalId),
|
|
2908
|
+
deletedAt: (internalId) => deletedAtById.get(internalId) ?? null
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
// src/chat-reader.ts
|
|
2913
|
+
function toApiBlock(block) {
|
|
2914
|
+
if (block.type === "tool_result") {
|
|
2915
|
+
return {
|
|
2916
|
+
type: "tool_result",
|
|
2917
|
+
tool_use_id: String(block.toolUseId ?? ""),
|
|
2918
|
+
content: block.content
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
return block;
|
|
2922
|
+
}
|
|
2923
|
+
function createChatReader({
|
|
2924
|
+
archive: archive2,
|
|
2925
|
+
metadata: metadata2
|
|
2926
|
+
}) {
|
|
2927
|
+
function findChat(id) {
|
|
2928
|
+
const code = parseChatId(id);
|
|
2929
|
+
if (code === null) return null;
|
|
2930
|
+
return archive2.read.findChatByChatId(code);
|
|
2931
|
+
}
|
|
2932
|
+
function deriveTitle(customTitle, firstUserText) {
|
|
2933
|
+
if (customTitle && customTitle.trim()) return customTitle;
|
|
2934
|
+
const text2 = firstUserText?.trim().split("\n")[0]?.trim();
|
|
2935
|
+
return text2 && text2.length > 0 ? text2 : "Untitled";
|
|
2936
|
+
}
|
|
2937
|
+
function key(agent, sourceId) {
|
|
2938
|
+
return [agent, sourceId].join("\0");
|
|
2939
|
+
}
|
|
2940
|
+
function listChats({
|
|
2941
|
+
includeTrashed
|
|
2942
|
+
}) {
|
|
2943
|
+
const visibility = loadChatVisibility(metadata2, { includeTrashed });
|
|
2944
|
+
const rows = archive2.read.listChatRows();
|
|
2945
|
+
const tsRangeByKey = new Map(
|
|
2946
|
+
archive2.read.listChatTsRanges().map((r) => [key(r.agent, r.sourceId), r])
|
|
2947
|
+
);
|
|
2948
|
+
const sourcePathByKey = new Map(
|
|
2949
|
+
archive2.read.listLatestRawSourcePaths().map((r) => [key(r.agent, r.sourceId), r.sourcePath])
|
|
2950
|
+
);
|
|
2951
|
+
const firstUserTextByKey = new Map(
|
|
2952
|
+
archive2.read.listFirstUserTexts().map((r) => [key(r.agent, r.sourceId), r.text])
|
|
2953
|
+
);
|
|
2954
|
+
const customTitleById = metadata2.listCustomTitles();
|
|
2955
|
+
const chats2 = [];
|
|
2956
|
+
for (const row of rows) {
|
|
2957
|
+
if (!visibility.isVisible(row.id)) continue;
|
|
2958
|
+
const isDeleted = visibility.isTrashed(row.id);
|
|
2959
|
+
const rowKey = key(row.agent, row.sourceId);
|
|
2960
|
+
const tsRange = tsRangeByKey.get(rowKey);
|
|
2961
|
+
const firstSeenAtMs = row.firstSeenAt.getTime();
|
|
2962
|
+
const chat = {
|
|
2963
|
+
id: formatChatId(row.chatId),
|
|
2964
|
+
sourceId: row.sourceId,
|
|
2965
|
+
agent: row.agent,
|
|
2966
|
+
title: deriveTitle(
|
|
2967
|
+
customTitleById.get(row.id),
|
|
2968
|
+
firstUserTextByKey.get(rowKey)
|
|
2969
|
+
),
|
|
2970
|
+
project: row.project ?? "",
|
|
2971
|
+
projectPath: row.projectPath ?? null,
|
|
2972
|
+
sourceFilePath: sourcePathByKey.get(rowKey) ?? null,
|
|
2973
|
+
createdAt: tsRange?.minTs ?? firstSeenAtMs,
|
|
2974
|
+
updatedAt: tsRange?.maxTs ?? firstSeenAtMs,
|
|
2975
|
+
deletedAt: visibility.deletedAt(row.id)
|
|
2976
|
+
};
|
|
2977
|
+
if (isDeleted) chat.isDeleted = true;
|
|
2978
|
+
chats2.push(chat);
|
|
2979
|
+
}
|
|
2980
|
+
return chats2;
|
|
2981
|
+
}
|
|
2982
|
+
function getMessages(id, { includeTrashed }) {
|
|
2983
|
+
const row = findChat(id);
|
|
2984
|
+
if (!row) return null;
|
|
2985
|
+
const visibility = loadChatVisibility(metadata2, { includeTrashed });
|
|
2986
|
+
if (!visibility.isVisible(row.id)) return null;
|
|
2987
|
+
const rows = archive2.read.listMessagesByChat(row.agent, row.sourceId);
|
|
2988
|
+
return rows.map((m) => ({
|
|
2989
|
+
role: m.role,
|
|
2990
|
+
content: m.blocks.map(toApiBlock),
|
|
2991
|
+
timestamp: m.ts.toISOString()
|
|
2992
|
+
}));
|
|
2993
|
+
}
|
|
2994
|
+
return { listChats, getMessages, findChat };
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
// src/app.ts
|
|
2998
|
+
function createApp({ archive: archive2, metadata: metadata2, webDistDir: webDistDir2 }) {
|
|
2999
|
+
const app2 = new Hono2();
|
|
3000
|
+
const reader = createChatReader({ archive: archive2, metadata: metadata2 });
|
|
3001
|
+
app2.get("/api/chats", (c) => {
|
|
3002
|
+
const chats2 = reader.listChats({
|
|
3003
|
+
includeTrashed: c.req.query("includeTrashed") === "true"
|
|
3004
|
+
});
|
|
3005
|
+
return c.json({ chats: chats2 });
|
|
3006
|
+
});
|
|
3007
|
+
app2.delete("/api/chats/:id", (c) => {
|
|
3008
|
+
const row = reader.findChat(c.req.param("id"));
|
|
3009
|
+
if (!row) {
|
|
3010
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
3011
|
+
}
|
|
3012
|
+
metadata2.softDelete(row.id);
|
|
3013
|
+
return c.body(null, 204);
|
|
3014
|
+
});
|
|
3015
|
+
app2.post("/api/chats/:id/restore", (c) => {
|
|
3016
|
+
const row = reader.findChat(c.req.param("id"));
|
|
3017
|
+
if (!row) {
|
|
3018
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
3019
|
+
}
|
|
3020
|
+
metadata2.restore(row.id);
|
|
3021
|
+
return c.body(null, 204);
|
|
3022
|
+
});
|
|
3023
|
+
app2.patch("/api/chats/:id/title", async (c) => {
|
|
3024
|
+
const row = reader.findChat(c.req.param("id"));
|
|
3025
|
+
if (!row) {
|
|
3026
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
3027
|
+
}
|
|
3028
|
+
let body;
|
|
3029
|
+
try {
|
|
3030
|
+
body = await c.req.json();
|
|
3031
|
+
} catch {
|
|
3032
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
3033
|
+
}
|
|
3034
|
+
if (!body || typeof body !== "object" || typeof body.title !== "string") {
|
|
3035
|
+
return c.json({ error: "Invalid title" }, 400);
|
|
3036
|
+
}
|
|
3037
|
+
const raw2 = body.title;
|
|
3038
|
+
if (raw2.length > 200) {
|
|
3039
|
+
return c.json({ error: "Title too long" }, 400);
|
|
3040
|
+
}
|
|
3041
|
+
const trimmed = raw2.trim();
|
|
3042
|
+
metadata2.setCustomTitle(row.id, trimmed.length > 0 ? trimmed : null);
|
|
3043
|
+
return c.body(null, 204);
|
|
3044
|
+
});
|
|
3045
|
+
app2.get("/api/chats/:id", (c) => {
|
|
3046
|
+
const messages2 = reader.getMessages(c.req.param("id"), {
|
|
3047
|
+
includeTrashed: c.req.query("includeTrashed") === "true"
|
|
3048
|
+
});
|
|
3049
|
+
if (messages2 === null) {
|
|
3050
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
3051
|
+
}
|
|
3052
|
+
return c.json({ messages: messages2 });
|
|
3053
|
+
});
|
|
3054
|
+
if (webDistDir2) {
|
|
3055
|
+
app2.use("*", serveStatic({ root: webDistDir2 }));
|
|
3056
|
+
app2.use("*", serveStatic({ root: webDistDir2, path: "index.html" }));
|
|
3057
|
+
}
|
|
3058
|
+
return app2;
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
// src/archive/repository.ts
|
|
3062
|
+
import crypto5 from "crypto";
|
|
3063
|
+
|
|
2863
3064
|
// ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/entity.js
|
|
2864
3065
|
var entityKind = /* @__PURE__ */ Symbol.for("drizzle:entityKind");
|
|
2865
3066
|
function is(value, type) {
|
|
@@ -4754,17 +4955,82 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
|
|
|
4754
4955
|
return result;
|
|
4755
4956
|
}
|
|
4756
4957
|
|
|
4757
|
-
// src/
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4958
|
+
// src/storage/openStore.ts
|
|
4959
|
+
import fs2 from "fs";
|
|
4960
|
+
import path from "path";
|
|
4961
|
+
import { fileURLToPath } from "url";
|
|
4962
|
+
import Database from "better-sqlite3";
|
|
4963
|
+
|
|
4964
|
+
// ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/better-sqlite3/driver.js
|
|
4965
|
+
import Client from "better-sqlite3";
|
|
4966
|
+
|
|
4967
|
+
// ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/selection-proxy.js
|
|
4968
|
+
var SelectionProxyHandler = class _SelectionProxyHandler {
|
|
4969
|
+
static [entityKind] = "SelectionProxyHandler";
|
|
4970
|
+
config;
|
|
4971
|
+
constructor(config) {
|
|
4972
|
+
this.config = { ...config };
|
|
4973
|
+
}
|
|
4974
|
+
get(subquery, prop) {
|
|
4975
|
+
if (prop === "_") {
|
|
4976
|
+
return {
|
|
4977
|
+
...subquery["_"],
|
|
4978
|
+
selectedFields: new Proxy(
|
|
4979
|
+
subquery._.selectedFields,
|
|
4980
|
+
this
|
|
4981
|
+
)
|
|
4982
|
+
};
|
|
4983
|
+
}
|
|
4984
|
+
if (prop === ViewBaseConfig) {
|
|
4985
|
+
return {
|
|
4986
|
+
...subquery[ViewBaseConfig],
|
|
4987
|
+
selectedFields: new Proxy(
|
|
4988
|
+
subquery[ViewBaseConfig].selectedFields,
|
|
4989
|
+
this
|
|
4990
|
+
)
|
|
4991
|
+
};
|
|
4992
|
+
}
|
|
4993
|
+
if (typeof prop === "symbol") {
|
|
4994
|
+
return subquery[prop];
|
|
4995
|
+
}
|
|
4996
|
+
const columns = is(subquery, Subquery) ? subquery._.selectedFields : is(subquery, View) ? subquery[ViewBaseConfig].selectedFields : subquery;
|
|
4997
|
+
const value = columns[prop];
|
|
4998
|
+
if (is(value, SQL.Aliased)) {
|
|
4999
|
+
if (this.config.sqlAliasedBehavior === "sql" && !value.isSelectionField) {
|
|
5000
|
+
return value.sql;
|
|
5001
|
+
}
|
|
5002
|
+
const newValue = value.clone();
|
|
5003
|
+
newValue.isSelectionField = true;
|
|
5004
|
+
return newValue;
|
|
5005
|
+
}
|
|
5006
|
+
if (is(value, SQL)) {
|
|
5007
|
+
if (this.config.sqlBehavior === "sql") {
|
|
5008
|
+
return value;
|
|
5009
|
+
}
|
|
5010
|
+
throw new Error(
|
|
5011
|
+
`You tried to reference "${prop}" field from a subquery, which is a raw SQL field, but it doesn't have an alias declared. Please add an alias to the field using ".as('alias')" method.`
|
|
5012
|
+
);
|
|
5013
|
+
}
|
|
5014
|
+
if (is(value, Column)) {
|
|
5015
|
+
if (this.config.alias) {
|
|
5016
|
+
return new Proxy(
|
|
5017
|
+
value,
|
|
5018
|
+
new ColumnAliasProxyHandler(
|
|
5019
|
+
new Proxy(
|
|
5020
|
+
value.table,
|
|
5021
|
+
new TableAliasProxyHandler(this.config.alias, this.config.replaceOriginalName ?? false)
|
|
5022
|
+
)
|
|
5023
|
+
)
|
|
5024
|
+
);
|
|
5025
|
+
}
|
|
5026
|
+
return value;
|
|
5027
|
+
}
|
|
5028
|
+
if (typeof value !== "object" || value === null) {
|
|
5029
|
+
return value;
|
|
5030
|
+
}
|
|
5031
|
+
return new Proxy(value, new _SelectionProxyHandler(this.config));
|
|
5032
|
+
}
|
|
5033
|
+
};
|
|
4768
5034
|
|
|
4769
5035
|
// ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/sqlite-core/foreign-keys.js
|
|
4770
5036
|
var ForeignKeyBuilder2 = class {
|
|
@@ -5318,74 +5584,6 @@ function text(a, b = {}) {
|
|
|
5318
5584
|
return new SQLiteTextBuilder(name, config);
|
|
5319
5585
|
}
|
|
5320
5586
|
|
|
5321
|
-
// ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/selection-proxy.js
|
|
5322
|
-
var SelectionProxyHandler = class _SelectionProxyHandler {
|
|
5323
|
-
static [entityKind] = "SelectionProxyHandler";
|
|
5324
|
-
config;
|
|
5325
|
-
constructor(config) {
|
|
5326
|
-
this.config = { ...config };
|
|
5327
|
-
}
|
|
5328
|
-
get(subquery, prop) {
|
|
5329
|
-
if (prop === "_") {
|
|
5330
|
-
return {
|
|
5331
|
-
...subquery["_"],
|
|
5332
|
-
selectedFields: new Proxy(
|
|
5333
|
-
subquery._.selectedFields,
|
|
5334
|
-
this
|
|
5335
|
-
)
|
|
5336
|
-
};
|
|
5337
|
-
}
|
|
5338
|
-
if (prop === ViewBaseConfig) {
|
|
5339
|
-
return {
|
|
5340
|
-
...subquery[ViewBaseConfig],
|
|
5341
|
-
selectedFields: new Proxy(
|
|
5342
|
-
subquery[ViewBaseConfig].selectedFields,
|
|
5343
|
-
this
|
|
5344
|
-
)
|
|
5345
|
-
};
|
|
5346
|
-
}
|
|
5347
|
-
if (typeof prop === "symbol") {
|
|
5348
|
-
return subquery[prop];
|
|
5349
|
-
}
|
|
5350
|
-
const columns = is(subquery, Subquery) ? subquery._.selectedFields : is(subquery, View) ? subquery[ViewBaseConfig].selectedFields : subquery;
|
|
5351
|
-
const value = columns[prop];
|
|
5352
|
-
if (is(value, SQL.Aliased)) {
|
|
5353
|
-
if (this.config.sqlAliasedBehavior === "sql" && !value.isSelectionField) {
|
|
5354
|
-
return value.sql;
|
|
5355
|
-
}
|
|
5356
|
-
const newValue = value.clone();
|
|
5357
|
-
newValue.isSelectionField = true;
|
|
5358
|
-
return newValue;
|
|
5359
|
-
}
|
|
5360
|
-
if (is(value, SQL)) {
|
|
5361
|
-
if (this.config.sqlBehavior === "sql") {
|
|
5362
|
-
return value;
|
|
5363
|
-
}
|
|
5364
|
-
throw new Error(
|
|
5365
|
-
`You tried to reference "${prop}" field from a subquery, which is a raw SQL field, but it doesn't have an alias declared. Please add an alias to the field using ".as('alias')" method.`
|
|
5366
|
-
);
|
|
5367
|
-
}
|
|
5368
|
-
if (is(value, Column)) {
|
|
5369
|
-
if (this.config.alias) {
|
|
5370
|
-
return new Proxy(
|
|
5371
|
-
value,
|
|
5372
|
-
new ColumnAliasProxyHandler(
|
|
5373
|
-
new Proxy(
|
|
5374
|
-
value.table,
|
|
5375
|
-
new TableAliasProxyHandler(this.config.alias, this.config.replaceOriginalName ?? false)
|
|
5376
|
-
)
|
|
5377
|
-
)
|
|
5378
|
-
);
|
|
5379
|
-
}
|
|
5380
|
-
return value;
|
|
5381
|
-
}
|
|
5382
|
-
if (typeof value !== "object" || value === null) {
|
|
5383
|
-
return value;
|
|
5384
|
-
}
|
|
5385
|
-
return new Proxy(value, new _SelectionProxyHandler(this.config));
|
|
5386
|
-
}
|
|
5387
|
-
};
|
|
5388
|
-
|
|
5389
5587
|
// ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/sqlite-core/columns/all.js
|
|
5390
5588
|
function getSQLiteColumnBuilders() {
|
|
5391
5589
|
return {
|
|
@@ -8169,257 +8367,6 @@ var SQLiteTransaction = class extends BaseSQLiteDatabase {
|
|
|
8169
8367
|
}
|
|
8170
8368
|
};
|
|
8171
8369
|
|
|
8172
|
-
// src/archive/schema.ts
|
|
8173
|
-
var archiveMeta = sqliteTable("archive_meta", {
|
|
8174
|
-
id: integer("id").primaryKey(),
|
|
8175
|
-
archiveUuid: text("archive_uuid").notNull(),
|
|
8176
|
-
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull()
|
|
8177
|
-
});
|
|
8178
|
-
var schemaVersion = sqliteTable("schema_version", {
|
|
8179
|
-
version: integer("version").primaryKey(),
|
|
8180
|
-
appliedAt: integer("applied_at", { mode: "timestamp_ms" }).notNull()
|
|
8181
|
-
});
|
|
8182
|
-
var chats = sqliteTable(
|
|
8183
|
-
"chats",
|
|
8184
|
-
{
|
|
8185
|
-
id: text("id").primaryKey(),
|
|
8186
|
-
chatId: text("chat_id").notNull().unique(),
|
|
8187
|
-
agent: text("agent").notNull(),
|
|
8188
|
-
sourceId: text("source_id").notNull(),
|
|
8189
|
-
firstSeenAt: integer("first_seen_at", { mode: "timestamp_ms" }).notNull(),
|
|
8190
|
-
project: text("project"),
|
|
8191
|
-
projectPath: text("project_path")
|
|
8192
|
-
},
|
|
8193
|
-
(t) => [uniqueIndex("chats_agent_source_idx").on(t.agent, t.sourceId)]
|
|
8194
|
-
);
|
|
8195
|
-
var rawMessages = sqliteTable(
|
|
8196
|
-
"raw_messages",
|
|
8197
|
-
{
|
|
8198
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
8199
|
-
agent: text("agent").notNull(),
|
|
8200
|
-
sourceId: text("source_id").notNull(),
|
|
8201
|
-
sourcePath: text("source_path").notNull(),
|
|
8202
|
-
sourceLocator: text("source_locator").notNull(),
|
|
8203
|
-
rawPayload: text("raw_payload").notNull(),
|
|
8204
|
-
payloadHash: text("payload_hash").notNull(),
|
|
8205
|
-
ingestedAt: integer("ingested_at", { mode: "timestamp_ms" }).notNull()
|
|
8206
|
-
},
|
|
8207
|
-
(t) => [
|
|
8208
|
-
uniqueIndex("raw_messages_idem_idx").on(t.agent, t.sourceId, t.payloadHash)
|
|
8209
|
-
]
|
|
8210
|
-
);
|
|
8211
|
-
var messages = sqliteTable(
|
|
8212
|
-
"messages",
|
|
8213
|
-
{
|
|
8214
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
8215
|
-
agent: text("agent").notNull(),
|
|
8216
|
-
sourceId: text("source_id").notNull(),
|
|
8217
|
-
messageId: text("message_id").notNull(),
|
|
8218
|
-
role: text("role").notNull(),
|
|
8219
|
-
ts: integer("ts", { mode: "timestamp_ms" }).notNull(),
|
|
8220
|
-
text: text("text").notNull(),
|
|
8221
|
-
blocks: text("blocks", { mode: "json" }).notNull(),
|
|
8222
|
-
rawId: integer("raw_id").notNull().references(() => rawMessages.id)
|
|
8223
|
-
},
|
|
8224
|
-
(t) => [
|
|
8225
|
-
uniqueIndex("messages_canonical_idx").on(t.agent, t.sourceId, t.messageId)
|
|
8226
|
-
]
|
|
8227
|
-
);
|
|
8228
|
-
var chatScanState = sqliteTable(
|
|
8229
|
-
"session_scan_state",
|
|
8230
|
-
{
|
|
8231
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
8232
|
-
agent: text("agent").notNull(),
|
|
8233
|
-
sourceId: text("source_id").notNull(),
|
|
8234
|
-
sourcePath: text("source_path").notNull(),
|
|
8235
|
-
lastMtimeMs: integer("last_mtime_ms").notNull(),
|
|
8236
|
-
lastSizeBytes: integer("last_size_bytes").notNull(),
|
|
8237
|
-
lastScannedAt: integer("last_scanned_at", {
|
|
8238
|
-
mode: "timestamp_ms"
|
|
8239
|
-
}).notNull()
|
|
8240
|
-
},
|
|
8241
|
-
(t) => [uniqueIndex("session_scan_state_idx").on(t.agent, t.sourceId)]
|
|
8242
|
-
);
|
|
8243
|
-
var ingestionEvents = sqliteTable("ingestion_events", {
|
|
8244
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
8245
|
-
agent: text("agent").notNull(),
|
|
8246
|
-
sourceId: text("source_id").notNull(),
|
|
8247
|
-
sourcePath: text("source_path").notNull(),
|
|
8248
|
-
eventType: text("event_type").notNull(),
|
|
8249
|
-
detail: text("detail", { mode: "json" }).notNull(),
|
|
8250
|
-
observedAt: integer("observed_at", { mode: "timestamp_ms" }).notNull()
|
|
8251
|
-
});
|
|
8252
|
-
|
|
8253
|
-
// src/visibility.ts
|
|
8254
|
-
function loadChatVisibility(metadata2, opts) {
|
|
8255
|
-
const deleted = metadata2.listDeleted();
|
|
8256
|
-
const trashed = new Set(deleted.map((r) => r.id));
|
|
8257
|
-
const deletedAtById = new Map(
|
|
8258
|
-
deleted.map((r) => [r.id, r.deletedAt?.getTime() ?? null])
|
|
8259
|
-
);
|
|
8260
|
-
const showTrashed = opts.includeTrashed === true;
|
|
8261
|
-
return {
|
|
8262
|
-
isTrashed: (internalId) => trashed.has(internalId),
|
|
8263
|
-
isVisible: (internalId) => showTrashed || !trashed.has(internalId),
|
|
8264
|
-
deletedAt: (internalId) => deletedAtById.get(internalId) ?? null
|
|
8265
|
-
};
|
|
8266
|
-
}
|
|
8267
|
-
|
|
8268
|
-
// src/app.ts
|
|
8269
|
-
function toApiBlock(block) {
|
|
8270
|
-
if (block.type === "tool_result") {
|
|
8271
|
-
return {
|
|
8272
|
-
type: "tool_result",
|
|
8273
|
-
tool_use_id: String(block.toolUseId ?? ""),
|
|
8274
|
-
content: block.content
|
|
8275
|
-
};
|
|
8276
|
-
}
|
|
8277
|
-
return block;
|
|
8278
|
-
}
|
|
8279
|
-
var CLAUDE_CODE_AGENT = "claude-code";
|
|
8280
|
-
function createApp({ archive: archive2, metadata: metadata2, webDistDir: webDistDir2 }) {
|
|
8281
|
-
const app2 = new Hono2();
|
|
8282
|
-
function findArchiveChatBySourceId(sourceId) {
|
|
8283
|
-
return archive2.db.select().from(chats).where(eq(chats.sourceId, sourceId)).get();
|
|
8284
|
-
}
|
|
8285
|
-
app2.get("/api/chats", (c) => {
|
|
8286
|
-
const visibility = loadChatVisibility(metadata2, {
|
|
8287
|
-
includeTrashed: c.req.query("includeTrashed") === "true"
|
|
8288
|
-
});
|
|
8289
|
-
const rows = archive2.db.select().from(chats).all();
|
|
8290
|
-
const chats2 = [];
|
|
8291
|
-
for (const row of rows) {
|
|
8292
|
-
if (!visibility.isVisible(row.id)) continue;
|
|
8293
|
-
const isDeleted = visibility.isTrashed(row.id);
|
|
8294
|
-
const latestRaw = archive2.db.select({ sourcePath: rawMessages.sourcePath }).from(rawMessages).where(
|
|
8295
|
-
and(
|
|
8296
|
-
eq(rawMessages.agent, CLAUDE_CODE_AGENT),
|
|
8297
|
-
eq(rawMessages.sourceId, row.sourceId)
|
|
8298
|
-
)
|
|
8299
|
-
).orderBy(desc(rawMessages.ingestedAt)).limit(1).get();
|
|
8300
|
-
const tsRange = archive2.db.select({
|
|
8301
|
-
minTs: sql`min(${messages.ts})`,
|
|
8302
|
-
maxTs: sql`max(${messages.ts})`
|
|
8303
|
-
}).from(messages).where(
|
|
8304
|
-
and(
|
|
8305
|
-
eq(messages.agent, CLAUDE_CODE_AGENT),
|
|
8306
|
-
eq(messages.sourceId, row.sourceId)
|
|
8307
|
-
)
|
|
8308
|
-
).get();
|
|
8309
|
-
const firstSeenAtMs = row.firstSeenAt.getTime();
|
|
8310
|
-
const chat = {
|
|
8311
|
-
id: row.sourceId,
|
|
8312
|
-
chatId: row.chatId,
|
|
8313
|
-
agent: row.agent,
|
|
8314
|
-
title: deriveTitle(archive2, metadata2, row),
|
|
8315
|
-
project: row.project ?? "",
|
|
8316
|
-
projectPath: row.projectPath ?? null,
|
|
8317
|
-
sourceFilePath: latestRaw?.sourcePath ?? null,
|
|
8318
|
-
createdAt: tsRange?.minTs ?? firstSeenAtMs,
|
|
8319
|
-
updatedAt: tsRange?.maxTs ?? firstSeenAtMs,
|
|
8320
|
-
deletedAt: visibility.deletedAt(row.id)
|
|
8321
|
-
};
|
|
8322
|
-
if (isDeleted) chat.isDeleted = true;
|
|
8323
|
-
chats2.push(chat);
|
|
8324
|
-
}
|
|
8325
|
-
return c.json({ chats: chats2 });
|
|
8326
|
-
});
|
|
8327
|
-
app2.delete("/api/chats/:id", (c) => {
|
|
8328
|
-
const id = c.req.param("id");
|
|
8329
|
-
const row = findArchiveChatBySourceId(id);
|
|
8330
|
-
if (!row) {
|
|
8331
|
-
return c.json({ error: "Chat not found" }, 404);
|
|
8332
|
-
}
|
|
8333
|
-
metadata2.softDelete(row.id);
|
|
8334
|
-
return c.body(null, 204);
|
|
8335
|
-
});
|
|
8336
|
-
app2.post("/api/chats/:id/restore", (c) => {
|
|
8337
|
-
const id = c.req.param("id");
|
|
8338
|
-
const row = findArchiveChatBySourceId(id);
|
|
8339
|
-
if (!row) {
|
|
8340
|
-
return c.json({ error: "Chat not found" }, 404);
|
|
8341
|
-
}
|
|
8342
|
-
metadata2.restore(row.id);
|
|
8343
|
-
return c.body(null, 204);
|
|
8344
|
-
});
|
|
8345
|
-
app2.patch("/api/chats/:id/title", async (c) => {
|
|
8346
|
-
const id = c.req.param("id");
|
|
8347
|
-
const row = findArchiveChatBySourceId(id);
|
|
8348
|
-
if (!row) {
|
|
8349
|
-
return c.json({ error: "Chat not found" }, 404);
|
|
8350
|
-
}
|
|
8351
|
-
let body;
|
|
8352
|
-
try {
|
|
8353
|
-
body = await c.req.json();
|
|
8354
|
-
} catch {
|
|
8355
|
-
return c.json({ error: "Invalid JSON body" }, 400);
|
|
8356
|
-
}
|
|
8357
|
-
if (!body || typeof body !== "object" || typeof body.title !== "string") {
|
|
8358
|
-
return c.json({ error: "Invalid title" }, 400);
|
|
8359
|
-
}
|
|
8360
|
-
const raw2 = body.title;
|
|
8361
|
-
if (raw2.length > 200) {
|
|
8362
|
-
return c.json({ error: "Title too long" }, 400);
|
|
8363
|
-
}
|
|
8364
|
-
const trimmed = raw2.trim();
|
|
8365
|
-
metadata2.setCustomTitle(row.id, trimmed.length > 0 ? trimmed : null);
|
|
8366
|
-
return c.body(null, 204);
|
|
8367
|
-
});
|
|
8368
|
-
app2.get("/api/chats/:id", (c) => {
|
|
8369
|
-
const id = c.req.param("id");
|
|
8370
|
-
const row = findArchiveChatBySourceId(id);
|
|
8371
|
-
if (!row) {
|
|
8372
|
-
return c.json({ error: "Chat not found" }, 404);
|
|
8373
|
-
}
|
|
8374
|
-
const visibility = loadChatVisibility(metadata2, {
|
|
8375
|
-
includeTrashed: c.req.query("includeTrashed") === "true"
|
|
8376
|
-
});
|
|
8377
|
-
if (!visibility.isVisible(row.id)) {
|
|
8378
|
-
return c.json({ error: "Chat not found" }, 404);
|
|
8379
|
-
}
|
|
8380
|
-
const rows = archive2.db.select().from(messages).where(
|
|
8381
|
-
and(
|
|
8382
|
-
eq(messages.agent, CLAUDE_CODE_AGENT),
|
|
8383
|
-
eq(messages.sourceId, id)
|
|
8384
|
-
)
|
|
8385
|
-
).orderBy(asc(messages.ts)).all();
|
|
8386
|
-
const messages2 = rows.map((m) => ({
|
|
8387
|
-
role: m.role,
|
|
8388
|
-
content: m.blocks.map(toApiBlock),
|
|
8389
|
-
timestamp: m.ts.toISOString()
|
|
8390
|
-
}));
|
|
8391
|
-
return c.json({ messages: messages2 });
|
|
8392
|
-
});
|
|
8393
|
-
function deriveTitle(archive3, metadata3, row) {
|
|
8394
|
-
const custom = metadata3.getCustomTitle(row.id);
|
|
8395
|
-
if (custom && custom.trim()) return custom;
|
|
8396
|
-
const firstUser = archive3.db.select({ text: messages.text }).from(messages).where(
|
|
8397
|
-
and(
|
|
8398
|
-
eq(messages.agent, CLAUDE_CODE_AGENT),
|
|
8399
|
-
eq(messages.sourceId, row.sourceId),
|
|
8400
|
-
eq(messages.role, "user")
|
|
8401
|
-
)
|
|
8402
|
-
).orderBy(asc(messages.ts)).limit(1).get();
|
|
8403
|
-
const text2 = firstUser?.text?.trim().split("\n")[0]?.trim();
|
|
8404
|
-
return text2 && text2.length > 0 ? text2 : "Untitled";
|
|
8405
|
-
}
|
|
8406
|
-
if (webDistDir2) {
|
|
8407
|
-
app2.use("*", serveStatic({ root: webDistDir2 }));
|
|
8408
|
-
app2.use("*", serveStatic({ root: webDistDir2, path: "index.html" }));
|
|
8409
|
-
}
|
|
8410
|
-
return app2;
|
|
8411
|
-
}
|
|
8412
|
-
|
|
8413
|
-
// src/archive/repository.ts
|
|
8414
|
-
import crypto5 from "crypto";
|
|
8415
|
-
import fs2 from "fs";
|
|
8416
|
-
import path from "path";
|
|
8417
|
-
import { fileURLToPath } from "url";
|
|
8418
|
-
import Database from "better-sqlite3";
|
|
8419
|
-
|
|
8420
|
-
// ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/better-sqlite3/driver.js
|
|
8421
|
-
import Client from "better-sqlite3";
|
|
8422
|
-
|
|
8423
8370
|
// ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/better-sqlite3/session.js
|
|
8424
8371
|
var BetterSQLiteSession = class extends SQLiteSession {
|
|
8425
8372
|
constructor(client, dialect, schema, options = {}) {
|
|
@@ -8579,7 +8526,7 @@ function drizzle(...params) {
|
|
|
8579
8526
|
})(drizzle || (drizzle = {}));
|
|
8580
8527
|
|
|
8581
8528
|
// ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/migrator.js
|
|
8582
|
-
import
|
|
8529
|
+
import crypto4 from "crypto";
|
|
8583
8530
|
import fs from "fs";
|
|
8584
8531
|
function readMigrationFiles(config) {
|
|
8585
8532
|
const migrationFolderTo = config.migrationsFolder;
|
|
@@ -8601,7 +8548,7 @@ function readMigrationFiles(config) {
|
|
|
8601
8548
|
sql: result,
|
|
8602
8549
|
bps: journalEntry.breakpoints,
|
|
8603
8550
|
folderMillis: journalEntry.when,
|
|
8604
|
-
hash:
|
|
8551
|
+
hash: crypto4.createHash("sha256").update(query).digest("hex")
|
|
8605
8552
|
});
|
|
8606
8553
|
} catch {
|
|
8607
8554
|
throw new Error(`No file ${migrationPath} found in ${migrationFolderTo} folder`);
|
|
@@ -8616,49 +8563,184 @@ function migrate(db, config) {
|
|
|
8616
8563
|
db.dialect.migrate(migrations, db.session, config);
|
|
8617
8564
|
}
|
|
8618
8565
|
|
|
8619
|
-
// src/
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
var CHAT_ID_LENGTH = 6;
|
|
8623
|
-
var MAX_RETRIES = 5;
|
|
8624
|
-
function generateChatId({
|
|
8625
|
-
isTaken,
|
|
8626
|
-
randomIndex = () => crypto4.randomInt(CROCKFORD_ALPHABET.length)
|
|
8627
|
-
}) {
|
|
8628
|
-
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
8629
|
-
let code = "";
|
|
8630
|
-
for (let i = 0; i < CHAT_ID_LENGTH; i++) {
|
|
8631
|
-
code += CROCKFORD_ALPHABET[randomIndex()];
|
|
8632
|
-
}
|
|
8633
|
-
if (!isTaken(code)) {
|
|
8634
|
-
return code;
|
|
8635
|
-
}
|
|
8636
|
-
}
|
|
8637
|
-
throw new Error(
|
|
8638
|
-
`Failed to generate unique chat_id after ${MAX_RETRIES + 1} attempts`
|
|
8639
|
-
);
|
|
8640
|
-
}
|
|
8641
|
-
|
|
8642
|
-
// src/archive/repository.ts
|
|
8643
|
-
function resolveMigrationsFolder() {
|
|
8644
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
8566
|
+
// src/storage/openStore.ts
|
|
8567
|
+
function resolveMigrationsFolder(callerUrl, migrationsSubdir) {
|
|
8568
|
+
const here = path.dirname(fileURLToPath(callerUrl));
|
|
8645
8569
|
const candidates = [
|
|
8646
|
-
path.join(here, "
|
|
8647
|
-
path.join(here, "
|
|
8570
|
+
path.join(here, "../..", migrationsSubdir),
|
|
8571
|
+
path.join(here, ".", migrationsSubdir)
|
|
8648
8572
|
];
|
|
8649
8573
|
const found = candidates.find((p) => fs2.existsSync(p));
|
|
8650
8574
|
if (!found) {
|
|
8651
|
-
throw new Error(
|
|
8575
|
+
throw new Error(`Could not locate migrations folder "${migrationsSubdir}"`);
|
|
8652
8576
|
}
|
|
8653
8577
|
return found;
|
|
8654
8578
|
}
|
|
8579
|
+
function openStore({
|
|
8580
|
+
dataDir: dataDir2,
|
|
8581
|
+
dbFile,
|
|
8582
|
+
callerUrl,
|
|
8583
|
+
migrationsSubdir,
|
|
8584
|
+
schema
|
|
8585
|
+
}) {
|
|
8586
|
+
const migrationsFolder = resolveMigrationsFolder(callerUrl, migrationsSubdir);
|
|
8587
|
+
fs2.mkdirSync(dataDir2, { recursive: true });
|
|
8588
|
+
const sqlite = new Database(path.join(dataDir2, dbFile));
|
|
8589
|
+
const db = drizzle(sqlite, { schema });
|
|
8590
|
+
migrate(db, { migrationsFolder });
|
|
8591
|
+
return { db, sqlite };
|
|
8592
|
+
}
|
|
8593
|
+
|
|
8594
|
+
// src/archive/schema.ts
|
|
8595
|
+
var schema_exports = {};
|
|
8596
|
+
__export(schema_exports, {
|
|
8597
|
+
archiveMeta: () => archiveMeta,
|
|
8598
|
+
chats: () => chats,
|
|
8599
|
+
ingestionEvents: () => ingestionEvents,
|
|
8600
|
+
messages: () => messages,
|
|
8601
|
+
rawMessages: () => rawMessages,
|
|
8602
|
+
schemaVersion: () => schemaVersion
|
|
8603
|
+
});
|
|
8604
|
+
var archiveMeta = sqliteTable("archive_meta", {
|
|
8605
|
+
id: integer("id").primaryKey(),
|
|
8606
|
+
archiveUuid: text("archive_uuid").notNull(),
|
|
8607
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull()
|
|
8608
|
+
});
|
|
8609
|
+
var schemaVersion = sqliteTable("schema_version", {
|
|
8610
|
+
version: integer("version").primaryKey(),
|
|
8611
|
+
appliedAt: integer("applied_at", { mode: "timestamp_ms" }).notNull()
|
|
8612
|
+
});
|
|
8613
|
+
var chats = sqliteTable(
|
|
8614
|
+
"chats",
|
|
8615
|
+
{
|
|
8616
|
+
id: text("id").primaryKey(),
|
|
8617
|
+
chatId: text("chat_id").notNull().unique(),
|
|
8618
|
+
agent: text("agent").notNull(),
|
|
8619
|
+
sourceId: text("source_id").notNull(),
|
|
8620
|
+
firstSeenAt: integer("first_seen_at", { mode: "timestamp_ms" }).notNull(),
|
|
8621
|
+
project: text("project"),
|
|
8622
|
+
projectPath: text("project_path")
|
|
8623
|
+
},
|
|
8624
|
+
(t) => [uniqueIndex("chats_agent_source_idx").on(t.agent, t.sourceId)]
|
|
8625
|
+
);
|
|
8626
|
+
var rawMessages = sqliteTable(
|
|
8627
|
+
"raw_messages",
|
|
8628
|
+
{
|
|
8629
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
8630
|
+
agent: text("agent").notNull(),
|
|
8631
|
+
sourceId: text("source_id").notNull(),
|
|
8632
|
+
sourcePath: text("source_path").notNull(),
|
|
8633
|
+
sourceLocator: text("source_locator").notNull(),
|
|
8634
|
+
rawPayload: text("raw_payload").notNull(),
|
|
8635
|
+
payloadHash: text("payload_hash").notNull(),
|
|
8636
|
+
ingestedAt: integer("ingested_at", { mode: "timestamp_ms" }).notNull()
|
|
8637
|
+
},
|
|
8638
|
+
(t) => [
|
|
8639
|
+
uniqueIndex("raw_messages_idem_idx").on(t.agent, t.sourceId, t.payloadHash)
|
|
8640
|
+
]
|
|
8641
|
+
);
|
|
8642
|
+
var messages = sqliteTable(
|
|
8643
|
+
"messages",
|
|
8644
|
+
{
|
|
8645
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
8646
|
+
agent: text("agent").notNull(),
|
|
8647
|
+
sourceId: text("source_id").notNull(),
|
|
8648
|
+
messageId: text("message_id").notNull(),
|
|
8649
|
+
role: text("role").notNull(),
|
|
8650
|
+
ts: integer("ts", { mode: "timestamp_ms" }).notNull(),
|
|
8651
|
+
text: text("text").notNull(),
|
|
8652
|
+
blocks: text("blocks", { mode: "json" }).notNull(),
|
|
8653
|
+
rawId: integer("raw_id").notNull().references(() => rawMessages.id)
|
|
8654
|
+
},
|
|
8655
|
+
(t) => [
|
|
8656
|
+
uniqueIndex("messages_canonical_idx").on(t.agent, t.sourceId, t.messageId)
|
|
8657
|
+
]
|
|
8658
|
+
);
|
|
8659
|
+
var ingestionEvents = sqliteTable("ingestion_events", {
|
|
8660
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
8661
|
+
agent: text("agent").notNull(),
|
|
8662
|
+
sourceId: text("source_id").notNull(),
|
|
8663
|
+
sourcePath: text("source_path").notNull(),
|
|
8664
|
+
eventType: text("event_type").notNull(),
|
|
8665
|
+
detail: text("detail", { mode: "json" }).notNull(),
|
|
8666
|
+
observedAt: integer("observed_at", { mode: "timestamp_ms" }).notNull()
|
|
8667
|
+
});
|
|
8668
|
+
|
|
8669
|
+
// src/archive/read-seam.ts
|
|
8670
|
+
function createArchiveReadSeam(db) {
|
|
8671
|
+
return {
|
|
8672
|
+
listChatRows() {
|
|
8673
|
+
return db.select().from(chats).all();
|
|
8674
|
+
},
|
|
8675
|
+
findChatBySourceId(sourceId) {
|
|
8676
|
+
return db.select().from(chats).where(eq(chats.sourceId, sourceId)).get() ?? null;
|
|
8677
|
+
},
|
|
8678
|
+
findChatByChatId(chatId) {
|
|
8679
|
+
return db.select().from(chats).where(eq(chats.chatId, chatId)).get() ?? null;
|
|
8680
|
+
},
|
|
8681
|
+
listMessagesByChat(agent, sourceId) {
|
|
8682
|
+
return db.select().from(messages).where(and(eq(messages.agent, agent), eq(messages.sourceId, sourceId))).orderBy(asc(messages.ts)).all();
|
|
8683
|
+
},
|
|
8684
|
+
listChatTsRanges() {
|
|
8685
|
+
return db.select({
|
|
8686
|
+
agent: messages.agent,
|
|
8687
|
+
sourceId: messages.sourceId,
|
|
8688
|
+
minTs: sql`min(${messages.ts})`,
|
|
8689
|
+
maxTs: sql`max(${messages.ts})`
|
|
8690
|
+
}).from(messages).groupBy(messages.agent, messages.sourceId).all();
|
|
8691
|
+
},
|
|
8692
|
+
listLatestRawSourcePaths() {
|
|
8693
|
+
const ranked = db.select({
|
|
8694
|
+
agent: rawMessages.agent,
|
|
8695
|
+
sourceId: rawMessages.sourceId,
|
|
8696
|
+
sourcePath: rawMessages.sourcePath,
|
|
8697
|
+
rn: sql`row_number() over (partition by ${rawMessages.agent}, ${rawMessages.sourceId} order by ${rawMessages.ingestedAt} desc)`.as(
|
|
8698
|
+
"rn"
|
|
8699
|
+
)
|
|
8700
|
+
}).from(rawMessages).as("ranked_raw");
|
|
8701
|
+
return db.select({
|
|
8702
|
+
agent: ranked.agent,
|
|
8703
|
+
sourceId: ranked.sourceId,
|
|
8704
|
+
sourcePath: ranked.sourcePath
|
|
8705
|
+
}).from(ranked).where(eq(ranked.rn, 1)).all();
|
|
8706
|
+
},
|
|
8707
|
+
listFirstUserTexts() {
|
|
8708
|
+
const ranked = db.select({
|
|
8709
|
+
agent: messages.agent,
|
|
8710
|
+
sourceId: messages.sourceId,
|
|
8711
|
+
text: messages.text,
|
|
8712
|
+
rn: sql`row_number() over (partition by ${messages.agent}, ${messages.sourceId} order by ${messages.ts} asc)`.as(
|
|
8713
|
+
"rn"
|
|
8714
|
+
)
|
|
8715
|
+
}).from(messages).where(eq(messages.role, "user")).as("ranked_user");
|
|
8716
|
+
return db.select({
|
|
8717
|
+
agent: ranked.agent,
|
|
8718
|
+
sourceId: ranked.sourceId,
|
|
8719
|
+
text: ranked.text
|
|
8720
|
+
}).from(ranked).where(eq(ranked.rn, 1)).all();
|
|
8721
|
+
},
|
|
8722
|
+
listIngestionEvents() {
|
|
8723
|
+
return db.select().from(ingestionEvents).all();
|
|
8724
|
+
}
|
|
8725
|
+
};
|
|
8726
|
+
}
|
|
8727
|
+
|
|
8728
|
+
// src/archive/repository.ts
|
|
8729
|
+
function parseTs(ts) {
|
|
8730
|
+
const d = new Date(ts);
|
|
8731
|
+
if (Number.isNaN(d.getTime())) return /* @__PURE__ */ new Date(0);
|
|
8732
|
+
return d;
|
|
8733
|
+
}
|
|
8655
8734
|
function createArchiveRepository({
|
|
8656
8735
|
dataDir: dataDir2
|
|
8657
8736
|
}) {
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8737
|
+
const { db, sqlite } = openStore({
|
|
8738
|
+
dataDir: dataDir2,
|
|
8739
|
+
dbFile: "archive.db",
|
|
8740
|
+
callerUrl: import.meta.url,
|
|
8741
|
+
migrationsSubdir: "drizzle/archive",
|
|
8742
|
+
schema: schema_exports
|
|
8743
|
+
});
|
|
8662
8744
|
const drizzleMigrations = sqlite.prepare("SELECT id, created_at FROM __drizzle_migrations ORDER BY id ASC").all();
|
|
8663
8745
|
for (const m of drizzleMigrations) {
|
|
8664
8746
|
db.insert(schemaVersion).values({ version: m.id, appliedAt: new Date(m.created_at) }).onConflictDoNothing().run();
|
|
@@ -8671,8 +8753,13 @@ function createArchiveRepository({
|
|
|
8671
8753
|
createdAt: /* @__PURE__ */ new Date()
|
|
8672
8754
|
}).run();
|
|
8673
8755
|
}
|
|
8756
|
+
function nextChatId() {
|
|
8757
|
+
return generateChatId({
|
|
8758
|
+
isTaken: (candidate) => db.select({ id: chats.id }).from(chats).where(eq(chats.chatId, candidate)).get() !== void 0
|
|
8759
|
+
});
|
|
8760
|
+
}
|
|
8674
8761
|
return {
|
|
8675
|
-
db,
|
|
8762
|
+
read: createArchiveReadSeam(db),
|
|
8676
8763
|
getArchiveUuid() {
|
|
8677
8764
|
const row = db.select().from(archiveMeta).get();
|
|
8678
8765
|
if (!row) {
|
|
@@ -8684,9 +8771,178 @@ function createArchiveRepository({
|
|
|
8684
8771
|
return db.select().from(schemaVersion).all().map((r) => ({ version: r.version, appliedAt: r.appliedAt }));
|
|
8685
8772
|
},
|
|
8686
8773
|
generateChatId() {
|
|
8687
|
-
return
|
|
8688
|
-
|
|
8689
|
-
|
|
8774
|
+
return nextChatId();
|
|
8775
|
+
},
|
|
8776
|
+
ensureChat(agent, sourceId, firstSeenAt, project, projectPath) {
|
|
8777
|
+
const existingChat = db.select().from(chats).where(and(eq(chats.agent, agent), eq(chats.sourceId, sourceId))).get();
|
|
8778
|
+
if (existingChat) {
|
|
8779
|
+
const updates = {};
|
|
8780
|
+
if (project && existingChat.project !== project) {
|
|
8781
|
+
updates.project = project;
|
|
8782
|
+
}
|
|
8783
|
+
if (projectPath && existingChat.projectPath !== projectPath) {
|
|
8784
|
+
updates.projectPath = projectPath;
|
|
8785
|
+
}
|
|
8786
|
+
if (Object.keys(updates).length > 0) {
|
|
8787
|
+
db.update(chats).set(updates).where(eq(chats.id, existingChat.id)).run();
|
|
8788
|
+
}
|
|
8789
|
+
return existingChat.id;
|
|
8790
|
+
}
|
|
8791
|
+
const id = crypto5.randomUUID();
|
|
8792
|
+
db.insert(chats).values({
|
|
8793
|
+
id,
|
|
8794
|
+
chatId: nextChatId(),
|
|
8795
|
+
agent,
|
|
8796
|
+
sourceId,
|
|
8797
|
+
firstSeenAt,
|
|
8798
|
+
project: project ?? null,
|
|
8799
|
+
projectPath: projectPath ?? null
|
|
8800
|
+
}).run();
|
|
8801
|
+
return id;
|
|
8802
|
+
},
|
|
8803
|
+
insertRawMessage(input) {
|
|
8804
|
+
const payloadJson = JSON.stringify(input.payload);
|
|
8805
|
+
const payloadHash = crypto5.createHash("sha256").update(payloadJson).digest("hex");
|
|
8806
|
+
const existing2 = db.select({ id: rawMessages.id }).from(rawMessages).where(
|
|
8807
|
+
and(
|
|
8808
|
+
eq(rawMessages.agent, input.agent),
|
|
8809
|
+
eq(rawMessages.sourceId, input.sourceId),
|
|
8810
|
+
eq(rawMessages.payloadHash, payloadHash)
|
|
8811
|
+
)
|
|
8812
|
+
).get();
|
|
8813
|
+
if (existing2) return { id: existing2.id, inserted: false };
|
|
8814
|
+
const inserted = db.insert(rawMessages).values({
|
|
8815
|
+
agent: input.agent,
|
|
8816
|
+
sourceId: input.sourceId,
|
|
8817
|
+
sourcePath: input.sourcePath,
|
|
8818
|
+
sourceLocator: input.sourceLocator,
|
|
8819
|
+
rawPayload: payloadJson,
|
|
8820
|
+
payloadHash,
|
|
8821
|
+
ingestedAt: input.ingestedAt
|
|
8822
|
+
}).returning({ id: rawMessages.id }).get();
|
|
8823
|
+
return { id: inserted.id, inserted: true };
|
|
8824
|
+
},
|
|
8825
|
+
upsertNormalizedMessage({ agent, sourceId, message, rawId }) {
|
|
8826
|
+
const existing2 = db.select().from(messages).where(
|
|
8827
|
+
and(
|
|
8828
|
+
eq(messages.agent, agent),
|
|
8829
|
+
eq(messages.sourceId, sourceId),
|
|
8830
|
+
eq(messages.messageId, message.messageId)
|
|
8831
|
+
)
|
|
8832
|
+
).get();
|
|
8833
|
+
const ts = parseTs(message.ts);
|
|
8834
|
+
if (!existing2) {
|
|
8835
|
+
db.insert(messages).values({
|
|
8836
|
+
agent,
|
|
8837
|
+
sourceId,
|
|
8838
|
+
messageId: message.messageId,
|
|
8839
|
+
role: message.role,
|
|
8840
|
+
ts,
|
|
8841
|
+
text: message.text,
|
|
8842
|
+
blocks: message.blocks,
|
|
8843
|
+
rawId
|
|
8844
|
+
}).run();
|
|
8845
|
+
return true;
|
|
8846
|
+
}
|
|
8847
|
+
if (ts.getTime() >= existing2.ts.getTime()) {
|
|
8848
|
+
db.update(messages).set({
|
|
8849
|
+
role: message.role,
|
|
8850
|
+
ts,
|
|
8851
|
+
text: message.text,
|
|
8852
|
+
blocks: message.blocks,
|
|
8853
|
+
rawId
|
|
8854
|
+
}).where(eq(messages.id, existing2.id)).run();
|
|
8855
|
+
return true;
|
|
8856
|
+
}
|
|
8857
|
+
return false;
|
|
8858
|
+
},
|
|
8859
|
+
recordIngestionEvent(event) {
|
|
8860
|
+
db.insert(ingestionEvents).values({
|
|
8861
|
+
agent: event.agent,
|
|
8862
|
+
sourceId: event.sourceId,
|
|
8863
|
+
sourcePath: event.sourcePath,
|
|
8864
|
+
eventType: event.eventType,
|
|
8865
|
+
detail: event.detail,
|
|
8866
|
+
observedAt: event.observedAt ?? /* @__PURE__ */ new Date()
|
|
8867
|
+
}).run();
|
|
8868
|
+
},
|
|
8869
|
+
close() {
|
|
8870
|
+
sqlite.close();
|
|
8871
|
+
}
|
|
8872
|
+
};
|
|
8873
|
+
}
|
|
8874
|
+
|
|
8875
|
+
// src/checkpoint/schema.ts
|
|
8876
|
+
var schema_exports2 = {};
|
|
8877
|
+
__export(schema_exports2, {
|
|
8878
|
+
chatScanState: () => chatScanState
|
|
8879
|
+
});
|
|
8880
|
+
var chatScanState = sqliteTable(
|
|
8881
|
+
"chat_scan_state",
|
|
8882
|
+
{
|
|
8883
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
8884
|
+
agent: text("agent").notNull(),
|
|
8885
|
+
sourceId: text("source_id").notNull(),
|
|
8886
|
+
sourcePath: text("source_path").notNull(),
|
|
8887
|
+
lastMtimeMs: integer("last_mtime_ms").notNull(),
|
|
8888
|
+
lastSizeBytes: integer("last_size_bytes").notNull(),
|
|
8889
|
+
lastScannedAt: integer("last_scanned_at", {
|
|
8890
|
+
mode: "timestamp_ms"
|
|
8891
|
+
}).notNull()
|
|
8892
|
+
},
|
|
8893
|
+
(t) => [uniqueIndex("chat_scan_state_idx").on(t.agent, t.sourceId)]
|
|
8894
|
+
);
|
|
8895
|
+
|
|
8896
|
+
// src/checkpoint/repository.ts
|
|
8897
|
+
function createCheckpointRepository({
|
|
8898
|
+
dataDir: dataDir2
|
|
8899
|
+
}) {
|
|
8900
|
+
const { db, sqlite } = openStore({
|
|
8901
|
+
dataDir: dataDir2,
|
|
8902
|
+
dbFile: "checkpoint.db",
|
|
8903
|
+
callerUrl: import.meta.url,
|
|
8904
|
+
migrationsSubdir: "drizzle/checkpoint",
|
|
8905
|
+
schema: schema_exports2
|
|
8906
|
+
});
|
|
8907
|
+
return {
|
|
8908
|
+
db,
|
|
8909
|
+
getScanState(agent, sourceId) {
|
|
8910
|
+
const row = db.select({
|
|
8911
|
+
lastMtimeMs: chatScanState.lastMtimeMs,
|
|
8912
|
+
lastSizeBytes: chatScanState.lastSizeBytes,
|
|
8913
|
+
lastScannedAt: chatScanState.lastScannedAt
|
|
8914
|
+
}).from(chatScanState).where(
|
|
8915
|
+
and(
|
|
8916
|
+
eq(chatScanState.agent, agent),
|
|
8917
|
+
eq(chatScanState.sourceId, sourceId)
|
|
8918
|
+
)
|
|
8919
|
+
).get();
|
|
8920
|
+
return row ?? void 0;
|
|
8921
|
+
},
|
|
8922
|
+
recordScanState(agent, sourceId, sourcePath, stat4, scannedAt) {
|
|
8923
|
+
const existing = db.select({ id: chatScanState.id }).from(chatScanState).where(
|
|
8924
|
+
and(
|
|
8925
|
+
eq(chatScanState.agent, agent),
|
|
8926
|
+
eq(chatScanState.sourceId, sourceId)
|
|
8927
|
+
)
|
|
8928
|
+
).get();
|
|
8929
|
+
if (existing) {
|
|
8930
|
+
db.update(chatScanState).set({
|
|
8931
|
+
sourcePath,
|
|
8932
|
+
lastMtimeMs: stat4.mtimeMs,
|
|
8933
|
+
lastSizeBytes: stat4.sizeBytes,
|
|
8934
|
+
lastScannedAt: scannedAt
|
|
8935
|
+
}).where(eq(chatScanState.id, existing.id)).run();
|
|
8936
|
+
} else {
|
|
8937
|
+
db.insert(chatScanState).values({
|
|
8938
|
+
agent,
|
|
8939
|
+
sourceId,
|
|
8940
|
+
sourcePath,
|
|
8941
|
+
lastMtimeMs: stat4.mtimeMs,
|
|
8942
|
+
lastSizeBytes: stat4.sizeBytes,
|
|
8943
|
+
lastScannedAt: scannedAt
|
|
8944
|
+
}).run();
|
|
8945
|
+
}
|
|
8690
8946
|
},
|
|
8691
8947
|
close() {
|
|
8692
8948
|
sqlite.close();
|
|
@@ -8697,10 +8953,12 @@ function createArchiveRepository({
|
|
|
8697
8953
|
// src/metadata/repository.ts
|
|
8698
8954
|
import fs3 from "fs";
|
|
8699
8955
|
import path2 from "path";
|
|
8700
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8701
|
-
import Database2 from "better-sqlite3";
|
|
8702
8956
|
|
|
8703
8957
|
// src/metadata/schema.ts
|
|
8958
|
+
var schema_exports3 = {};
|
|
8959
|
+
__export(schema_exports3, {
|
|
8960
|
+
chatsMeta: () => chatsMeta
|
|
8961
|
+
});
|
|
8704
8962
|
var chatsMeta = sqliteTable("chats_meta", {
|
|
8705
8963
|
id: text("id").primaryKey(),
|
|
8706
8964
|
isDeleted: integer("is_deleted", { mode: "boolean" }).notNull().default(false),
|
|
@@ -8713,30 +8971,31 @@ var chatsMeta = sqliteTable("chats_meta", {
|
|
|
8713
8971
|
});
|
|
8714
8972
|
|
|
8715
8973
|
// src/metadata/repository.ts
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
throw new Error("Could not locate drizzle migrations folder");
|
|
8725
|
-
}
|
|
8726
|
-
return found;
|
|
8974
|
+
var DB_FILE = "metadata.db";
|
|
8975
|
+
var LEGACY_DB_FILE = "data.db";
|
|
8976
|
+
function migrateLegacyDbFile(dataDir2) {
|
|
8977
|
+
const legacy = path2.join(dataDir2, LEGACY_DB_FILE);
|
|
8978
|
+
const current = path2.join(dataDir2, DB_FILE);
|
|
8979
|
+
if (!fs3.existsSync(legacy)) return;
|
|
8980
|
+
if (fs3.existsSync(current)) return;
|
|
8981
|
+
fs3.renameSync(legacy, current);
|
|
8727
8982
|
}
|
|
8728
|
-
var
|
|
8983
|
+
var CLAUDE_CODE_AGENT = "claude-code";
|
|
8729
8984
|
function createMetadataRepository({
|
|
8730
8985
|
dataDir: dataDir2,
|
|
8731
8986
|
lookupInternalId,
|
|
8732
|
-
ensureChat
|
|
8987
|
+
ensureChat
|
|
8733
8988
|
}) {
|
|
8734
|
-
|
|
8735
|
-
const sqlite =
|
|
8736
|
-
|
|
8737
|
-
|
|
8989
|
+
migrateLegacyDbFile(dataDir2);
|
|
8990
|
+
const { db, sqlite } = openStore({
|
|
8991
|
+
dataDir: dataDir2,
|
|
8992
|
+
dbFile: DB_FILE,
|
|
8993
|
+
callerUrl: import.meta.url,
|
|
8994
|
+
migrationsSubdir: "drizzle",
|
|
8995
|
+
schema: schema_exports3
|
|
8996
|
+
});
|
|
8738
8997
|
if (lookupInternalId) {
|
|
8739
|
-
rekeyLegacyRows(sqlite, db, lookupInternalId,
|
|
8998
|
+
rekeyLegacyRows(sqlite, db, lookupInternalId, ensureChat);
|
|
8740
8999
|
}
|
|
8741
9000
|
return {
|
|
8742
9001
|
softDelete(internalId) {
|
|
@@ -8772,6 +9031,14 @@ function createMetadataRepository({
|
|
|
8772
9031
|
const row = db.select({ customTitle: chatsMeta.customTitle }).from(chatsMeta).where(eq(chatsMeta.id, internalId)).get();
|
|
8773
9032
|
return row?.customTitle ?? null;
|
|
8774
9033
|
},
|
|
9034
|
+
listCustomTitles() {
|
|
9035
|
+
const rows = db.select({ id: chatsMeta.id, customTitle: chatsMeta.customTitle }).from(chatsMeta).where(isNotNull(chatsMeta.customTitle)).all();
|
|
9036
|
+
const byId = /* @__PURE__ */ new Map();
|
|
9037
|
+
for (const row of rows) {
|
|
9038
|
+
if (row.customTitle !== null) byId.set(row.id, row.customTitle);
|
|
9039
|
+
}
|
|
9040
|
+
return byId;
|
|
9041
|
+
},
|
|
8775
9042
|
setCustomTitle(internalId, title) {
|
|
8776
9043
|
const now = /* @__PURE__ */ new Date();
|
|
8777
9044
|
db.insert(chatsMeta).values({
|
|
@@ -8787,15 +9054,15 @@ function createMetadataRepository({
|
|
|
8787
9054
|
};
|
|
8788
9055
|
}
|
|
8789
9056
|
var REKEY_USER_VERSION = 4;
|
|
8790
|
-
function rekeyLegacyRows(sqlite, db, lookupInternalId,
|
|
9057
|
+
function rekeyLegacyRows(sqlite, db, lookupInternalId, ensureChat) {
|
|
8791
9058
|
const current = sqlite.pragma("user_version", { simple: true });
|
|
8792
9059
|
if (current >= REKEY_USER_VERSION) return;
|
|
8793
9060
|
const rows = db.select({ id: chatsMeta.id }).from(chatsMeta).all();
|
|
8794
9061
|
for (const row of rows) {
|
|
8795
|
-
let target = lookupInternalId(
|
|
9062
|
+
let target = lookupInternalId(CLAUDE_CODE_AGENT, row.id);
|
|
8796
9063
|
if (target === null) {
|
|
8797
|
-
if (!
|
|
8798
|
-
target =
|
|
9064
|
+
if (!ensureChat) continue;
|
|
9065
|
+
target = ensureChat(CLAUDE_CODE_AGENT, row.id);
|
|
8799
9066
|
}
|
|
8800
9067
|
if (target === row.id) continue;
|
|
8801
9068
|
db.update(chatsMeta).set({ id: target, updatedAt: /* @__PURE__ */ new Date() }).where(eq(chatsMeta.id, row.id)).run();
|
|
@@ -8804,28 +9071,21 @@ function rekeyLegacyRows(sqlite, db, lookupInternalId, ensureChat2) {
|
|
|
8804
9071
|
}
|
|
8805
9072
|
|
|
8806
9073
|
// src/ingestion/ingest.ts
|
|
8807
|
-
import crypto6 from "crypto";
|
|
8808
9074
|
import fs4 from "fs";
|
|
8809
9075
|
async function runIngestion(opts) {
|
|
8810
9076
|
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
8811
9077
|
const result = {
|
|
8812
9078
|
scanned: 0,
|
|
8813
9079
|
rawInserted: 0,
|
|
8814
|
-
|
|
9080
|
+
normalizedUpserted: 0,
|
|
8815
9081
|
skippedByMtime: 0
|
|
8816
9082
|
};
|
|
8817
9083
|
for (const plugin of opts.plugins) {
|
|
8818
9084
|
for await (const ref of plugin.discover(opts.env)) {
|
|
8819
9085
|
result.scanned += 1;
|
|
8820
9086
|
const stat4 = safeStat(ref.sourcePath);
|
|
8821
|
-
const prior = opts.
|
|
8822
|
-
|
|
8823
|
-
eq(chatScanState.agent, plugin.id),
|
|
8824
|
-
eq(chatScanState.sourceId, ref.sourceId)
|
|
8825
|
-
)
|
|
8826
|
-
).get();
|
|
8827
|
-
ensureChat(
|
|
8828
|
-
opts.archive,
|
|
9087
|
+
const prior = opts.checkpoint.getScanState(plugin.id, ref.sourceId);
|
|
9088
|
+
opts.archive.ensureChat(
|
|
8829
9089
|
plugin.id,
|
|
8830
9090
|
ref.sourceId,
|
|
8831
9091
|
now(),
|
|
@@ -8837,49 +9097,31 @@ async function runIngestion(opts) {
|
|
|
8837
9097
|
continue;
|
|
8838
9098
|
}
|
|
8839
9099
|
for await (const raw2 of plugin.extractRaw(ref)) {
|
|
8840
|
-
const
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
).
|
|
8849
|
-
|
|
8850
|
-
if (
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
sourceId: ref.sourceId,
|
|
8856
|
-
sourcePath: raw2.sourcePath,
|
|
8857
|
-
sourceLocator: raw2.sourceLocator,
|
|
8858
|
-
rawPayload: payloadJson,
|
|
8859
|
-
payloadHash,
|
|
8860
|
-
ingestedAt: now()
|
|
8861
|
-
}).returning({ id: rawMessages.id }).get();
|
|
8862
|
-
rawId = inserted.id;
|
|
8863
|
-
result.rawInserted += 1;
|
|
8864
|
-
}
|
|
8865
|
-
const canonical = plugin.normalize(raw2);
|
|
8866
|
-
if (!canonical) continue;
|
|
8867
|
-
const upserted = upsertCanonical(
|
|
8868
|
-
opts.archive,
|
|
8869
|
-
plugin.id,
|
|
8870
|
-
ref.sourceId,
|
|
8871
|
-
canonical,
|
|
9100
|
+
const { id: rawId, inserted } = opts.archive.insertRawMessage({
|
|
9101
|
+
agent: plugin.id,
|
|
9102
|
+
sourceId: ref.sourceId,
|
|
9103
|
+
sourcePath: raw2.sourcePath,
|
|
9104
|
+
sourceLocator: raw2.sourceLocator,
|
|
9105
|
+
payload: raw2.payload,
|
|
9106
|
+
ingestedAt: now()
|
|
9107
|
+
});
|
|
9108
|
+
if (inserted) result.rawInserted += 1;
|
|
9109
|
+
const normalized = plugin.normalize(raw2);
|
|
9110
|
+
if (!normalized) continue;
|
|
9111
|
+
const upserted = opts.archive.upsertNormalizedMessage({
|
|
9112
|
+
agent: plugin.id,
|
|
9113
|
+
sourceId: ref.sourceId,
|
|
9114
|
+
message: normalized,
|
|
8872
9115
|
rawId
|
|
8873
|
-
);
|
|
8874
|
-
if (upserted) result.
|
|
9116
|
+
});
|
|
9117
|
+
if (upserted) result.normalizedUpserted += 1;
|
|
8875
9118
|
}
|
|
8876
9119
|
if (stat4) {
|
|
8877
|
-
recordScanState(
|
|
8878
|
-
opts.archive,
|
|
9120
|
+
opts.checkpoint.recordScanState(
|
|
8879
9121
|
plugin.id,
|
|
8880
9122
|
ref.sourceId,
|
|
8881
9123
|
ref.sourcePath,
|
|
8882
|
-
stat4,
|
|
9124
|
+
{ mtimeMs: stat4.mtimeMs, sizeBytes: stat4.size },
|
|
8883
9125
|
now()
|
|
8884
9126
|
);
|
|
8885
9127
|
}
|
|
@@ -8894,93 +9136,6 @@ function safeStat(p) {
|
|
|
8894
9136
|
return null;
|
|
8895
9137
|
}
|
|
8896
9138
|
}
|
|
8897
|
-
function recordScanState(archive2, agent, sourceId, sourcePath, stat4, scannedAt) {
|
|
8898
|
-
const existing = archive2.db.select({ id: chatScanState.id }).from(chatScanState).where(
|
|
8899
|
-
and(eq(chatScanState.agent, agent), eq(chatScanState.sourceId, sourceId))
|
|
8900
|
-
).get();
|
|
8901
|
-
if (existing) {
|
|
8902
|
-
archive2.db.update(chatScanState).set({
|
|
8903
|
-
sourcePath,
|
|
8904
|
-
lastMtimeMs: stat4.mtimeMs,
|
|
8905
|
-
lastSizeBytes: stat4.size,
|
|
8906
|
-
lastScannedAt: scannedAt
|
|
8907
|
-
}).where(eq(chatScanState.id, existing.id)).run();
|
|
8908
|
-
} else {
|
|
8909
|
-
archive2.db.insert(chatScanState).values({
|
|
8910
|
-
agent,
|
|
8911
|
-
sourceId,
|
|
8912
|
-
sourcePath,
|
|
8913
|
-
lastMtimeMs: stat4.mtimeMs,
|
|
8914
|
-
lastSizeBytes: stat4.size,
|
|
8915
|
-
lastScannedAt: scannedAt
|
|
8916
|
-
}).run();
|
|
8917
|
-
}
|
|
8918
|
-
}
|
|
8919
|
-
function ensureChat(archive2, agent, sourceId, firstSeenAt, project, projectPath) {
|
|
8920
|
-
const existing = archive2.db.select().from(chats).where(and(eq(chats.agent, agent), eq(chats.sourceId, sourceId))).get();
|
|
8921
|
-
if (existing) {
|
|
8922
|
-
const updates = {};
|
|
8923
|
-
if (project && existing.project !== project) updates.project = project;
|
|
8924
|
-
if (projectPath && existing.projectPath !== projectPath) {
|
|
8925
|
-
updates.projectPath = projectPath;
|
|
8926
|
-
}
|
|
8927
|
-
if (Object.keys(updates).length > 0) {
|
|
8928
|
-
archive2.db.update(chats).set(updates).where(eq(chats.id, existing.id)).run();
|
|
8929
|
-
}
|
|
8930
|
-
return existing.id;
|
|
8931
|
-
}
|
|
8932
|
-
const id = crypto6.randomUUID();
|
|
8933
|
-
const chatId = archive2.generateChatId();
|
|
8934
|
-
archive2.db.insert(chats).values({
|
|
8935
|
-
id,
|
|
8936
|
-
chatId,
|
|
8937
|
-
agent,
|
|
8938
|
-
sourceId,
|
|
8939
|
-
firstSeenAt,
|
|
8940
|
-
project: project ?? null,
|
|
8941
|
-
projectPath: projectPath ?? null
|
|
8942
|
-
}).run();
|
|
8943
|
-
return id;
|
|
8944
|
-
}
|
|
8945
|
-
function upsertCanonical(archive2, agent, sourceId, msg, rawId) {
|
|
8946
|
-
const existing = archive2.db.select().from(messages).where(
|
|
8947
|
-
and(
|
|
8948
|
-
eq(messages.agent, agent),
|
|
8949
|
-
eq(messages.sourceId, sourceId),
|
|
8950
|
-
eq(messages.messageId, msg.messageId)
|
|
8951
|
-
)
|
|
8952
|
-
).get();
|
|
8953
|
-
const ts = parseTs(msg.ts);
|
|
8954
|
-
if (!existing) {
|
|
8955
|
-
archive2.db.insert(messages).values({
|
|
8956
|
-
agent,
|
|
8957
|
-
sourceId,
|
|
8958
|
-
messageId: msg.messageId,
|
|
8959
|
-
role: msg.role,
|
|
8960
|
-
ts,
|
|
8961
|
-
text: msg.text,
|
|
8962
|
-
blocks: msg.blocks,
|
|
8963
|
-
rawId
|
|
8964
|
-
}).run();
|
|
8965
|
-
return true;
|
|
8966
|
-
}
|
|
8967
|
-
if (ts.getTime() >= existing.ts.getTime()) {
|
|
8968
|
-
archive2.db.update(messages).set({
|
|
8969
|
-
role: msg.role,
|
|
8970
|
-
ts,
|
|
8971
|
-
text: msg.text,
|
|
8972
|
-
blocks: msg.blocks,
|
|
8973
|
-
rawId
|
|
8974
|
-
}).where(eq(messages.id, existing.id)).run();
|
|
8975
|
-
return true;
|
|
8976
|
-
}
|
|
8977
|
-
return false;
|
|
8978
|
-
}
|
|
8979
|
-
function parseTs(ts) {
|
|
8980
|
-
const d = new Date(ts);
|
|
8981
|
-
if (Number.isNaN(d.getTime())) return /* @__PURE__ */ new Date(0);
|
|
8982
|
-
return d;
|
|
8983
|
-
}
|
|
8984
9139
|
|
|
8985
9140
|
// src/ingestion/background.ts
|
|
8986
9141
|
function startIngestionInBackground(opts, bg = {}) {
|
|
@@ -10767,14 +10922,14 @@ function startWatcher(opts) {
|
|
|
10767
10922
|
const binding = pathBindings.get(removedPath);
|
|
10768
10923
|
if (!binding) return;
|
|
10769
10924
|
try {
|
|
10770
|
-
opts.archive.
|
|
10925
|
+
opts.archive.recordIngestionEvent({
|
|
10771
10926
|
agent: binding.plugin.id,
|
|
10772
10927
|
sourceId: binding.ref.sourceId,
|
|
10773
10928
|
sourcePath: removedPath,
|
|
10774
10929
|
eventType: "unlink_observed",
|
|
10775
10930
|
detail: { path: removedPath },
|
|
10776
10931
|
observedAt: /* @__PURE__ */ new Date()
|
|
10777
|
-
})
|
|
10932
|
+
});
|
|
10778
10933
|
} catch (err) {
|
|
10779
10934
|
onError(err);
|
|
10780
10935
|
}
|
|
@@ -10788,13 +10943,22 @@ function startWatcher(opts) {
|
|
|
10788
10943
|
void runIngestion({
|
|
10789
10944
|
plugins: opts.plugins,
|
|
10790
10945
|
archive: opts.archive,
|
|
10946
|
+
checkpoint: opts.checkpoint,
|
|
10791
10947
|
env: opts.env
|
|
10792
|
-
}).catch((err) => onError(err));
|
|
10948
|
+
}).then((result) => opts.onIngest?.(result)).catch((err) => onError(err));
|
|
10793
10949
|
}, debounceMs);
|
|
10794
10950
|
pendingTimers.set(changedPath, timer);
|
|
10795
10951
|
}
|
|
10796
10952
|
return {
|
|
10797
10953
|
ready,
|
|
10954
|
+
notifyChange(path5) {
|
|
10955
|
+
if (watcher2) watcher2.emit("change", path5);
|
|
10956
|
+
else scheduleIngest(path5);
|
|
10957
|
+
},
|
|
10958
|
+
notifyUnlink(path5) {
|
|
10959
|
+
if (watcher2) watcher2.emit("unlink", path5);
|
|
10960
|
+
else recordUnlink(path5);
|
|
10961
|
+
},
|
|
10798
10962
|
async close() {
|
|
10799
10963
|
closed = true;
|
|
10800
10964
|
for (const t of pendingTimers.values()) clearTimeout(t);
|
|
@@ -10992,7 +11156,7 @@ Environment:
|
|
|
10992
11156
|
`;
|
|
10993
11157
|
|
|
10994
11158
|
// src/index.ts
|
|
10995
|
-
var __dirname = path4.dirname(
|
|
11159
|
+
var __dirname = path4.dirname(fileURLToPath2(import.meta.url));
|
|
10996
11160
|
var pkgPath = path4.join(__dirname, "../../package.json");
|
|
10997
11161
|
var pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
|
|
10998
11162
|
var action = parseCliArgs(process.argv.slice(2), {
|
|
@@ -11015,16 +11179,19 @@ var dataDir = path4.join(os.homedir(), ".chat-logbook");
|
|
|
11015
11179
|
var webDistDir = path4.join(__dirname, "../../web/dist");
|
|
11016
11180
|
var port = action.port;
|
|
11017
11181
|
var archive = createArchiveRepository({ dataDir });
|
|
11182
|
+
var checkpoint = createCheckpointRepository({ dataDir });
|
|
11018
11183
|
var metadata = createMetadataRepository({ dataDir });
|
|
11019
11184
|
var app = createApp({ archive, metadata, webDistDir });
|
|
11020
11185
|
var initialIngest = startIngestionInBackground({
|
|
11021
11186
|
plugins,
|
|
11022
11187
|
archive,
|
|
11188
|
+
checkpoint,
|
|
11023
11189
|
env: { homeDir: os.homedir() }
|
|
11024
11190
|
});
|
|
11025
11191
|
var watcher = startWatcher({
|
|
11026
11192
|
plugins,
|
|
11027
11193
|
archive,
|
|
11194
|
+
checkpoint,
|
|
11028
11195
|
env: { homeDir: os.homedir() }
|
|
11029
11196
|
});
|
|
11030
11197
|
void initialIngest.done.then(() => watcher.ready).catch(() => {
|