@kyleparrott/where-was-i 0.1.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/LICENSE +89 -0
- package/README.md +167 -0
- package/dist/src/cli.js +423 -0
- package/dist/src/core/codex.js +303 -0
- package/dist/src/core/config.js +168 -0
- package/dist/src/core/database.js +432 -0
- package/dist/src/core/doctor.js +113 -0
- package/dist/src/core/embeddings.js +118 -0
- package/dist/src/core/indexer.js +60 -0
- package/dist/src/core/paths.js +20 -0
- package/dist/src/core/reset.js +18 -0
- package/dist/src/core/search-mode.js +17 -0
- package/dist/src/core/search.js +562 -0
- package/dist/src/core/semantic.js +220 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/core/vector.js +311 -0
- package/dist/src/mcp.js +345 -0
- package/dist/src/web-client.js +61 -0
- package/dist/src/web-settings.js +157 -0
- package/dist/src/web-style.js +797 -0
- package/dist/src/web-utils.js +81 -0
- package/dist/src/web-views.js +389 -0
- package/dist/src/web.js +512 -0
- package/docs/assets/web-ui.png +0 -0
- package/package.json +64 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const MAX_CHUNK_CHARS = 6000;
|
|
5
|
+
const CHUNK_OVERLAP = 400;
|
|
6
|
+
export function discoverCodexSessionFiles(codexHome, includeArchived = false) {
|
|
7
|
+
const roots = [path.join(codexHome, "sessions")];
|
|
8
|
+
if (includeArchived) {
|
|
9
|
+
roots.push(path.join(codexHome, "archived_sessions"));
|
|
10
|
+
}
|
|
11
|
+
const files = [];
|
|
12
|
+
for (const root of roots) {
|
|
13
|
+
if (!fs.existsSync(root)) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
walkJsonl(root, files);
|
|
17
|
+
}
|
|
18
|
+
return files.sort();
|
|
19
|
+
}
|
|
20
|
+
export function parseCodexSessionFile(filePath, options = {}) {
|
|
21
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
22
|
+
const lines = raw.split(/\r?\n/);
|
|
23
|
+
const events = [];
|
|
24
|
+
const fileSessionId = sessionIdFromPath(filePath);
|
|
25
|
+
let sessionId = fileSessionId;
|
|
26
|
+
let cwd = null;
|
|
27
|
+
let startedAt = null;
|
|
28
|
+
let updatedAt = null;
|
|
29
|
+
let title = null;
|
|
30
|
+
const turns = [];
|
|
31
|
+
const messages = [];
|
|
32
|
+
const chunks = [];
|
|
33
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
34
|
+
const line = lines[index];
|
|
35
|
+
if (!line) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const event = JSON.parse(line);
|
|
40
|
+
events.push({ event, lineNumber: index + 1 });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const { event } of events) {
|
|
47
|
+
const payload = event.payload ?? {};
|
|
48
|
+
const timestamp = stringValue(event.timestamp) ?? stringValue(payload.timestamp);
|
|
49
|
+
startedAt ??= timestamp;
|
|
50
|
+
updatedAt = timestamp ?? updatedAt;
|
|
51
|
+
if (event.type === "session_meta") {
|
|
52
|
+
sessionId = stringValue(payload.id) ?? sessionId;
|
|
53
|
+
cwd = stringValue(payload.cwd) ?? cwd;
|
|
54
|
+
}
|
|
55
|
+
if (event.type === "turn_context") {
|
|
56
|
+
cwd = stringValue(payload.cwd) ?? cwd;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const conversationId = sessionId;
|
|
60
|
+
let currentTurn = null;
|
|
61
|
+
let messageOrdinal = 0;
|
|
62
|
+
let chunkOrdinal = 0;
|
|
63
|
+
const seenMessages = new Set();
|
|
64
|
+
for (const { event, lineNumber } of events) {
|
|
65
|
+
const payload = event.payload ?? {};
|
|
66
|
+
const timestamp = stringValue(event.timestamp) ?? stringValue(payload.timestamp);
|
|
67
|
+
const role = inferMessageRole(event, options);
|
|
68
|
+
if (!role) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const text = extractText(event, role).trim();
|
|
72
|
+
if (!text || isSyntheticTranscriptMessage(text)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!title && role === "user") {
|
|
76
|
+
title = compactTitle(text);
|
|
77
|
+
}
|
|
78
|
+
const fingerprint = messageFingerprint(role, timestamp, text);
|
|
79
|
+
if (seenMessages.has(fingerprint)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
seenMessages.add(fingerprint);
|
|
83
|
+
if (role === "user" || !currentTurn) {
|
|
84
|
+
currentTurn = {
|
|
85
|
+
id: turnId(sessionId, turns.length),
|
|
86
|
+
sessionId,
|
|
87
|
+
source: "codex",
|
|
88
|
+
sourcePath: filePath,
|
|
89
|
+
ordinal: turns.length,
|
|
90
|
+
startedAt: timestamp,
|
|
91
|
+
updatedAt: timestamp,
|
|
92
|
+
messageCount: 0
|
|
93
|
+
};
|
|
94
|
+
turns.push(currentTurn);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
currentTurn.updatedAt = timestamp ?? currentTurn.updatedAt;
|
|
98
|
+
}
|
|
99
|
+
currentTurn.messageCount += 1;
|
|
100
|
+
const kind = `${event.type ?? "event"}:${stringValue(payload.type) ?? "unknown"}`;
|
|
101
|
+
const messageIdValue = messageId(sessionId, lineNumber, role, kind, text);
|
|
102
|
+
const message = {
|
|
103
|
+
id: messageIdValue,
|
|
104
|
+
sessionId,
|
|
105
|
+
conversationId,
|
|
106
|
+
turnId: currentTurn.id,
|
|
107
|
+
source: "codex",
|
|
108
|
+
sourcePath: filePath,
|
|
109
|
+
ordinal: messageOrdinal,
|
|
110
|
+
role,
|
|
111
|
+
kind,
|
|
112
|
+
timestamp,
|
|
113
|
+
text,
|
|
114
|
+
lineStart: lineNumber,
|
|
115
|
+
lineEnd: lineNumber,
|
|
116
|
+
metadata: {
|
|
117
|
+
eventType: event.type ?? null,
|
|
118
|
+
payloadType: stringValue(payload.type) ?? null,
|
|
119
|
+
callId: stringValue(payload.call_id) ?? stringValue(payload.callId) ?? null
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
messages.push(message);
|
|
123
|
+
let chunkIndex = 0;
|
|
124
|
+
for (const part of splitChunkText(text)) {
|
|
125
|
+
chunks.push({
|
|
126
|
+
id: chunkId(filePath, messageIdValue, chunkIndex),
|
|
127
|
+
messageId: messageIdValue,
|
|
128
|
+
sessionId,
|
|
129
|
+
conversationId,
|
|
130
|
+
turnId: currentTurn.id,
|
|
131
|
+
source: "codex",
|
|
132
|
+
sourcePath: filePath,
|
|
133
|
+
ordinal: chunkOrdinal,
|
|
134
|
+
chunkIndex,
|
|
135
|
+
role,
|
|
136
|
+
kind,
|
|
137
|
+
timestamp,
|
|
138
|
+
text: part,
|
|
139
|
+
lineStart: lineNumber,
|
|
140
|
+
lineEnd: lineNumber,
|
|
141
|
+
metadata: {
|
|
142
|
+
eventType: event.type ?? null,
|
|
143
|
+
payloadType: stringValue(payload.type) ?? null,
|
|
144
|
+
callId: stringValue(payload.call_id) ?? stringValue(payload.callId) ?? null
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
chunkOrdinal += 1;
|
|
148
|
+
chunkIndex += 1;
|
|
149
|
+
}
|
|
150
|
+
messageOrdinal += 1;
|
|
151
|
+
}
|
|
152
|
+
const stat = fs.statSync(filePath);
|
|
153
|
+
const session = {
|
|
154
|
+
id: sessionId,
|
|
155
|
+
conversationId,
|
|
156
|
+
source: "codex",
|
|
157
|
+
sourcePath: filePath,
|
|
158
|
+
title,
|
|
159
|
+
cwd,
|
|
160
|
+
startedAt,
|
|
161
|
+
updatedAt: updatedAt ?? stat.mtime.toISOString(),
|
|
162
|
+
messageCount: messages.length,
|
|
163
|
+
turnCount: turns.length
|
|
164
|
+
};
|
|
165
|
+
return { session, turns, messages, chunks };
|
|
166
|
+
}
|
|
167
|
+
function walkJsonl(root, files) {
|
|
168
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
169
|
+
const fullPath = path.join(root, entry.name);
|
|
170
|
+
if (entry.isDirectory()) {
|
|
171
|
+
walkJsonl(fullPath, files);
|
|
172
|
+
}
|
|
173
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
174
|
+
files.push(fullPath);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function inferMessageRole(event, options) {
|
|
179
|
+
const payload = event.payload ?? {};
|
|
180
|
+
const payloadType = stringValue(payload.type);
|
|
181
|
+
const payloadRole = stringValue(payload.role);
|
|
182
|
+
if (payloadRole === "user")
|
|
183
|
+
return "user";
|
|
184
|
+
if (payloadRole === "assistant")
|
|
185
|
+
return "assistant";
|
|
186
|
+
if (event.type === "event_msg" && payloadType === "user_message")
|
|
187
|
+
return "user";
|
|
188
|
+
if (event.type === "event_msg" && payloadType === "agent_message")
|
|
189
|
+
return "assistant";
|
|
190
|
+
if (options.includeTools && isToolEvent(event))
|
|
191
|
+
return "tool";
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
function extractText(event, role) {
|
|
195
|
+
const payload = event.payload ?? {};
|
|
196
|
+
const parts = [];
|
|
197
|
+
const content = payload.content;
|
|
198
|
+
if (Array.isArray(content)) {
|
|
199
|
+
for (const item of content) {
|
|
200
|
+
if (isRecord(item)) {
|
|
201
|
+
const itemType = stringValue(item.type);
|
|
202
|
+
if (itemType === "input_text" || itemType === "output_text" || itemType === "text") {
|
|
203
|
+
const text = stringValue(item.text);
|
|
204
|
+
if (text)
|
|
205
|
+
parts.push(text);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const contentText = stringValue(content);
|
|
212
|
+
if (contentText)
|
|
213
|
+
parts.push(contentText);
|
|
214
|
+
}
|
|
215
|
+
const message = stringValue(payload.message);
|
|
216
|
+
if (message)
|
|
217
|
+
parts.push(message);
|
|
218
|
+
if (role === "tool") {
|
|
219
|
+
const output = stringValue(payload.output) ?? stringValue(payload.formatted_output) ?? stringValue(payload.aggregated_output);
|
|
220
|
+
if (output)
|
|
221
|
+
parts.push(output);
|
|
222
|
+
const stdout = stringValue(payload.stdout);
|
|
223
|
+
const stderr = stringValue(payload.stderr);
|
|
224
|
+
if (stdout)
|
|
225
|
+
parts.push(stdout);
|
|
226
|
+
if (stderr)
|
|
227
|
+
parts.push(stderr);
|
|
228
|
+
const toolName = stringValue(payload.name) ?? stringValue(payload.tool) ?? stringValue(payload.namespace);
|
|
229
|
+
const args = stringValue(payload.arguments);
|
|
230
|
+
const command = stringValue(payload.command);
|
|
231
|
+
const query = stringValue(payload.query);
|
|
232
|
+
if (toolName || args || command || query) {
|
|
233
|
+
parts.push([toolName, args, command, query].filter(Boolean).join(" "));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return parts.join("\n\n");
|
|
237
|
+
}
|
|
238
|
+
function isToolEvent(event) {
|
|
239
|
+
const payload = event.payload ?? {};
|
|
240
|
+
const payloadType = stringValue(payload.type);
|
|
241
|
+
if (payloadType === "function_call" ||
|
|
242
|
+
payloadType === "function_call_output" ||
|
|
243
|
+
payloadType === "web_search_call" ||
|
|
244
|
+
payloadType === "custom_tool_call" ||
|
|
245
|
+
payloadType === "custom_tool_call_output" ||
|
|
246
|
+
payloadType === "tool_search_call" ||
|
|
247
|
+
payloadType === "tool_search_output") {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
if (event.type === "event_msg" &&
|
|
251
|
+
(payloadType === "exec_command_end" ||
|
|
252
|
+
payloadType === "mcp_tool_call_end" ||
|
|
253
|
+
payloadType === "patch_apply_end" ||
|
|
254
|
+
payloadType === "web_search_end")) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
function splitChunkText(text) {
|
|
260
|
+
if (text.length <= MAX_CHUNK_CHARS) {
|
|
261
|
+
return [text];
|
|
262
|
+
}
|
|
263
|
+
const chunks = [];
|
|
264
|
+
let start = 0;
|
|
265
|
+
while (start < text.length) {
|
|
266
|
+
const end = Math.min(text.length, start + MAX_CHUNK_CHARS);
|
|
267
|
+
chunks.push(text.slice(start, end));
|
|
268
|
+
if (end === text.length) {
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
start = Math.max(0, end - CHUNK_OVERLAP);
|
|
272
|
+
}
|
|
273
|
+
return chunks;
|
|
274
|
+
}
|
|
275
|
+
function sessionIdFromPath(filePath) {
|
|
276
|
+
const match = path.basename(filePath).match(/rollout-[^-]+-[^-]+-(.+)\.jsonl$/);
|
|
277
|
+
return match?.[1] ?? crypto.createHash("sha1").update(filePath).digest("hex");
|
|
278
|
+
}
|
|
279
|
+
function turnId(sessionId, ordinal) {
|
|
280
|
+
return `${sessionId}:turn:${ordinal}`;
|
|
281
|
+
}
|
|
282
|
+
function messageId(sessionId, lineNumber, role, kind, text) {
|
|
283
|
+
const hash = crypto.createHash("sha1").update(`${role}:${kind}:${text}`).digest("hex").slice(0, 12);
|
|
284
|
+
return `${sessionId}:message:${lineNumber}:${hash}`;
|
|
285
|
+
}
|
|
286
|
+
function chunkId(filePath, messageIdValue, chunkIndex) {
|
|
287
|
+
return crypto.createHash("sha1").update(`${filePath}:${messageIdValue}:${chunkIndex}`).digest("hex");
|
|
288
|
+
}
|
|
289
|
+
function compactTitle(text) {
|
|
290
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
291
|
+
}
|
|
292
|
+
function messageFingerprint(role, timestamp, text) {
|
|
293
|
+
return `${role}:${timestamp ?? ""}:${text.replace(/\s+/g, " ").trim()}`;
|
|
294
|
+
}
|
|
295
|
+
function isSyntheticTranscriptMessage(text) {
|
|
296
|
+
return text.startsWith("<turn_aborted>") || text.startsWith("# In app browser:");
|
|
297
|
+
}
|
|
298
|
+
function stringValue(value) {
|
|
299
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
300
|
+
}
|
|
301
|
+
function isRecord(value) {
|
|
302
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
303
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { DEFAULT_EMBEDDING_TIMEOUT_MS } from "./embeddings.js";
|
|
6
|
+
import { expandHome } from "./paths.js";
|
|
7
|
+
const startupIndexModeSchema = z.enum(["off", "background", "blocking"]);
|
|
8
|
+
const optionalStringSchema = z.string().optional();
|
|
9
|
+
const embeddingProviderSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
baseUrl: optionalStringSchema,
|
|
12
|
+
model: optionalStringSchema,
|
|
13
|
+
apiKey: optionalStringSchema,
|
|
14
|
+
apiKeyEnv: optionalStringSchema,
|
|
15
|
+
timeoutMs: z.number().int().positive().optional()
|
|
16
|
+
})
|
|
17
|
+
.passthrough();
|
|
18
|
+
const rawWhereWasIConfigSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
dbPath: optionalStringSchema,
|
|
21
|
+
codexHome: optionalStringSchema,
|
|
22
|
+
startup: z
|
|
23
|
+
.object({
|
|
24
|
+
lexical: startupIndexModeSchema.optional(),
|
|
25
|
+
semantic: startupIndexModeSchema.optional()
|
|
26
|
+
})
|
|
27
|
+
.passthrough()
|
|
28
|
+
.optional(),
|
|
29
|
+
semantic: z
|
|
30
|
+
.object({
|
|
31
|
+
enabled: z.boolean().optional(),
|
|
32
|
+
startupMaxChunks: z.union([z.number().int().positive(), z.null()]).optional(),
|
|
33
|
+
baseUrl: optionalStringSchema,
|
|
34
|
+
model: optionalStringSchema,
|
|
35
|
+
apiKey: optionalStringSchema,
|
|
36
|
+
apiKeyEnv: optionalStringSchema,
|
|
37
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
38
|
+
provider: embeddingProviderSchema.optional()
|
|
39
|
+
})
|
|
40
|
+
.passthrough()
|
|
41
|
+
.optional()
|
|
42
|
+
})
|
|
43
|
+
.passthrough();
|
|
44
|
+
export function defaultConfigPath() {
|
|
45
|
+
return path.join(os.homedir(), ".where-was-i", "config.json");
|
|
46
|
+
}
|
|
47
|
+
export function loadWhereWasIConfig(configPath) {
|
|
48
|
+
const resolvedConfigPath = path.resolve(expandHome(configPath ?? process.env.WHERE_WAS_I_CONFIG ?? defaultConfigPath()));
|
|
49
|
+
const raw = readConfigFile(resolvedConfigPath, Boolean(configPath || process.env.WHERE_WAS_I_CONFIG));
|
|
50
|
+
const rawSemantic = raw.semantic ?? {};
|
|
51
|
+
const rawProvider = rawSemantic.provider ?? rawSemantic;
|
|
52
|
+
const apiKeyEnv = process.env.WHERE_WAS_I_EMBEDDING_API_KEY_ENV ?? rawProvider.apiKeyEnv;
|
|
53
|
+
const apiKeyFromNamedEnv = apiKeyEnv ? process.env[apiKeyEnv] : undefined;
|
|
54
|
+
const startupMaxChunksEnv = envOptionalPositiveInteger(process.env.WHERE_WAS_I_SEMANTIC_STARTUP_MAX_CHUNKS);
|
|
55
|
+
return {
|
|
56
|
+
configPath: resolvedConfigPath,
|
|
57
|
+
dbPath: expandHome(process.env.WHERE_WAS_I_INDEX_PATH ?? raw.dbPath ?? path.join(os.homedir(), ".where-was-i", "index.sqlite")),
|
|
58
|
+
codexHome: expandHome(process.env.CODEX_HOME ?? raw.codexHome ?? path.join(os.homedir(), ".codex")),
|
|
59
|
+
startup: {
|
|
60
|
+
lexical: envStartupMode(process.env.WHERE_WAS_I_STARTUP_LEXICAL) ?? raw.startup?.lexical ?? "off",
|
|
61
|
+
semantic: envStartupMode(process.env.WHERE_WAS_I_STARTUP_SEMANTIC) ?? raw.startup?.semantic ?? "off"
|
|
62
|
+
},
|
|
63
|
+
semantic: {
|
|
64
|
+
startupMaxChunks: startupMaxChunksEnv !== undefined
|
|
65
|
+
? startupMaxChunksEnv
|
|
66
|
+
: rawSemantic.startupMaxChunks !== undefined
|
|
67
|
+
? rawSemantic.startupMaxChunks
|
|
68
|
+
: 100,
|
|
69
|
+
embedding: definedEmbeddingConfig({
|
|
70
|
+
baseUrl: process.env.WHERE_WAS_I_EMBEDDING_BASE_URL ?? rawProvider.baseUrl,
|
|
71
|
+
model: process.env.WHERE_WAS_I_EMBEDDING_MODEL ?? rawProvider.model,
|
|
72
|
+
apiKey: process.env.WHERE_WAS_I_EMBEDDING_API_KEY ?? apiKeyFromNamedEnv ?? rawProvider.apiKey,
|
|
73
|
+
timeoutMs: envPositiveInteger(process.env.WHERE_WAS_I_EMBEDDING_TIMEOUT_MS) ?? rawProvider.timeoutMs
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function mergeEmbeddingConfig(config, overrides = {}) {
|
|
79
|
+
return definedEmbeddingConfig({
|
|
80
|
+
...config.semantic.embedding,
|
|
81
|
+
...definedEmbeddingConfig(overrides)
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
export function defaultConfigTemplate() {
|
|
85
|
+
return {
|
|
86
|
+
dbPath: "~/.where-was-i/index.sqlite",
|
|
87
|
+
codexHome: "~/.codex",
|
|
88
|
+
startup: {
|
|
89
|
+
lexical: "off",
|
|
90
|
+
semantic: "off"
|
|
91
|
+
},
|
|
92
|
+
semantic: {
|
|
93
|
+
startupMaxChunks: 100,
|
|
94
|
+
provider: {
|
|
95
|
+
baseUrl: "",
|
|
96
|
+
model: "",
|
|
97
|
+
apiKeyEnv: "WHERE_WAS_I_EMBEDDING_API_KEY",
|
|
98
|
+
timeoutMs: DEFAULT_EMBEDDING_TIMEOUT_MS
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function validateWhereWasIConfigObject(value, configPath = "<config>") {
|
|
104
|
+
if (!isObject(value)) {
|
|
105
|
+
throw new Error(`Config file must contain a JSON object: ${configPath}`);
|
|
106
|
+
}
|
|
107
|
+
const result = rawWhereWasIConfigSchema.safeParse(value);
|
|
108
|
+
if (!result.success) {
|
|
109
|
+
throw new Error(`Invalid config file ${configPath}: ${formatZodIssues(result.error.issues)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function readConfigFile(configPath, required) {
|
|
113
|
+
if (!fs.existsSync(configPath)) {
|
|
114
|
+
if (required) {
|
|
115
|
+
throw new Error(`Config file does not exist: ${configPath}`);
|
|
116
|
+
}
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
120
|
+
validateWhereWasIConfigObject(parsed, configPath);
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
123
|
+
function definedEmbeddingConfig(config) {
|
|
124
|
+
return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined && value !== ""));
|
|
125
|
+
}
|
|
126
|
+
function envStartupMode(value) {
|
|
127
|
+
if (value === undefined) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
if (value === "off" || value === "background" || value === "blocking") {
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`Invalid startup index mode: ${value}`);
|
|
134
|
+
}
|
|
135
|
+
function envOptionalPositiveInteger(value) {
|
|
136
|
+
if (value === undefined) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
if (value === "" || value === "0" || value.toLowerCase() === "off") {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const parsed = Number(value);
|
|
143
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
144
|
+
throw new Error(`Invalid positive integer environment value: ${value}`);
|
|
145
|
+
}
|
|
146
|
+
return parsed;
|
|
147
|
+
}
|
|
148
|
+
function envPositiveInteger(value) {
|
|
149
|
+
if (value === undefined || value === "") {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const parsed = Number(value);
|
|
153
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
154
|
+
throw new Error(`Invalid positive integer environment value: ${value}`);
|
|
155
|
+
}
|
|
156
|
+
return parsed;
|
|
157
|
+
}
|
|
158
|
+
function formatZodIssues(issues) {
|
|
159
|
+
return issues
|
|
160
|
+
.map((issue) => {
|
|
161
|
+
const issuePath = issue.path.length > 0 ? issue.path.join(".") : "<root>";
|
|
162
|
+
return `${issuePath}: ${issue.message}`;
|
|
163
|
+
})
|
|
164
|
+
.join("; ");
|
|
165
|
+
}
|
|
166
|
+
function isObject(value) {
|
|
167
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
168
|
+
}
|