valija 0.1.0 → 0.2.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 +27 -1
- package/dist/program.js +1292 -375
- package/dist/program.js.map +1 -1
- package/package.json +2 -1
package/dist/program.js
CHANGED
|
@@ -21,6 +21,75 @@ var DomainError = class extends Error {
|
|
|
21
21
|
// src/context/domain/errors.ts
|
|
22
22
|
var contextErr = (code, message) => err(new DomainError(code, message));
|
|
23
23
|
|
|
24
|
+
// src/context/domain/services/context-pack.ts
|
|
25
|
+
var DEFAULT_BUDGET_TOKENS = 4e3;
|
|
26
|
+
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
27
|
+
var estimateItemTokens = (item) => estimateTokens(
|
|
28
|
+
`${item.type} ${item.createdAt.toISOString().slice(0, 10)} ${item.tags.join(" ")}
|
|
29
|
+
|
|
30
|
+
${item.content}`
|
|
31
|
+
);
|
|
32
|
+
var SECTION_TYPE_ORDER = ["decision", "preference", "progress", "fact"];
|
|
33
|
+
var estimatePreambleTokens = (input) => estimateTokens(
|
|
34
|
+
`Context pack: ${input.projectName} \u2014 ${input.items.length} items, generated ${input.generatedAt.toISOString()}`
|
|
35
|
+
);
|
|
36
|
+
function assembleContextPack(input) {
|
|
37
|
+
const draft = {
|
|
38
|
+
sections: [],
|
|
39
|
+
included: /* @__PURE__ */ new Set(),
|
|
40
|
+
budget: input.budgetTokens ?? Number.POSITIVE_INFINITY,
|
|
41
|
+
usedTokens: estimatePreambleTokens(input)
|
|
42
|
+
};
|
|
43
|
+
addPinned(draft, input.items);
|
|
44
|
+
addLatestHandoff(draft, input.items);
|
|
45
|
+
addTypeSections(draft, input.items);
|
|
46
|
+
return {
|
|
47
|
+
projectName: input.projectName,
|
|
48
|
+
generatedAt: input.generatedAt,
|
|
49
|
+
sections: draft.sections,
|
|
50
|
+
includedCount: draft.included.size,
|
|
51
|
+
totalCount: input.items.length,
|
|
52
|
+
estimatedTokens: draft.usedTokens
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function addPinned(draft, items) {
|
|
56
|
+
const pinned = items.filter((item) => item.pinned);
|
|
57
|
+
if (pinned.length === 0) return;
|
|
58
|
+
draft.usedTokens += estimateTokens("Pinned");
|
|
59
|
+
const kept = [];
|
|
60
|
+
for (const item of pinned) {
|
|
61
|
+
const cost = estimateItemTokens(item);
|
|
62
|
+
if (kept.length > 0 && draft.usedTokens + cost > draft.budget) break;
|
|
63
|
+
kept.push(item);
|
|
64
|
+
draft.usedTokens += cost;
|
|
65
|
+
draft.included.add(item.id);
|
|
66
|
+
}
|
|
67
|
+
draft.sections.push({ kind: "pinned", items: kept });
|
|
68
|
+
}
|
|
69
|
+
function addLatestHandoff(draft, items) {
|
|
70
|
+
const handoff = items.find((item) => item.type === "handoff" && !draft.included.has(item.id));
|
|
71
|
+
if (handoff === void 0) return;
|
|
72
|
+
const cost = estimateItemTokens(handoff) + estimateTokens("Latest handoff");
|
|
73
|
+
if (draft.usedTokens + cost > draft.budget) return;
|
|
74
|
+
draft.usedTokens += cost;
|
|
75
|
+
draft.sections.push({ kind: "latest-handoff", items: [handoff] });
|
|
76
|
+
draft.included.add(handoff.id);
|
|
77
|
+
}
|
|
78
|
+
function addTypeSections(draft, items) {
|
|
79
|
+
for (const type of SECTION_TYPE_ORDER) {
|
|
80
|
+
const candidates = items.filter((item) => item.type === type && !draft.included.has(item.id));
|
|
81
|
+
const kept = [];
|
|
82
|
+
for (const item of candidates) {
|
|
83
|
+
const cost = estimateItemTokens(item) + (kept.length === 0 ? estimateTokens(type) : 0);
|
|
84
|
+
if (draft.usedTokens + cost > draft.budget) break;
|
|
85
|
+
kept.push(item);
|
|
86
|
+
draft.usedTokens += cost;
|
|
87
|
+
draft.included.add(item.id);
|
|
88
|
+
}
|
|
89
|
+
if (kept.length > 0) draft.sections.push({ kind: "by-type", type, items: kept });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
24
93
|
// src/context/domain/values/project-name.ts
|
|
25
94
|
var PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
26
95
|
function parseProjectName(raw) {
|
|
@@ -34,62 +103,7 @@ function parseProjectName(raw) {
|
|
|
34
103
|
return ok(normalized);
|
|
35
104
|
}
|
|
36
105
|
|
|
37
|
-
// src/context/application/
|
|
38
|
-
var ExportPack = class {
|
|
39
|
-
constructor(sessions, getContextPack) {
|
|
40
|
-
this.sessions = sessions;
|
|
41
|
-
this.getContextPack = getContextPack;
|
|
42
|
-
}
|
|
43
|
-
sessions;
|
|
44
|
-
getContextPack;
|
|
45
|
-
execute(project, format) {
|
|
46
|
-
if (format === "md") {
|
|
47
|
-
const pack = this.getContextPack.execute({ project, budgetTokens: Number.MAX_SAFE_INTEGER });
|
|
48
|
-
return pack.ok ? ok(pack.value.markdown) : pack;
|
|
49
|
-
}
|
|
50
|
-
const name = parseProjectName(project);
|
|
51
|
-
if (!name.ok) return name;
|
|
52
|
-
const session = this.sessions.open();
|
|
53
|
-
if (!session.ok) return session;
|
|
54
|
-
try {
|
|
55
|
-
const found = session.value.projects.findByName(name.value);
|
|
56
|
-
if (found === null) {
|
|
57
|
-
return contextErr("PROJECT_NOT_FOUND", `No project named "${name.value}".`);
|
|
58
|
-
}
|
|
59
|
-
const items = session.value.items.findByProject(found.id).map((item) => ({
|
|
60
|
-
id: item.id,
|
|
61
|
-
type: item.type,
|
|
62
|
-
content: item.content,
|
|
63
|
-
tags: item.tags,
|
|
64
|
-
pinned: item.pinned,
|
|
65
|
-
source: item.source ?? null,
|
|
66
|
-
createdAt: item.createdAt.toISOString()
|
|
67
|
-
}));
|
|
68
|
-
return ok(JSON.stringify({ project: name.value, items }, null, 2));
|
|
69
|
-
} finally {
|
|
70
|
-
session.value.close();
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// src/context/application/get-context-pack.ts
|
|
76
|
-
var DEFAULT_BUDGET_TOKENS = 4e3;
|
|
77
|
-
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
78
|
-
var SECTION_ORDER = ["decision", "preference", "progress", "fact"];
|
|
79
|
-
var SECTION_LABELS = {
|
|
80
|
-
decision: "Decisions",
|
|
81
|
-
preference: "Preferences",
|
|
82
|
-
progress: "Progress",
|
|
83
|
-
fact: "Facts"
|
|
84
|
-
};
|
|
85
|
-
var renderItem = (item) => {
|
|
86
|
-
const date = item.createdAt.toISOString().slice(0, 10);
|
|
87
|
-
const tags = item.tags.length > 0 ? ` \xB7 ${item.tags.map((t) => `#${t}`).join(" ")}` : "";
|
|
88
|
-
return `### ${item.type} \xB7 ${date}${tags}
|
|
89
|
-
|
|
90
|
-
${item.content}
|
|
91
|
-
`;
|
|
92
|
-
};
|
|
106
|
+
// src/context/application/use-cases/get-context-pack.use-case.ts
|
|
93
107
|
var GetContextPack = class {
|
|
94
108
|
constructor(sessions, clock) {
|
|
95
109
|
this.sessions = sessions;
|
|
@@ -100,107 +114,62 @@ var GetContextPack = class {
|
|
|
100
114
|
execute(input) {
|
|
101
115
|
const name = parseProjectName(input.project);
|
|
102
116
|
if (!name.ok) return name;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (!session.ok) return session;
|
|
106
|
-
try {
|
|
107
|
-
const project = session.value.projects.findByName(name.value);
|
|
117
|
+
return this.sessions.withSession((session) => {
|
|
118
|
+
const project = session.projects.findByName(name.value);
|
|
108
119
|
if (project === null) {
|
|
109
120
|
return contextErr("PROJECT_NOT_FOUND", `No project named "${name.value}".`);
|
|
110
121
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const pinned = all.filter((i) => i.pinned);
|
|
121
|
-
const pinnedKept = [];
|
|
122
|
-
for (const item of pinned) {
|
|
123
|
-
const cost = estimateTokens(renderItem(item));
|
|
124
|
-
if (pinnedKept.length > 0 && used + cost > budget) break;
|
|
125
|
-
pinnedKept.push(item);
|
|
126
|
-
used += cost;
|
|
127
|
-
}
|
|
128
|
-
if (pinnedKept.length > 0) {
|
|
129
|
-
parts.push("\n## Pinned\n");
|
|
130
|
-
for (const item of pinnedKept) {
|
|
131
|
-
parts.push(renderItem(item));
|
|
132
|
-
included.add(item.id);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
const handoff = all.find((i) => i.type === "handoff" && !included.has(i.id));
|
|
136
|
-
if (handoff) {
|
|
137
|
-
const rendered = renderItem(handoff);
|
|
138
|
-
const cost = estimateTokens(rendered);
|
|
139
|
-
if (used + cost <= budget) {
|
|
140
|
-
parts.push("\n## Latest handoff\n", rendered);
|
|
141
|
-
included.add(handoff.id);
|
|
142
|
-
used += cost;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
for (const type of SECTION_ORDER) {
|
|
146
|
-
const items = all.filter((i) => i.type === type && !included.has(i.id));
|
|
147
|
-
let sectionOpened = false;
|
|
148
|
-
for (const item of items) {
|
|
149
|
-
const rendered = renderItem(item);
|
|
150
|
-
const sectionHeader = sectionOpened ? "" : `
|
|
151
|
-
## ${SECTION_LABELS[type]}
|
|
152
|
-
`;
|
|
153
|
-
const cost = estimateTokens(sectionHeader + rendered);
|
|
154
|
-
if (used + cost > budget) break;
|
|
155
|
-
if (!sectionOpened) {
|
|
156
|
-
parts.push(sectionHeader);
|
|
157
|
-
sectionOpened = true;
|
|
158
|
-
}
|
|
159
|
-
parts.push(rendered);
|
|
160
|
-
included.add(item.id);
|
|
161
|
-
used += cost;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
const markdown = header + parts.join("\n");
|
|
165
|
-
return ok({
|
|
166
|
-
markdown,
|
|
167
|
-
includedCount: included.size,
|
|
168
|
-
totalCount: all.length,
|
|
169
|
-
estimatedTokens: estimateTokens(markdown)
|
|
170
|
-
});
|
|
171
|
-
} finally {
|
|
172
|
-
session.value.close();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// src/context/application/list-projects.ts
|
|
178
|
-
var ListProjects = class {
|
|
179
|
-
constructor(sessions) {
|
|
180
|
-
this.sessions = sessions;
|
|
181
|
-
}
|
|
182
|
-
sessions;
|
|
183
|
-
execute() {
|
|
184
|
-
const session = this.sessions.open();
|
|
185
|
-
if (!session.ok) return session;
|
|
186
|
-
try {
|
|
187
|
-
const entries = session.value.projects.list().map((summary) => ({
|
|
188
|
-
name: summary.project.name,
|
|
189
|
-
...summary.project.description === void 0 ? {} : { description: summary.project.description },
|
|
190
|
-
itemCount: summary.itemCount,
|
|
191
|
-
lastActivityAt: summary.lastActivityAt?.toISOString() ?? null
|
|
192
|
-
}));
|
|
193
|
-
return ok(entries);
|
|
194
|
-
} finally {
|
|
195
|
-
session.value.close();
|
|
196
|
-
}
|
|
122
|
+
return ok(
|
|
123
|
+
assembleContextPack({
|
|
124
|
+
projectName: name.value,
|
|
125
|
+
items: session.items.findByProject(project.id),
|
|
126
|
+
generatedAt: this.clock.now(),
|
|
127
|
+
budgetTokens: input.budgetTokens ?? DEFAULT_BUDGET_TOKENS
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
});
|
|
197
131
|
}
|
|
198
132
|
};
|
|
199
133
|
|
|
200
134
|
// src/context/domain/entities/context-item.ts
|
|
135
|
+
import { createHash } from "crypto";
|
|
136
|
+
function createContextItem(input) {
|
|
137
|
+
return {
|
|
138
|
+
id: input.id,
|
|
139
|
+
projectId: input.projectId,
|
|
140
|
+
type: input.type,
|
|
141
|
+
content: input.content,
|
|
142
|
+
tags: input.tags,
|
|
143
|
+
pinned: input.pinned,
|
|
144
|
+
...input.source === void 0 ? {} : { source: input.source },
|
|
145
|
+
archived: false,
|
|
146
|
+
createdAt: input.now,
|
|
147
|
+
updatedAt: input.now
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function importedItemId(projectId, source, conversationId, chunkIndex) {
|
|
151
|
+
const digest = createHash("sha256").update(`${projectId} ${source} ${conversationId} ${chunkIndex}`).digest("hex");
|
|
152
|
+
return `imp-${digest.slice(0, 32)}`;
|
|
153
|
+
}
|
|
154
|
+
function createImportedContextItem(input) {
|
|
155
|
+
return {
|
|
156
|
+
id: importedItemId(input.projectId, input.source, input.conversationId, input.chunkIndex),
|
|
157
|
+
projectId: input.projectId,
|
|
158
|
+
type: "imported",
|
|
159
|
+
content: input.content,
|
|
160
|
+
tags: input.tags,
|
|
161
|
+
pinned: false,
|
|
162
|
+
source: `${input.source}-import`,
|
|
163
|
+
archived: false,
|
|
164
|
+
createdAt: input.createdAt,
|
|
165
|
+
updatedAt: input.now
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/context/domain/values/content.ts
|
|
201
170
|
var MAX_CONTENT_BYTES = 32 * 1024;
|
|
202
|
-
function
|
|
203
|
-
const trimmed =
|
|
171
|
+
function parseContent(raw) {
|
|
172
|
+
const trimmed = raw.trim();
|
|
204
173
|
if (trimmed.length === 0) {
|
|
205
174
|
return contextErr("CONTENT_EMPTY", "Content must not be empty.");
|
|
206
175
|
}
|
|
@@ -214,18 +183,6 @@ function validateContent(content) {
|
|
|
214
183
|
return ok(trimmed);
|
|
215
184
|
}
|
|
216
185
|
|
|
217
|
-
// src/context/domain/values/item-type.ts
|
|
218
|
-
var ITEM_TYPES = ["decision", "progress", "preference", "fact", "handoff"];
|
|
219
|
-
function parseItemType(raw) {
|
|
220
|
-
if (ITEM_TYPES.includes(raw)) {
|
|
221
|
-
return ok(raw);
|
|
222
|
-
}
|
|
223
|
-
return contextErr(
|
|
224
|
-
"INVALID_ITEM_TYPE",
|
|
225
|
-
`Item type must be one of ${ITEM_TYPES.join(", ")}. Got: "${raw}"`
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
186
|
// src/context/domain/values/tag.ts
|
|
230
187
|
var PATTERN2 = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
231
188
|
var MAX_TAGS = 10;
|
|
@@ -249,7 +206,103 @@ function parseTags(raw) {
|
|
|
249
206
|
return ok(tags);
|
|
250
207
|
}
|
|
251
208
|
|
|
252
|
-
// src/context/application/
|
|
209
|
+
// src/context/application/use-cases/import-items.use-case.ts
|
|
210
|
+
var ImportItems = class {
|
|
211
|
+
constructor(sessions, clock, idGen) {
|
|
212
|
+
this.sessions = sessions;
|
|
213
|
+
this.clock = clock;
|
|
214
|
+
this.idGen = idGen;
|
|
215
|
+
}
|
|
216
|
+
sessions;
|
|
217
|
+
clock;
|
|
218
|
+
idGen;
|
|
219
|
+
execute(input) {
|
|
220
|
+
const name = parseProjectName(input.projectName);
|
|
221
|
+
if (!name.ok) return name;
|
|
222
|
+
return this.sessions.withSession((session) => {
|
|
223
|
+
const { project, created } = this.findOrCreateProject(session, name.value);
|
|
224
|
+
const failures = [];
|
|
225
|
+
let imported = 0;
|
|
226
|
+
for (const draft of input.items) {
|
|
227
|
+
const saved = this.saveDraft(session, project.id, draft);
|
|
228
|
+
if (saved.ok) imported += 1;
|
|
229
|
+
else failures.push({ conversationId: draft.conversationId, reason: saved.error.message });
|
|
230
|
+
}
|
|
231
|
+
return ok({ projectCreated: created, imported, failed: failures.length, failures });
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/** Re-validate tags and content at the vault boundary — defense in depth, even though the renderer aims well under the limit. */
|
|
235
|
+
saveDraft(session, projectId, draft) {
|
|
236
|
+
const tags = parseTags(draft.tags);
|
|
237
|
+
if (!tags.ok) return tags;
|
|
238
|
+
const content = parseContent(draft.content);
|
|
239
|
+
if (!content.ok) return content;
|
|
240
|
+
session.items.save(
|
|
241
|
+
createImportedContextItem({
|
|
242
|
+
projectId,
|
|
243
|
+
source: draft.source,
|
|
244
|
+
conversationId: draft.conversationId,
|
|
245
|
+
chunkIndex: draft.chunkIndex,
|
|
246
|
+
content: content.value,
|
|
247
|
+
tags: tags.value,
|
|
248
|
+
createdAt: draft.createdAt,
|
|
249
|
+
now: this.clock.now()
|
|
250
|
+
})
|
|
251
|
+
);
|
|
252
|
+
return ok(void 0);
|
|
253
|
+
}
|
|
254
|
+
findOrCreateProject(session, name) {
|
|
255
|
+
const existing = session.projects.findByName(name);
|
|
256
|
+
if (existing !== null) return { project: existing, created: false };
|
|
257
|
+
const now = this.clock.now();
|
|
258
|
+
const project = { id: this.idGen.next(), name, createdAt: now, updatedAt: now };
|
|
259
|
+
session.projects.save(project);
|
|
260
|
+
return { project, created: true };
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// src/context/application/use-cases/list-projects.use-case.ts
|
|
265
|
+
var ListProjects = class {
|
|
266
|
+
constructor(sessions) {
|
|
267
|
+
this.sessions = sessions;
|
|
268
|
+
}
|
|
269
|
+
sessions;
|
|
270
|
+
execute() {
|
|
271
|
+
return this.sessions.withSession((session) => {
|
|
272
|
+
const entries = session.projects.list().map((summary) => ({
|
|
273
|
+
name: summary.project.name,
|
|
274
|
+
...summary.project.description === void 0 ? {} : { description: summary.project.description },
|
|
275
|
+
itemCount: summary.itemCount,
|
|
276
|
+
lastActivityAt: summary.lastActivityAt?.toISOString() ?? null
|
|
277
|
+
}));
|
|
278
|
+
return ok(entries);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// src/context/domain/values/item-type.ts
|
|
284
|
+
var ITEM_TYPES = ["decision", "progress", "preference", "fact", "handoff"];
|
|
285
|
+
function parseItemType(raw) {
|
|
286
|
+
if (ITEM_TYPES.includes(raw)) {
|
|
287
|
+
return ok(raw);
|
|
288
|
+
}
|
|
289
|
+
return contextErr(
|
|
290
|
+
"INVALID_ITEM_TYPE",
|
|
291
|
+
`Item type must be one of ${ITEM_TYPES.join(", ")}. Got: "${raw}"`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
var STORABLE_ITEM_TYPES = [...ITEM_TYPES, "imported"];
|
|
295
|
+
function parseStorableItemType(raw) {
|
|
296
|
+
if (STORABLE_ITEM_TYPES.includes(raw)) {
|
|
297
|
+
return ok(raw);
|
|
298
|
+
}
|
|
299
|
+
return contextErr(
|
|
300
|
+
"INVALID_ITEM_TYPE",
|
|
301
|
+
`Item type must be one of ${STORABLE_ITEM_TYPES.join(", ")}. Got: "${raw}"`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/context/application/use-cases/save-context.use-case.ts
|
|
253
306
|
var SaveContext = class {
|
|
254
307
|
constructor(sessions, clock, idGen) {
|
|
255
308
|
this.sessions = sessions;
|
|
@@ -260,129 +313,134 @@ var SaveContext = class {
|
|
|
260
313
|
clock;
|
|
261
314
|
idGen;
|
|
262
315
|
execute(input) {
|
|
316
|
+
const validated = this.parseInput(input);
|
|
317
|
+
if (!validated.ok) return validated;
|
|
318
|
+
return this.sessions.withSession((session) => {
|
|
319
|
+
const { project, created } = this.findOrCreateProject(session, validated.value.name);
|
|
320
|
+
const item = createContextItem({
|
|
321
|
+
id: this.idGen.next(),
|
|
322
|
+
projectId: project.id,
|
|
323
|
+
type: validated.value.type,
|
|
324
|
+
content: validated.value.content,
|
|
325
|
+
tags: validated.value.tags,
|
|
326
|
+
pinned: validated.value.pinned,
|
|
327
|
+
...validated.value.source === void 0 ? {} : { source: validated.value.source },
|
|
328
|
+
now: this.clock.now()
|
|
329
|
+
});
|
|
330
|
+
session.items.save(item);
|
|
331
|
+
return ok({
|
|
332
|
+
itemId: item.id,
|
|
333
|
+
project: validated.value.name,
|
|
334
|
+
type: validated.value.type,
|
|
335
|
+
projectCreated: created
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/** Everything is parsed before a session is opened: bad input never touches the vault. */
|
|
340
|
+
parseInput(input) {
|
|
263
341
|
const name = parseProjectName(input.project);
|
|
264
342
|
if (!name.ok) return name;
|
|
265
343
|
const type = parseItemType(input.type ?? "fact");
|
|
266
344
|
if (!type.ok) return type;
|
|
267
345
|
const tags = parseTags(input.tags ?? []);
|
|
268
346
|
if (!tags.ok) return tags;
|
|
269
|
-
const content =
|
|
347
|
+
const content = parseContent(input.content);
|
|
270
348
|
if (!content.ok) return content;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
session.value.items.save({
|
|
288
|
-
id: itemId,
|
|
289
|
-
projectId: project.id,
|
|
290
|
-
type: type.value,
|
|
291
|
-
content: content.value,
|
|
292
|
-
tags: tags.value,
|
|
293
|
-
pinned: input.pinned ?? false,
|
|
294
|
-
...input.source === void 0 ? {} : { source: input.source },
|
|
295
|
-
archived: false,
|
|
296
|
-
createdAt: now,
|
|
297
|
-
updatedAt: now
|
|
298
|
-
});
|
|
299
|
-
return ok({ itemId, project: name.value, type: type.value, projectCreated });
|
|
300
|
-
} finally {
|
|
301
|
-
session.value.close();
|
|
302
|
-
}
|
|
349
|
+
return ok({
|
|
350
|
+
name: name.value,
|
|
351
|
+
type: type.value,
|
|
352
|
+
tags: tags.value,
|
|
353
|
+
content: content.value,
|
|
354
|
+
pinned: input.pinned ?? false,
|
|
355
|
+
...input.source === void 0 ? {} : { source: input.source }
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
findOrCreateProject(session, name) {
|
|
359
|
+
const existing = session.projects.findByName(name);
|
|
360
|
+
if (existing !== null) return { project: existing, created: false };
|
|
361
|
+
const now = this.clock.now();
|
|
362
|
+
const project = { id: this.idGen.next(), name, createdAt: now, updatedAt: now };
|
|
363
|
+
session.projects.save(project);
|
|
364
|
+
return { project, created: true };
|
|
303
365
|
}
|
|
304
366
|
};
|
|
305
367
|
|
|
306
|
-
// src/context/application/
|
|
368
|
+
// src/context/application/dto/context-item-view.ts
|
|
369
|
+
var toContextItemView = (item, projectName) => ({
|
|
370
|
+
id: item.id,
|
|
371
|
+
project: projectName,
|
|
372
|
+
type: item.type,
|
|
373
|
+
content: item.content,
|
|
374
|
+
tags: [...item.tags],
|
|
375
|
+
pinned: item.pinned,
|
|
376
|
+
...item.source === void 0 ? {} : { source: item.source },
|
|
377
|
+
createdAt: item.createdAt.toISOString()
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// src/context/application/use-cases/search-context.use-case.ts
|
|
381
|
+
var MAX_LIMIT = 100;
|
|
382
|
+
var DEFAULT_LIMIT = 20;
|
|
307
383
|
var SearchContext = class {
|
|
308
384
|
constructor(sessions) {
|
|
309
385
|
this.sessions = sessions;
|
|
310
386
|
}
|
|
311
387
|
sessions;
|
|
312
388
|
execute(input) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const name = parseProjectName(input.project);
|
|
319
|
-
if (!name.ok) return name;
|
|
320
|
-
const project = session.value.projects.findByName(name.value);
|
|
321
|
-
if (project === null) {
|
|
322
|
-
return contextErr("PROJECT_NOT_FOUND", `No project named "${name.value}".`);
|
|
323
|
-
}
|
|
324
|
-
projectId = project.id;
|
|
325
|
-
}
|
|
326
|
-
const limit = Math.min(Math.max(input.limit ?? 20, 1), 100);
|
|
327
|
-
const hits = session.value.items.search(input.query, projectId, limit).map((result) => ({
|
|
328
|
-
id: result.item.id,
|
|
329
|
-
project: result.projectName,
|
|
330
|
-
type: result.item.type,
|
|
331
|
-
content: result.item.content,
|
|
332
|
-
tags: [...result.item.tags],
|
|
333
|
-
pinned: result.item.pinned,
|
|
334
|
-
createdAt: result.item.createdAt.toISOString()
|
|
335
|
-
}));
|
|
389
|
+
return this.sessions.withSession((session) => {
|
|
390
|
+
const scope = this.resolveProjectScope(session, input.project);
|
|
391
|
+
if (!scope.ok) return scope;
|
|
392
|
+
const limit = Math.min(Math.max(input.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
|
|
393
|
+
const hits = session.items.search(input.query, scope.value, limit).map((result) => toContextItemView(result.item, result.projectName));
|
|
336
394
|
return ok(hits);
|
|
337
|
-
}
|
|
338
|
-
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
/** No project given → search everywhere; otherwise the project must exist. */
|
|
398
|
+
resolveProjectScope(session, project) {
|
|
399
|
+
if (project === void 0) return ok(void 0);
|
|
400
|
+
const name = parseProjectName(project);
|
|
401
|
+
if (!name.ok) return name;
|
|
402
|
+
const found = session.projects.findByName(name.value);
|
|
403
|
+
if (found === null) {
|
|
404
|
+
return contextErr("PROJECT_NOT_FOUND", `No project named "${name.value}".`);
|
|
339
405
|
}
|
|
406
|
+
return ok(found.id);
|
|
340
407
|
}
|
|
341
408
|
};
|
|
342
409
|
|
|
343
|
-
// src/context/application/show-project.ts
|
|
410
|
+
// src/context/application/use-cases/show-project.use-case.ts
|
|
344
411
|
var ShowProject = class {
|
|
345
412
|
constructor(sessions) {
|
|
346
413
|
this.sessions = sessions;
|
|
347
414
|
}
|
|
348
415
|
sessions;
|
|
349
|
-
execute(
|
|
350
|
-
const
|
|
351
|
-
if (!
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const parsed = parseItemType(type);
|
|
355
|
-
if (!parsed.ok) return parsed;
|
|
356
|
-
typeValue = parsed.value;
|
|
357
|
-
}
|
|
358
|
-
const session = this.sessions.open();
|
|
359
|
-
if (!session.ok) return session;
|
|
360
|
-
try {
|
|
361
|
-
const found = session.value.projects.findByName(name.value);
|
|
416
|
+
execute(input) {
|
|
417
|
+
const filters = this.parseFilters(input);
|
|
418
|
+
if (!filters.ok) return filters;
|
|
419
|
+
return this.sessions.withSession((session) => {
|
|
420
|
+
const found = session.projects.findByName(filters.value.name);
|
|
362
421
|
if (found === null) {
|
|
363
|
-
return contextErr("PROJECT_NOT_FOUND", `No project named "${
|
|
422
|
+
return contextErr("PROJECT_NOT_FOUND", `No project named "${filters.value.name}".`);
|
|
364
423
|
}
|
|
365
|
-
const items = session.
|
|
424
|
+
const items = session.items.findByProject(
|
|
366
425
|
found.id,
|
|
367
|
-
|
|
368
|
-
);
|
|
369
|
-
return ok(
|
|
370
|
-
items.map((item) => ({
|
|
371
|
-
id: item.id,
|
|
372
|
-
project: name.value,
|
|
373
|
-
type: item.type,
|
|
374
|
-
content: item.content,
|
|
375
|
-
tags: [...item.tags],
|
|
376
|
-
pinned: item.pinned,
|
|
377
|
-
createdAt: item.createdAt.toISOString()
|
|
378
|
-
}))
|
|
426
|
+
filters.value.type === void 0 ? {} : { type: filters.value.type }
|
|
379
427
|
);
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
428
|
+
return ok(items.map((item) => toContextItemView(item, filters.value.name)));
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
parseFilters(input) {
|
|
432
|
+
const name = parseProjectName(input.project);
|
|
433
|
+
if (!name.ok) return name;
|
|
434
|
+
if (input.type === void 0) return ok({ name: name.value });
|
|
435
|
+
const parsed = parseStorableItemType(input.type);
|
|
436
|
+
if (!parsed.ok) return parsed;
|
|
437
|
+
return ok({ name: name.value, type: parsed.value });
|
|
383
438
|
}
|
|
384
439
|
};
|
|
385
440
|
|
|
441
|
+
// src/shared/infra/migrations.ts
|
|
442
|
+
import { copyFileSync, rmSync } from "fs";
|
|
443
|
+
|
|
386
444
|
// src/shared/infra/migrations/001-init.ts
|
|
387
445
|
var MIGRATION_001 = `
|
|
388
446
|
CREATE TABLE projects (
|
|
@@ -433,24 +491,92 @@ CREATE TRIGGER items_au AFTER UPDATE ON context_items BEGIN
|
|
|
433
491
|
END;
|
|
434
492
|
`;
|
|
435
493
|
|
|
494
|
+
// src/shared/infra/migrations/002-imported-type.ts
|
|
495
|
+
var MIGRATION_002 = `
|
|
496
|
+
DROP TRIGGER items_ai;
|
|
497
|
+
DROP TRIGGER items_ad;
|
|
498
|
+
DROP TRIGGER items_au;
|
|
499
|
+
|
|
500
|
+
CREATE TABLE context_items_new (
|
|
501
|
+
id TEXT PRIMARY KEY,
|
|
502
|
+
project_id TEXT NOT NULL REFERENCES projects(id),
|
|
503
|
+
type TEXT NOT NULL CHECK (type IN ('decision','progress','preference','fact','handoff','imported')),
|
|
504
|
+
content TEXT NOT NULL,
|
|
505
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
506
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
507
|
+
source TEXT,
|
|
508
|
+
archived INTEGER NOT NULL DEFAULT 0,
|
|
509
|
+
created_at TEXT NOT NULL,
|
|
510
|
+
updated_at TEXT NOT NULL
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
INSERT INTO context_items_new
|
|
514
|
+
(id, project_id, type, content, tags, pinned, source, archived, created_at, updated_at)
|
|
515
|
+
SELECT
|
|
516
|
+
id, project_id, type, content, tags, pinned, source, archived, created_at, updated_at
|
|
517
|
+
FROM context_items;
|
|
518
|
+
|
|
519
|
+
DROP TABLE context_items;
|
|
520
|
+
ALTER TABLE context_items_new RENAME TO context_items;
|
|
521
|
+
|
|
522
|
+
CREATE INDEX idx_items_project ON context_items(project_id, created_at DESC);
|
|
523
|
+
|
|
524
|
+
CREATE TRIGGER items_ai AFTER INSERT ON context_items BEGIN
|
|
525
|
+
INSERT INTO context_items_fts(rowid, content, tags)
|
|
526
|
+
VALUES (new.rowid, new.content, new.tags);
|
|
527
|
+
END;
|
|
528
|
+
|
|
529
|
+
CREATE TRIGGER items_ad AFTER DELETE ON context_items BEGIN
|
|
530
|
+
INSERT INTO context_items_fts(context_items_fts, rowid, content, tags)
|
|
531
|
+
VALUES ('delete', old.rowid, old.content, old.tags);
|
|
532
|
+
END;
|
|
533
|
+
|
|
534
|
+
CREATE TRIGGER items_au AFTER UPDATE ON context_items BEGIN
|
|
535
|
+
INSERT INTO context_items_fts(context_items_fts, rowid, content, tags)
|
|
536
|
+
VALUES ('delete', old.rowid, old.content, old.tags);
|
|
537
|
+
INSERT INTO context_items_fts(rowid, content, tags)
|
|
538
|
+
VALUES (new.rowid, new.content, new.tags);
|
|
539
|
+
END;
|
|
540
|
+
|
|
541
|
+
INSERT INTO context_items_fts(context_items_fts) VALUES('rebuild');
|
|
542
|
+
`;
|
|
543
|
+
|
|
436
544
|
// src/shared/infra/migrations.ts
|
|
437
545
|
var MIGRATIONS = [
|
|
438
|
-
{ version: 1, sql: MIGRATION_001 }
|
|
546
|
+
{ version: 1, sql: MIGRATION_001 },
|
|
547
|
+
{ version: 2, sql: MIGRATION_002, backup: true }
|
|
439
548
|
];
|
|
440
|
-
function migrate(db) {
|
|
549
|
+
function migrate(db, dbPath) {
|
|
441
550
|
db.exec("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
|
|
442
|
-
const
|
|
443
|
-
const current = row ? Number(row.value) : 0;
|
|
551
|
+
const current = schemaVersion(db);
|
|
444
552
|
for (const migration of MIGRATIONS) {
|
|
445
553
|
if (migration.version <= current) continue;
|
|
446
|
-
db
|
|
447
|
-
db.exec(migration.sql);
|
|
448
|
-
db.prepare(
|
|
449
|
-
"INSERT INTO meta (key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
|
|
450
|
-
).run(String(migration.version));
|
|
451
|
-
})();
|
|
554
|
+
applyMigration(db, migration, current, dbPath);
|
|
452
555
|
}
|
|
453
556
|
}
|
|
557
|
+
function applyMigration(db, migration, current, dbPath) {
|
|
558
|
+
const backupPath = migration.backup === true && current >= 1 && dbPath !== void 0 ? backupCiphertext(db, dbPath, migration.version) : null;
|
|
559
|
+
db.transaction(() => {
|
|
560
|
+
db.exec(migration.sql);
|
|
561
|
+
setSchemaVersion(db, migration.version);
|
|
562
|
+
})();
|
|
563
|
+
if (backupPath !== null) rmSync(backupPath, { force: true });
|
|
564
|
+
}
|
|
565
|
+
function backupCiphertext(db, dbPath, version) {
|
|
566
|
+
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
567
|
+
const backupPath = `${dbPath}.pre-${String(version).padStart(3, "0")}.bak`;
|
|
568
|
+
copyFileSync(dbPath, backupPath);
|
|
569
|
+
return backupPath;
|
|
570
|
+
}
|
|
571
|
+
function setSchemaVersion(db, version) {
|
|
572
|
+
db.prepare(
|
|
573
|
+
"INSERT INTO meta (key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
|
|
574
|
+
).run(String(version));
|
|
575
|
+
}
|
|
576
|
+
function schemaVersion(db) {
|
|
577
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
578
|
+
return row ? Number(row.value) : 0;
|
|
579
|
+
}
|
|
454
580
|
|
|
455
581
|
// src/shared/infra/sqlite.ts
|
|
456
582
|
import { mkdirSync } from "fs";
|
|
@@ -672,23 +798,44 @@ var SqliteProjectRepository = class {
|
|
|
672
798
|
}
|
|
673
799
|
};
|
|
674
800
|
|
|
675
|
-
// src/context/infra/
|
|
801
|
+
// src/context/infra/vault-sessions.ts
|
|
676
802
|
var LOCKED_MESSAGE = 'Vault is locked. Ask the user to run "valija unlock" in a terminal.';
|
|
677
|
-
|
|
803
|
+
function runWithSession(open, action) {
|
|
804
|
+
const session = open();
|
|
805
|
+
if (!session.ok) return session;
|
|
806
|
+
try {
|
|
807
|
+
return action(session.value);
|
|
808
|
+
} finally {
|
|
809
|
+
session.value.close();
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
var SqliteVaultSessions = class {
|
|
678
813
|
constructor(paths, keychain) {
|
|
679
814
|
this.paths = paths;
|
|
680
815
|
this.keychain = keychain;
|
|
681
816
|
}
|
|
682
817
|
paths;
|
|
683
818
|
keychain;
|
|
819
|
+
withSession(action) {
|
|
820
|
+
return runWithSession(() => this.open(), action);
|
|
821
|
+
}
|
|
684
822
|
open() {
|
|
685
823
|
const header = readVaultHeader(this.paths.header);
|
|
686
824
|
if (!header.ok) return header;
|
|
687
|
-
const keyHex = this.
|
|
825
|
+
const keyHex = this.requireKey(header.value.vaultId);
|
|
826
|
+
if (!keyHex.ok) return keyHex;
|
|
827
|
+
return this.openRepositories(header.value.vaultId, keyHex.value);
|
|
828
|
+
}
|
|
829
|
+
/** The session key must be present in the OS keychain — otherwise the vault is locked. */
|
|
830
|
+
requireKey(vaultId) {
|
|
831
|
+
const keyHex = this.keychain.getKey(vaultId);
|
|
688
832
|
if (keyHex === null) return vaultErr("VAULT_LOCKED", LOCKED_MESSAGE);
|
|
833
|
+
return ok(keyHex);
|
|
834
|
+
}
|
|
835
|
+
openRepositories(vaultId, keyHex) {
|
|
689
836
|
try {
|
|
690
837
|
const db = openVaultDb(this.paths.db, keyHex);
|
|
691
|
-
migrate(db);
|
|
838
|
+
migrate(db, this.paths.db);
|
|
692
839
|
return ok({
|
|
693
840
|
projects: new SqliteProjectRepository(db),
|
|
694
841
|
items: new SqliteContextItemRepository(db),
|
|
@@ -696,7 +843,7 @@ var SqliteVaultSessionFactory = class {
|
|
|
696
843
|
});
|
|
697
844
|
} catch (error) {
|
|
698
845
|
if (isWrongKeyError(error)) {
|
|
699
|
-
this.keychain.deleteKey(
|
|
846
|
+
this.keychain.deleteKey(vaultId);
|
|
700
847
|
return vaultErr("VAULT_LOCKED", LOCKED_MESSAGE);
|
|
701
848
|
}
|
|
702
849
|
return vaultErr("STORAGE_ERROR", `Could not open vault: ${error.message}`);
|
|
@@ -704,6 +851,607 @@ var SqliteVaultSessionFactory = class {
|
|
|
704
851
|
}
|
|
705
852
|
};
|
|
706
853
|
|
|
854
|
+
// src/importers/domain/errors.ts
|
|
855
|
+
var importerErr = (code, message) => err(new DomainError(code, message));
|
|
856
|
+
|
|
857
|
+
// src/importers/domain/services/chunk-render.ts
|
|
858
|
+
var DEFAULT_BYTE_BUDGET = 28 * 1024;
|
|
859
|
+
var HEADER_RESERVE = 768;
|
|
860
|
+
var SEPARATOR = "\n\n";
|
|
861
|
+
var MAX_TITLE_CHARS = 120;
|
|
862
|
+
var encoder = new TextEncoder();
|
|
863
|
+
var byteLength = (text) => encoder.encode(text).length;
|
|
864
|
+
var ROLE_LABEL = { user: "User", assistant: "Assistant", system: "System" };
|
|
865
|
+
var SOURCE_LABEL = {
|
|
866
|
+
chatgpt: "ChatGPT",
|
|
867
|
+
claude: "Claude",
|
|
868
|
+
generic: "Generic"
|
|
869
|
+
};
|
|
870
|
+
var renderMessage = (message) => `**${ROLE_LABEL[message.role]}:** ${message.content}`;
|
|
871
|
+
function hardSplit(text, limit) {
|
|
872
|
+
const pieces = [];
|
|
873
|
+
let buffer = "";
|
|
874
|
+
let bufferBytes = 0;
|
|
875
|
+
for (const char of text) {
|
|
876
|
+
const charBytes = byteLength(char);
|
|
877
|
+
if (bufferBytes + charBytes > limit && buffer.length > 0) {
|
|
878
|
+
pieces.push(buffer);
|
|
879
|
+
buffer = "";
|
|
880
|
+
bufferBytes = 0;
|
|
881
|
+
}
|
|
882
|
+
buffer += char;
|
|
883
|
+
bufferBytes += charBytes;
|
|
884
|
+
}
|
|
885
|
+
if (buffer.length > 0) pieces.push(buffer);
|
|
886
|
+
return pieces;
|
|
887
|
+
}
|
|
888
|
+
function packBodies(messages, limit) {
|
|
889
|
+
const bodies = [];
|
|
890
|
+
let current = [];
|
|
891
|
+
let currentBytes = 0;
|
|
892
|
+
const flush = () => {
|
|
893
|
+
if (current.length > 0) {
|
|
894
|
+
bodies.push(current.join(SEPARATOR));
|
|
895
|
+
current = [];
|
|
896
|
+
currentBytes = 0;
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
for (const message of messages) {
|
|
900
|
+
const block = renderMessage(message);
|
|
901
|
+
const blockBytes = byteLength(block);
|
|
902
|
+
if (blockBytes > limit) {
|
|
903
|
+
flush();
|
|
904
|
+
for (const piece of hardSplit(block, limit)) bodies.push(piece);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
const cost = (current.length > 0 ? byteLength(SEPARATOR) : 0) + blockBytes;
|
|
908
|
+
if (currentBytes + cost > limit) flush();
|
|
909
|
+
current.push(block);
|
|
910
|
+
currentBytes += current.length > 1 ? byteLength(SEPARATOR) + blockBytes : blockBytes;
|
|
911
|
+
}
|
|
912
|
+
flush();
|
|
913
|
+
return bodies;
|
|
914
|
+
}
|
|
915
|
+
function provenanceHeader(conversation, source, part, total) {
|
|
916
|
+
const title = conversation.title.length > MAX_TITLE_CHARS ? `${conversation.title.slice(0, MAX_TITLE_CHARS - 3)}...` : conversation.title;
|
|
917
|
+
const date = conversation.createdAt.toISOString().slice(0, 10);
|
|
918
|
+
return `> Imported from ${SOURCE_LABEL[source]} \xB7 "${title}" \xB7 ${date} \xB7 part ${part}/${total}`;
|
|
919
|
+
}
|
|
920
|
+
function renderConversationChunks(conversation, source, byteBudget = DEFAULT_BYTE_BUDGET) {
|
|
921
|
+
const limit = byteBudget - HEADER_RESERVE;
|
|
922
|
+
const bodies = packBodies(conversation.messages, limit);
|
|
923
|
+
const total = bodies.length;
|
|
924
|
+
return bodies.map(
|
|
925
|
+
(body, index) => `${provenanceHeader(conversation, source, index + 1, total)}
|
|
926
|
+
|
|
927
|
+
${body}`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/importers/domain/services/selection.ts
|
|
932
|
+
function parsePickSpec(spec, count) {
|
|
933
|
+
const tokens = spec.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
|
|
934
|
+
if (tokens.length === 0) {
|
|
935
|
+
return importerErr("INVALID_SELECTION", "--pick is empty.");
|
|
936
|
+
}
|
|
937
|
+
const indices = /* @__PURE__ */ new Set();
|
|
938
|
+
for (const token of tokens) {
|
|
939
|
+
const range = token.match(/^(\d+)-(\d+)$/);
|
|
940
|
+
const single = token.match(/^(\d+)$/);
|
|
941
|
+
if (range) {
|
|
942
|
+
const start = Number(range[1]);
|
|
943
|
+
const end = Number(range[2]);
|
|
944
|
+
if (start < 1 || end > count || start > end) {
|
|
945
|
+
return importerErr(
|
|
946
|
+
"INVALID_SELECTION",
|
|
947
|
+
`--pick range "${token}" is out of bounds (valid: 1-${count}).`
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
for (let i = start; i <= end; i++) indices.add(i - 1);
|
|
951
|
+
} else if (single) {
|
|
952
|
+
const index = Number(single[1]);
|
|
953
|
+
if (index < 1 || index > count) {
|
|
954
|
+
return importerErr(
|
|
955
|
+
"INVALID_SELECTION",
|
|
956
|
+
`--pick index "${token}" is out of bounds (valid: 1-${count}).`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
indices.add(index - 1);
|
|
960
|
+
} else {
|
|
961
|
+
return importerErr("INVALID_SELECTION", `--pick token "${token}" is not a number or range.`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return ok([...indices].sort((a, b) => a - b));
|
|
965
|
+
}
|
|
966
|
+
function selectConversations(conversations, filters) {
|
|
967
|
+
let selected = [...conversations];
|
|
968
|
+
if (filters.pick !== void 0) {
|
|
969
|
+
const picked = parsePickSpec(filters.pick, conversations.length);
|
|
970
|
+
if (!picked.ok) return picked;
|
|
971
|
+
selected = picked.value.map((index) => conversations[index]).filter((conversation) => conversation !== void 0);
|
|
972
|
+
}
|
|
973
|
+
if (filters.since !== void 0) {
|
|
974
|
+
const since = new Date(filters.since);
|
|
975
|
+
if (Number.isNaN(since.getTime())) {
|
|
976
|
+
return importerErr(
|
|
977
|
+
"INVALID_SELECTION",
|
|
978
|
+
`--since "${filters.since}" is not a valid date (use YYYY-MM-DD).`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
selected = selected.filter(
|
|
982
|
+
(conversation) => conversation.createdAt.getTime() >= since.getTime()
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
if (filters.query !== void 0) {
|
|
986
|
+
const needle = filters.query.toLowerCase();
|
|
987
|
+
selected = selected.filter((conversation) => conversation.title.toLowerCase().includes(needle));
|
|
988
|
+
}
|
|
989
|
+
if (selected.length === 0) {
|
|
990
|
+
return importerErr("NO_CONVERSATIONS_SELECTED", "No conversations matched the given filters.");
|
|
991
|
+
}
|
|
992
|
+
return ok(selected);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// src/importers/application/use-cases/import-conversations.use-case.ts
|
|
996
|
+
var ImportConversations = class {
|
|
997
|
+
constructor(reader2, registry, importItems, clock) {
|
|
998
|
+
this.reader = reader2;
|
|
999
|
+
this.registry = registry;
|
|
1000
|
+
this.importItems = importItems;
|
|
1001
|
+
this.clock = clock;
|
|
1002
|
+
}
|
|
1003
|
+
reader;
|
|
1004
|
+
registry;
|
|
1005
|
+
importItems;
|
|
1006
|
+
clock;
|
|
1007
|
+
execute(input) {
|
|
1008
|
+
const docs = this.reader.read(input.filePath);
|
|
1009
|
+
if (!docs.ok) return docs;
|
|
1010
|
+
const resolved = this.resolveParser(docs.value, input.from);
|
|
1011
|
+
if (!resolved.ok) return resolved;
|
|
1012
|
+
const parsed = resolved.value.parser.parse(resolved.value.doc);
|
|
1013
|
+
if (!parsed.ok) return parsed;
|
|
1014
|
+
const source = resolved.value.parser.source;
|
|
1015
|
+
const conversations = [...parsed.value.conversations].sort(
|
|
1016
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
1017
|
+
);
|
|
1018
|
+
const parseFailures = parsed.value.failures.map((failure) => ({
|
|
1019
|
+
conversation: failure.title,
|
|
1020
|
+
reason: failure.reason
|
|
1021
|
+
}));
|
|
1022
|
+
const mode = this.resolveMode(input);
|
|
1023
|
+
if (mode === "list") {
|
|
1024
|
+
return ok(
|
|
1025
|
+
this.summary(mode, source, input, parseFailures, {
|
|
1026
|
+
listing: this.toListing(conversations, source)
|
|
1027
|
+
})
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
const selected = selectConversations(conversations, {
|
|
1031
|
+
...input.all === void 0 ? {} : { all: input.all },
|
|
1032
|
+
...input.pick === void 0 ? {} : { pick: input.pick },
|
|
1033
|
+
...input.query === void 0 ? {} : { query: input.query },
|
|
1034
|
+
...input.since === void 0 ? {} : { since: input.since }
|
|
1035
|
+
});
|
|
1036
|
+
if (!selected.ok) return selected;
|
|
1037
|
+
const { items, skipped, contributed } = this.buildItems(selected.value, source);
|
|
1038
|
+
if (mode === "dry-run") {
|
|
1039
|
+
return ok(
|
|
1040
|
+
this.summary(mode, source, input, parseFailures, {
|
|
1041
|
+
imported: items.length,
|
|
1042
|
+
conversations: contributed,
|
|
1043
|
+
skipped
|
|
1044
|
+
})
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
const written = this.importItems.execute({ projectName: input.projectName ?? "", items });
|
|
1048
|
+
if (!written.ok) return written;
|
|
1049
|
+
const importFailures = written.value.failures.map((failure) => ({
|
|
1050
|
+
conversation: failure.conversationId,
|
|
1051
|
+
reason: failure.reason
|
|
1052
|
+
}));
|
|
1053
|
+
return ok(
|
|
1054
|
+
this.summary(mode, source, input, [...parseFailures, ...importFailures], {
|
|
1055
|
+
imported: written.value.imported,
|
|
1056
|
+
conversations: contributed,
|
|
1057
|
+
skipped
|
|
1058
|
+
})
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
/** With `--from`, use that parser and surface its parse error verbatim; otherwise first `detect()` wins. */
|
|
1062
|
+
resolveParser(docs, from) {
|
|
1063
|
+
if (from !== void 0) {
|
|
1064
|
+
const parser = this.registry.forSource(from);
|
|
1065
|
+
const doc = docs.find((candidate) => parser.detect(candidate)) ?? docs[0];
|
|
1066
|
+
if (doc === void 0) return importerErr("EMPTY_EXPORT", "The export was empty.");
|
|
1067
|
+
return ok({ parser, doc });
|
|
1068
|
+
}
|
|
1069
|
+
for (const parser of this.registry.autodetect) {
|
|
1070
|
+
const doc = docs.find((candidate) => parser.detect(candidate));
|
|
1071
|
+
if (doc !== void 0) return ok({ parser, doc });
|
|
1072
|
+
}
|
|
1073
|
+
return importerErr(
|
|
1074
|
+
"UNSUPPORTED_SOURCE",
|
|
1075
|
+
"Could not detect the export format. Pass --from chatgpt, claude, or generic."
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
/** No selection flag (or an explicit --list) lists; otherwise --dry-run previews, else import. */
|
|
1079
|
+
resolveMode(input) {
|
|
1080
|
+
const hasSelection = input.all === true || input.pick !== void 0 || input.query !== void 0 || input.since !== void 0;
|
|
1081
|
+
if (input.list === true || !hasSelection) return "list";
|
|
1082
|
+
return input.dryRun === true ? "dry-run" : "import";
|
|
1083
|
+
}
|
|
1084
|
+
toListing(conversations, source) {
|
|
1085
|
+
return conversations.map((conversation, index) => ({
|
|
1086
|
+
index: index + 1,
|
|
1087
|
+
title: conversation.title,
|
|
1088
|
+
date: conversation.createdAt.toISOString().slice(0, 10),
|
|
1089
|
+
messageCount: conversation.messages.length,
|
|
1090
|
+
estimatedChunks: renderConversationChunks(conversation, source).length
|
|
1091
|
+
}));
|
|
1092
|
+
}
|
|
1093
|
+
buildItems(conversations, source) {
|
|
1094
|
+
const now = this.clock.now();
|
|
1095
|
+
const items = [];
|
|
1096
|
+
let skipped = 0;
|
|
1097
|
+
let contributed = 0;
|
|
1098
|
+
for (const conversation of conversations) {
|
|
1099
|
+
const chunks = renderConversationChunks(conversation, source);
|
|
1100
|
+
if (chunks.length === 0) {
|
|
1101
|
+
skipped += 1;
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
contributed += 1;
|
|
1105
|
+
const createdAt = conversation.createdAt.getTime() === 0 ? now : conversation.createdAt;
|
|
1106
|
+
chunks.forEach((content, chunkIndex) => {
|
|
1107
|
+
items.push({
|
|
1108
|
+
source,
|
|
1109
|
+
conversationId: conversation.id,
|
|
1110
|
+
chunkIndex,
|
|
1111
|
+
content,
|
|
1112
|
+
createdAt,
|
|
1113
|
+
tags: ["imported", source]
|
|
1114
|
+
});
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
return { items, skipped, contributed };
|
|
1118
|
+
}
|
|
1119
|
+
summary(mode, source, input, failures, counts) {
|
|
1120
|
+
return {
|
|
1121
|
+
mode,
|
|
1122
|
+
source,
|
|
1123
|
+
...input.projectName === void 0 ? {} : { project: input.projectName },
|
|
1124
|
+
...counts.listing === void 0 ? {} : { listing: counts.listing },
|
|
1125
|
+
imported: counts.imported ?? 0,
|
|
1126
|
+
conversations: counts.conversations ?? 0,
|
|
1127
|
+
skipped: counts.skipped ?? 0,
|
|
1128
|
+
failed: failures.length,
|
|
1129
|
+
failures
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
// src/importers/infra/file-export-reader.ts
|
|
1135
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1136
|
+
import { unzipSync } from "fflate";
|
|
1137
|
+
var MAX_ENTRY_BYTES = 128 * 1024 * 1024;
|
|
1138
|
+
var MAX_TOTAL_BYTES = 256 * 1024 * 1024;
|
|
1139
|
+
var decoder = new TextDecoder();
|
|
1140
|
+
function parseJsonBytes(bytes) {
|
|
1141
|
+
try {
|
|
1142
|
+
return ok(JSON.parse(decoder.decode(bytes)));
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
return importerErr("MALFORMED_EXPORT", `Invalid JSON: ${error.message}`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
var FileExportReader = class {
|
|
1148
|
+
constructor(maxEntryBytes = MAX_ENTRY_BYTES, maxTotalBytes = MAX_TOTAL_BYTES) {
|
|
1149
|
+
this.maxEntryBytes = maxEntryBytes;
|
|
1150
|
+
this.maxTotalBytes = maxTotalBytes;
|
|
1151
|
+
}
|
|
1152
|
+
maxEntryBytes;
|
|
1153
|
+
maxTotalBytes;
|
|
1154
|
+
read(filePath) {
|
|
1155
|
+
let bytes;
|
|
1156
|
+
try {
|
|
1157
|
+
bytes = readFileSync2(filePath);
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
return importerErr(
|
|
1160
|
+
"UNREADABLE_FILE",
|
|
1161
|
+
`Could not read "${filePath}": ${error.message}`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
return filePath.toLowerCase().endsWith(".zip") ? this.readZip(bytes) : this.readJson(bytes);
|
|
1165
|
+
}
|
|
1166
|
+
readJson(bytes) {
|
|
1167
|
+
const parsed = parseJsonBytes(bytes);
|
|
1168
|
+
if (!parsed.ok) return parsed;
|
|
1169
|
+
return ok([parsed.value]);
|
|
1170
|
+
}
|
|
1171
|
+
readZip(bytes) {
|
|
1172
|
+
let entries;
|
|
1173
|
+
let total = 0;
|
|
1174
|
+
try {
|
|
1175
|
+
entries = unzipSync(bytes, {
|
|
1176
|
+
filter: (file) => {
|
|
1177
|
+
if (!file.name.toLowerCase().endsWith(".json")) return false;
|
|
1178
|
+
if (file.originalSize > this.maxEntryBytes) {
|
|
1179
|
+
throw new Error(`entry "${file.name}" exceeds the ${this.maxEntryBytes}-byte cap`);
|
|
1180
|
+
}
|
|
1181
|
+
total += file.originalSize;
|
|
1182
|
+
if (total > this.maxTotalBytes) {
|
|
1183
|
+
throw new Error(`archive exceeds the ${this.maxTotalBytes}-byte total cap`);
|
|
1184
|
+
}
|
|
1185
|
+
return true;
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
return importerErr(
|
|
1190
|
+
"CORRUPT_ARCHIVE",
|
|
1191
|
+
`Could not read the archive: ${error.message}`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
const docs = [];
|
|
1195
|
+
for (const entryBytes of Object.values(entries)) {
|
|
1196
|
+
const parsed = parseJsonBytes(entryBytes);
|
|
1197
|
+
if (!parsed.ok) return parsed;
|
|
1198
|
+
docs.push(parsed.value);
|
|
1199
|
+
}
|
|
1200
|
+
if (docs.length === 0) {
|
|
1201
|
+
return importerErr("EMPTY_EXPORT", "The archive contained no .json files.");
|
|
1202
|
+
}
|
|
1203
|
+
return ok(docs);
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
// src/importers/infra/parsers/chatgpt-parser.ts
|
|
1208
|
+
import { z as z2 } from "zod";
|
|
1209
|
+
var ChatgptMessage = z2.object({
|
|
1210
|
+
author: z2.object({ role: z2.string().optional() }).nullish(),
|
|
1211
|
+
content: z2.object({ parts: z2.array(z2.unknown()).optional() }).nullish(),
|
|
1212
|
+
create_time: z2.number().nullish()
|
|
1213
|
+
});
|
|
1214
|
+
var ChatgptConversation = z2.object({
|
|
1215
|
+
id: z2.string().optional(),
|
|
1216
|
+
conversation_id: z2.string().optional(),
|
|
1217
|
+
title: z2.string().optional(),
|
|
1218
|
+
create_time: z2.number().nullish(),
|
|
1219
|
+
mapping: z2.record(z2.string(), z2.object({ message: ChatgptMessage.nullish() }))
|
|
1220
|
+
});
|
|
1221
|
+
var HasMapping = z2.object({ mapping: z2.record(z2.string(), z2.unknown()) });
|
|
1222
|
+
function toArray(doc) {
|
|
1223
|
+
if (Array.isArray(doc)) return doc;
|
|
1224
|
+
if (doc !== null && typeof doc === "object" && "conversations" in doc) {
|
|
1225
|
+
const inner = doc.conversations;
|
|
1226
|
+
return Array.isArray(inner) ? inner : null;
|
|
1227
|
+
}
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
function mapRole(role) {
|
|
1231
|
+
return role === "user" || role === "assistant" || role === "system" ? role : null;
|
|
1232
|
+
}
|
|
1233
|
+
function joinParts(parts) {
|
|
1234
|
+
if (!parts) return "";
|
|
1235
|
+
return parts.filter((part) => typeof part === "string").join("\n");
|
|
1236
|
+
}
|
|
1237
|
+
function linearize(data, index) {
|
|
1238
|
+
const timed = [];
|
|
1239
|
+
for (const node of Object.values(data.mapping)) {
|
|
1240
|
+
const raw = node.message;
|
|
1241
|
+
const role = mapRole(raw?.author?.role);
|
|
1242
|
+
if (raw === null || raw === void 0 || role === null) continue;
|
|
1243
|
+
const text = joinParts(raw.content?.parts);
|
|
1244
|
+
if (text.trim().length === 0) continue;
|
|
1245
|
+
const time = typeof raw.create_time === "number" ? raw.create_time : 0;
|
|
1246
|
+
const message = {
|
|
1247
|
+
role,
|
|
1248
|
+
content: text,
|
|
1249
|
+
...typeof raw.create_time === "number" ? { createdAt: new Date(raw.create_time * 1e3) } : {}
|
|
1250
|
+
};
|
|
1251
|
+
timed.push({ message, time });
|
|
1252
|
+
}
|
|
1253
|
+
timed.sort((a, b) => a.time - b.time);
|
|
1254
|
+
const firstTime = timed[0]?.time;
|
|
1255
|
+
const convTime = data.create_time ?? firstTime;
|
|
1256
|
+
return {
|
|
1257
|
+
id: data.id ?? data.conversation_id ?? `chatgpt-${index}`,
|
|
1258
|
+
title: data.title ?? "Untitled",
|
|
1259
|
+
createdAt: typeof convTime === "number" ? new Date(convTime * 1e3) : /* @__PURE__ */ new Date(0),
|
|
1260
|
+
messages: timed.map((entry) => entry.message)
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
var ChatgptParser = class {
|
|
1264
|
+
source = "chatgpt";
|
|
1265
|
+
detect(doc) {
|
|
1266
|
+
const arr = toArray(doc);
|
|
1267
|
+
const first = arr?.[0];
|
|
1268
|
+
return first !== void 0 && HasMapping.safeParse(first).success;
|
|
1269
|
+
}
|
|
1270
|
+
parse(doc) {
|
|
1271
|
+
const arr = toArray(doc);
|
|
1272
|
+
if (arr === null) {
|
|
1273
|
+
return importerErr("MALFORMED_EXPORT", "Expected an array of ChatGPT conversations.");
|
|
1274
|
+
}
|
|
1275
|
+
const conversations = [];
|
|
1276
|
+
const failures = [];
|
|
1277
|
+
for (const [index, raw] of arr.entries()) {
|
|
1278
|
+
const parsed = ChatgptConversation.safeParse(raw);
|
|
1279
|
+
if (!parsed.success) {
|
|
1280
|
+
failures.push({
|
|
1281
|
+
title: `Conversation ${index + 1}`,
|
|
1282
|
+
reason: parsed.error.issues[0]?.message ?? "invalid conversation shape"
|
|
1283
|
+
});
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
conversations.push(linearize(parsed.data, index));
|
|
1287
|
+
}
|
|
1288
|
+
if (conversations.length === 0 && failures.length === 0) {
|
|
1289
|
+
return importerErr("EMPTY_EXPORT", "No conversations found in the ChatGPT export.");
|
|
1290
|
+
}
|
|
1291
|
+
return ok({ conversations, failures });
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
// src/importers/infra/parsers/claude-parser.ts
|
|
1296
|
+
import { z as z3 } from "zod";
|
|
1297
|
+
var ClaudeMessage = z3.object({
|
|
1298
|
+
sender: z3.string().optional(),
|
|
1299
|
+
text: z3.string().optional(),
|
|
1300
|
+
content: z3.array(z3.object({ type: z3.string().optional(), text: z3.string().optional() })).optional(),
|
|
1301
|
+
created_at: z3.string().optional()
|
|
1302
|
+
});
|
|
1303
|
+
var ClaudeConversation = z3.object({
|
|
1304
|
+
uuid: z3.string().optional(),
|
|
1305
|
+
name: z3.string().optional(),
|
|
1306
|
+
created_at: z3.string().optional(),
|
|
1307
|
+
chat_messages: z3.array(ClaudeMessage)
|
|
1308
|
+
});
|
|
1309
|
+
var HasChatMessages = z3.object({ chat_messages: z3.array(z3.unknown()) });
|
|
1310
|
+
function parseDate(iso) {
|
|
1311
|
+
if (iso === void 0) return null;
|
|
1312
|
+
const date = new Date(iso);
|
|
1313
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
1314
|
+
}
|
|
1315
|
+
function messageText(message) {
|
|
1316
|
+
if (message.text !== void 0 && message.text.trim().length > 0) return message.text;
|
|
1317
|
+
if (message.content === void 0) return "";
|
|
1318
|
+
return message.content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
1319
|
+
}
|
|
1320
|
+
var ClaudeParser = class {
|
|
1321
|
+
source = "claude";
|
|
1322
|
+
detect(doc) {
|
|
1323
|
+
const first = Array.isArray(doc) ? doc[0] : void 0;
|
|
1324
|
+
return first !== void 0 && HasChatMessages.safeParse(first).success;
|
|
1325
|
+
}
|
|
1326
|
+
parse(doc) {
|
|
1327
|
+
if (!Array.isArray(doc)) {
|
|
1328
|
+
return importerErr("MALFORMED_EXPORT", "Expected an array of Claude conversations.");
|
|
1329
|
+
}
|
|
1330
|
+
const conversations = [];
|
|
1331
|
+
const failures = [];
|
|
1332
|
+
for (const [index, raw] of doc.entries()) {
|
|
1333
|
+
const parsed = ClaudeConversation.safeParse(raw);
|
|
1334
|
+
if (!parsed.success) {
|
|
1335
|
+
failures.push({
|
|
1336
|
+
title: `Conversation ${index + 1}`,
|
|
1337
|
+
reason: parsed.error.issues[0]?.message ?? "invalid conversation shape"
|
|
1338
|
+
});
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
const messages = [];
|
|
1342
|
+
for (const rawMessage of parsed.data.chat_messages) {
|
|
1343
|
+
const role = rawMessage.sender === "human" ? "user" : "assistant";
|
|
1344
|
+
const text = messageText(rawMessage);
|
|
1345
|
+
if (text.trim().length === 0) continue;
|
|
1346
|
+
const at = parseDate(rawMessage.created_at);
|
|
1347
|
+
messages.push({ role, content: text, ...at === null ? {} : { createdAt: at } });
|
|
1348
|
+
}
|
|
1349
|
+
const firstAt = messages[0]?.createdAt;
|
|
1350
|
+
const convAt = parseDate(parsed.data.created_at) ?? firstAt ?? /* @__PURE__ */ new Date(0);
|
|
1351
|
+
conversations.push({
|
|
1352
|
+
id: parsed.data.uuid ?? `claude-${index}`,
|
|
1353
|
+
title: parsed.data.name ?? "Untitled",
|
|
1354
|
+
createdAt: convAt,
|
|
1355
|
+
messages
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
if (conversations.length === 0 && failures.length === 0) {
|
|
1359
|
+
return importerErr("EMPTY_EXPORT", "No conversations found in the Claude export.");
|
|
1360
|
+
}
|
|
1361
|
+
return ok({ conversations, failures });
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
// src/importers/infra/parsers/generic-parser.ts
|
|
1366
|
+
import { z as z4 } from "zod";
|
|
1367
|
+
var GENERIC_IMPORT_VERSION = 1;
|
|
1368
|
+
var GenericMessage = z4.object({
|
|
1369
|
+
role: z4.enum(["user", "assistant", "system"]),
|
|
1370
|
+
content: z4.string(),
|
|
1371
|
+
createdAt: z4.string().optional()
|
|
1372
|
+
});
|
|
1373
|
+
var GenericConversation = z4.object({
|
|
1374
|
+
id: z4.string(),
|
|
1375
|
+
title: z4.string().optional(),
|
|
1376
|
+
createdAt: z4.string(),
|
|
1377
|
+
messages: z4.array(GenericMessage)
|
|
1378
|
+
});
|
|
1379
|
+
var GenericEnvelope = z4.object({
|
|
1380
|
+
valija_import_version: z4.number(),
|
|
1381
|
+
conversations: z4.array(z4.unknown())
|
|
1382
|
+
});
|
|
1383
|
+
function parseDate2(iso) {
|
|
1384
|
+
if (iso === void 0) return null;
|
|
1385
|
+
const date = new Date(iso);
|
|
1386
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
1387
|
+
}
|
|
1388
|
+
var GenericParser = class {
|
|
1389
|
+
source = "generic";
|
|
1390
|
+
detect(doc) {
|
|
1391
|
+
return doc !== null && typeof doc === "object" && !Array.isArray(doc) && "valija_import_version" in doc;
|
|
1392
|
+
}
|
|
1393
|
+
parse(doc) {
|
|
1394
|
+
const envelope = GenericEnvelope.safeParse(doc);
|
|
1395
|
+
if (!envelope.success) {
|
|
1396
|
+
return importerErr("MALFORMED_EXPORT", "Not a valija generic import envelope.");
|
|
1397
|
+
}
|
|
1398
|
+
if (envelope.data.valija_import_version !== GENERIC_IMPORT_VERSION) {
|
|
1399
|
+
return importerErr(
|
|
1400
|
+
"UNSUPPORTED_GENERIC_VERSION",
|
|
1401
|
+
`Unsupported generic import version ${envelope.data.valija_import_version}; this valija supports version ${GENERIC_IMPORT_VERSION}.`
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
const conversations = [];
|
|
1405
|
+
const failures = [];
|
|
1406
|
+
for (const [index, raw] of envelope.data.conversations.entries()) {
|
|
1407
|
+
const parsed = GenericConversation.safeParse(raw);
|
|
1408
|
+
if (!parsed.success) {
|
|
1409
|
+
failures.push({
|
|
1410
|
+
title: `Conversation ${index + 1}`,
|
|
1411
|
+
reason: parsed.error.issues[0]?.message ?? "invalid conversation shape"
|
|
1412
|
+
});
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
const createdAt = parseDate2(parsed.data.createdAt);
|
|
1416
|
+
if (createdAt === null) {
|
|
1417
|
+
failures.push({
|
|
1418
|
+
title: parsed.data.title ?? parsed.data.id,
|
|
1419
|
+
reason: `Invalid createdAt: "${parsed.data.createdAt}"`
|
|
1420
|
+
});
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
const messages = parsed.data.messages.map((message) => {
|
|
1424
|
+
const at = parseDate2(message.createdAt);
|
|
1425
|
+
return {
|
|
1426
|
+
role: message.role,
|
|
1427
|
+
content: message.content,
|
|
1428
|
+
...at === null ? {} : { createdAt: at }
|
|
1429
|
+
};
|
|
1430
|
+
});
|
|
1431
|
+
conversations.push({
|
|
1432
|
+
id: parsed.data.id,
|
|
1433
|
+
title: parsed.data.title ?? "Untitled",
|
|
1434
|
+
createdAt,
|
|
1435
|
+
messages
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
if (conversations.length === 0 && failures.length === 0) {
|
|
1439
|
+
return importerErr("EMPTY_EXPORT", "No conversations found in the generic export.");
|
|
1440
|
+
}
|
|
1441
|
+
return ok({ conversations, failures });
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
// src/importers/infra/parser-registry.ts
|
|
1446
|
+
var chatgpt = new ChatgptParser();
|
|
1447
|
+
var claude = new ClaudeParser();
|
|
1448
|
+
var generic = new GenericParser();
|
|
1449
|
+
var BY_SOURCE = { chatgpt, claude, generic };
|
|
1450
|
+
var parserRegistry = {
|
|
1451
|
+
autodetect: [chatgpt, claude],
|
|
1452
|
+
forSource: (source) => BY_SOURCE[source]
|
|
1453
|
+
};
|
|
1454
|
+
|
|
707
1455
|
// src/shared/infra/vault-paths.ts
|
|
708
1456
|
import { homedir } from "os";
|
|
709
1457
|
import { join } from "path";
|
|
@@ -716,10 +1464,22 @@ function resolveVaultPaths(rootOverride) {
|
|
|
716
1464
|
};
|
|
717
1465
|
}
|
|
718
1466
|
|
|
719
|
-
// src/vault/
|
|
1467
|
+
// src/vault/domain/values/key-hex.ts
|
|
720
1468
|
var bytesToHex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
721
1469
|
var isKeyHex = (value) => /^[0-9a-f]{64}$/i.test(value);
|
|
722
1470
|
|
|
1471
|
+
// src/vault/domain/values/passphrase.ts
|
|
1472
|
+
var MIN_PASSPHRASE_LENGTH = 8;
|
|
1473
|
+
function parsePassphrase(raw) {
|
|
1474
|
+
if (raw.length < MIN_PASSPHRASE_LENGTH) {
|
|
1475
|
+
return vaultErr(
|
|
1476
|
+
"WEAK_PASSPHRASE",
|
|
1477
|
+
`Passphrase must be at least ${MIN_PASSPHRASE_LENGTH} characters.`
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
return ok(raw);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
723
1483
|
// src/vault/application/ports/crypto.ts
|
|
724
1484
|
var DEFAULT_KDF_PARAMS = {
|
|
725
1485
|
algorithm: "argon2id",
|
|
@@ -728,8 +1488,7 @@ var DEFAULT_KDF_PARAMS = {
|
|
|
728
1488
|
parallelism: 1
|
|
729
1489
|
};
|
|
730
1490
|
|
|
731
|
-
// src/vault/application/create-vault.ts
|
|
732
|
-
var MIN_PASSPHRASE_LENGTH = 8;
|
|
1491
|
+
// src/vault/application/use-cases/create-vault.use-case.ts
|
|
733
1492
|
var CreateVault = class {
|
|
734
1493
|
constructor(store, crypto, keychain, clock, idGen) {
|
|
735
1494
|
this.store = store;
|
|
@@ -747,26 +1506,38 @@ var CreateVault = class {
|
|
|
747
1506
|
if (this.store.headerExists()) {
|
|
748
1507
|
return vaultErr("VAULT_ALREADY_EXISTS", "A vault already exists on this machine.");
|
|
749
1508
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1509
|
+
const parsed = parsePassphrase(passphrase);
|
|
1510
|
+
if (!parsed.ok) return parsed;
|
|
1511
|
+
const vault = await this.forgeVault(parsed.value);
|
|
1512
|
+
this.store.writeHeader(vault.header);
|
|
1513
|
+
const initialized = this.store.initializeDb(vault.keyHex);
|
|
1514
|
+
if (!initialized.ok) return initialized;
|
|
1515
|
+
this.keychain.setKey(vault.header.vaultId, vault.keyHex);
|
|
1516
|
+
return ok({
|
|
1517
|
+
vaultId: vault.header.vaultId,
|
|
1518
|
+
keyHex: vault.keyHex,
|
|
1519
|
+
createdAt: vault.header.createdAt
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
/** Mint the vault identity: id + salt + key derived from the passphrase. */
|
|
1523
|
+
async forgeVault(passphrase) {
|
|
756
1524
|
const vaultId = this.idGen.next();
|
|
757
1525
|
const salt = this.crypto.generateSalt();
|
|
758
1526
|
const key = await this.crypto.deriveKey(passphrase, salt, DEFAULT_KDF_PARAMS);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1527
|
+
return {
|
|
1528
|
+
header: {
|
|
1529
|
+
vaultId,
|
|
1530
|
+
schemaVersion: 1,
|
|
1531
|
+
kdf: DEFAULT_KDF_PARAMS,
|
|
1532
|
+
salt,
|
|
1533
|
+
createdAt: this.clock.now().toISOString()
|
|
1534
|
+
},
|
|
1535
|
+
keyHex: bytesToHex(key)
|
|
1536
|
+
};
|
|
766
1537
|
}
|
|
767
1538
|
};
|
|
768
1539
|
|
|
769
|
-
// src/vault/application/lock-vault.ts
|
|
1540
|
+
// src/vault/application/use-cases/lock-vault.use-case.ts
|
|
770
1541
|
var LockVault = class {
|
|
771
1542
|
constructor(store, keychain) {
|
|
772
1543
|
this.store = store;
|
|
@@ -782,7 +1553,7 @@ var LockVault = class {
|
|
|
782
1553
|
}
|
|
783
1554
|
};
|
|
784
1555
|
|
|
785
|
-
// src/vault/application/unlock-vault.ts
|
|
1556
|
+
// src/vault/application/use-cases/unlock-vault.use-case.ts
|
|
786
1557
|
var UnlockVault = class {
|
|
787
1558
|
constructor(store, crypto, keychain) {
|
|
788
1559
|
this.store = store;
|
|
@@ -795,30 +1566,30 @@ var UnlockVault = class {
|
|
|
795
1566
|
async execute(input) {
|
|
796
1567
|
const header = this.store.readHeader();
|
|
797
1568
|
if (!header.ok) return header;
|
|
798
|
-
|
|
1569
|
+
const keyHex = await this.resolveKey(input, header.value);
|
|
1570
|
+
if (!keyHex.ok) return keyHex;
|
|
1571
|
+
const verified = this.store.verifyKey(keyHex.value);
|
|
1572
|
+
if (!verified.ok) return verified;
|
|
1573
|
+
this.keychain.setKey(header.value.vaultId, keyHex.value);
|
|
1574
|
+
return ok({ vaultId: header.value.vaultId });
|
|
1575
|
+
}
|
|
1576
|
+
/** A recovery key is used as-is; a passphrase is derived with the header's salt + KDF params. */
|
|
1577
|
+
async resolveKey(input, header) {
|
|
799
1578
|
if (input.recoveryKeyHex !== void 0) {
|
|
800
1579
|
if (!isKeyHex(input.recoveryKeyHex)) {
|
|
801
1580
|
return vaultErr("WRONG_PASSPHRASE", "Recovery key must be 64 hex characters.");
|
|
802
1581
|
}
|
|
803
|
-
|
|
804
|
-
} else if (input.passphrase !== void 0) {
|
|
805
|
-
const key = await this.crypto.deriveKey(
|
|
806
|
-
input.passphrase,
|
|
807
|
-
header.value.salt,
|
|
808
|
-
header.value.kdf
|
|
809
|
-
);
|
|
810
|
-
keyHex = bytesToHex(key);
|
|
811
|
-
} else {
|
|
812
|
-
return vaultErr("WRONG_PASSPHRASE", "Provide a passphrase or a recovery key.");
|
|
1582
|
+
return ok(input.recoveryKeyHex.toLowerCase());
|
|
813
1583
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1584
|
+
if (input.passphrase !== void 0) {
|
|
1585
|
+
const key = await this.crypto.deriveKey(input.passphrase, header.salt, header.kdf);
|
|
1586
|
+
return ok(bytesToHex(key));
|
|
1587
|
+
}
|
|
1588
|
+
return vaultErr("WRONG_PASSPHRASE", "Provide a passphrase or a recovery key.");
|
|
818
1589
|
}
|
|
819
1590
|
};
|
|
820
1591
|
|
|
821
|
-
// src/vault/application/vault-status.ts
|
|
1592
|
+
// src/vault/application/use-cases/vault-status.use-case.ts
|
|
822
1593
|
var VaultStatus = class {
|
|
823
1594
|
constructor(store, keychain) {
|
|
824
1595
|
this.store = store;
|
|
@@ -882,7 +1653,7 @@ var FileVaultStore = class {
|
|
|
882
1653
|
initializeDb(keyHex) {
|
|
883
1654
|
try {
|
|
884
1655
|
const db = openVaultDb(this.paths.db, keyHex);
|
|
885
|
-
migrate(db);
|
|
1656
|
+
migrate(db, this.paths.db);
|
|
886
1657
|
db.close();
|
|
887
1658
|
return ok(void 0);
|
|
888
1659
|
} catch (error) {
|
|
@@ -937,8 +1708,8 @@ function buildContainer() {
|
|
|
937
1708
|
const store = new FileVaultStore(paths);
|
|
938
1709
|
const crypto = new Argon2VaultCrypto();
|
|
939
1710
|
const keychain = new OsKeychain();
|
|
940
|
-
const sessions = new
|
|
941
|
-
const
|
|
1711
|
+
const sessions = new SqliteVaultSessions(paths, keychain);
|
|
1712
|
+
const importItems = new ImportItems(sessions, systemClock, ulidIds);
|
|
942
1713
|
return {
|
|
943
1714
|
paths,
|
|
944
1715
|
createVault: new CreateVault(store, crypto, keychain, systemClock, ulidIds),
|
|
@@ -948,19 +1719,64 @@ function buildContainer() {
|
|
|
948
1719
|
saveContext: new SaveContext(sessions, systemClock, ulidIds),
|
|
949
1720
|
listProjects: new ListProjects(sessions),
|
|
950
1721
|
searchContext: new SearchContext(sessions),
|
|
951
|
-
getContextPack,
|
|
952
|
-
|
|
953
|
-
|
|
1722
|
+
getContextPack: new GetContextPack(sessions, systemClock),
|
|
1723
|
+
showProject: new ShowProject(sessions),
|
|
1724
|
+
importConversations: new ImportConversations(
|
|
1725
|
+
new FileExportReader(),
|
|
1726
|
+
parserRegistry,
|
|
1727
|
+
importItems,
|
|
1728
|
+
systemClock
|
|
1729
|
+
)
|
|
954
1730
|
};
|
|
955
1731
|
}
|
|
956
1732
|
|
|
957
1733
|
// src/delivery/mcp/server.ts
|
|
958
1734
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
959
1735
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
960
|
-
import { z as
|
|
1736
|
+
import { z as z5 } from "zod";
|
|
1737
|
+
|
|
1738
|
+
// src/delivery/context-pack-markdown.ts
|
|
1739
|
+
var TYPE_LABELS = {
|
|
1740
|
+
decision: "Decisions",
|
|
1741
|
+
preference: "Preferences",
|
|
1742
|
+
progress: "Progress",
|
|
1743
|
+
fact: "Facts",
|
|
1744
|
+
handoff: "Handoffs"
|
|
1745
|
+
};
|
|
1746
|
+
var sectionTitle = (section) => {
|
|
1747
|
+
switch (section.kind) {
|
|
1748
|
+
case "pinned":
|
|
1749
|
+
return "Pinned";
|
|
1750
|
+
case "latest-handoff":
|
|
1751
|
+
return "Latest handoff";
|
|
1752
|
+
case "by-type":
|
|
1753
|
+
return TYPE_LABELS[section.type];
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
var renderItem = (item) => {
|
|
1757
|
+
const date = item.createdAt.toISOString().slice(0, 10);
|
|
1758
|
+
const tags = item.tags.length > 0 ? ` \xB7 ${item.tags.map((t) => `#${t}`).join(" ")}` : "";
|
|
1759
|
+
return `### ${item.type} \xB7 ${date}${tags}
|
|
1760
|
+
|
|
1761
|
+
${item.content}
|
|
1762
|
+
`;
|
|
1763
|
+
};
|
|
1764
|
+
function renderContextPackMarkdown(pack) {
|
|
1765
|
+
const header = `# Context pack: ${pack.projectName}
|
|
1766
|
+
|
|
1767
|
+
> ${pack.totalCount} items in vault \xB7 generated ${pack.generatedAt.toISOString()}
|
|
1768
|
+
`;
|
|
1769
|
+
const parts = pack.sections.flatMap((section) => [
|
|
1770
|
+
`
|
|
1771
|
+
## ${sectionTitle(section)}
|
|
1772
|
+
`,
|
|
1773
|
+
...section.items.map(renderItem)
|
|
1774
|
+
]);
|
|
1775
|
+
return header + parts.join("\n");
|
|
1776
|
+
}
|
|
961
1777
|
|
|
962
1778
|
// src/delivery/version.ts
|
|
963
|
-
var VERSION = "0.
|
|
1779
|
+
var VERSION = "0.2.0";
|
|
964
1780
|
|
|
965
1781
|
// src/delivery/mcp/server.ts
|
|
966
1782
|
var ok2 = (text) => ({ content: [{ type: "text", text }] });
|
|
@@ -984,10 +1800,10 @@ function buildMcpServer(c) {
|
|
|
984
1800
|
title: "Save context",
|
|
985
1801
|
description: DESCRIPTIONS.save_context,
|
|
986
1802
|
inputSchema: {
|
|
987
|
-
project:
|
|
988
|
-
content:
|
|
989
|
-
type:
|
|
990
|
-
tags:
|
|
1803
|
+
project: z5.string().describe("Project slug, e.g. 'vault-app'. Created if it does not exist."),
|
|
1804
|
+
content: z5.string().describe("Self-contained markdown. Max 32 KB."),
|
|
1805
|
+
type: z5.enum(ITEM_TYPES).optional().describe("Kind of context. Defaults to 'fact'."),
|
|
1806
|
+
tags: z5.array(z5.string()).max(10).optional().describe("Lowercase tags for recall.")
|
|
991
1807
|
}
|
|
992
1808
|
},
|
|
993
1809
|
async ({ project, content, type, tags }) => {
|
|
@@ -1011,8 +1827,8 @@ function buildMcpServer(c) {
|
|
|
1011
1827
|
title: "Save handoff",
|
|
1012
1828
|
description: DESCRIPTIONS.save_handoff,
|
|
1013
1829
|
inputSchema: {
|
|
1014
|
-
project:
|
|
1015
|
-
content:
|
|
1830
|
+
project: z5.string().describe("Project slug. Created if it does not exist."),
|
|
1831
|
+
content: z5.string().describe("Goal, what was done, blockers, exact next step, relevant files or links.")
|
|
1016
1832
|
}
|
|
1017
1833
|
},
|
|
1018
1834
|
async ({ project, content }) => {
|
|
@@ -1033,8 +1849,8 @@ function buildMcpServer(c) {
|
|
|
1033
1849
|
title: "Get context pack",
|
|
1034
1850
|
description: DESCRIPTIONS.get_context,
|
|
1035
1851
|
inputSchema: {
|
|
1036
|
-
project:
|
|
1037
|
-
budget:
|
|
1852
|
+
project: z5.string().describe("Project slug to load."),
|
|
1853
|
+
budget: z5.number().int().positive().optional().describe("Token budget. Default 4000.")
|
|
1038
1854
|
}
|
|
1039
1855
|
},
|
|
1040
1856
|
async ({ project, budget }) => {
|
|
@@ -1043,7 +1859,7 @@ function buildMcpServer(c) {
|
|
|
1043
1859
|
...budget === void 0 ? {} : { budgetTokens: budget }
|
|
1044
1860
|
});
|
|
1045
1861
|
if (!r.ok) return err2(r.error);
|
|
1046
|
-
return ok2(r.value
|
|
1862
|
+
return ok2(renderContextPackMarkdown(r.value));
|
|
1047
1863
|
}
|
|
1048
1864
|
);
|
|
1049
1865
|
server.registerTool(
|
|
@@ -1052,9 +1868,9 @@ function buildMcpServer(c) {
|
|
|
1052
1868
|
title: "Search context",
|
|
1053
1869
|
description: DESCRIPTIONS.search_context,
|
|
1054
1870
|
inputSchema: {
|
|
1055
|
-
query:
|
|
1056
|
-
project:
|
|
1057
|
-
limit:
|
|
1871
|
+
query: z5.string().describe("Full-text query, e.g. 'auth decision'."),
|
|
1872
|
+
project: z5.string().optional().describe("Limit to one project."),
|
|
1873
|
+
limit: z5.number().int().positive().max(100).optional().describe("Max results. Default 20.")
|
|
1058
1874
|
}
|
|
1059
1875
|
},
|
|
1060
1876
|
async ({ query, project, limit }) => {
|
|
@@ -1095,7 +1911,7 @@ ${lines.join("\n")}`);
|
|
|
1095
1911
|
{
|
|
1096
1912
|
title: "Save session context to valija",
|
|
1097
1913
|
description: "Distill this session into a self-contained context save.",
|
|
1098
|
-
argsSchema: { project:
|
|
1914
|
+
argsSchema: { project: z5.string().optional() }
|
|
1099
1915
|
},
|
|
1100
1916
|
({ project }) => ({
|
|
1101
1917
|
messages: [
|
|
@@ -1119,7 +1935,7 @@ ${lines.join("\n")}`);
|
|
|
1119
1935
|
{
|
|
1120
1936
|
title: "Load project context from valija",
|
|
1121
1937
|
description: "Load a project's context pack and resume work.",
|
|
1122
|
-
argsSchema: { project:
|
|
1938
|
+
argsSchema: { project: z5.string() }
|
|
1123
1939
|
},
|
|
1124
1940
|
({ project }) => ({
|
|
1125
1941
|
messages: [
|
|
@@ -1177,7 +1993,10 @@ function projectsCommand(c) {
|
|
|
1177
1993
|
}
|
|
1178
1994
|
}
|
|
1179
1995
|
function showCommand(c, project, options) {
|
|
1180
|
-
const result = c.showProject.execute(
|
|
1996
|
+
const result = c.showProject.execute({
|
|
1997
|
+
project,
|
|
1998
|
+
...options.type === void 0 ? {} : { type: options.type }
|
|
1999
|
+
});
|
|
1181
2000
|
if (!result.ok) fail(result.error);
|
|
1182
2001
|
if (result.value.length === 0) {
|
|
1183
2002
|
console.log("No items.");
|
|
@@ -1207,9 +2026,17 @@ function searchCommand(c, query, options) {
|
|
|
1207
2026
|
);
|
|
1208
2027
|
}
|
|
1209
2028
|
}
|
|
2029
|
+
var exportMarkdown = (c, project) => {
|
|
2030
|
+
const pack = c.getContextPack.execute({ project, budgetTokens: Number.POSITIVE_INFINITY });
|
|
2031
|
+
return pack.ok ? ok(renderContextPackMarkdown(pack.value)) : pack;
|
|
2032
|
+
};
|
|
2033
|
+
var exportJson = (c, project) => {
|
|
2034
|
+
const items = c.showProject.execute({ project });
|
|
2035
|
+
return items.ok ? ok(JSON.stringify({ project, items: items.value }, null, 2)) : items;
|
|
2036
|
+
};
|
|
1210
2037
|
function exportCommand(c, project, options) {
|
|
1211
2038
|
const format = options.json ? "json" : "md";
|
|
1212
|
-
const result =
|
|
2039
|
+
const result = options.json ? exportJson(c, project) : exportMarkdown(c, project);
|
|
1213
2040
|
if (!result.ok) fail(result.error);
|
|
1214
2041
|
if (options.output !== void 0) {
|
|
1215
2042
|
writeFileSync2(options.output, result.value, "utf8");
|
|
@@ -1220,10 +2047,10 @@ function exportCommand(c, project, options) {
|
|
|
1220
2047
|
}
|
|
1221
2048
|
|
|
1222
2049
|
// src/delivery/cli/doctor.ts
|
|
1223
|
-
import { existsSync as existsSync4, readFileSync as
|
|
2050
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
1224
2051
|
|
|
1225
2052
|
// src/delivery/cli/installer.ts
|
|
1226
|
-
import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as
|
|
2053
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1227
2054
|
import { homedir as homedir2 } from "os";
|
|
1228
2055
|
import { dirname as dirname2, join as join2 } from "path";
|
|
1229
2056
|
var CLIENTS = ["claude-code", "claude-desktop", "cursor"];
|
|
@@ -1248,24 +2075,32 @@ function clientConfigPath(client, platform = process.platform) {
|
|
|
1248
2075
|
}
|
|
1249
2076
|
}
|
|
1250
2077
|
}
|
|
1251
|
-
function
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
existing = parsed;
|
|
1262
|
-
backupPath = `${configPath}.backup-${Date.now()}`;
|
|
1263
|
-
copyFileSync(configPath, backupPath);
|
|
1264
|
-
} else {
|
|
2078
|
+
function readExistingConfig(configPath) {
|
|
2079
|
+
if (!existsSync3(configPath)) return {};
|
|
2080
|
+
const parsed = JSON.parse(readFileSync3(configPath, "utf8"));
|
|
2081
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2082
|
+
throw new Error(`${configPath} does not contain a JSON object; not touching it.`);
|
|
2083
|
+
}
|
|
2084
|
+
return parsed;
|
|
2085
|
+
}
|
|
2086
|
+
function backupExisting(configPath) {
|
|
2087
|
+
if (!existsSync3(configPath)) {
|
|
1265
2088
|
mkdirSync3(dirname2(configPath), { recursive: true });
|
|
2089
|
+
return null;
|
|
1266
2090
|
}
|
|
2091
|
+
const backupPath = `${configPath}.backup-${Date.now()}`;
|
|
2092
|
+
copyFileSync2(configPath, backupPath);
|
|
2093
|
+
return backupPath;
|
|
2094
|
+
}
|
|
2095
|
+
function mergeValijaEntry(existing) {
|
|
1267
2096
|
const servers = typeof existing.mcpServers === "object" && existing.mcpServers !== null ? existing.mcpServers : {};
|
|
1268
|
-
|
|
2097
|
+
return { ...existing, mcpServers: { ...servers, valija: MCP_ENTRY } };
|
|
2098
|
+
}
|
|
2099
|
+
function installIntoClient(client) {
|
|
2100
|
+
const configPath = clientConfigPath(client);
|
|
2101
|
+
const existing = readExistingConfig(configPath);
|
|
2102
|
+
const backupPath = backupExisting(configPath);
|
|
2103
|
+
const merged = mergeValijaEntry(existing);
|
|
1269
2104
|
writeFileSync3(configPath, `${JSON.stringify(merged, null, 2)}
|
|
1270
2105
|
`, "utf8");
|
|
1271
2106
|
return { configPath, backupPath };
|
|
@@ -1278,59 +2113,69 @@ function manualInstructions(client) {
|
|
|
1278
2113
|
}
|
|
1279
2114
|
|
|
1280
2115
|
// src/delivery/cli/doctor.ts
|
|
1281
|
-
|
|
1282
|
-
const checks = [];
|
|
2116
|
+
function checkNode() {
|
|
1283
2117
|
const [major] = process.versions.node.split(".").map(Number);
|
|
1284
|
-
|
|
2118
|
+
return {
|
|
1285
2119
|
name: "node",
|
|
1286
2120
|
ok: (major ?? 0) >= 22,
|
|
1287
2121
|
detail: `v${process.versions.node} (need >=22)`,
|
|
1288
2122
|
fatal: true
|
|
1289
|
-
}
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
async function checkSqlcipher() {
|
|
1290
2126
|
try {
|
|
1291
2127
|
const { default: Db } = await import("better-sqlite3-multiple-ciphers");
|
|
1292
2128
|
const db = new Db(":memory:");
|
|
1293
2129
|
db.pragma("cipher='sqlcipher'");
|
|
1294
2130
|
db.close();
|
|
1295
|
-
|
|
2131
|
+
return { name: "sqlcipher", ok: true, detail: "native module loads" };
|
|
1296
2132
|
} catch (e) {
|
|
1297
|
-
|
|
2133
|
+
return { name: "sqlcipher", ok: false, detail: e.message, fatal: true };
|
|
1298
2134
|
}
|
|
2135
|
+
}
|
|
2136
|
+
function checkKeychain() {
|
|
1299
2137
|
try {
|
|
1300
2138
|
const keychain = new OsKeychain();
|
|
1301
2139
|
keychain.setKey("doctor-probe", "test");
|
|
1302
2140
|
const roundtrip = keychain.getKey("doctor-probe") === "test";
|
|
1303
2141
|
keychain.deleteKey("doctor-probe");
|
|
1304
|
-
|
|
2142
|
+
return { name: "keychain", ok: roundtrip, detail: "OS keychain read/write" };
|
|
1305
2143
|
} catch (e) {
|
|
1306
|
-
|
|
2144
|
+
return { name: "keychain", ok: false, detail: e.message };
|
|
1307
2145
|
}
|
|
2146
|
+
}
|
|
2147
|
+
function checkVault(c) {
|
|
1308
2148
|
const status = c.vaultStatus.execute();
|
|
1309
|
-
if (status.ok) {
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
detail = "config exists but is not valid JSON";
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
checks.push({ name: client, ok: installed, detail });
|
|
2149
|
+
if (!status.ok) return { name: "vault", ok: false, detail: status.error.message };
|
|
2150
|
+
return {
|
|
2151
|
+
name: "vault",
|
|
2152
|
+
ok: status.value.initialized,
|
|
2153
|
+
detail: status.value.initialized ? `${status.value.unlocked ? "unlocked" : "locked"} at ${status.value.dbPath}` : 'not initialized \u2014 run "valija init"'
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
function checkClient(client) {
|
|
2157
|
+
const path = clientConfigPath(client);
|
|
2158
|
+
if (!existsSync4(path)) return { name: client, ok: false, detail: "config not found" };
|
|
2159
|
+
try {
|
|
2160
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
2161
|
+
const installed = parsed.mcpServers?.valija !== void 0;
|
|
2162
|
+
return {
|
|
2163
|
+
name: client,
|
|
2164
|
+
ok: installed,
|
|
2165
|
+
detail: installed ? "valija installed" : "config found, valija not installed"
|
|
2166
|
+
};
|
|
2167
|
+
} catch {
|
|
2168
|
+
return { name: client, ok: false, detail: "config exists but is not valid JSON" };
|
|
1333
2169
|
}
|
|
2170
|
+
}
|
|
2171
|
+
async function doctorCommand(c) {
|
|
2172
|
+
const checks = [
|
|
2173
|
+
checkNode(),
|
|
2174
|
+
await checkSqlcipher(),
|
|
2175
|
+
checkKeychain(),
|
|
2176
|
+
checkVault(c),
|
|
2177
|
+
...CLIENTS.map(checkClient)
|
|
2178
|
+
];
|
|
1334
2179
|
let fatal = false;
|
|
1335
2180
|
for (const check of checks) {
|
|
1336
2181
|
console.log(`${check.ok ? "\u2713" : "\u2717"} ${check.name.padEnd(16)} ${check.detail}`);
|
|
@@ -1339,6 +2184,75 @@ async function doctorCommand(c) {
|
|
|
1339
2184
|
if (fatal) process.exit(1);
|
|
1340
2185
|
}
|
|
1341
2186
|
|
|
2187
|
+
// src/importers/domain/values/import-source.ts
|
|
2188
|
+
var IMPORT_SOURCES = ["chatgpt", "claude", "generic"];
|
|
2189
|
+
function parseImportSource(raw) {
|
|
2190
|
+
if (IMPORT_SOURCES.includes(raw)) {
|
|
2191
|
+
return ok(raw);
|
|
2192
|
+
}
|
|
2193
|
+
return importerErr(
|
|
2194
|
+
"UNSUPPORTED_SOURCE",
|
|
2195
|
+
`Source must be one of ${IMPORT_SOURCES.join(", ")}. Got: "${raw}"`
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// src/delivery/cli/import-command.ts
|
|
2200
|
+
function importCommand(c, file, options) {
|
|
2201
|
+
const hasSelection = options.all === true || options.pick !== void 0 || options.query !== void 0 || options.since !== void 0;
|
|
2202
|
+
if (hasSelection && options.project === void 0) {
|
|
2203
|
+
console.error("error: -p <project> is required to import. Run with --list first to preview.");
|
|
2204
|
+
process.exit(1);
|
|
2205
|
+
}
|
|
2206
|
+
const input = {
|
|
2207
|
+
filePath: file,
|
|
2208
|
+
...options.project === void 0 ? {} : { projectName: options.project },
|
|
2209
|
+
...options.from === void 0 ? {} : { from: resolveSource(options.from) },
|
|
2210
|
+
...options.list === void 0 ? {} : { list: options.list },
|
|
2211
|
+
...options.pick === void 0 ? {} : { pick: options.pick },
|
|
2212
|
+
...options.query === void 0 ? {} : { query: options.query },
|
|
2213
|
+
...options.since === void 0 ? {} : { since: options.since },
|
|
2214
|
+
...options.all === void 0 ? {} : { all: options.all },
|
|
2215
|
+
...options.dryRun === void 0 ? {} : { dryRun: options.dryRun }
|
|
2216
|
+
};
|
|
2217
|
+
const result = c.importConversations.execute(input);
|
|
2218
|
+
if (!result.ok) fail(result.error);
|
|
2219
|
+
if (result.value.mode === "list") renderListing(result.value);
|
|
2220
|
+
else renderSummary(result.value);
|
|
2221
|
+
}
|
|
2222
|
+
function resolveSource(raw) {
|
|
2223
|
+
const parsed = parseImportSource(raw);
|
|
2224
|
+
if (!parsed.ok) fail(parsed.error);
|
|
2225
|
+
return parsed.value;
|
|
2226
|
+
}
|
|
2227
|
+
function renderListing(summary) {
|
|
2228
|
+
const rows = summary.listing ?? [];
|
|
2229
|
+
if (rows.length === 0) {
|
|
2230
|
+
console.log("No conversations found in this export.");
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
console.log(`${rows.length} conversation(s) in this ${summary.source} export:
|
|
2234
|
+
`);
|
|
2235
|
+
console.log(" # DATE CHUNKS TITLE");
|
|
2236
|
+
for (const row of rows) {
|
|
2237
|
+
console.log(
|
|
2238
|
+
`${String(row.index).padStart(3)} ${row.date} ${String(row.estimatedChunks).padStart(6)} ${truncate(row.title, 48)}`
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
console.log(
|
|
2242
|
+
"\nSelect with --pick 1,3-5, --query <text>, --since <YYYY-MM-DD>, or --all. Add --dry-run to preview."
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
function renderSummary(summary) {
|
|
2246
|
+
const target = summary.project === void 0 ? "" : ` into "${summary.project}"`;
|
|
2247
|
+
const verb = summary.mode === "dry-run" ? "Would import" : "Imported";
|
|
2248
|
+
console.log(
|
|
2249
|
+
`${verb} ${summary.imported} item(s) from ${summary.conversations} conversation(s)${target} (skipped ${summary.skipped}, failed ${summary.failed}).`
|
|
2250
|
+
);
|
|
2251
|
+
for (const failure of summary.failures) {
|
|
2252
|
+
console.error(` failed: ${failure.conversation} \u2014 ${failure.reason}`);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
|
|
1342
2256
|
// src/vault/infra/recovery-kit.ts
|
|
1343
2257
|
function renderRecoveryKit(vaultId, keyHex, createdAt) {
|
|
1344
2258
|
return `================================================================
|
|
@@ -1490,7 +2404,7 @@ program.command("unlock").description("Unlock the vault for this session (key go
|
|
|
1490
2404
|
program.command("lock").description("Lock the vault (remove the key from the OS keychain).").action(() => lockCommand(container));
|
|
1491
2405
|
program.command("status").description("Show vault location and lock state.").action(() => statusCommand(container));
|
|
1492
2406
|
program.command("projects").description("List projects with item counts and last activity.").action(() => projectsCommand(container));
|
|
1493
|
-
program.command("show").description("Print the items of a project.").argument("<project>", "project slug").option("--type <type>", "filter by type (decision|progress|preference|fact|handoff)").action(
|
|
2407
|
+
program.command("show").description("Print the items of a project.").argument("<project>", "project slug").option("--type <type>", "filter by type (decision|progress|preference|fact|handoff|imported)").action(
|
|
1494
2408
|
(project, options) => showCommand(container, project, options)
|
|
1495
2409
|
);
|
|
1496
2410
|
program.command("search").description("Full-text search across saved context.").argument("<query>", "search terms").option("-p, --project <project>", "limit to one project").action(
|
|
@@ -1499,6 +2413,9 @@ program.command("search").description("Full-text search across saved context.").
|
|
|
1499
2413
|
program.command("export").description("Export a project's context pack (markdown by default).").argument("<project>", "project slug").option("--json", "export as JSON instead of markdown").option("-o, --output <file>", "write to a file instead of stdout").action(
|
|
1500
2414
|
(project, options) => exportCommand(container, project, options)
|
|
1501
2415
|
);
|
|
2416
|
+
program.command("import").description("Import chatbot history (ChatGPT/Claude/generic export) into the vault.").argument("<file>", "path to an export file (.json or .zip)").option("-p, --project <project>", "target project (required to import; created if new)").option("--from <source>", "chatgpt | claude | generic (auto-detected if omitted)").option("--list", "list conversations and exit (the default when no selection is given)").option("--pick <spec>", "1-based indices/ranges into the list, e.g. 1,3-5").option("--query <text>", "select conversations whose title contains this text").option("--since <date>", "select conversations on or after YYYY-MM-DD").option("--all", "import every conversation").option("--dry-run", "report what would be imported; write nothing").action(
|
|
2417
|
+
(file, options) => importCommand(container, file, options)
|
|
2418
|
+
);
|
|
1502
2419
|
program.command("install").description("Wire the valija MCP server into an AI tool's config.").argument("<client>", `one of: ${CLIENTS.join(", ")}`).action((client) => {
|
|
1503
2420
|
if (!CLIENTS.includes(client)) {
|
|
1504
2421
|
console.error(`error: unknown client "${client}". Use one of: ${CLIENTS.join(", ")}`);
|