@makefinks/daemon 0.8.0 → 0.9.1

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.
@@ -10,6 +10,81 @@ 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(/&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
+ if (!key) continue;
57
+ const value = attr[2] ?? "";
58
+ attrs[key] = unescapeXmlAttribute(value);
59
+ }
60
+ const url = attrs.href;
61
+ if (!url) continue;
62
+ const lineOffset = attrs.lineOffset !== undefined ? Number(attrs.lineOffset) : undefined;
63
+ const lineLimit = attrs.lineLimit !== undefined ? Number(attrs.lineLimit) : undefined;
64
+ const totalLines = attrs.totalLines !== undefined ? Number(attrs.totalLines) : undefined;
65
+ let remainingLines: number | null | undefined = undefined;
66
+ if (attrs.remainingLines === "unknown") remainingLines = null;
67
+ else if (attrs.remainingLines !== undefined) remainingLines = Number(attrs.remainingLines);
68
+ const error = attrs.error;
69
+ const success = !error;
70
+ items.push({
71
+ url,
72
+ lineOffset: Number.isFinite(lineOffset) ? lineOffset : undefined,
73
+ lineLimit: Number.isFinite(lineLimit) ? lineLimit : undefined,
74
+ totalLines: Number.isFinite(totalLines) ? totalLines : undefined,
75
+ remainingLines:
76
+ remainingLines === null || (typeof remainingLines === "number" && Number.isFinite(remainingLines))
77
+ ? remainingLines
78
+ : undefined,
79
+ error,
80
+ success,
81
+ });
82
+ match = regex.exec(result);
83
+ }
84
+
85
+ return { items, error };
86
+ }
87
+
13
88
  function computeCoveragePercent(intervals: Array<[number, number]>, totalLines: number): number | undefined {
14
89
  if (!Number.isFinite(totalLines) || totalLines <= 0) return undefined;
15
90
  if (intervals.length === 0) return undefined;
@@ -43,7 +118,6 @@ export function deriveUrlMenuItems(params: {
43
118
 
44
119
  const intervalsByUrl = new Map<string, Array<[number, number]>>();
45
120
  const totalLinesByUrl = new Map<string, number>();
46
- const highlightsCountByUrl = new Map<string, number>();
47
121
  const lastSeenIndexByUrl = new Map<string, number>();
48
122
  const statusByUrl = new Map<string, "ok" | "error">();
49
123
  const errorByUrl = new Map<string, string>();
@@ -57,58 +131,147 @@ export function deriveUrlMenuItems(params: {
57
131
  if (block.type !== "tool") continue;
58
132
  if (block.call.name !== "fetchUrls" && block.call.name !== "renderUrl") continue;
59
133
 
60
- const input = block.call.input as { url?: string } | undefined;
61
- const url = input?.url;
62
- if (!url) continue;
134
+ const input = block.call.input as { url?: string; requests?: Array<{ url?: string }> } | undefined;
135
+ const urls: string[] = [];
136
+ if (input?.url && typeof input.url === "string") {
137
+ urls.push(input.url);
138
+ }
139
+ if (Array.isArray(input?.requests)) {
140
+ for (const request of input.requests) {
141
+ if (request?.url && typeof request.url === "string") {
142
+ urls.push(request.url);
143
+ }
144
+ }
145
+ }
146
+ if (urls.length === 0) continue;
63
147
 
64
- lastSeenIndexByUrl.set(url, blockIndex);
148
+ for (const url of urls) {
149
+ lastSeenIndexByUrl.set(url, blockIndex);
150
+ }
65
151
 
66
152
  const result = block.result as
67
153
  | {
154
+ success?: boolean;
155
+ error?: string;
156
+ results?: Array<{
157
+ success?: boolean;
158
+ url?: string;
159
+ lineOffset?: number;
160
+ lineLimit?: number;
161
+ totalLines?: number;
162
+ error?: string;
163
+ }>;
68
164
  lineOffset?: number;
69
165
  lineLimit?: number;
70
166
  totalLines?: number;
71
- highlights?: unknown[];
72
- success?: boolean;
73
- error?: string;
74
167
  }
75
168
  | undefined;
76
169
 
77
- if (!result || typeof result !== "object") continue;
170
+ if (!result) continue;
78
171
 
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
- }
172
+ if (typeof result === "string") {
173
+ const parsed = parseFetchUrlsXml(result);
174
+ for (const item of parsed.items) {
175
+ const itemUrl = item.url;
85
176
 
86
- if (Array.isArray(result.highlights)) {
87
- highlightsCountByUrl.set(url, result.highlights.length);
177
+ if (!item.success && item.error && item.error.trim().length > 0) {
178
+ statusByUrl.set(itemUrl, "error");
179
+ errorByUrl.set(itemUrl, item.error.trim());
180
+ } else if (item.success) {
181
+ statusByUrl.set(itemUrl, "ok");
182
+ }
183
+
184
+ if (typeof item.totalLines === "number" && item.totalLines > 0) {
185
+ const prev = totalLinesByUrl.get(itemUrl) ?? 0;
186
+ totalLinesByUrl.set(itemUrl, Math.max(prev, item.totalLines));
187
+ }
188
+
189
+ if (
190
+ typeof item.lineOffset === "number" &&
191
+ typeof item.lineLimit === "number" &&
192
+ item.lineLimit > 0 &&
193
+ item.lineOffset >= 0
194
+ ) {
195
+ const start = item.lineOffset;
196
+ const end = item.lineOffset + item.lineLimit;
197
+ const list = intervalsByUrl.get(itemUrl) ?? [];
198
+ list.push([start, end]);
199
+ intervalsByUrl.set(itemUrl, list);
200
+ }
201
+ }
202
+ if (parsed.items.length === 0 && parsed.error) {
203
+ for (const url of urls) {
204
+ statusByUrl.set(url, "error");
205
+ errorByUrl.set(url, parsed.error);
206
+ }
207
+ }
208
+ continue;
88
209
  }
89
210
 
90
- if (
211
+ if (typeof result !== "object") continue;
212
+
213
+ if (Array.isArray(result.results)) {
214
+ for (const item of result.results) {
215
+ if (!item || typeof item !== "object") continue;
216
+ const itemUrl = typeof item.url === "string" ? item.url : null;
217
+ if (!itemUrl) continue;
218
+
219
+ if (item.success === false && typeof item.error === "string" && item.error.trim().length > 0) {
220
+ statusByUrl.set(itemUrl, "error");
221
+ errorByUrl.set(itemUrl, item.error.trim());
222
+ } else if (item.success === true) {
223
+ statusByUrl.set(itemUrl, "ok");
224
+ }
225
+
226
+ if (typeof item.totalLines === "number" && Number.isFinite(item.totalLines) && item.totalLines > 0) {
227
+ const prev = totalLinesByUrl.get(itemUrl) ?? 0;
228
+ totalLinesByUrl.set(itemUrl, Math.max(prev, item.totalLines));
229
+ }
230
+
231
+ if (
232
+ typeof item.lineOffset === "number" &&
233
+ typeof item.lineLimit === "number" &&
234
+ Number.isFinite(item.lineOffset) &&
235
+ Number.isFinite(item.lineLimit) &&
236
+ item.lineLimit > 0 &&
237
+ item.lineOffset >= 0
238
+ ) {
239
+ const start = item.lineOffset;
240
+ const end = item.lineOffset + item.lineLimit;
241
+ const list = intervalsByUrl.get(itemUrl) ?? [];
242
+ list.push([start, end]);
243
+ intervalsByUrl.set(itemUrl, list);
244
+ }
245
+ }
246
+ } else if (
247
+ result.success === false &&
248
+ typeof result.error === "string" &&
249
+ result.error.trim().length > 0
250
+ ) {
251
+ for (const url of urls) {
252
+ statusByUrl.set(url, "error");
253
+ errorByUrl.set(url, result.error.trim());
254
+ }
255
+ } else if (result.success === true) {
256
+ for (const url of urls) {
257
+ statusByUrl.set(url, "ok");
258
+ }
259
+ } else if (
91
260
  typeof result.totalLines === "number" &&
92
261
  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 (
262
+ result.totalLines > 0 &&
100
263
  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
264
+ typeof result.lineLimit === "number"
106
265
  ) {
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);
266
+ for (const url of urls) {
267
+ const prev = totalLinesByUrl.get(url) ?? 0;
268
+ totalLinesByUrl.set(url, Math.max(prev, result.totalLines));
269
+ const start = result.lineOffset;
270
+ const end = result.lineOffset + result.lineLimit;
271
+ const list = intervalsByUrl.get(url) ?? [];
272
+ list.push([start, end]);
273
+ intervalsByUrl.set(url, list);
274
+ }
112
275
  }
113
276
  }
114
277
 
@@ -134,7 +297,6 @@ export function deriveUrlMenuItems(params: {
134
297
  const urls = [...lastSeenIndexByUrl.keys()];
135
298
  return urls.map((url) => {
136
299
  const groundedCount = lookupGroundedCount(url);
137
- const highlightsCount = highlightsCountByUrl.get(url);
138
300
  const totalLines = totalLinesByUrl.get(url);
139
301
  const intervals = intervalsByUrl.get(url) ?? [];
140
302
  const readPercent = totalLines !== undefined ? computeCoveragePercent(intervals, totalLines) : undefined;
@@ -146,7 +308,6 @@ export function deriveUrlMenuItems(params: {
146
308
  url,
147
309
  groundedCount,
148
310
  readPercent,
149
- highlightsCount,
150
311
  status,
151
312
  error,
152
313
  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
 
@@ -8,6 +8,10 @@ function normalizeWhitespace(text: string): string {
8
8
  return text.replace(/\r\n/g, "\n").replace(/\t/g, " ");
9
9
  }
10
10
 
11
+ function escapeXmlAttribute(value: string): string {
12
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
13
+ }
14
+
11
15
  function truncateText(text: string, maxChars: number): { text: string; truncated: boolean } {
12
16
  if (text.length <= maxChars) return { text, truncated: false };
13
17
  if (maxChars <= 1) return { text: "…", truncated: true };
@@ -61,6 +65,10 @@ type ExaLikeItem = {
61
65
  text?: unknown;
62
66
  lineOffset?: unknown;
63
67
  lineLimit?: unknown;
68
+ success?: unknown;
69
+ error?: unknown;
70
+ remainingLines?: unknown;
71
+ totalLines?: unknown;
64
72
  };
65
73
 
66
74
  function extractExaItems(data: unknown): ExaLikeItem[] | null {
@@ -105,43 +113,124 @@ function formatExaSearchResult(result: unknown): string | null {
105
113
  }
106
114
 
107
115
  function formatExaFetchResult(result: unknown): string | null {
116
+ if (typeof result === "string") {
117
+ const trimmed = result.trim();
118
+ if (!trimmed) return null;
119
+ if (trimmed.startsWith("<fetchUrls") && trimmed.includes("error=") && !trimmed.includes("</fetchUrls>")) {
120
+ return trimmed;
121
+ }
122
+
123
+ const rawLines = trimmed
124
+ .split("\n")
125
+ .map((line) => line.trimEnd())
126
+ .filter((line) => line.trim().length > 0);
127
+ const hasFetchHeader = rawLines.some((line) => line.includes("<fetchUrls"));
128
+ if (!hasFetchHeader) return trimmed;
129
+
130
+ const headerLine = rawLines.find((line) => line.includes("<fetchUrls"));
131
+ const closingLine = rawLines.find((line) => line.includes("</fetchUrls>"));
132
+ const urlLines = rawLines.filter((line) => line.includes("<url"));
133
+ const errorLines = urlLines.filter((line) => line.includes("error="));
134
+ const normalLines = urlLines.filter((line) => !line.includes("error="));
135
+
136
+ let contentLine: string | undefined;
137
+ if (normalLines.length === 1) {
138
+ const firstUrlIdx = rawLines.findIndex((line) => line.includes("<url") && line.includes("href="));
139
+ if (firstUrlIdx >= 0) {
140
+ contentLine = rawLines
141
+ .slice(firstUrlIdx + 1)
142
+ .find(
143
+ (line) => !line.includes("</url>") && !line.includes("<url") && !line.includes("</fetchUrls>")
144
+ )
145
+ ?.trim();
146
+ }
147
+ }
148
+
149
+ const lines: string[] = [];
150
+ if (headerLine) lines.push(headerLine);
151
+ if (normalLines[0]) lines.push(normalLines[0]);
152
+
153
+ const firstErrorLine = errorLines[0];
154
+ if (firstErrorLine) {
155
+ lines.push(firstErrorLine);
156
+ } else if (contentLine) {
157
+ lines.push(contentLine);
158
+ }
159
+
160
+ if (contentLine && errorLines.length > 0 && lines.length < 4) {
161
+ lines.push(contentLine);
162
+ }
163
+
164
+ if (closingLine && lines.length < 4) {
165
+ lines.push(closingLine);
166
+ }
167
+
168
+ return lines.length > 0 ? lines.join("\n") : trimmed;
169
+ }
108
170
  if (!isRecord(result)) return null;
109
171
  if (result.success === false && typeof result.error === "string") {
110
172
  return `error: ${result.error}`;
111
173
  }
112
174
  if (result.success !== true) return null;
113
175
  const data = extractToolDataContainer(result);
114
- const candidate = isRecord(data) ? (data as ExaLikeItem & { remainingLines?: unknown }) : {};
115
- const label = formatExaItemLabel(candidate);
116
- const url = typeof candidate.url === "string" ? candidate.url : "";
117
- const title = typeof candidate.title === "string" ? candidate.title : "";
118
- const lineOffset = typeof candidate.lineOffset === "number" ? candidate.lineOffset : undefined;
119
- const lineLimit = typeof candidate.lineLimit === "number" ? candidate.lineLimit : undefined;
120
- const remainingLines =
121
- typeof (candidate as { remainingLines?: unknown }).remainingLines === "number" ||
122
- (candidate as { remainingLines?: unknown }).remainingLines === null
123
- ? (candidate as { remainingLines: number | null }).remainingLines
124
- : undefined;
125
- const rangeParts: string[] = [];
126
- if (lineOffset !== undefined) rangeParts.push(`lineOffset=${lineOffset}`);
127
- if (lineLimit !== undefined) rangeParts.push(`lineLimit=${lineLimit}`);
128
- if (remainingLines !== undefined) {
129
- rangeParts.push(remainingLines === null ? "remainingLines=unknown" : `remainingLines=${remainingLines}`);
130
- }
131
- const remainingSuffix = rangeParts.length > 0 ? ` (${rangeParts.join(", ")})` : "";
176
+ const container = isRecord(data) ? data : {};
177
+ const results = Array.isArray(container.results) ? container.results : null;
178
+ if (!results) return null;
179
+
180
+ const lines: string[] = ["<fetchUrls>"];
181
+ const maxItems = 3;
182
+ const MAX_LINES = 2;
183
+ const MAX_CHARS = 160;
184
+
185
+ for (const item of results.slice(0, maxItems)) {
186
+ if (!isRecord(item)) continue;
187
+
188
+ const candidate = item as ExaLikeItem & {
189
+ remainingLines?: unknown;
190
+ totalLines?: unknown;
191
+ error?: unknown;
192
+ };
193
+ const url = typeof candidate.url === "string" ? candidate.url : "";
194
+ if (!url) continue;
195
+
196
+ const attributes: string[] = [`href="${escapeXmlAttribute(url)}"`];
197
+ if (typeof candidate.lineOffset === "number") attributes.push(`lineOffset="${candidate.lineOffset}"`);
198
+ if (typeof candidate.lineLimit === "number") attributes.push(`lineLimit="${candidate.lineLimit}"`);
199
+ if (typeof candidate.totalLines === "number") attributes.push(`totalLines="${candidate.totalLines}"`);
200
+ if (typeof candidate.remainingLines === "number") {
201
+ attributes.push(`remainingLines="${candidate.remainingLines}"`);
202
+ } else if (candidate.remainingLines === null) {
203
+ attributes.push(`remainingLines="unknown"`);
204
+ }
132
205
 
133
- const headerBase = url && title ? `${label} ${url}` : label;
134
- const header = `${headerBase}${remainingSuffix}`;
206
+ if (candidate.success === false && typeof candidate.error === "string") {
207
+ attributes.push(`error="${escapeXmlAttribute(candidate.error)}"`);
208
+ lines.push(` <url ${attributes.join(" ")} />`);
209
+ continue;
210
+ }
135
211
 
136
- const text = typeof candidate.text === "string" ? candidate.text : "";
137
- if (!text.trim()) return header;
212
+ const text = typeof candidate.text === "string" ? candidate.text : "";
213
+ if (!text.trim()) {
214
+ lines.push(` <url ${attributes.join(" ")} />`);
215
+ continue;
216
+ }
138
217
 
139
- // Provide a small snippet; downstream truncation enforces the hard caps.
140
- const snippet = normalizeWhitespace(text)
141
- .replace(/\n{3,}/g, "\n\n")
142
- .trim();
218
+ const snippetLines = normalizeWhitespace(text)
219
+ .replace(/\n{3,}/g, "\n\n")
220
+ .trim()
221
+ .split("\n")
222
+ .slice(0, MAX_LINES)
223
+ .map((l) => (l.length > MAX_CHARS ? `${l.slice(0, MAX_CHARS - 1)}…` : l));
143
224
 
144
- return `${header}\n${snippet}`;
225
+ lines.push(` <url ${attributes.join(" ")}>`);
226
+ for (const line of snippetLines) {
227
+ lines.push(` ${escapeXmlAttribute(line)}`);
228
+ }
229
+ lines.push(" </url>");
230
+ }
231
+
232
+ lines.push("</fetchUrls>");
233
+ return lines.length > 2 ? lines.join("\n") : null;
145
234
  }
146
235
 
147
236
  function formatRenderUrlResult(result: unknown): string | null {