@makefinks/daemon 0.8.0 → 0.9.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.
@@ -1,4 +1,5 @@
1
1
  import type { ToolLayoutConfig, ToolHeader } from "../types";
2
+ import { COLORS } from "../../../ui/constants";
2
3
  import { registerToolLayout } from "../registry";
3
4
 
4
5
  type UnknownRecord = Record<string, unknown>;
@@ -7,42 +8,77 @@ function isRecord(value: unknown): value is UnknownRecord {
7
8
  return typeof value === "object" && value !== null && !Array.isArray(value);
8
9
  }
9
10
 
10
- interface FetchUrlsInput {
11
+ interface FetchUrlsRequestInput {
11
12
  url: string;
12
13
  lineOffset?: number;
13
14
  lineLimit?: number;
14
- highlightQuery?: string;
15
15
  }
16
16
 
17
- function extractFetchUrlsInput(input: unknown): FetchUrlsInput | null {
17
+ type FetchUrlsResultItem = {
18
+ success?: unknown;
19
+ url?: unknown;
20
+ text?: unknown;
21
+ lineOffset?: unknown;
22
+ lineLimit?: unknown;
23
+ remainingLines?: unknown;
24
+ error?: unknown;
25
+ title?: unknown;
26
+ };
27
+
28
+ function extractFetchUrlsRequests(input: unknown): FetchUrlsRequestInput[] | null {
29
+ if (!isRecord(input)) return null;
30
+ if (!("requests" in input) || !Array.isArray(input.requests)) return null;
31
+
32
+ const requests: FetchUrlsRequestInput[] = [];
33
+ for (const item of input.requests) {
34
+ if (!isRecord(item)) continue;
35
+ if (!("url" in item) || typeof item.url !== "string") continue;
36
+ const lineOffset =
37
+ "lineOffset" in item && typeof item.lineOffset === "number" ? item.lineOffset : undefined;
38
+ const lineLimit = "lineLimit" in item && typeof item.lineLimit === "number" ? item.lineLimit : undefined;
39
+ requests.push({ url: item.url, lineOffset, lineLimit });
40
+ }
41
+
42
+ return requests.length > 0 ? requests : null;
43
+ }
44
+
45
+ function extractRenderUrlInput(input: unknown): FetchUrlsRequestInput | null {
46
+ if (!input) return null;
18
47
  if (!isRecord(input)) return null;
19
48
  if (!("url" in input) || typeof input.url !== "string") return null;
20
49
 
21
50
  const lineOffset =
22
51
  "lineOffset" in input && typeof input.lineOffset === "number" ? input.lineOffset : undefined;
23
52
  const lineLimit = "lineLimit" in input && typeof input.lineLimit === "number" ? input.lineLimit : undefined;
24
- const highlightQuery =
25
- "highlightQuery" in input && typeof input.highlightQuery === "string" ? input.highlightQuery : undefined;
26
- return { url: input.url, lineOffset, lineLimit, highlightQuery };
53
+ return { url: input.url, lineOffset, lineLimit };
27
54
  }
28
55
 
29
- function mergeFetchUrlsDefaults(input: FetchUrlsInput | null, result?: unknown): FetchUrlsInput | null {
30
- if (!input) return null;
31
- if (!result || typeof result !== "object") return input;
56
+ function extractFetchUrlsResults(result?: unknown): FetchUrlsResultItem[] | null {
57
+ if (!result || typeof result !== "object") return null;
58
+ const record = result as Record<string, unknown>;
59
+ const container = extractToolDataContainer(record);
60
+ if (!isRecord(container)) return null;
61
+
62
+ if (Array.isArray(container.results)) {
63
+ return container.results.filter((item): item is FetchUrlsResultItem => isRecord(item));
64
+ }
32
65
 
33
- const resultRecord = result as Record<string, unknown>;
66
+ return null;
67
+ }
68
+
69
+ function mergeFetchUrlsDefaults(
70
+ input: FetchUrlsRequestInput,
71
+ result?: FetchUrlsResultItem | null
72
+ ): FetchUrlsRequestInput {
73
+ if (!result) return input;
34
74
  const lineOffset =
35
- input.lineOffset ?? (typeof resultRecord.lineOffset === "number" ? resultRecord.lineOffset : undefined);
36
- const lineLimit =
37
- input.lineLimit ?? (typeof resultRecord.lineLimit === "number" ? resultRecord.lineLimit : undefined);
75
+ input.lineOffset ?? (typeof result.lineOffset === "number" ? result.lineOffset : undefined);
76
+ const lineLimit = input.lineLimit ?? (typeof result.lineLimit === "number" ? result.lineLimit : undefined);
38
77
 
39
78
  return { ...input, lineOffset, lineLimit };
40
79
  }
41
80
 
42
- function formatFetchUrlsHeader(input: FetchUrlsInput): string {
43
- if (input.highlightQuery) {
44
- return `highlight: "${input.highlightQuery}"`;
45
- }
81
+ function formatFetchUrlsHeader(input: FetchUrlsRequestInput): string {
46
82
  const parts: string[] = [];
47
83
  if (input.lineOffset !== undefined) {
48
84
  parts.push(`lineOffset=${input.lineOffset}`);
@@ -57,6 +93,10 @@ function normalizeWhitespace(text: string): string {
57
93
  return text.replace(/\r\n/g, "\n").replace(/\t/g, " ");
58
94
  }
59
95
 
96
+ function escapeXmlAttribute(value: string): string {
97
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
98
+ }
99
+
60
100
  type ExaLikeItem = {
61
101
  title?: unknown;
62
102
  url?: unknown;
@@ -64,6 +104,8 @@ type ExaLikeItem = {
64
104
  lineOffset?: unknown;
65
105
  lineLimit?: unknown;
66
106
  remainingLines?: unknown;
107
+ totalLines?: unknown;
108
+ error?: unknown;
67
109
  };
68
110
 
69
111
  function formatExaItemLabel(item: ExaLikeItem): string {
@@ -78,76 +120,69 @@ function extractToolDataContainer(result: UnknownRecord): unknown {
78
120
  }
79
121
 
80
122
  function formatFetchUrlsResult(result: unknown): string[] | null {
123
+ if (typeof result === "string") {
124
+ const lines = result.split("\n");
125
+ const MAX_LINES = 8;
126
+ if (lines.length <= MAX_LINES) return lines;
127
+ return [...lines.slice(0, MAX_LINES - 1), " ..."];
128
+ }
81
129
  if (!isRecord(result)) return null;
82
130
  if (result.success === false && typeof result.error === "string") {
83
131
  return [`error: ${result.error}`];
84
132
  }
85
133
  if (result.success !== true) return null;
86
134
 
87
- if (Array.isArray(result.highlights)) {
88
- return formatHighlightsResult(result);
89
- }
135
+ const items = extractFetchUrlsResults(result);
136
+ if (!items) return null;
90
137
 
91
- const data = extractToolDataContainer(result);
92
- const candidate = isRecord(data) ? (data as ExaLikeItem) : {};
93
- const label = formatExaItemLabel(candidate);
94
- const url = typeof candidate.url === "string" ? candidate.url : "";
95
- const title = typeof candidate.title === "string" ? candidate.title : "";
96
- const lineOffset = typeof candidate.lineOffset === "number" ? candidate.lineOffset : undefined;
97
- const lineLimit = typeof candidate.lineLimit === "number" ? candidate.lineLimit : undefined;
98
- const remainingLines =
99
- typeof candidate.remainingLines === "number" || candidate.remainingLines === null
100
- ? candidate.remainingLines
101
- : undefined;
102
- const rangeParts: string[] = [];
103
- if (lineOffset !== undefined) rangeParts.push(`lineOffset=${lineOffset}`);
104
- if (lineLimit !== undefined) rangeParts.push(`lineLimit=${lineLimit}`);
105
- if (remainingLines !== undefined) {
106
- rangeParts.push(remainingLines === null ? "remainingLines=unknown" : `remainingLines=${remainingLines}`);
107
- }
108
- const remainingSuffix = rangeParts.length > 0 ? ` (${rangeParts.join(", ")})` : "";
109
-
110
- const headerBase = url && title ? `${label} — ${url}` : label;
111
- const header = `${headerBase}${remainingSuffix}`;
112
-
113
- const text = typeof candidate.text === "string" ? candidate.text : "";
114
- if (!text.trim()) return [header];
115
-
116
- const MAX_LINES = 4;
138
+ const lines: string[] = ["<fetchUrls>"];
139
+ const MAX_LINES = 2;
117
140
  const MAX_CHARS = 160;
118
- const snippet = normalizeWhitespace(text)
119
- .replace(/\n{3,}/g, "\n\n")
120
- .trim()
121
- .split("\n")
122
- .slice(0, MAX_LINES)
123
- .map((l) => (l.length > MAX_CHARS ? `${l.slice(0, MAX_CHARS - 1)}…` : l));
124
-
125
- return [header, ...snippet];
126
- }
141
+ const maxItems = 3;
142
+
143
+ for (const item of items.slice(0, maxItems)) {
144
+ const candidate = item as ExaLikeItem;
145
+ const url = typeof candidate.url === "string" ? candidate.url : "";
146
+ if (!url) continue;
147
+
148
+ const attributes: string[] = [`href="${escapeXmlAttribute(url)}"`];
149
+ if (typeof candidate.lineOffset === "number") attributes.push(`lineOffset="${candidate.lineOffset}"`);
150
+ if (typeof candidate.lineLimit === "number") attributes.push(`lineLimit="${candidate.lineLimit}"`);
151
+ if (typeof candidate.totalLines === "number") attributes.push(`totalLines="${candidate.totalLines}"`);
152
+ if (typeof candidate.remainingLines === "number") {
153
+ attributes.push(`remainingLines="${candidate.remainingLines}"`);
154
+ } else if (candidate.remainingLines === null) {
155
+ attributes.push(`remainingLines="unknown"`);
156
+ }
127
157
 
128
- function formatHighlightsResult(result: UnknownRecord): string[] {
129
- const highlights = result.highlights as unknown[];
130
- const highlightQuery = typeof result.highlightQuery === "string" ? result.highlightQuery : "";
131
- const count = highlights.length;
158
+ if (candidate.success === false && typeof candidate.error === "string") {
159
+ attributes.push(`error="${escapeXmlAttribute(candidate.error)}"`);
160
+ lines.push(` <url ${attributes.join(" ")} />`);
161
+ continue;
162
+ }
132
163
 
133
- const lines: string[] = [`${count} highlight${count !== 1 ? "s" : ""} for "${highlightQuery}"`];
164
+ const text = typeof candidate.text === "string" ? candidate.text : "";
165
+ if (!text.trim()) {
166
+ lines.push(` <url ${attributes.join(" ")} />`);
167
+ continue;
168
+ }
134
169
 
135
- const MAX_HIGHLIGHTS = 3;
136
- const MAX_CHARS = 120;
170
+ const snippetLines = normalizeWhitespace(text)
171
+ .replace(/\n{3,}/g, "\n\n")
172
+ .trim()
173
+ .split("\n")
174
+ .slice(0, MAX_LINES)
175
+ .map((l) => (l.length > MAX_CHARS ? `${l.slice(0, MAX_CHARS - 1)}…` : l));
137
176
 
138
- highlights.slice(0, MAX_HIGHLIGHTS).forEach((h, idx) => {
139
- if (typeof h === "string") {
140
- const clean = h.replace(/\n+/g, " ").trim();
141
- const truncated = clean.length > MAX_CHARS ? `${clean.slice(0, MAX_CHARS - 1)}…` : clean;
142
- lines.push(` ${idx + 1}. "${truncated}"`);
177
+ lines.push(` <url ${attributes.join(" ")}>`);
178
+ for (const line of snippetLines) {
179
+ lines.push(` ${escapeXmlAttribute(line)}`);
143
180
  }
144
- });
145
-
146
- if (highlights.length > MAX_HIGHLIGHTS) {
147
- lines.push(` ...and ${highlights.length - MAX_HIGHLIGHTS} more`);
181
+ lines.push(" </url>");
148
182
  }
149
183
 
150
- return lines;
184
+ lines.push("</fetchUrls>");
185
+ return lines.length > 2 ? lines : null;
151
186
  }
152
187
 
153
188
  function formatRenderUrlResult(result: unknown): string[] | null {
@@ -188,15 +223,38 @@ export const fetchUrlsLayout: ToolLayoutConfig = {
188
223
  abbreviation: "fetch",
189
224
 
190
225
  getHeader: (input, result): ToolHeader | null => {
191
- const urlInput = mergeFetchUrlsDefaults(extractFetchUrlsInput(input), result);
192
- if (!urlInput) return null;
193
- const headerSuffix = formatFetchUrlsHeader(urlInput);
226
+ const requests = extractFetchUrlsRequests(input);
227
+ if (!requests) return null;
228
+ const items = extractFetchUrlsResults(result);
229
+ const firstResult = items?.[0] ?? null;
230
+ const first = mergeFetchUrlsDefaults(requests[0] as FetchUrlsRequestInput, firstResult);
231
+ if (requests.length === 1) {
232
+ const headerSuffix = formatFetchUrlsHeader(first);
233
+ return {
234
+ primary: first.url,
235
+ secondary: headerSuffix || undefined,
236
+ };
237
+ }
238
+
194
239
  return {
195
- primary: urlInput.url,
196
- secondary: headerSuffix || undefined,
240
+ primary: `${requests.length} urls`,
197
241
  };
198
242
  },
199
243
 
244
+ getBody: (input, result): ToolBody | null => {
245
+ const requests = extractFetchUrlsRequests(input);
246
+ if (!requests) return null;
247
+ if (requests.length === 1) return null;
248
+ const items = extractFetchUrlsResults(result) ?? [];
249
+ const lines = requests.map((request, index) => {
250
+ const merged = mergeFetchUrlsDefaults(request, items[index] ?? null);
251
+ const suffix = formatFetchUrlsHeader(merged);
252
+ const text = suffix ? `${merged.url} ${suffix}` : merged.url;
253
+ return { text, color: COLORS.REASONING_DIM };
254
+ });
255
+ return { lines };
256
+ },
257
+
200
258
  formatResult: formatFetchUrlsResult,
201
259
  };
202
260
 
@@ -204,11 +262,15 @@ export const renderUrlLayout: ToolLayoutConfig = {
204
262
  abbreviation: "render",
205
263
 
206
264
  getHeader: (input, result): ToolHeader | null => {
207
- const urlInput = mergeFetchUrlsDefaults(extractFetchUrlsInput(input), result);
265
+ const urlInput = extractRenderUrlInput(input);
208
266
  if (!urlInput) return null;
209
- const headerSuffix = formatFetchUrlsHeader(urlInput);
267
+ const merged = mergeFetchUrlsDefaults(
268
+ urlInput,
269
+ isRecord(result) ? (result as FetchUrlsResultItem) : null
270
+ );
271
+ const headerSuffix = formatFetchUrlsHeader(merged);
210
272
  return {
211
- primary: urlInput.url,
273
+ primary: merged.url,
212
274
  secondary: headerSuffix || undefined,
213
275
  };
214
276
  },
@@ -355,42 +355,33 @@ export function handleSettingsMenuKey(key: KeyEvent, ctx: SettingsMenuContext):
355
355
  }
356
356
  settingIdx++;
357
357
 
358
- if (ctx.interactionMode === "voice") {
359
- if (ctx.selectedIdx === settingIdx) {
360
- const next = !ctx.manager.memoryEnabled;
361
- ctx.manager.memoryEnabled = next;
362
- ctx.setMemoryEnabled(next);
363
- ctx.persistPreferences({ memoryEnabled: next });
364
- key.preventDefault();
365
- return true;
366
- }
367
- settingIdx++;
368
-
369
- if (ctx.selectedIdx === settingIdx) {
370
- const speeds: SpeechSpeed[] = [1.0, 1.25, 1.5, 1.75, 2.0];
371
- const currentSpeed = ctx.manager.speechSpeed;
372
- const currentIndex = speeds.indexOf(currentSpeed);
373
- const nextIndex = (currentIndex + 1) % speeds.length;
374
- const nextSpeed = speeds[nextIndex] ?? 1.0;
375
- ctx.manager.speechSpeed = nextSpeed;
376
- ctx.setSpeechSpeed(nextSpeed);
377
- ctx.persistPreferences({ speechSpeed: nextSpeed });
378
- key.preventDefault();
379
- return true;
380
- }
381
- settingIdx++;
358
+ if (ctx.selectedIdx === settingIdx) {
359
+ const next = !ctx.manager.memoryEnabled;
360
+ ctx.manager.memoryEnabled = next;
361
+ ctx.setMemoryEnabled(next);
362
+ ctx.persistPreferences({ memoryEnabled: next });
363
+ key.preventDefault();
364
+ return true;
382
365
  }
383
- if (ctx.interactionMode !== "voice") {
384
- if (ctx.selectedIdx === settingIdx) {
385
- const next = !ctx.manager.memoryEnabled;
386
- ctx.manager.memoryEnabled = next;
387
- ctx.setMemoryEnabled(next);
388
- ctx.persistPreferences({ memoryEnabled: next });
366
+ settingIdx++;
367
+
368
+ if (ctx.selectedIdx === settingIdx) {
369
+ if (ctx.interactionMode !== "voice") {
389
370
  key.preventDefault();
390
371
  return true;
391
372
  }
392
- settingIdx++;
373
+ const speeds: SpeechSpeed[] = [1.0, 1.25, 1.5, 1.75, 2.0];
374
+ const currentSpeed = ctx.manager.speechSpeed;
375
+ const currentIndex = speeds.indexOf(currentSpeed);
376
+ const nextIndex = (currentIndex + 1) % speeds.length;
377
+ const nextSpeed = speeds[nextIndex] ?? 1.0;
378
+ ctx.manager.speechSpeed = nextSpeed;
379
+ ctx.setSpeechSpeed(nextSpeed);
380
+ ctx.persistPreferences({ speechSpeed: nextSpeed });
381
+ key.preventDefault();
382
+ return true;
393
383
  }
384
+ settingIdx++;
394
385
 
395
386
  if (ctx.selectedIdx === settingIdx) {
396
387
  const next = !ctx.showFullReasoning;
@@ -447,7 +447,6 @@ export interface UrlMenuItem {
447
447
  url: string;
448
448
  groundedCount: number;
449
449
  readPercent?: number;
450
- highlightsCount?: number;
451
450
  status: "ok" | "error";
452
451
  error?: string;
453
452
  lastSeenIndex: number;
@@ -10,6 +10,80 @@ function normalizeUrlKey(rawUrl: string): string {
10
10
  }
11
11
  }
12
12
 
13
+ function unescapeXmlAttribute(value: string): string {
14
+ return value
15
+ .replace(/&quot;/g, '"')
16
+ .replace(/&lt;/g, "<")
17
+ .replace(/&gt;/g, ">")
18
+ .replace(/&amp;/g, "&");
19
+ }
20
+
21
+ function parseFetchUrlsXml(result: string): {
22
+ items: Array<{
23
+ url: string;
24
+ lineOffset?: number;
25
+ lineLimit?: number;
26
+ totalLines?: number;
27
+ remainingLines?: number | null;
28
+ error?: string;
29
+ success: boolean;
30
+ }>;
31
+ error?: string;
32
+ } {
33
+ const items: Array<{
34
+ url: string;
35
+ lineOffset?: number;
36
+ lineLimit?: number;
37
+ totalLines?: number;
38
+ remainingLines?: number | null;
39
+ error?: string;
40
+ success: boolean;
41
+ }> = [];
42
+
43
+ let error: string | undefined;
44
+ const errorMatch = result.match(/<fetchUrls[^>]*\serror="([^"]+)"[^>]*\/?>/);
45
+ if (errorMatch?.[1]) {
46
+ error = unescapeXmlAttribute(errorMatch[1]);
47
+ }
48
+
49
+ const regex = /<url\s+([^>]*?)(\/?)>/g;
50
+ let match = regex.exec(result);
51
+ while (match) {
52
+ const attrText = match[1] ?? "";
53
+ const attrs: Record<string, string> = {};
54
+ for (const attr of attrText.matchAll(/(\w+)="([^"]*)"/g)) {
55
+ const key = attr[1];
56
+ const value = attr[2] ?? "";
57
+ attrs[key] = unescapeXmlAttribute(value);
58
+ }
59
+ const url = attrs.href;
60
+ if (!url) continue;
61
+ const lineOffset = attrs.lineOffset !== undefined ? Number(attrs.lineOffset) : undefined;
62
+ const lineLimit = attrs.lineLimit !== undefined ? Number(attrs.lineLimit) : undefined;
63
+ const totalLines = attrs.totalLines !== undefined ? Number(attrs.totalLines) : undefined;
64
+ let remainingLines: number | null | undefined = undefined;
65
+ if (attrs.remainingLines === "unknown") remainingLines = null;
66
+ else if (attrs.remainingLines !== undefined) remainingLines = Number(attrs.remainingLines);
67
+ const error = attrs.error;
68
+ const success = !error;
69
+ items.push({
70
+ url,
71
+ lineOffset: Number.isFinite(lineOffset) ? lineOffset : undefined,
72
+ lineLimit: Number.isFinite(lineLimit) ? lineLimit : undefined,
73
+ totalLines: Number.isFinite(totalLines) ? totalLines : undefined,
74
+ remainingLines:
75
+ remainingLines === null || (typeof remainingLines === "number" && Number.isFinite(remainingLines))
76
+ ? remainingLines
77
+ : undefined,
78
+ error,
79
+ success,
80
+ });
81
+ match = regex.exec(result);
82
+ }
83
+
84
+ return { items, error };
85
+ }
86
+
13
87
  function computeCoveragePercent(intervals: Array<[number, number]>, totalLines: number): number | undefined {
14
88
  if (!Number.isFinite(totalLines) || totalLines <= 0) return undefined;
15
89
  if (intervals.length === 0) return undefined;
@@ -43,7 +117,6 @@ export function deriveUrlMenuItems(params: {
43
117
 
44
118
  const intervalsByUrl = new Map<string, Array<[number, number]>>();
45
119
  const totalLinesByUrl = new Map<string, number>();
46
- const highlightsCountByUrl = new Map<string, number>();
47
120
  const lastSeenIndexByUrl = new Map<string, number>();
48
121
  const statusByUrl = new Map<string, "ok" | "error">();
49
122
  const errorByUrl = new Map<string, string>();
@@ -57,58 +130,147 @@ export function deriveUrlMenuItems(params: {
57
130
  if (block.type !== "tool") continue;
58
131
  if (block.call.name !== "fetchUrls" && block.call.name !== "renderUrl") continue;
59
132
 
60
- const input = block.call.input as { url?: string } | undefined;
61
- const url = input?.url;
62
- if (!url) continue;
133
+ const input = block.call.input as { url?: string; requests?: Array<{ url?: string }> } | undefined;
134
+ const urls: string[] = [];
135
+ if (input?.url && typeof input.url === "string") {
136
+ urls.push(input.url);
137
+ }
138
+ if (Array.isArray(input?.requests)) {
139
+ for (const request of input.requests) {
140
+ if (request?.url && typeof request.url === "string") {
141
+ urls.push(request.url);
142
+ }
143
+ }
144
+ }
145
+ if (urls.length === 0) continue;
63
146
 
64
- lastSeenIndexByUrl.set(url, blockIndex);
147
+ for (const url of urls) {
148
+ lastSeenIndexByUrl.set(url, blockIndex);
149
+ }
65
150
 
66
151
  const result = block.result as
67
152
  | {
153
+ success?: boolean;
154
+ error?: string;
155
+ results?: Array<{
156
+ success?: boolean;
157
+ url?: string;
158
+ lineOffset?: number;
159
+ lineLimit?: number;
160
+ totalLines?: number;
161
+ error?: string;
162
+ }>;
68
163
  lineOffset?: number;
69
164
  lineLimit?: number;
70
165
  totalLines?: number;
71
- highlights?: unknown[];
72
- success?: boolean;
73
- error?: string;
74
166
  }
75
167
  | undefined;
76
168
 
77
- if (!result || typeof result !== "object") continue;
169
+ if (!result) continue;
78
170
 
79
- if (result.success === false && typeof result.error === "string" && result.error.trim().length > 0) {
80
- statusByUrl.set(url, "error");
81
- errorByUrl.set(url, result.error.trim());
82
- } else if (result.success === true) {
83
- statusByUrl.set(url, "ok");
84
- }
171
+ if (typeof result === "string") {
172
+ const parsed = parseFetchUrlsXml(result);
173
+ for (const item of parsed.items) {
174
+ const itemUrl = item.url;
85
175
 
86
- if (Array.isArray(result.highlights)) {
87
- highlightsCountByUrl.set(url, result.highlights.length);
176
+ if (!item.success && item.error && item.error.trim().length > 0) {
177
+ statusByUrl.set(itemUrl, "error");
178
+ errorByUrl.set(itemUrl, item.error.trim());
179
+ } else if (item.success) {
180
+ statusByUrl.set(itemUrl, "ok");
181
+ }
182
+
183
+ if (typeof item.totalLines === "number" && item.totalLines > 0) {
184
+ const prev = totalLinesByUrl.get(itemUrl) ?? 0;
185
+ totalLinesByUrl.set(itemUrl, Math.max(prev, item.totalLines));
186
+ }
187
+
188
+ if (
189
+ typeof item.lineOffset === "number" &&
190
+ typeof item.lineLimit === "number" &&
191
+ item.lineLimit > 0 &&
192
+ item.lineOffset >= 0
193
+ ) {
194
+ const start = item.lineOffset;
195
+ const end = item.lineOffset + item.lineLimit;
196
+ const list = intervalsByUrl.get(itemUrl) ?? [];
197
+ list.push([start, end]);
198
+ intervalsByUrl.set(itemUrl, list);
199
+ }
200
+ }
201
+ if (parsed.items.length === 0 && parsed.error) {
202
+ for (const url of urls) {
203
+ statusByUrl.set(url, "error");
204
+ errorByUrl.set(url, parsed.error);
205
+ }
206
+ }
207
+ continue;
88
208
  }
89
209
 
90
- if (
210
+ if (typeof result !== "object") continue;
211
+
212
+ if (Array.isArray(result.results)) {
213
+ for (const item of result.results) {
214
+ if (!item || typeof item !== "object") continue;
215
+ const itemUrl = typeof item.url === "string" ? item.url : null;
216
+ if (!itemUrl) continue;
217
+
218
+ if (item.success === false && typeof item.error === "string" && item.error.trim().length > 0) {
219
+ statusByUrl.set(itemUrl, "error");
220
+ errorByUrl.set(itemUrl, item.error.trim());
221
+ } else if (item.success === true) {
222
+ statusByUrl.set(itemUrl, "ok");
223
+ }
224
+
225
+ if (typeof item.totalLines === "number" && Number.isFinite(item.totalLines) && item.totalLines > 0) {
226
+ const prev = totalLinesByUrl.get(itemUrl) ?? 0;
227
+ totalLinesByUrl.set(itemUrl, Math.max(prev, item.totalLines));
228
+ }
229
+
230
+ if (
231
+ typeof item.lineOffset === "number" &&
232
+ typeof item.lineLimit === "number" &&
233
+ Number.isFinite(item.lineOffset) &&
234
+ Number.isFinite(item.lineLimit) &&
235
+ item.lineLimit > 0 &&
236
+ item.lineOffset >= 0
237
+ ) {
238
+ const start = item.lineOffset;
239
+ const end = item.lineOffset + item.lineLimit;
240
+ const list = intervalsByUrl.get(itemUrl) ?? [];
241
+ list.push([start, end]);
242
+ intervalsByUrl.set(itemUrl, list);
243
+ }
244
+ }
245
+ } else if (
246
+ result.success === false &&
247
+ typeof result.error === "string" &&
248
+ result.error.trim().length > 0
249
+ ) {
250
+ for (const url of urls) {
251
+ statusByUrl.set(url, "error");
252
+ errorByUrl.set(url, result.error.trim());
253
+ }
254
+ } else if (result.success === true) {
255
+ for (const url of urls) {
256
+ statusByUrl.set(url, "ok");
257
+ }
258
+ } else if (
91
259
  typeof result.totalLines === "number" &&
92
260
  Number.isFinite(result.totalLines) &&
93
- result.totalLines > 0
94
- ) {
95
- const prev = totalLinesByUrl.get(url) ?? 0;
96
- totalLinesByUrl.set(url, Math.max(prev, result.totalLines));
97
- }
98
-
99
- if (
261
+ result.totalLines > 0 &&
100
262
  typeof result.lineOffset === "number" &&
101
- typeof result.lineLimit === "number" &&
102
- Number.isFinite(result.lineOffset) &&
103
- Number.isFinite(result.lineLimit) &&
104
- result.lineLimit > 0 &&
105
- result.lineOffset >= 0
263
+ typeof result.lineLimit === "number"
106
264
  ) {
107
- const start = result.lineOffset;
108
- const end = result.lineOffset + result.lineLimit;
109
- const list = intervalsByUrl.get(url) ?? [];
110
- list.push([start, end]);
111
- intervalsByUrl.set(url, list);
265
+ for (const url of urls) {
266
+ const prev = totalLinesByUrl.get(url) ?? 0;
267
+ totalLinesByUrl.set(url, Math.max(prev, result.totalLines));
268
+ const start = result.lineOffset;
269
+ const end = result.lineOffset + result.lineLimit;
270
+ const list = intervalsByUrl.get(url) ?? [];
271
+ list.push([start, end]);
272
+ intervalsByUrl.set(url, list);
273
+ }
112
274
  }
113
275
  }
114
276
 
@@ -134,7 +296,6 @@ export function deriveUrlMenuItems(params: {
134
296
  const urls = [...lastSeenIndexByUrl.keys()];
135
297
  return urls.map((url) => {
136
298
  const groundedCount = lookupGroundedCount(url);
137
- const highlightsCount = highlightsCountByUrl.get(url);
138
299
  const totalLines = totalLinesByUrl.get(url);
139
300
  const intervals = intervalsByUrl.get(url) ?? [];
140
301
  const readPercent = totalLines !== undefined ? computeCoveragePercent(intervals, totalLines) : undefined;
@@ -146,7 +307,6 @@ export function deriveUrlMenuItems(params: {
146
307
  url,
147
308
  groundedCount,
148
309
  readPercent,
149
- highlightsCount,
150
310
  status,
151
311
  error,
152
312
  lastSeenIndex,
@@ -12,6 +12,7 @@ const PREFERENCES_VERSION = 1;
12
12
  const APP_DIR_NAME = "daemon";
13
13
  const PREFERENCES_FILE = "preferences.json";
14
14
  const CREDENTIALS_FILE = "credentials.json";
15
+ const CONFIG_DIR_ENV = "DAEMON_CONFIG_DIR";
15
16
 
16
17
  /** Keys that belong in credentials.json (secrets) vs preferences.json (settings) */
17
18
  const CREDENTIAL_KEYS = ["openRouterApiKey", "openAiApiKey", "exaApiKey"] as const;
@@ -32,6 +33,8 @@ function getBaseConfigDir(): string {
32
33
  }
33
34
 
34
35
  export function getAppConfigDir(): string {
36
+ const override = process.env[CONFIG_DIR_ENV]?.trim();
37
+ if (override) return override;
35
38
  return path.join(getBaseConfigDir(), APP_DIR_NAME);
36
39
  }
37
40