@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.
- package/package.json +1 -1
- package/src/ai/system-prompt.ts +47 -41
- package/src/ai/tools/fetch-urls.ts +153 -125
- package/src/components/SettingsMenu.tsx +36 -27
- package/src/components/UrlMenu.tsx +2 -7
- package/src/components/tool-layouts/layouts/subagent.tsx +16 -0
- package/src/components/tool-layouts/layouts/url-tools.ts +142 -80
- package/src/hooks/keyboard-handlers.ts +22 -31
- package/src/types/index.ts +0 -1
- package/src/utils/derive-url-menu-items.ts +197 -37
- package/src/utils/preferences.ts +3 -0
- package/src/utils/tool-output-preview.ts +111 -27
|
@@ -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
|
|
11
|
+
interface FetchUrlsRequestInput {
|
|
11
12
|
url: string;
|
|
12
13
|
lineOffset?: number;
|
|
13
14
|
lineLimit?: number;
|
|
14
|
-
highlightQuery?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
if (!
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
135
|
+
const items = extractFetchUrlsResults(result);
|
|
136
|
+
if (!items) return null;
|
|
90
137
|
|
|
91
|
-
const
|
|
92
|
-
const
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
|
192
|
-
if (!
|
|
193
|
-
const
|
|
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:
|
|
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 =
|
|
265
|
+
const urlInput = extractRenderUrlInput(input);
|
|
208
266
|
if (!urlInput) return null;
|
|
209
|
-
const
|
|
267
|
+
const merged = mergeFetchUrlsDefaults(
|
|
268
|
+
urlInput,
|
|
269
|
+
isRecord(result) ? (result as FetchUrlsResultItem) : null
|
|
270
|
+
);
|
|
271
|
+
const headerSuffix = formatFetchUrlsHeader(merged);
|
|
210
272
|
return {
|
|
211
|
-
primary:
|
|
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.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
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;
|
package/src/types/index.ts
CHANGED
|
@@ -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(/"/g, '"')
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/&/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
|
|
62
|
-
if (
|
|
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
|
-
|
|
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
|
|
169
|
+
if (!result) continue;
|
|
78
170
|
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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,
|
package/src/utils/preferences.ts
CHANGED
|
@@ -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
|
|