chat-logbook 0.12.0 → 0.13.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 +3 -5
- package/api/dist/drizzle/0004_add_tags.sql +16 -0
- package/api/dist/drizzle/archive/0008_add_chats_project_idx.sql +1 -0
- package/api/dist/drizzle/archive/meta/_journal.json +7 -0
- package/api/dist/drizzle/meta/_journal.json +7 -0
- package/api/dist/index.js +317 -42
- package/package.json +1 -1
- package/web/dist/assets/index-BlF09kci.css +2 -0
- package/web/dist/assets/index-CHr9-eHr.js +57 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BJlGZyK5.js +0 -57
- package/web/dist/assets/index-DSNHfjXl.css +0 -2
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ The full problem statement, user stories, and direction live in the [PRD](docs/P
|
|
|
42
42
|
- **Local archive.** Every conversation chat-logbook reads is copied into `~/.chat-logbook/archive.db`. The UI reads from the archive, so a chat stays visible even after the source JSONL is gone.
|
|
43
43
|
- **Live updates.** While Claude Code is actively writing to a chat, new messages appear in chat-logbook within seconds — no restart needed.
|
|
44
44
|
- **Chat metadata at a glance.** A ⓘ button on the conversation header opens a popover showing when the chat started, when it was last updated, the agent, the project working directory, and the chat's IDs — with one-click copy.
|
|
45
|
+
- **Tags.** Create tags, give them colors, and assign them to chats. They show as colored chips on the chat and in the list. Rename or delete them anytime.
|
|
46
|
+
- **Filter by tag or project.** Narrow the chat list to a project, or to chats that carry all of several selected tags.
|
|
45
47
|
|
|
46
48
|
## Quick start
|
|
47
49
|
|
|
@@ -109,13 +111,9 @@ No. chat-logbook reads files those tools write on your machine. It doesn't talk
|
|
|
109
111
|
|
|
110
112
|
## Roadmap
|
|
111
113
|
|
|
112
|
-
Two phases of work are in flight, in this order.
|
|
113
|
-
|
|
114
|
-
**Custom titles and tags.** Rename a chat to something you'll recognize, attach tags with custom colors, and filter the list by tag or by project. This is the path from "I have a lot of conversations" to "I can find the one I need."
|
|
115
|
-
|
|
116
114
|
**Spotlight search.** A keyboard-driven overlay (`⌘K` or `/`) that searches across chats, messages, tags, and projects from one input. A match lands you on the exact message with the term highlighted, and you can jump between every match without leaving the overlay.
|
|
117
115
|
|
|
118
|
-
Beyond
|
|
116
|
+
Beyond this, several smaller things are on the list without firm timing — live updates while an AI tool is writing, annotations and highlights for marking what matters, editing your own past messages, CLI commands for working with conversations from a terminal. Some will ship; some are still being weighed.
|
|
119
117
|
|
|
120
118
|
Explicit non-goals: semantic / vector search, cloud sync inside the OSS core, modification of source directories like `~/.claude/`, and bulk export to bespoke formats (the archive itself is the export). See the [PRD](docs/PRD.md) for the full out-of-scope list.
|
|
121
119
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
CREATE TABLE `tags` (
|
|
2
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
3
|
+
`name` text NOT NULL,
|
|
4
|
+
`color` text NOT NULL,
|
|
5
|
+
`created_at` integer NOT NULL,
|
|
6
|
+
`updated_at` integer NOT NULL
|
|
7
|
+
);
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
CREATE TABLE `chat_tags` (
|
|
10
|
+
`chat_id` text NOT NULL,
|
|
11
|
+
`tag_id` text NOT NULL,
|
|
12
|
+
PRIMARY KEY(`chat_id`, `tag_id`),
|
|
13
|
+
FOREIGN KEY (`tag_id`) REFERENCES `tags`(`id`) ON UPDATE no action ON DELETE cascade
|
|
14
|
+
);
|
|
15
|
+
--> statement-breakpoint
|
|
16
|
+
CREATE INDEX `chat_tags_tag_id_idx` ON `chat_tags` (`tag_id`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
CREATE INDEX `chats_project_idx` ON `chats` (`project`);
|
package/api/dist/index.js
CHANGED
|
@@ -588,13 +588,13 @@ import updateNotifier from "update-notifier";
|
|
|
588
588
|
// ../node_modules/.pnpm/hono@4.12.9/node_modules/hono/dist/compose.js
|
|
589
589
|
var compose = (middleware, onError, onNotFound) => {
|
|
590
590
|
return (context, next) => {
|
|
591
|
-
let
|
|
591
|
+
let index2 = -1;
|
|
592
592
|
return dispatch(0);
|
|
593
593
|
async function dispatch(i) {
|
|
594
|
-
if (i <=
|
|
594
|
+
if (i <= index2) {
|
|
595
595
|
throw new Error("next() called multiple times");
|
|
596
596
|
}
|
|
597
|
-
|
|
597
|
+
index2 = i;
|
|
598
598
|
let res;
|
|
599
599
|
let isError = false;
|
|
600
600
|
let handler;
|
|
@@ -692,8 +692,8 @@ var handleParsingNestedValues = (form, key, value) => {
|
|
|
692
692
|
}
|
|
693
693
|
let nestedForm = form;
|
|
694
694
|
const keys = key.split(".");
|
|
695
|
-
keys.forEach((key2,
|
|
696
|
-
if (
|
|
695
|
+
keys.forEach((key2, index2) => {
|
|
696
|
+
if (index2 === keys.length - 1) {
|
|
697
697
|
nestedForm[key2] = value;
|
|
698
698
|
} else {
|
|
699
699
|
if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
|
|
@@ -719,8 +719,8 @@ var splitRoutingPath = (routePath) => {
|
|
|
719
719
|
};
|
|
720
720
|
var extractGroupsFromPath = (path5) => {
|
|
721
721
|
const groups = [];
|
|
722
|
-
path5 = path5.replace(/\{[^}]+\}/g, (match2,
|
|
723
|
-
const mark = `@${
|
|
722
|
+
path5 = path5.replace(/\{[^}]+\}/g, (match2, index2) => {
|
|
723
|
+
const mark = `@${index2}`;
|
|
724
724
|
groups.push([mark, match2]);
|
|
725
725
|
return mark;
|
|
726
726
|
});
|
|
@@ -2021,8 +2021,8 @@ function match(method, path5) {
|
|
|
2021
2021
|
if (!match3) {
|
|
2022
2022
|
return [[], emptyParam];
|
|
2023
2023
|
}
|
|
2024
|
-
const
|
|
2025
|
-
return [matcher[1][
|
|
2024
|
+
const index2 = match3.indexOf("", 1);
|
|
2025
|
+
return [matcher[1][index2], match3];
|
|
2026
2026
|
});
|
|
2027
2027
|
this.match = match2;
|
|
2028
2028
|
return match2(method, path5);
|
|
@@ -2057,7 +2057,7 @@ var Node = class _Node {
|
|
|
2057
2057
|
#index;
|
|
2058
2058
|
#varIndex;
|
|
2059
2059
|
#children = /* @__PURE__ */ Object.create(null);
|
|
2060
|
-
insert(tokens,
|
|
2060
|
+
insert(tokens, index2, paramMap, context, pathErrorCheckOnly) {
|
|
2061
2061
|
if (tokens.length === 0) {
|
|
2062
2062
|
if (this.#index !== void 0) {
|
|
2063
2063
|
throw PATH_ERROR;
|
|
@@ -2065,7 +2065,7 @@ var Node = class _Node {
|
|
|
2065
2065
|
if (pathErrorCheckOnly) {
|
|
2066
2066
|
return;
|
|
2067
2067
|
}
|
|
2068
|
-
this.#index =
|
|
2068
|
+
this.#index = index2;
|
|
2069
2069
|
return;
|
|
2070
2070
|
}
|
|
2071
2071
|
const [token, ...restTokens] = tokens;
|
|
@@ -2115,7 +2115,7 @@ var Node = class _Node {
|
|
|
2115
2115
|
node = this.#children[token] = new _Node();
|
|
2116
2116
|
}
|
|
2117
2117
|
}
|
|
2118
|
-
node.insert(restTokens,
|
|
2118
|
+
node.insert(restTokens, index2, paramMap, context, pathErrorCheckOnly);
|
|
2119
2119
|
}
|
|
2120
2120
|
buildRegExpStr() {
|
|
2121
2121
|
const childKeys = Object.keys(this.#children).sort(compareKey);
|
|
@@ -2140,7 +2140,7 @@ var Node = class _Node {
|
|
|
2140
2140
|
var Trie = class {
|
|
2141
2141
|
#context = { varIndex: 0 };
|
|
2142
2142
|
#root = new Node();
|
|
2143
|
-
insert(path5,
|
|
2143
|
+
insert(path5, index2, pathErrorCheckOnly) {
|
|
2144
2144
|
const paramAssoc = [];
|
|
2145
2145
|
const groups = [];
|
|
2146
2146
|
for (let i = 0; ; ) {
|
|
@@ -2166,7 +2166,7 @@ var Trie = class {
|
|
|
2166
2166
|
}
|
|
2167
2167
|
}
|
|
2168
2168
|
}
|
|
2169
|
-
this.#root.insert(tokens,
|
|
2169
|
+
this.#root.insert(tokens, index2, paramAssoc, this.#context, pathErrorCheckOnly);
|
|
2170
2170
|
return paramAssoc;
|
|
2171
2171
|
}
|
|
2172
2172
|
buildRegExp() {
|
|
@@ -2860,6 +2860,21 @@ var serveStatic = (options = { root: "" }) => {
|
|
|
2860
2860
|
};
|
|
2861
2861
|
};
|
|
2862
2862
|
|
|
2863
|
+
// src/metadata/tag-colors.ts
|
|
2864
|
+
var TAG_COLORS = [
|
|
2865
|
+
"yellow",
|
|
2866
|
+
"orange",
|
|
2867
|
+
"red",
|
|
2868
|
+
"magenta",
|
|
2869
|
+
"violet",
|
|
2870
|
+
"blue",
|
|
2871
|
+
"cyan",
|
|
2872
|
+
"green"
|
|
2873
|
+
];
|
|
2874
|
+
function isColorToken(value) {
|
|
2875
|
+
return typeof value === "string" && TAG_COLORS.includes(value);
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2863
2878
|
// src/archive/chat-id.ts
|
|
2864
2879
|
import crypto3 from "crypto";
|
|
2865
2880
|
var CROCKFORD_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
@@ -2922,7 +2937,8 @@ function toApiBlock(block) {
|
|
|
2922
2937
|
}
|
|
2923
2938
|
function createChatReader({
|
|
2924
2939
|
archive: archive2,
|
|
2925
|
-
metadata: metadata2
|
|
2940
|
+
metadata: metadata2,
|
|
2941
|
+
tags: tags3
|
|
2926
2942
|
}) {
|
|
2927
2943
|
function findChat(id) {
|
|
2928
2944
|
const code = parseChatId(id);
|
|
@@ -2938,10 +2954,24 @@ function createChatReader({
|
|
|
2938
2954
|
return [agent, sourceId].join("\0");
|
|
2939
2955
|
}
|
|
2940
2956
|
function listChats({
|
|
2941
|
-
includeTrashed
|
|
2957
|
+
includeTrashed,
|
|
2958
|
+
projects,
|
|
2959
|
+
tags: tagSelection
|
|
2942
2960
|
}) {
|
|
2943
2961
|
const visibility = loadChatVisibility(metadata2, { includeTrashed });
|
|
2944
|
-
const rows = archive2.read.listChatRows();
|
|
2962
|
+
const rows = archive2.read.listChatRows(projects ? { projects } : void 0);
|
|
2963
|
+
let passesTagFilter = null;
|
|
2964
|
+
if (tagSelection) {
|
|
2965
|
+
const realTagIds = tagSelection.filter((t) => t !== "");
|
|
2966
|
+
const wantUntagged = tagSelection.includes("");
|
|
2967
|
+
const allowedByTags = realTagIds.length > 0 ? new Set(tags3.listChatIdsWithAllTags(realTagIds)) : null;
|
|
2968
|
+
const taggedChatIds = wantUntagged ? new Set(tags3.listTagsByChat().keys()) : null;
|
|
2969
|
+
passesTagFilter = (chatInternalId) => {
|
|
2970
|
+
if (allowedByTags && !allowedByTags.has(chatInternalId)) return false;
|
|
2971
|
+
if (taggedChatIds && taggedChatIds.has(chatInternalId)) return false;
|
|
2972
|
+
return true;
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2945
2975
|
const tsRangeByKey = new Map(
|
|
2946
2976
|
archive2.read.listChatTsRanges().map((r) => [key(r.agent, r.sourceId), r])
|
|
2947
2977
|
);
|
|
@@ -2952,9 +2982,11 @@ function createChatReader({
|
|
|
2952
2982
|
archive2.read.listFirstUserTexts().map((r) => [key(r.agent, r.sourceId), r.text])
|
|
2953
2983
|
);
|
|
2954
2984
|
const customTitleById = metadata2.listCustomTitles();
|
|
2985
|
+
const tagsByChatId = tags3.listTagsByChat();
|
|
2955
2986
|
const chats2 = [];
|
|
2956
2987
|
for (const row of rows) {
|
|
2957
2988
|
if (!visibility.isVisible(row.id)) continue;
|
|
2989
|
+
if (passesTagFilter && !passesTagFilter(row.id)) continue;
|
|
2958
2990
|
const isDeleted = visibility.isTrashed(row.id);
|
|
2959
2991
|
const rowKey = key(row.agent, row.sourceId);
|
|
2960
2992
|
const tsRange = tsRangeByKey.get(rowKey);
|
|
@@ -2972,7 +3004,8 @@ function createChatReader({
|
|
|
2972
3004
|
sourceFilePath: sourcePathByKey.get(rowKey) ?? null,
|
|
2973
3005
|
createdAt: tsRange?.minTs ?? firstSeenAtMs,
|
|
2974
3006
|
updatedAt: tsRange?.maxTs ?? firstSeenAtMs,
|
|
2975
|
-
deletedAt: visibility.deletedAt(row.id)
|
|
3007
|
+
deletedAt: visibility.deletedAt(row.id),
|
|
3008
|
+
tags: tagsByChatId.get(row.id) ?? []
|
|
2976
3009
|
};
|
|
2977
3010
|
if (isDeleted) chat.isDeleted = true;
|
|
2978
3011
|
chats2.push(chat);
|
|
@@ -2995,12 +3028,17 @@ function createChatReader({
|
|
|
2995
3028
|
}
|
|
2996
3029
|
|
|
2997
3030
|
// src/app.ts
|
|
2998
|
-
function createApp({ archive: archive2, metadata: metadata2, webDistDir: webDistDir2 }) {
|
|
3031
|
+
function createApp({ archive: archive2, metadata: metadata2, tags: tags3, webDistDir: webDistDir2 }) {
|
|
2999
3032
|
const app2 = new Hono2();
|
|
3000
|
-
const reader = createChatReader({ archive: archive2, metadata: metadata2 });
|
|
3033
|
+
const reader = createChatReader({ archive: archive2, metadata: metadata2, tags: tags3 });
|
|
3001
3034
|
app2.get("/api/chats", (c) => {
|
|
3035
|
+
const projects = c.req.queries("project");
|
|
3036
|
+
const tagsParam = c.req.query("tags");
|
|
3037
|
+
const tags4 = tagsParam === void 0 ? void 0 : tagsParam.split(",");
|
|
3002
3038
|
const chats2 = reader.listChats({
|
|
3003
|
-
includeTrashed: c.req.query("includeTrashed") === "true"
|
|
3039
|
+
includeTrashed: c.req.query("includeTrashed") === "true",
|
|
3040
|
+
projects,
|
|
3041
|
+
tags: tags4
|
|
3004
3042
|
});
|
|
3005
3043
|
return c.json({ chats: chats2 });
|
|
3006
3044
|
});
|
|
@@ -3051,6 +3089,85 @@ function createApp({ archive: archive2, metadata: metadata2, webDistDir: webDist
|
|
|
3051
3089
|
}
|
|
3052
3090
|
return c.json({ messages: messages2 });
|
|
3053
3091
|
});
|
|
3092
|
+
app2.get("/api/tags", (c) => {
|
|
3093
|
+
return c.json({ tags: tags3.listTags() });
|
|
3094
|
+
});
|
|
3095
|
+
app2.post("/api/tags", async (c) => {
|
|
3096
|
+
let body;
|
|
3097
|
+
try {
|
|
3098
|
+
body = await c.req.json();
|
|
3099
|
+
} catch {
|
|
3100
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
3101
|
+
}
|
|
3102
|
+
const name = body && typeof body === "object" ? body.name : void 0;
|
|
3103
|
+
const color = body && typeof body === "object" ? body.color : void 0;
|
|
3104
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
3105
|
+
return c.json({ error: "Invalid name" }, 400);
|
|
3106
|
+
}
|
|
3107
|
+
if (!isColorToken(color)) {
|
|
3108
|
+
return c.json({ error: "Invalid color" }, 400);
|
|
3109
|
+
}
|
|
3110
|
+
const tag = tags3.createTag(name.trim(), color);
|
|
3111
|
+
return c.json({ tag }, 201);
|
|
3112
|
+
});
|
|
3113
|
+
app2.patch("/api/tags/:id", async (c) => {
|
|
3114
|
+
const id = c.req.param("id");
|
|
3115
|
+
if (!tags3.getTag(id)) {
|
|
3116
|
+
return c.json({ error: "Tag not found" }, 404);
|
|
3117
|
+
}
|
|
3118
|
+
let body;
|
|
3119
|
+
try {
|
|
3120
|
+
body = await c.req.json();
|
|
3121
|
+
} catch {
|
|
3122
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
3123
|
+
}
|
|
3124
|
+
const name = body && typeof body === "object" ? body.name : void 0;
|
|
3125
|
+
const color = body && typeof body === "object" ? body.color : void 0;
|
|
3126
|
+
if (name !== void 0) {
|
|
3127
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
3128
|
+
return c.json({ error: "Invalid name" }, 400);
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
if (color !== void 0 && !isColorToken(color)) {
|
|
3132
|
+
return c.json({ error: "Invalid color" }, 400);
|
|
3133
|
+
}
|
|
3134
|
+
if (typeof name === "string") tags3.renameTag(id, name.trim());
|
|
3135
|
+
if (isColorToken(color)) tags3.recolorTag(id, color);
|
|
3136
|
+
return c.body(null, 204);
|
|
3137
|
+
});
|
|
3138
|
+
app2.delete("/api/tags/:id", (c) => {
|
|
3139
|
+
if (!tags3.getTag(c.req.param("id"))) {
|
|
3140
|
+
return c.json({ error: "Tag not found" }, 404);
|
|
3141
|
+
}
|
|
3142
|
+
const result = tags3.deleteTag(c.req.param("id"));
|
|
3143
|
+
return c.json(result);
|
|
3144
|
+
});
|
|
3145
|
+
app2.post("/api/chats/:id/tags", async (c) => {
|
|
3146
|
+
const row = reader.findChat(c.req.param("id"));
|
|
3147
|
+
if (!row) {
|
|
3148
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
3149
|
+
}
|
|
3150
|
+
let body;
|
|
3151
|
+
try {
|
|
3152
|
+
body = await c.req.json();
|
|
3153
|
+
} catch {
|
|
3154
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
3155
|
+
}
|
|
3156
|
+
const tagId = body && typeof body === "object" ? body.tagId : void 0;
|
|
3157
|
+
if (typeof tagId !== "string" || !tags3.getTag(tagId)) {
|
|
3158
|
+
return c.json({ error: "Tag not found" }, 404);
|
|
3159
|
+
}
|
|
3160
|
+
tags3.assignTag(row.id, tagId);
|
|
3161
|
+
return c.body(null, 204);
|
|
3162
|
+
});
|
|
3163
|
+
app2.delete("/api/chats/:id/tags/:tagId", (c) => {
|
|
3164
|
+
const row = reader.findChat(c.req.param("id"));
|
|
3165
|
+
if (!row) {
|
|
3166
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
3167
|
+
}
|
|
3168
|
+
tags3.removeTag(row.id, c.req.param("tagId"));
|
|
3169
|
+
return c.body(null, 204);
|
|
3170
|
+
});
|
|
3054
3171
|
if (webDistDir2) {
|
|
3055
3172
|
app2.use("*", serveStatic({ root: webDistDir2 }));
|
|
3056
3173
|
app2.use("*", serveStatic({ root: webDistDir2, path: "index.html" }));
|
|
@@ -4440,8 +4557,8 @@ function haveSameKeys(left, right) {
|
|
|
4440
4557
|
if (leftKeys.length !== rightKeys.length) {
|
|
4441
4558
|
return false;
|
|
4442
4559
|
}
|
|
4443
|
-
for (const [
|
|
4444
|
-
if (key !== rightKeys[
|
|
4560
|
+
for (const [index2, key] of leftKeys.entries()) {
|
|
4561
|
+
if (key !== rightKeys[index2]) {
|
|
4445
4562
|
return false;
|
|
4446
4563
|
}
|
|
4447
4564
|
}
|
|
@@ -4821,19 +4938,19 @@ function extractTablesRelationalConfig(schema, configHelpers) {
|
|
|
4821
4938
|
const relations2 = value.config(
|
|
4822
4939
|
configHelpers(value.table)
|
|
4823
4940
|
);
|
|
4824
|
-
let
|
|
4941
|
+
let primaryKey2;
|
|
4825
4942
|
for (const [relationName, relation] of Object.entries(relations2)) {
|
|
4826
4943
|
if (tableName) {
|
|
4827
4944
|
const tableConfig = tablesConfig[tableName];
|
|
4828
4945
|
tableConfig.relations[relationName] = relation;
|
|
4829
|
-
if (
|
|
4830
|
-
tableConfig.primaryKey.push(...
|
|
4946
|
+
if (primaryKey2) {
|
|
4947
|
+
tableConfig.primaryKey.push(...primaryKey2);
|
|
4831
4948
|
}
|
|
4832
4949
|
} else {
|
|
4833
4950
|
if (!(dbName in relationsBuffer)) {
|
|
4834
4951
|
relationsBuffer[dbName] = {
|
|
4835
4952
|
relations: {},
|
|
4836
|
-
primaryKey
|
|
4953
|
+
primaryKey: primaryKey2
|
|
4837
4954
|
};
|
|
4838
4955
|
}
|
|
4839
4956
|
relationsBuffer[dbName].relations[relationName] = relation;
|
|
@@ -5677,10 +5794,49 @@ var Index = class {
|
|
|
5677
5794
|
this.config = { ...config, table };
|
|
5678
5795
|
}
|
|
5679
5796
|
};
|
|
5797
|
+
function index(name) {
|
|
5798
|
+
return new IndexBuilderOn(name, false);
|
|
5799
|
+
}
|
|
5680
5800
|
function uniqueIndex(name) {
|
|
5681
5801
|
return new IndexBuilderOn(name, true);
|
|
5682
5802
|
}
|
|
5683
5803
|
|
|
5804
|
+
// ../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/primary-keys.js
|
|
5805
|
+
function primaryKey(...config) {
|
|
5806
|
+
if (config[0].columns) {
|
|
5807
|
+
return new PrimaryKeyBuilder2(config[0].columns, config[0].name);
|
|
5808
|
+
}
|
|
5809
|
+
return new PrimaryKeyBuilder2(config);
|
|
5810
|
+
}
|
|
5811
|
+
var PrimaryKeyBuilder2 = class {
|
|
5812
|
+
static [entityKind] = "SQLitePrimaryKeyBuilder";
|
|
5813
|
+
/** @internal */
|
|
5814
|
+
columns;
|
|
5815
|
+
/** @internal */
|
|
5816
|
+
name;
|
|
5817
|
+
constructor(columns, name) {
|
|
5818
|
+
this.columns = columns;
|
|
5819
|
+
this.name = name;
|
|
5820
|
+
}
|
|
5821
|
+
/** @internal */
|
|
5822
|
+
build(table) {
|
|
5823
|
+
return new PrimaryKey2(table, this.columns, this.name);
|
|
5824
|
+
}
|
|
5825
|
+
};
|
|
5826
|
+
var PrimaryKey2 = class {
|
|
5827
|
+
constructor(table, columns, name) {
|
|
5828
|
+
this.table = table;
|
|
5829
|
+
this.columns = columns;
|
|
5830
|
+
this.name = name;
|
|
5831
|
+
}
|
|
5832
|
+
static [entityKind] = "SQLitePrimaryKey";
|
|
5833
|
+
columns;
|
|
5834
|
+
name;
|
|
5835
|
+
getName() {
|
|
5836
|
+
return this.name ?? `${this.table[SQLiteTable.Symbol.Name]}_${this.columns.map((column) => column.name).join("_")}_pk`;
|
|
5837
|
+
}
|
|
5838
|
+
};
|
|
5839
|
+
|
|
5684
5840
|
// ../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/utils.js
|
|
5685
5841
|
function extractUsedTable(table) {
|
|
5686
5842
|
if (is(table, SQLiteTable)) {
|
|
@@ -6029,8 +6185,8 @@ var SQLiteDialect = class {
|
|
|
6029
6185
|
}
|
|
6030
6186
|
const joinsArray = [];
|
|
6031
6187
|
if (joins) {
|
|
6032
|
-
for (const [
|
|
6033
|
-
if (
|
|
6188
|
+
for (const [index2, joinMeta] of joins.entries()) {
|
|
6189
|
+
if (index2 === 0) {
|
|
6034
6190
|
joinsArray.push(sql` `);
|
|
6035
6191
|
}
|
|
6036
6192
|
const table = joinMeta.table;
|
|
@@ -6050,7 +6206,7 @@ var SQLiteDialect = class {
|
|
|
6050
6206
|
sql`${sql.raw(joinMeta.joinType)} join ${table}${onSql}`
|
|
6051
6207
|
);
|
|
6052
6208
|
}
|
|
6053
|
-
if (
|
|
6209
|
+
if (index2 < joins.length - 1) {
|
|
6054
6210
|
joinsArray.push(sql` `);
|
|
6055
6211
|
}
|
|
6056
6212
|
}
|
|
@@ -6063,9 +6219,9 @@ var SQLiteDialect = class {
|
|
|
6063
6219
|
buildOrderBy(orderBy) {
|
|
6064
6220
|
const orderByList = [];
|
|
6065
6221
|
if (orderBy) {
|
|
6066
|
-
for (const [
|
|
6222
|
+
for (const [index2, orderByValue] of orderBy.entries()) {
|
|
6067
6223
|
orderByList.push(orderByValue);
|
|
6068
|
-
if (
|
|
6224
|
+
if (index2 < orderBy.length - 1) {
|
|
6069
6225
|
orderByList.push(sql`, `);
|
|
6070
6226
|
}
|
|
6071
6227
|
}
|
|
@@ -6118,9 +6274,9 @@ var SQLiteDialect = class {
|
|
|
6118
6274
|
const havingSql = having ? sql` having ${having}` : void 0;
|
|
6119
6275
|
const groupByList = [];
|
|
6120
6276
|
if (groupBy) {
|
|
6121
|
-
for (const [
|
|
6277
|
+
for (const [index2, groupByValue] of groupBy.entries()) {
|
|
6122
6278
|
groupByList.push(groupByValue);
|
|
6123
|
-
if (
|
|
6279
|
+
if (index2 < groupBy.length - 1) {
|
|
6124
6280
|
groupByList.push(sql`, `);
|
|
6125
6281
|
}
|
|
6126
6282
|
}
|
|
@@ -8621,7 +8777,11 @@ var chats = sqliteTable(
|
|
|
8621
8777
|
project: text("project"),
|
|
8622
8778
|
projectPath: text("project_path")
|
|
8623
8779
|
},
|
|
8624
|
-
(t) => [
|
|
8780
|
+
(t) => [
|
|
8781
|
+
uniqueIndex("chats_agent_source_idx").on(t.agent, t.sourceId),
|
|
8782
|
+
// Backs the server-side Project filter (`WHERE coalesce(project,'') IN …`).
|
|
8783
|
+
index("chats_project_idx").on(t.project)
|
|
8784
|
+
]
|
|
8625
8785
|
);
|
|
8626
8786
|
var rawMessages = sqliteTable(
|
|
8627
8787
|
"raw_messages",
|
|
@@ -8669,7 +8829,11 @@ var ingestionEvents = sqliteTable("ingestion_events", {
|
|
|
8669
8829
|
// src/archive/read-seam.ts
|
|
8670
8830
|
function createArchiveReadSeam(db) {
|
|
8671
8831
|
return {
|
|
8672
|
-
listChatRows() {
|
|
8832
|
+
listChatRows(opts) {
|
|
8833
|
+
const projects = opts?.projects;
|
|
8834
|
+
if (projects) {
|
|
8835
|
+
return db.select().from(chats).where(inArray(sql`coalesce(${chats.project}, '')`, projects)).all();
|
|
8836
|
+
}
|
|
8673
8837
|
return db.select().from(chats).all();
|
|
8674
8838
|
},
|
|
8675
8839
|
findChatBySourceId(sourceId) {
|
|
@@ -8957,7 +9121,9 @@ import path2 from "path";
|
|
|
8957
9121
|
// src/metadata/schema.ts
|
|
8958
9122
|
var schema_exports3 = {};
|
|
8959
9123
|
__export(schema_exports3, {
|
|
8960
|
-
|
|
9124
|
+
chatTags: () => chatTags,
|
|
9125
|
+
chatsMeta: () => chatsMeta,
|
|
9126
|
+
tags: () => tags
|
|
8961
9127
|
});
|
|
8962
9128
|
var chatsMeta = sqliteTable("chats_meta", {
|
|
8963
9129
|
id: text("id").primaryKey(),
|
|
@@ -8969,6 +9135,24 @@ var chatsMeta = sqliteTable("chats_meta", {
|
|
|
8969
9135
|
// view's independent "Deleted time" sort axis.
|
|
8970
9136
|
deletedAt: integer("deleted_at", { mode: "timestamp_ms" })
|
|
8971
9137
|
});
|
|
9138
|
+
var tags = sqliteTable("tags", {
|
|
9139
|
+
id: text("id").primaryKey(),
|
|
9140
|
+
name: text("name").notNull(),
|
|
9141
|
+
color: text("color").notNull(),
|
|
9142
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
9143
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull()
|
|
9144
|
+
});
|
|
9145
|
+
var chatTags = sqliteTable(
|
|
9146
|
+
"chat_tags",
|
|
9147
|
+
{
|
|
9148
|
+
chatId: text("chat_id").notNull(),
|
|
9149
|
+
tagId: text("tag_id").notNull().references(() => tags.id, { onDelete: "cascade" })
|
|
9150
|
+
},
|
|
9151
|
+
(table) => [
|
|
9152
|
+
primaryKey({ columns: [table.chatId, table.tagId] }),
|
|
9153
|
+
index("chat_tags_tag_id_idx").on(table.tagId)
|
|
9154
|
+
]
|
|
9155
|
+
);
|
|
8972
9156
|
|
|
8973
9157
|
// src/metadata/repository.ts
|
|
8974
9158
|
var DB_FILE = "metadata.db";
|
|
@@ -9070,6 +9254,96 @@ function rekeyLegacyRows(sqlite, db, lookupInternalId, ensureChat) {
|
|
|
9070
9254
|
sqlite.pragma(`user_version = ${REKEY_USER_VERSION}`);
|
|
9071
9255
|
}
|
|
9072
9256
|
|
|
9257
|
+
// src/metadata/tags.ts
|
|
9258
|
+
import { randomUUID } from "crypto";
|
|
9259
|
+
var DB_FILE2 = "metadata.db";
|
|
9260
|
+
function createTagRepository({
|
|
9261
|
+
dataDir: dataDir2
|
|
9262
|
+
}) {
|
|
9263
|
+
const { db } = openStore({
|
|
9264
|
+
dataDir: dataDir2,
|
|
9265
|
+
dbFile: DB_FILE2,
|
|
9266
|
+
callerUrl: import.meta.url,
|
|
9267
|
+
migrationsSubdir: "drizzle",
|
|
9268
|
+
schema: schema_exports3
|
|
9269
|
+
});
|
|
9270
|
+
function toTag(row) {
|
|
9271
|
+
return { id: row.id, name: row.name, color: row.color };
|
|
9272
|
+
}
|
|
9273
|
+
function assertColor(color) {
|
|
9274
|
+
if (!isColorToken(color)) {
|
|
9275
|
+
throw new Error(`Invalid tag color token: ${String(color)}`);
|
|
9276
|
+
}
|
|
9277
|
+
}
|
|
9278
|
+
return {
|
|
9279
|
+
createTag(name, color) {
|
|
9280
|
+
assertColor(color);
|
|
9281
|
+
const now = /* @__PURE__ */ new Date();
|
|
9282
|
+
const row = {
|
|
9283
|
+
id: randomUUID(),
|
|
9284
|
+
name,
|
|
9285
|
+
color,
|
|
9286
|
+
createdAt: now,
|
|
9287
|
+
updatedAt: now
|
|
9288
|
+
};
|
|
9289
|
+
db.insert(tags).values(row).run();
|
|
9290
|
+
return toTag(row);
|
|
9291
|
+
},
|
|
9292
|
+
listTags() {
|
|
9293
|
+
return db.select({ id: tags.id, name: tags.name, color: tags.color }).from(tags).all().map(toTag);
|
|
9294
|
+
},
|
|
9295
|
+
getTag(id) {
|
|
9296
|
+
const row = db.select({ id: tags.id, name: tags.name, color: tags.color }).from(tags).where(eq(tags.id, id)).get();
|
|
9297
|
+
return row ? toTag(row) : null;
|
|
9298
|
+
},
|
|
9299
|
+
renameTag(id, name) {
|
|
9300
|
+
db.update(tags).set({ name, updatedAt: /* @__PURE__ */ new Date() }).where(eq(tags.id, id)).run();
|
|
9301
|
+
},
|
|
9302
|
+
recolorTag(id, color) {
|
|
9303
|
+
assertColor(color);
|
|
9304
|
+
db.update(tags).set({ color, updatedAt: /* @__PURE__ */ new Date() }).where(eq(tags.id, id)).run();
|
|
9305
|
+
},
|
|
9306
|
+
deleteTag(id) {
|
|
9307
|
+
return db.transaction((tx) => {
|
|
9308
|
+
const removedFromChats = tx.select({ chatId: chatTags.chatId }).from(chatTags).where(eq(chatTags.tagId, id)).all().length;
|
|
9309
|
+
tx.delete(chatTags).where(eq(chatTags.tagId, id)).run();
|
|
9310
|
+
tx.delete(tags).where(eq(tags.id, id)).run();
|
|
9311
|
+
return { removedFromChats };
|
|
9312
|
+
});
|
|
9313
|
+
},
|
|
9314
|
+
assignTag(chatId, tagId) {
|
|
9315
|
+
db.insert(chatTags).values({ chatId, tagId }).onConflictDoNothing().run();
|
|
9316
|
+
},
|
|
9317
|
+
removeTag(chatId, tagId) {
|
|
9318
|
+
db.delete(chatTags).where(and(eq(chatTags.chatId, chatId), eq(chatTags.tagId, tagId))).run();
|
|
9319
|
+
},
|
|
9320
|
+
listTagsForChat(chatId) {
|
|
9321
|
+
return db.select({ id: tags.id, name: tags.name, color: tags.color }).from(chatTags).innerJoin(tags, eq(chatTags.tagId, tags.id)).where(eq(chatTags.chatId, chatId)).all().map(toTag);
|
|
9322
|
+
},
|
|
9323
|
+
listTagsByChat(chatIds) {
|
|
9324
|
+
const base = db.select({
|
|
9325
|
+
chatId: chatTags.chatId,
|
|
9326
|
+
id: tags.id,
|
|
9327
|
+
name: tags.name,
|
|
9328
|
+
color: tags.color
|
|
9329
|
+
}).from(chatTags).innerJoin(tags, eq(chatTags.tagId, tags.id));
|
|
9330
|
+
const rows = chatIds === void 0 ? base.all() : chatIds.length === 0 ? [] : base.where(inArray(chatTags.chatId, chatIds)).all();
|
|
9331
|
+
const byChat = /* @__PURE__ */ new Map();
|
|
9332
|
+
for (const row of rows) {
|
|
9333
|
+
const list = byChat.get(row.chatId) ?? [];
|
|
9334
|
+
list.push(toTag(row));
|
|
9335
|
+
byChat.set(row.chatId, list);
|
|
9336
|
+
}
|
|
9337
|
+
return byChat;
|
|
9338
|
+
},
|
|
9339
|
+
listChatIdsWithAllTags(tagIds) {
|
|
9340
|
+
const distinct = [...new Set(tagIds)];
|
|
9341
|
+
if (distinct.length === 0) return [];
|
|
9342
|
+
return db.select({ chatId: chatTags.chatId }).from(chatTags).where(inArray(chatTags.tagId, distinct)).groupBy(chatTags.chatId).having(sql`count(${chatTags.tagId}) = ${distinct.length}`).all().map((r) => r.chatId);
|
|
9343
|
+
}
|
|
9344
|
+
};
|
|
9345
|
+
}
|
|
9346
|
+
|
|
9073
9347
|
// src/ingestion/ingest.ts
|
|
9074
9348
|
import fs4 from "fs";
|
|
9075
9349
|
async function runIngestion(opts) {
|
|
@@ -10200,8 +10474,8 @@ function normalizePath(path5) {
|
|
|
10200
10474
|
}
|
|
10201
10475
|
function matchPatterns(patterns, testString, stats) {
|
|
10202
10476
|
const path5 = normalizePath(testString);
|
|
10203
|
-
for (let
|
|
10204
|
-
const pattern = patterns[
|
|
10477
|
+
for (let index2 = 0; index2 < patterns.length; index2++) {
|
|
10478
|
+
const pattern = patterns[index2];
|
|
10205
10479
|
if (pattern(path5, stats)) {
|
|
10206
10480
|
return true;
|
|
10207
10481
|
}
|
|
@@ -10549,8 +10823,8 @@ var FSWatcher = class extends EventEmitter {
|
|
|
10549
10823
|
const watchList = {};
|
|
10550
10824
|
this._watched.forEach((entry, dir) => {
|
|
10551
10825
|
const key = this.options.cwd ? sp2.relative(this.options.cwd, dir) : dir;
|
|
10552
|
-
const
|
|
10553
|
-
watchList[
|
|
10826
|
+
const index2 = key || ONE_DOT;
|
|
10827
|
+
watchList[index2] = entry.getChildren().sort();
|
|
10554
10828
|
});
|
|
10555
10829
|
return watchList;
|
|
10556
10830
|
}
|
|
@@ -11181,7 +11455,8 @@ var port = action.port;
|
|
|
11181
11455
|
var archive = createArchiveRepository({ dataDir });
|
|
11182
11456
|
var checkpoint = createCheckpointRepository({ dataDir });
|
|
11183
11457
|
var metadata = createMetadataRepository({ dataDir });
|
|
11184
|
-
var
|
|
11458
|
+
var tags2 = createTagRepository({ dataDir });
|
|
11459
|
+
var app = createApp({ archive, metadata, tags: tags2, webDistDir });
|
|
11185
11460
|
var initialIngest = startIngestionInBackground({
|
|
11186
11461
|
plugins,
|
|
11187
11462
|
archive,
|