droid-patch 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -129
- package/README.zh-CN.md +80 -127
- package/dist/{alias-DkFWCjWn.mjs → alias-CX4QSelz.mjs} +10 -7
- package/dist/alias-CX4QSelz.mjs.map +1 -0
- package/dist/cli.mjs +542 -3241
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/alias-DkFWCjWn.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as removeAlias, d as listAllMetadata, f as loadAliasMetadata, i as listAliases, l as createMetadata, m as patchDroid, n as createAlias, o as removeAliasesByFilter, p as saveAliasMetadata, r as createAliasForWrapper, t as clearAllAliases, u as formatPatches } from "./alias-
|
|
2
|
+
import { a as removeAlias, d as listAllMetadata, f as loadAliasMetadata, i as listAliases, l as createMetadata, m as patchDroid, n as createAlias, o as removeAliasesByFilter, p as saveAliasMetadata, r as createAliasForWrapper, t as clearAllAliases, u as formatPatches } from "./alias-CX4QSelz.mjs";
|
|
3
3
|
import bin from "tiny-bin";
|
|
4
4
|
import { styleText } from "node:util";
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -9,21 +9,16 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import { execSync } from "node:child_process";
|
|
10
10
|
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
11
11
|
|
|
12
|
-
//#region src/websearch-
|
|
12
|
+
//#region src/websearch-external.ts
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
* Since BUN_CONFIG_PRELOAD doesn't work with compiled binaries,
|
|
16
|
-
* use a local proxy server to intercept search requests instead
|
|
14
|
+
* WebSearch External Providers Mode (--websearch)
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
* The proxy is killed automatically when droid exits.
|
|
20
|
-
* @param factoryApiUrl - Custom Factory API URL (default: https://api.factory.ai)
|
|
16
|
+
* Priority: Smithery Exa > Google PSE > Serper > Brave > SearXNG > DuckDuckGo
|
|
21
17
|
*/
|
|
22
|
-
function
|
|
18
|
+
function generateSearchProxyServerCode() {
|
|
23
19
|
return `#!/usr/bin/env node
|
|
24
|
-
// Droid WebSearch Proxy Server
|
|
25
|
-
//
|
|
26
|
-
// This proxy runs as a child process of droid and is killed when droid exits
|
|
20
|
+
// Droid WebSearch Proxy Server (External Providers Mode)
|
|
21
|
+
// Priority: Smithery Exa > Google PSE > Serper > Brave > SearXNG > DuckDuckGo
|
|
27
22
|
|
|
28
23
|
const http = require('http');
|
|
29
24
|
const https = require('https');
|
|
@@ -31,75 +26,38 @@ const { execSync } = require('child_process');
|
|
|
31
26
|
const fs = require('fs');
|
|
32
27
|
|
|
33
28
|
const DEBUG = process.env.DROID_SEARCH_DEBUG === '1';
|
|
34
|
-
const PORT = parseInt(process.env.SEARCH_PROXY_PORT || '0');
|
|
35
|
-
const FACTORY_API = '
|
|
29
|
+
const PORT = parseInt(process.env.SEARCH_PROXY_PORT || '0');
|
|
30
|
+
const FACTORY_API = 'https://api.factory.ai';
|
|
36
31
|
|
|
37
|
-
function log(
|
|
38
|
-
if (DEBUG) console.error('[websearch]', ...args);
|
|
39
|
-
}
|
|
32
|
+
function log() { if (DEBUG) console.error.apply(console, ['[websearch]'].concat(Array.from(arguments))); }
|
|
40
33
|
|
|
41
|
-
// === Search
|
|
34
|
+
// === External Search Providers ===
|
|
42
35
|
|
|
43
|
-
// Smithery Exa MCP - highest priority, requires SMITHERY_API_KEY and SMITHERY_PROFILE
|
|
44
36
|
async function searchSmitheryExa(query, numResults) {
|
|
45
37
|
const apiKey = process.env.SMITHERY_API_KEY;
|
|
46
38
|
const profile = process.env.SMITHERY_PROFILE;
|
|
47
39
|
if (!apiKey || !profile) return null;
|
|
48
|
-
|
|
49
40
|
try {
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const requestBody = JSON.stringify({
|
|
56
|
-
jsonrpc: '2.0',
|
|
57
|
-
id: 1,
|
|
58
|
-
method: 'tools/call',
|
|
59
|
-
params: {
|
|
60
|
-
name: 'web_search_exa',
|
|
61
|
-
arguments: {
|
|
62
|
-
query: query,
|
|
63
|
-
numResults: numResults
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const curlCmd = \`curl -s -X POST "\${serverUrl}" -H "Content-Type: application/json" -d '\${requestBody.replace(/'/g, "'\\\\\\\\''")}'\`;
|
|
69
|
-
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 30000 });
|
|
70
|
-
const response = JSON.parse(jsonStr);
|
|
71
|
-
|
|
72
|
-
// Parse MCP response
|
|
41
|
+
const serverUrl = 'https://server.smithery.ai/exa/mcp?api_key=' + encodeURIComponent(apiKey) + '&profile=' + encodeURIComponent(profile);
|
|
42
|
+
const requestBody = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'web_search_exa', arguments: { query: query, numResults: numResults } } });
|
|
43
|
+
const bodyStr = requestBody.replace(/'/g, "'\\\\''");
|
|
44
|
+
const curlCmd = 'curl -s -X POST "' + serverUrl + '" -H "Content-Type: application/json" -d \\'' + bodyStr + "\\'";
|
|
45
|
+
const response = JSON.parse(execSync(curlCmd, { encoding: 'utf-8', timeout: 30000 }));
|
|
73
46
|
if (response.result && response.result.content) {
|
|
74
|
-
|
|
75
|
-
const textContent = response.result.content.find(c => c.type === 'text');
|
|
47
|
+
const textContent = response.result.content.find(function(c) { return c.type === 'text'; });
|
|
76
48
|
if (textContent && textContent.text) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return
|
|
81
|
-
title: item.title || '',
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
author: item.author || null,
|
|
86
|
-
score: item.score || null
|
|
87
|
-
}));
|
|
88
|
-
}
|
|
89
|
-
} catch (parseErr) {
|
|
90
|
-
log('Smithery response parsing failed');
|
|
49
|
+
const results = JSON.parse(textContent.text);
|
|
50
|
+
if (Array.isArray(results) && results.length > 0) {
|
|
51
|
+
return results.slice(0, numResults).map(function(item) {
|
|
52
|
+
return {
|
|
53
|
+
title: item.title || '', url: item.url || '',
|
|
54
|
+
content: item.text || item.snippet || (item.highlights ? item.highlights.join(' ') : '') || ''
|
|
55
|
+
};
|
|
56
|
+
});
|
|
91
57
|
}
|
|
92
58
|
}
|
|
93
59
|
}
|
|
94
|
-
|
|
95
|
-
if (response.error) {
|
|
96
|
-
log('Smithery Exa error:', response.error.message || response.error);
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
} catch (e) {
|
|
100
|
-
log('Smithery Exa failed:', e.message);
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
60
|
+
} catch (e) { log('Smithery failed:', e.message); }
|
|
103
61
|
return null;
|
|
104
62
|
}
|
|
105
63
|
|
|
@@ -107,3228 +65,554 @@ async function searchGooglePSE(query, numResults) {
|
|
|
107
65
|
const apiKey = process.env.GOOGLE_PSE_API_KEY;
|
|
108
66
|
const cx = process.env.GOOGLE_PSE_CX;
|
|
109
67
|
if (!apiKey || !cx) return null;
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
const url = \`https://www.googleapis.com/customsearch/v1?key=\${apiKey}&cx=\${cx}&q=\${encodeURIComponent(query)}&num=\${Math.min(numResults, 10)}\`;
|
|
113
|
-
log('Google PSE request:', url.replace(apiKey, '***'));
|
|
114
|
-
|
|
115
|
-
const curlCmd = \`curl -s "\${url}"\`;
|
|
116
|
-
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });
|
|
117
|
-
const data = JSON.parse(jsonStr);
|
|
118
|
-
|
|
119
|
-
if (data.error) {
|
|
120
|
-
log('Google PSE error:', data.error.message);
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
return (data.items || []).map(item => ({
|
|
124
|
-
title: item.title,
|
|
125
|
-
url: item.link,
|
|
126
|
-
content: item.snippet || '',
|
|
127
|
-
publishedDate: null,
|
|
128
|
-
author: null,
|
|
129
|
-
score: null
|
|
130
|
-
}));
|
|
131
|
-
} catch (e) {
|
|
132
|
-
log('Google PSE failed:', e.message);
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// SearXNG - self-hosted meta search engine
|
|
138
|
-
async function searchSearXNG(query, numResults) {
|
|
139
|
-
const searxngUrl = process.env.SEARXNG_URL;
|
|
140
|
-
if (!searxngUrl) return null;
|
|
141
|
-
|
|
142
68
|
try {
|
|
143
|
-
const url =
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const data = JSON.parse(jsonStr);
|
|
149
|
-
|
|
150
|
-
if (data.results && data.results.length > 0) {
|
|
151
|
-
return data.results.slice(0, numResults).map(item => ({
|
|
152
|
-
title: item.title,
|
|
153
|
-
url: item.url,
|
|
154
|
-
content: item.content || '',
|
|
155
|
-
publishedDate: null,
|
|
156
|
-
author: null,
|
|
157
|
-
score: null
|
|
158
|
-
}));
|
|
159
|
-
}
|
|
160
|
-
} catch (e) {
|
|
161
|
-
log('SearXNG failed:', e.message);
|
|
162
|
-
}
|
|
69
|
+
const url = 'https://www.googleapis.com/customsearch/v1?key=' + apiKey + '&cx=' + cx + '&q=' + encodeURIComponent(query) + '&num=' + Math.min(numResults, 10);
|
|
70
|
+
const data = JSON.parse(execSync('curl -s "' + url + '"', { encoding: 'utf-8', timeout: 15000 }));
|
|
71
|
+
if (data.error) return null;
|
|
72
|
+
return (data.items || []).map(function(item) { return { title: item.title, url: item.link, content: item.snippet || '' }; });
|
|
73
|
+
} catch (e) { log('Google PSE failed:', e.message); }
|
|
163
74
|
return null;
|
|
164
75
|
}
|
|
165
76
|
|
|
166
|
-
// Serper API - free tier available (2500 queries/month)
|
|
167
77
|
async function searchSerper(query, numResults) {
|
|
168
78
|
const apiKey = process.env.SERPER_API_KEY;
|
|
169
79
|
if (!apiKey) return null;
|
|
170
|
-
|
|
171
80
|
try {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });
|
|
176
|
-
const data = JSON.parse(jsonStr);
|
|
177
|
-
|
|
81
|
+
const bodyStr = JSON.stringify({ q: query, num: numResults }).replace(/'/g, "'\\\\''");
|
|
82
|
+
const curlCmd = 'curl -s "https://google.serper.dev/search" -H "X-API-KEY: ' + apiKey + '" -H "Content-Type: application/json" -d \\'' + bodyStr + "\\'";
|
|
83
|
+
const data = JSON.parse(execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 }));
|
|
178
84
|
if (data.organic && data.organic.length > 0) {
|
|
179
|
-
return data.organic.slice(0, numResults).map(item
|
|
180
|
-
title: item.title,
|
|
181
|
-
url: item.link,
|
|
182
|
-
content: item.snippet || '',
|
|
183
|
-
publishedDate: null,
|
|
184
|
-
author: null,
|
|
185
|
-
score: null
|
|
186
|
-
}));
|
|
85
|
+
return data.organic.slice(0, numResults).map(function(item) { return { title: item.title, url: item.link, content: item.snippet || '' }; });
|
|
187
86
|
}
|
|
188
|
-
} catch (e) {
|
|
189
|
-
log('Serper failed:', e.message);
|
|
190
|
-
}
|
|
87
|
+
} catch (e) { log('Serper failed:', e.message); }
|
|
191
88
|
return null;
|
|
192
89
|
}
|
|
193
90
|
|
|
194
|
-
// Brave Search API - free tier available
|
|
195
91
|
async function searchBrave(query, numResults) {
|
|
196
92
|
const apiKey = process.env.BRAVE_API_KEY;
|
|
197
93
|
if (!apiKey) return null;
|
|
198
|
-
|
|
199
94
|
try {
|
|
200
|
-
const url =
|
|
201
|
-
const curlCmd =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });
|
|
205
|
-
const data = JSON.parse(jsonStr);
|
|
206
|
-
|
|
95
|
+
const url = 'https://api.search.brave.com/res/v1/web/search?q=' + encodeURIComponent(query) + '&count=' + numResults;
|
|
96
|
+
const curlCmd = 'curl -s "' + url + '" -H "Accept: application/json" -H "X-Subscription-Token: ' + apiKey + '"';
|
|
97
|
+
const data = JSON.parse(execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 }));
|
|
207
98
|
if (data.web && data.web.results && data.web.results.length > 0) {
|
|
208
|
-
return data.web.results.slice(0, numResults).map(item
|
|
209
|
-
title: item.title,
|
|
210
|
-
url: item.url,
|
|
211
|
-
content: item.description || '',
|
|
212
|
-
publishedDate: null,
|
|
213
|
-
author: null,
|
|
214
|
-
score: null
|
|
215
|
-
}));
|
|
99
|
+
return data.web.results.slice(0, numResults).map(function(item) { return { title: item.title, url: item.url, content: item.description || '' }; });
|
|
216
100
|
}
|
|
217
|
-
} catch (e) {
|
|
218
|
-
log('Brave failed:', e.message);
|
|
219
|
-
}
|
|
101
|
+
} catch (e) { log('Brave failed:', e.message); }
|
|
220
102
|
return null;
|
|
221
103
|
}
|
|
222
104
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
105
|
+
async function searchSearXNG(query, numResults) {
|
|
106
|
+
const searxngUrl = process.env.SEARXNG_URL;
|
|
107
|
+
if (!searxngUrl) return null;
|
|
226
108
|
try {
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
109
|
+
const url = searxngUrl + '/search?q=' + encodeURIComponent(query) + '&format=json&engines=google,bing,duckduckgo';
|
|
110
|
+
const data = JSON.parse(execSync('curl -s "' + url + '" -H "Accept: application/json"', { encoding: 'utf-8', timeout: 15000 }));
|
|
111
|
+
if (data.results && data.results.length > 0) {
|
|
112
|
+
return data.results.slice(0, numResults).map(function(item) { return { title: item.title, url: item.url, content: item.content || '' }; });
|
|
113
|
+
}
|
|
114
|
+
} catch (e) { log('SearXNG failed:', e.message); }
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
231
117
|
|
|
118
|
+
async function searchDuckDuckGo(query, numResults) {
|
|
119
|
+
try {
|
|
120
|
+
const apiUrl = 'https://api.duckduckgo.com/?q=' + encodeURIComponent(query) + '&format=json&no_html=1&skip_disambig=1';
|
|
121
|
+
const data = JSON.parse(execSync('curl -s "' + apiUrl + '" -H "User-Agent: Mozilla/5.0"', { encoding: 'utf-8', timeout: 15000 }));
|
|
232
122
|
const results = [];
|
|
233
|
-
|
|
234
123
|
if (data.Abstract && data.AbstractURL) {
|
|
235
|
-
results.push({
|
|
236
|
-
title: data.Heading || query,
|
|
237
|
-
url: data.AbstractURL,
|
|
238
|
-
content: data.Abstract,
|
|
239
|
-
publishedDate: null,
|
|
240
|
-
author: null,
|
|
241
|
-
score: null
|
|
242
|
-
});
|
|
124
|
+
results.push({ title: data.Heading || query, url: data.AbstractURL, content: data.Abstract });
|
|
243
125
|
}
|
|
244
|
-
|
|
245
|
-
for (
|
|
246
|
-
|
|
247
|
-
if (topic.Text && topic.FirstURL) {
|
|
248
|
-
results.push({
|
|
249
|
-
title: topic.Text.substring(0, 100),
|
|
250
|
-
url: topic.FirstURL,
|
|
251
|
-
content: topic.Text,
|
|
252
|
-
publishedDate: null,
|
|
253
|
-
author: null,
|
|
254
|
-
score: null
|
|
255
|
-
});
|
|
256
|
-
}
|
|
126
|
+
var topics = data.RelatedTopics || [];
|
|
127
|
+
for (var i = 0; i < topics.length && results.length < numResults; i++) {
|
|
128
|
+
var topic = topics[i];
|
|
129
|
+
if (topic.Text && topic.FirstURL) results.push({ title: topic.Text.substring(0, 100), url: topic.FirstURL, content: topic.Text });
|
|
257
130
|
if (topic.Topics) {
|
|
258
|
-
for (
|
|
259
|
-
|
|
260
|
-
if (st.Text && st.FirstURL) {
|
|
261
|
-
results.push({
|
|
262
|
-
title: st.Text.substring(0, 100),
|
|
263
|
-
url: st.FirstURL,
|
|
264
|
-
content: st.Text,
|
|
265
|
-
publishedDate: null,
|
|
266
|
-
author: null,
|
|
267
|
-
score: null
|
|
268
|
-
});
|
|
269
|
-
}
|
|
131
|
+
for (var j = 0; j < topic.Topics.length && results.length < numResults; j++) {
|
|
132
|
+
var st = topic.Topics[j];
|
|
133
|
+
if (st.Text && st.FirstURL) results.push({ title: st.Text.substring(0, 100), url: st.FirstURL, content: st.Text });
|
|
270
134
|
}
|
|
271
135
|
}
|
|
272
136
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return results;
|
|
277
|
-
}
|
|
278
|
-
} catch (e) {
|
|
279
|
-
log('DDG API failed:', e.message);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return [];
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function parseDDGLiteHTML(html, maxResults) {
|
|
286
|
-
const results = [];
|
|
287
|
-
const linkRegex = /<a[^>]+rel="nofollow"[^>]+href="([^"]+)"[^>]*>([^<]+)<\\/a>/gi;
|
|
288
|
-
const snippetRegex = /<td[^>]*class="result-snippet"[^>]*>([^<]*)<\\/td>/gi;
|
|
289
|
-
|
|
290
|
-
const links = [];
|
|
291
|
-
let match;
|
|
292
|
-
|
|
293
|
-
while ((match = linkRegex.exec(html)) !== null && links.length < maxResults) {
|
|
294
|
-
let url = match[1];
|
|
295
|
-
if (url.includes('duckduckgo.com') && !url.includes('uddg=')) continue;
|
|
296
|
-
if (url.includes('uddg=')) {
|
|
297
|
-
const uddgMatch = url.match(/uddg=([^&]+)/);
|
|
298
|
-
if (uddgMatch) url = decodeURIComponent(uddgMatch[1]);
|
|
299
|
-
}
|
|
300
|
-
links.push({
|
|
301
|
-
url: url,
|
|
302
|
-
title: decodeHTMLEntities(match[2].trim())
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const snippets = [];
|
|
307
|
-
while ((match = snippetRegex.exec(html)) !== null && snippets.length < maxResults) {
|
|
308
|
-
snippets.push(decodeHTMLEntities(match[1].trim()));
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
for (let i = 0; i < links.length && results.length < maxResults; i++) {
|
|
312
|
-
results.push({
|
|
313
|
-
title: links[i].title,
|
|
314
|
-
url: links[i].url,
|
|
315
|
-
content: snippets[i] || '',
|
|
316
|
-
publishedDate: null,
|
|
317
|
-
author: null,
|
|
318
|
-
score: null
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return results;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function decodeHTMLEntities(str) {
|
|
326
|
-
return str
|
|
327
|
-
.replace(/&/g, '&')
|
|
328
|
-
.replace(/</g, '<')
|
|
329
|
-
.replace(/>/g, '>')
|
|
330
|
-
.replace(/"/g, '"')
|
|
331
|
-
.replace(/'/g, "'")
|
|
332
|
-
.replace(/ /g, ' ');
|
|
137
|
+
return results.length > 0 ? results : null;
|
|
138
|
+
} catch (e) { log('DuckDuckGo failed:', e.message); }
|
|
139
|
+
return null;
|
|
333
140
|
}
|
|
334
141
|
|
|
335
|
-
async function search(query, numResults
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const serperResults = await searchSerper(query, numResults);
|
|
360
|
-
if (serperResults && serperResults.length > 0) {
|
|
361
|
-
log('Using Serper');
|
|
362
|
-
return { results: serperResults, source: 'serper' };
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// 4. Brave Search
|
|
366
|
-
const braveResults = await searchBrave(query, numResults);
|
|
367
|
-
if (braveResults && braveResults.length > 0) {
|
|
368
|
-
log('Using Brave Search');
|
|
369
|
-
return { results: braveResults, source: 'brave' };
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// 5. SearXNG
|
|
373
|
-
const searxngResults = await searchSearXNG(query, numResults);
|
|
374
|
-
if (searxngResults && searxngResults.length > 0) {
|
|
375
|
-
log('Using SearXNG');
|
|
376
|
-
return { results: searxngResults, source: 'searxng' };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// 6. DuckDuckGo (last resort, limited results)
|
|
380
|
-
log('Using DuckDuckGo (fallback)');
|
|
381
|
-
const ddgResults = await searchDuckDuckGo(query, numResults);
|
|
382
|
-
return { results: ddgResults, source: 'duckduckgo' };
|
|
142
|
+
async function search(query, numResults) {
|
|
143
|
+
numResults = numResults || 10;
|
|
144
|
+
log('Search:', query);
|
|
145
|
+
|
|
146
|
+
// Priority: Smithery > Google PSE > Serper > Brave > SearXNG > DuckDuckGo
|
|
147
|
+
var results = await searchSmitheryExa(query, numResults);
|
|
148
|
+
if (results && results.length > 0) return { results: results, source: 'smithery-exa' };
|
|
149
|
+
|
|
150
|
+
results = await searchGooglePSE(query, numResults);
|
|
151
|
+
if (results && results.length > 0) return { results: results, source: 'google-pse' };
|
|
152
|
+
|
|
153
|
+
results = await searchSerper(query, numResults);
|
|
154
|
+
if (results && results.length > 0) return { results: results, source: 'serper' };
|
|
155
|
+
|
|
156
|
+
results = await searchBrave(query, numResults);
|
|
157
|
+
if (results && results.length > 0) return { results: results, source: 'brave' };
|
|
158
|
+
|
|
159
|
+
results = await searchSearXNG(query, numResults);
|
|
160
|
+
if (results && results.length > 0) return { results: results, source: 'searxng' };
|
|
161
|
+
|
|
162
|
+
results = await searchDuckDuckGo(query, numResults);
|
|
163
|
+
if (results && results.length > 0) return { results: results, source: 'duckduckgo' };
|
|
164
|
+
|
|
165
|
+
return { results: [], source: 'none' };
|
|
383
166
|
}
|
|
384
167
|
|
|
385
168
|
// === HTTP Proxy Server ===
|
|
386
169
|
|
|
387
170
|
const server = http.createServer(async (req, res) => {
|
|
388
|
-
const url = new URL(req.url,
|
|
171
|
+
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
389
172
|
|
|
390
|
-
// Health check
|
|
391
173
|
if (url.pathname === '/health') {
|
|
392
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
393
|
-
res.end(JSON.stringify({ status: 'ok',
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Search endpoint - intercept
|
|
398
|
-
if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {
|
|
399
|
-
let body = '';
|
|
400
|
-
req.on('data', c => body += c);
|
|
401
|
-
req.on('end', async () => {
|
|
402
|
-
try {
|
|
403
|
-
const { query, numResults } = JSON.parse(body);
|
|
404
|
-
log('Search query:', query);
|
|
405
|
-
const { results, source } = await search(query, numResults || 10);
|
|
406
|
-
log('Results:', results.length, 'from', source);
|
|
407
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
408
|
-
res.end(JSON.stringify({ results }));
|
|
409
|
-
} catch (e) {
|
|
410
|
-
log('Search error:', e.message);
|
|
411
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
412
|
-
res.end(JSON.stringify({ error: String(e), results: [] }));
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// === Standalone mode (controlled by STANDALONE_MODE env) ===
|
|
419
|
-
// Whitelist approach: only allow core LLM APIs, mock everything else
|
|
420
|
-
if (process.env.STANDALONE_MODE === '1') {
|
|
421
|
-
const pathname = url.pathname;
|
|
422
|
-
|
|
423
|
-
// Whitelist: Core APIs that should be forwarded to upstream
|
|
424
|
-
const isCoreLLMApi = pathname.startsWith('/api/llm/a/') || pathname.startsWith('/api/llm/o/');
|
|
425
|
-
// /api/tools/exa/search is already handled above
|
|
426
|
-
|
|
427
|
-
if (!isCoreLLMApi) {
|
|
428
|
-
// Special handling for specific routes
|
|
429
|
-
if (pathname === '/api/sessions/create') {
|
|
430
|
-
log('Mock (dynamic):', pathname);
|
|
431
|
-
const sessionId = \`local-\${Date.now()}-\${Math.random().toString(36).slice(2, 10)}\`;
|
|
432
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
433
|
-
res.end(JSON.stringify({ id: sessionId }));
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (pathname === '/api/cli/whoami') {
|
|
438
|
-
log('Mock (401):', pathname);
|
|
439
|
-
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
440
|
-
res.end(JSON.stringify({ error: 'Unauthorized', message: 'Local mode - use token fallback' }));
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (pathname === '/api/tools/get-url-contents') {
|
|
445
|
-
log('Mock (404):', pathname);
|
|
446
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
447
|
-
res.end(JSON.stringify({ error: 'Not available', message: 'Use local URL fetch fallback' }));
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// All other non-core APIs: return empty success
|
|
452
|
-
log('Mock (default):', pathname);
|
|
453
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
454
|
-
res.end(JSON.stringify({}));
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Proxy core LLM requests to upstream API
|
|
460
|
-
log('Proxy:', req.method, url.pathname);
|
|
461
|
-
|
|
462
|
-
const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);
|
|
463
|
-
// Choose http or https based on target protocol
|
|
464
|
-
const proxyModule = proxyUrl.protocol === 'https:' ? https : http;
|
|
465
|
-
const proxyReq = proxyModule.request(proxyUrl, {
|
|
466
|
-
method: req.method,
|
|
467
|
-
headers: { ...req.headers, host: proxyUrl.host }
|
|
468
|
-
}, proxyRes => {
|
|
469
|
-
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
470
|
-
proxyRes.pipe(res);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
proxyReq.on('error', e => {
|
|
474
|
-
log('Proxy error:', e.message);
|
|
475
|
-
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
476
|
-
res.end(JSON.stringify({ error: 'Proxy failed: ' + e.message }));
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
480
|
-
req.pipe(proxyReq);
|
|
481
|
-
} else {
|
|
482
|
-
proxyReq.end();
|
|
483
|
-
}
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
// If port is 0, system will automatically assign an available port
|
|
487
|
-
server.listen(PORT, '127.0.0.1', () => {
|
|
488
|
-
const actualPort = server.address().port;
|
|
489
|
-
const hasGoogle = process.env.GOOGLE_PSE_API_KEY && process.env.GOOGLE_PSE_CX;
|
|
490
|
-
|
|
491
|
-
// Write port file for parent process to read
|
|
492
|
-
const portFile = process.env.SEARCH_PROXY_PORT_FILE;
|
|
493
|
-
if (portFile) {
|
|
494
|
-
fs.writeFileSync(portFile, String(actualPort));
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Output PORT= line for wrapper script to parse
|
|
498
|
-
console.log('PORT=' + actualPort);
|
|
499
|
-
|
|
500
|
-
const hasSmithery = process.env.SMITHERY_API_KEY && process.env.SMITHERY_PROFILE;
|
|
501
|
-
log('Search proxy started on http://127.0.0.1:' + actualPort);
|
|
502
|
-
log('Smithery Exa:', hasSmithery ? 'configured (priority 1)' : 'not set');
|
|
503
|
-
log('Google PSE:', hasGoogle ? 'configured' : 'not set');
|
|
504
|
-
log('Serper:', process.env.SERPER_API_KEY ? 'configured' : 'not set');
|
|
505
|
-
log('Brave:', process.env.BRAVE_API_KEY ? 'configured' : 'not set');
|
|
506
|
-
log('SearXNG:', process.env.SEARXNG_URL || 'not set');
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
process.on('SIGTERM', () => { server.close(); process.exit(0); });
|
|
510
|
-
process.on('SIGINT', () => { server.close(); process.exit(0); });
|
|
511
|
-
`;
|
|
512
|
-
}
|
|
513
|
-
/**
|
|
514
|
-
* Generate unified Wrapper script
|
|
515
|
-
* Each droid instance runs its own proxy:
|
|
516
|
-
* - Uses port 0 to let system auto-assign available port
|
|
517
|
-
* - Proxy runs as child process
|
|
518
|
-
* - Proxy is killed when droid exits
|
|
519
|
-
* - Supports multiple droid instances running simultaneously
|
|
520
|
-
*/
|
|
521
|
-
function generateUnifiedWrapper(droidPath, proxyScriptPath, standalone = false) {
|
|
522
|
-
const standaloneEnv = standalone ? "STANDALONE_MODE=1 " : "";
|
|
523
|
-
return `#!/bin/bash
|
|
524
|
-
# Droid with WebSearch
|
|
525
|
-
# Auto-generated by droid-patch --websearch
|
|
526
|
-
# Each instance runs its own proxy on a system-assigned port
|
|
527
|
-
|
|
528
|
-
PROXY_SCRIPT="${proxyScriptPath}"
|
|
529
|
-
DROID_BIN="${droidPath}"
|
|
530
|
-
PROXY_PID=""
|
|
531
|
-
PORT_FILE="/tmp/droid-websearch-\$\$.port"
|
|
532
|
-
STANDALONE="${standalone ? "1" : "0"}"
|
|
533
|
-
|
|
534
|
-
# Passthrough for non-interactive/meta commands (avoid starting a proxy for help/version/etc)
|
|
535
|
-
should_passthrough() {
|
|
536
|
-
# Any help/version flags before "--"
|
|
537
|
-
for arg in "\$@"; do
|
|
538
|
-
if [ "\$arg" = "--" ]; then
|
|
539
|
-
break
|
|
540
|
-
fi
|
|
541
|
-
case "\$arg" in
|
|
542
|
-
--help|-h|--version|-V)
|
|
543
|
-
return 0
|
|
544
|
-
;;
|
|
545
|
-
esac
|
|
546
|
-
done
|
|
547
|
-
|
|
548
|
-
# Top-level command token
|
|
549
|
-
local end_opts=0
|
|
550
|
-
for arg in "\$@"; do
|
|
551
|
-
if [ "\$arg" = "--" ]; then
|
|
552
|
-
end_opts=1
|
|
553
|
-
continue
|
|
554
|
-
fi
|
|
555
|
-
if [ "\$end_opts" -eq 0 ] && [[ "\$arg" == -* ]]; then
|
|
556
|
-
continue
|
|
557
|
-
fi
|
|
558
|
-
case "\$arg" in
|
|
559
|
-
help|version|completion|completions|exec)
|
|
560
|
-
return 0
|
|
561
|
-
;;
|
|
562
|
-
esac
|
|
563
|
-
break
|
|
564
|
-
done
|
|
565
|
-
|
|
566
|
-
return 1
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
if should_passthrough "\$@"; then
|
|
570
|
-
exec "\$DROID_BIN" "\$@"
|
|
571
|
-
fi
|
|
572
|
-
|
|
573
|
-
# Cleanup function - kill proxy when droid exits
|
|
574
|
-
cleanup() {
|
|
575
|
-
if [ -n "\$PROXY_PID" ] && kill -0 "\$PROXY_PID" 2>/dev/null; then
|
|
576
|
-
[ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Stopping proxy (PID: \$PROXY_PID)" >&2
|
|
577
|
-
kill "\$PROXY_PID" 2>/dev/null
|
|
578
|
-
wait "\$PROXY_PID" 2>/dev/null
|
|
579
|
-
fi
|
|
580
|
-
rm -f "\$PORT_FILE"
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
# Set up trap to cleanup on exit
|
|
584
|
-
trap cleanup EXIT INT TERM
|
|
585
|
-
|
|
586
|
-
[ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Starting proxy..." >&2
|
|
587
|
-
[ "\$STANDALONE" = "1" ] && [ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Standalone mode enabled" >&2
|
|
588
|
-
|
|
589
|
-
# Start proxy with port 0 (system will assign available port)
|
|
590
|
-
# Proxy writes actual port to PORT_FILE
|
|
591
|
-
if [ -n "\$DROID_SEARCH_DEBUG" ]; then
|
|
592
|
-
${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="\$PORT_FILE" node "\$PROXY_SCRIPT" 2>&1 &
|
|
593
|
-
else
|
|
594
|
-
${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="\$PORT_FILE" node "\$PROXY_SCRIPT" >/dev/null 2>&1 &
|
|
595
|
-
fi
|
|
596
|
-
PROXY_PID=\$!
|
|
597
|
-
|
|
598
|
-
# Wait for proxy to start and get actual port (max 5 seconds)
|
|
599
|
-
for i in {1..50}; do
|
|
600
|
-
# Check if proxy process is still running
|
|
601
|
-
if ! kill -0 "\$PROXY_PID" 2>/dev/null; then
|
|
602
|
-
[ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy process died" >&2
|
|
603
|
-
break
|
|
604
|
-
fi
|
|
605
|
-
if [ -f "\$PORT_FILE" ]; then
|
|
606
|
-
ACTUAL_PORT=\$(cat "\$PORT_FILE" 2>/dev/null)
|
|
607
|
-
if [ -n "\$ACTUAL_PORT" ] && curl -s "http://127.0.0.1:\$ACTUAL_PORT/health" > /dev/null 2>&1; then
|
|
608
|
-
[ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy ready on port \$ACTUAL_PORT (PID: \$PROXY_PID)" >&2
|
|
609
|
-
break
|
|
610
|
-
fi
|
|
611
|
-
fi
|
|
612
|
-
sleep 0.1
|
|
613
|
-
done
|
|
614
|
-
|
|
615
|
-
# Check if proxy started successfully
|
|
616
|
-
if [ ! -f "\$PORT_FILE" ] || [ -z "\$(cat "\$PORT_FILE" 2>/dev/null)" ]; then
|
|
617
|
-
echo "[websearch] Failed to start proxy, running without websearch" >&2
|
|
618
|
-
cleanup
|
|
619
|
-
exec "\$DROID_BIN" "\$@"
|
|
620
|
-
fi
|
|
621
|
-
|
|
622
|
-
ACTUAL_PORT=\$(cat "\$PORT_FILE")
|
|
623
|
-
rm -f "\$PORT_FILE"
|
|
624
|
-
|
|
625
|
-
# Run droid with proxy
|
|
626
|
-
export FACTORY_API_BASE_URL_OVERRIDE="http://127.0.0.1:\$ACTUAL_PORT"
|
|
627
|
-
"\$DROID_BIN" "\$@"
|
|
628
|
-
DROID_EXIT_CODE=\$?
|
|
629
|
-
|
|
630
|
-
# Cleanup will be called by trap
|
|
631
|
-
exit \$DROID_EXIT_CODE
|
|
632
|
-
`;
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* Create unified WebSearch files
|
|
636
|
-
*
|
|
637
|
-
* Approach: Proxy server mode
|
|
638
|
-
* - wrapper script starts local proxy server
|
|
639
|
-
* - proxy server intercepts search requests, passes through other requests
|
|
640
|
-
* - uses FACTORY_API_BASE_URL_OVERRIDE env var to point to proxy
|
|
641
|
-
* - alias works directly, no extra steps needed
|
|
642
|
-
*
|
|
643
|
-
* @param outputDir - Directory to write files to
|
|
644
|
-
* @param droidPath - Path to droid binary
|
|
645
|
-
* @param aliasName - Alias name for the wrapper
|
|
646
|
-
* @param apiBase - Custom API base URL for proxy to forward requests to
|
|
647
|
-
* @param standalone - Standalone mode: mock non-LLM Factory APIs
|
|
648
|
-
*/
|
|
649
|
-
async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName, apiBase, standalone = false) {
|
|
650
|
-
if (!existsSync(outputDir)) await mkdir(outputDir, { recursive: true });
|
|
651
|
-
const proxyScriptPath = join(outputDir, `${aliasName}-proxy.js`);
|
|
652
|
-
const wrapperScriptPath = join(outputDir, aliasName);
|
|
653
|
-
await writeFile(proxyScriptPath, generateSearchProxyServer(apiBase || "https://api.factory.ai"));
|
|
654
|
-
console.log(`[*] Created proxy script: ${proxyScriptPath}`);
|
|
655
|
-
await writeFile(wrapperScriptPath, generateUnifiedWrapper(droidPath, proxyScriptPath, standalone));
|
|
656
|
-
await chmod(wrapperScriptPath, 493);
|
|
657
|
-
console.log(`[*] Created wrapper: ${wrapperScriptPath}`);
|
|
658
|
-
if (standalone) console.log(`[*] Standalone mode enabled`);
|
|
659
|
-
return {
|
|
660
|
-
wrapperScript: wrapperScriptPath,
|
|
661
|
-
preloadScript: proxyScriptPath
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
//#endregion
|
|
666
|
-
//#region src/statusline-patch.ts
|
|
667
|
-
function generateStatuslineMonitorScript() {
|
|
668
|
-
return `#!/usr/bin/env node
|
|
669
|
-
/* Auto-generated by droid-patch --statusline */
|
|
670
|
-
|
|
671
|
-
const fs = require('fs');
|
|
672
|
-
const os = require('os');
|
|
673
|
-
const path = require('path');
|
|
674
|
-
const { spawn, spawnSync } = require('child_process');
|
|
675
|
-
|
|
676
|
-
// This monitor does NOT draw directly to the terminal. It emits newline-delimited
|
|
677
|
-
// statusline frames to stdout. A wrapper (PTY proxy) is responsible for rendering
|
|
678
|
-
// the latest frame on a reserved bottom row to avoid flicker.
|
|
679
|
-
|
|
680
|
-
const FACTORY_HOME = path.join(os.homedir(), '.factory');
|
|
681
|
-
|
|
682
|
-
const SESSIONS_ROOT = path.join(FACTORY_HOME, 'sessions');
|
|
683
|
-
const LOG_PATH = path.join(FACTORY_HOME, 'logs', 'droid-log-single.log');
|
|
684
|
-
const CONFIG_PATH = path.join(FACTORY_HOME, 'config.json');
|
|
685
|
-
const GLOBAL_SETTINGS_PATH = path.join(FACTORY_HOME, 'settings.json');
|
|
686
|
-
|
|
687
|
-
const IS_APPLE_TERMINAL = process.env.TERM_PROGRAM === 'Apple_Terminal';
|
|
688
|
-
const MIN_RENDER_INTERVAL_MS = IS_APPLE_TERMINAL ? 1000 : 500;
|
|
689
|
-
|
|
690
|
-
const START_MS = Date.now();
|
|
691
|
-
const ARGS = process.argv.slice(2);
|
|
692
|
-
const PGID = Number(process.env.DROID_STATUSLINE_PGID || '');
|
|
693
|
-
const SESSION_ID_RE = /"sessionId":"([0-9a-f-]{36})"/i;
|
|
694
|
-
|
|
695
|
-
function sleep(ms) {
|
|
696
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function isPositiveInt(n) {
|
|
700
|
-
return Number.isFinite(n) && n > 0;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function extractSessionIdFromLine(line) {
|
|
704
|
-
if (!line) return null;
|
|
705
|
-
const m = String(line).match(SESSION_ID_RE);
|
|
706
|
-
return m ? m[1] : null;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
function parseLineTimestampMs(line) {
|
|
710
|
-
const s = String(line || '');
|
|
711
|
-
if (!s || s[0] !== '[') return null;
|
|
712
|
-
const end = s.indexOf(']');
|
|
713
|
-
if (end <= 1) return null;
|
|
714
|
-
const raw = s.slice(1, end);
|
|
715
|
-
const ms = Date.parse(raw);
|
|
716
|
-
return Number.isFinite(ms) ? ms : null;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function safeStatMtimeMs(p) {
|
|
720
|
-
try {
|
|
721
|
-
const stat = fs.statSync(p);
|
|
722
|
-
const ms = Number(stat?.mtimeMs ?? 0);
|
|
723
|
-
return Number.isFinite(ms) ? ms : 0;
|
|
724
|
-
} catch {
|
|
725
|
-
return 0;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function nextCompactionState(line, current) {
|
|
730
|
-
if (!line) return current;
|
|
731
|
-
if (line.includes('[Compaction] Start')) return true;
|
|
732
|
-
const endMarkers = ['End', 'Done', 'Finish', 'Finished', 'Complete', 'Completed'];
|
|
733
|
-
if (endMarkers.some(m => line.includes('[Compaction] ' + m))) return false;
|
|
734
|
-
return current;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function firstNonNull(promises) {
|
|
738
|
-
const list = Array.isArray(promises) ? promises : [];
|
|
739
|
-
if (list.length === 0) return Promise.resolve(null);
|
|
740
|
-
return new Promise((resolve) => {
|
|
741
|
-
let pending = list.length;
|
|
742
|
-
let done = false;
|
|
743
|
-
for (const p of list) {
|
|
744
|
-
Promise.resolve(p)
|
|
745
|
-
.then((value) => {
|
|
746
|
-
if (done) return;
|
|
747
|
-
if (value) {
|
|
748
|
-
done = true;
|
|
749
|
-
resolve(value);
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
pending -= 1;
|
|
753
|
-
if (pending <= 0) resolve(null);
|
|
754
|
-
})
|
|
755
|
-
.catch(() => {
|
|
756
|
-
if (done) return;
|
|
757
|
-
pending -= 1;
|
|
758
|
-
if (pending <= 0) resolve(null);
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function listPidsInProcessGroup(pgid) {
|
|
765
|
-
if (!isPositiveInt(pgid)) return [];
|
|
766
|
-
try {
|
|
767
|
-
const res = spawnSync('ps', ['-ax', '-o', 'pid=,pgid='], {
|
|
768
|
-
encoding: 'utf8',
|
|
769
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
770
|
-
timeout: 800,
|
|
771
|
-
});
|
|
772
|
-
if (!res || res.status !== 0) return [];
|
|
773
|
-
const out = String(res.stdout || '');
|
|
774
|
-
const pids = [];
|
|
775
|
-
for (const line of out.split('\\n')) {
|
|
776
|
-
const parts = line.trim().split(/\\s+/);
|
|
777
|
-
if (parts.length < 2) continue;
|
|
778
|
-
const pid = Number(parts[0]);
|
|
779
|
-
const g = Number(parts[1]);
|
|
780
|
-
if (Number.isFinite(pid) && g === pgid) pids.push(pid);
|
|
781
|
-
}
|
|
782
|
-
return pids;
|
|
783
|
-
} catch {
|
|
784
|
-
return [];
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
function resolveOpenSessionFromPids(pids) {
|
|
789
|
-
if (!Array.isArray(pids) || pids.length === 0) return null;
|
|
790
|
-
// lsof prints file names as lines prefixed with "n" when using -Fn
|
|
791
|
-
try {
|
|
792
|
-
const res = spawnSync('lsof', ['-p', pids.join(','), '-Fn'], {
|
|
793
|
-
encoding: 'utf8',
|
|
794
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
795
|
-
timeout: 1200,
|
|
796
|
-
});
|
|
797
|
-
if (!res || res.status !== 0) return null;
|
|
798
|
-
const out = String(res.stdout || '');
|
|
799
|
-
for (const line of out.split('\\n')) {
|
|
800
|
-
if (!line || line[0] !== 'n') continue;
|
|
801
|
-
const name = line.slice(1);
|
|
802
|
-
if (!name.startsWith(SESSIONS_ROOT + path.sep)) continue;
|
|
803
|
-
const m = name.match(/([0-9a-f-]{36})\\.(jsonl|settings\\.json)$/i);
|
|
804
|
-
if (!m) continue;
|
|
805
|
-
const id = m[1];
|
|
806
|
-
const workspaceDir = path.dirname(name);
|
|
807
|
-
if (path.dirname(workspaceDir) !== SESSIONS_ROOT) continue;
|
|
808
|
-
return { workspaceDir, id };
|
|
809
|
-
}
|
|
810
|
-
} catch {
|
|
811
|
-
return null;
|
|
812
|
-
}
|
|
813
|
-
return null;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
async function resolveSessionFromProcessGroup(shouldAbort, maxTries = 20) {
|
|
817
|
-
if (!isPositiveInt(PGID)) return null;
|
|
818
|
-
// Wait a little for droid to create/open the session files.
|
|
819
|
-
for (let i = 0; i < maxTries; i++) {
|
|
820
|
-
if (shouldAbort && shouldAbort()) return null;
|
|
821
|
-
const pids = listPidsInProcessGroup(PGID);
|
|
822
|
-
const found = resolveOpenSessionFromPids(pids);
|
|
823
|
-
if (found) return found;
|
|
824
|
-
await sleep(100);
|
|
825
|
-
}
|
|
826
|
-
return null;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function safeReadFile(filePath) {
|
|
830
|
-
try {
|
|
831
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
832
|
-
} catch {
|
|
833
|
-
return null;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
function safeJsonParse(text) {
|
|
838
|
-
if (!text) return null;
|
|
839
|
-
try {
|
|
840
|
-
// Factory settings/config files can contain comments. Strip them safely without
|
|
841
|
-
// breaking URLs like "http://..." which contain "//" inside strings.
|
|
842
|
-
const stripComments = (input) => {
|
|
843
|
-
let out = '';
|
|
844
|
-
let inString = false;
|
|
845
|
-
let escape = false;
|
|
846
|
-
for (let i = 0; i < input.length; i++) {
|
|
847
|
-
const ch = input[i];
|
|
848
|
-
const next = input[i + 1];
|
|
849
|
-
|
|
850
|
-
if (inString) {
|
|
851
|
-
out += ch;
|
|
852
|
-
if (escape) {
|
|
853
|
-
escape = false;
|
|
854
|
-
continue;
|
|
855
|
-
}
|
|
856
|
-
if (ch === '\\\\') {
|
|
857
|
-
escape = true;
|
|
858
|
-
continue;
|
|
859
|
-
}
|
|
860
|
-
if (ch === '"') {
|
|
861
|
-
inString = false;
|
|
862
|
-
}
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
if (ch === '"') {
|
|
867
|
-
inString = true;
|
|
868
|
-
out += ch;
|
|
869
|
-
continue;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// Line comment
|
|
873
|
-
if (ch === '/' && next === '/') {
|
|
874
|
-
while (i < input.length && input[i] !== '\\n') i++;
|
|
875
|
-
out += '\\n';
|
|
876
|
-
continue;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// Block comment
|
|
880
|
-
if (ch === '/' && next === '*') {
|
|
881
|
-
i += 2;
|
|
882
|
-
while (i < input.length && !(input[i] === '*' && input[i + 1] === '/')) i++;
|
|
883
|
-
i += 1;
|
|
884
|
-
continue;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
out += ch;
|
|
888
|
-
}
|
|
889
|
-
return out;
|
|
890
|
-
};
|
|
891
|
-
|
|
892
|
-
return JSON.parse(stripComments(text));
|
|
893
|
-
} catch {
|
|
894
|
-
return null;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
function readJsonFile(filePath) {
|
|
899
|
-
return safeJsonParse(safeReadFile(filePath));
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
function isUuid(text) {
|
|
903
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(text);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
function parseResume(args) {
|
|
907
|
-
for (let i = 0; i < args.length; i++) {
|
|
908
|
-
const a = args[i];
|
|
909
|
-
if (a === '-r' || a === '--resume') {
|
|
910
|
-
const next = args[i + 1];
|
|
911
|
-
if (next && isUuid(next)) return { resumeFlag: true, resumeId: next };
|
|
912
|
-
return { resumeFlag: true, resumeId: null };
|
|
913
|
-
}
|
|
914
|
-
if (a.startsWith('--resume=')) {
|
|
915
|
-
const value = a.slice('--resume='.length);
|
|
916
|
-
return { resumeFlag: true, resumeId: isUuid(value) ? value : null };
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
return { resumeFlag: false, resumeId: null };
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
function sanitizeWorkspaceDirName(cwd) {
|
|
923
|
-
return String(cwd)
|
|
924
|
-
.replace(/[:]/g, '')
|
|
925
|
-
.replace(/[\\\\/]/g, '-')
|
|
926
|
-
.replace(/\\s+/g, '-');
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
function listSessionCandidates(workspaceDir) {
|
|
930
|
-
let files = [];
|
|
931
|
-
try {
|
|
932
|
-
files = fs.readdirSync(workspaceDir);
|
|
933
|
-
} catch {
|
|
934
|
-
return [];
|
|
935
|
-
}
|
|
936
|
-
const candidates = [];
|
|
937
|
-
for (const file of files) {
|
|
938
|
-
const m = file.match(/^([0-9a-f-]{36})\\.(jsonl|settings\\.json)$/i);
|
|
939
|
-
if (!m) continue;
|
|
940
|
-
const id = m[1];
|
|
941
|
-
const fullPath = path.join(workspaceDir, file);
|
|
942
|
-
try {
|
|
943
|
-
const stat = fs.statSync(fullPath);
|
|
944
|
-
candidates.push({ id, fullPath, mtimeMs: stat.mtimeMs });
|
|
945
|
-
} catch {
|
|
946
|
-
// ignore
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
return candidates;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function findWorkspaceDirForSessionId(workspaceDirs, sessionId) {
|
|
953
|
-
for (const dir of workspaceDirs) {
|
|
954
|
-
try {
|
|
955
|
-
const settingsPath = path.join(dir, sessionId + '.settings.json');
|
|
956
|
-
if (fs.existsSync(settingsPath)) return dir;
|
|
957
|
-
} catch {
|
|
958
|
-
// ignore
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
return null;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function pickLatestSessionAcross(workspaceDirs) {
|
|
965
|
-
let best = null;
|
|
966
|
-
for (const dir of workspaceDirs) {
|
|
967
|
-
const candidates = listSessionCandidates(dir);
|
|
968
|
-
for (const c of candidates) {
|
|
969
|
-
if (!best || c.mtimeMs > best.mtimeMs) {
|
|
970
|
-
best = { workspaceDir: dir, id: c.id, mtimeMs: c.mtimeMs };
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
return best ? { workspaceDir: best.workspaceDir, id: best.id } : null;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
async function waitForNewSessionAcross(workspaceDirs, knownIdsByWorkspace, startMs, shouldAbort) {
|
|
978
|
-
for (let i = 0; i < 80; i++) {
|
|
979
|
-
if (shouldAbort && shouldAbort()) return null;
|
|
980
|
-
let best = null;
|
|
981
|
-
for (const dir of workspaceDirs) {
|
|
982
|
-
const known = knownIdsByWorkspace.get(dir) || new Set();
|
|
983
|
-
const candidates = listSessionCandidates(dir);
|
|
984
|
-
for (const c of candidates) {
|
|
985
|
-
if (!(c.mtimeMs >= startMs - 50 || !known.has(c.id))) continue;
|
|
986
|
-
if (!best || c.mtimeMs > best.mtimeMs) {
|
|
987
|
-
best = { workspaceDir: dir, id: c.id, mtimeMs: c.mtimeMs };
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
if (best?.id) return { workspaceDir: best.workspaceDir, id: best.id };
|
|
992
|
-
await sleep(100);
|
|
993
|
-
}
|
|
994
|
-
return null;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
function safeRealpath(p) {
|
|
998
|
-
try {
|
|
999
|
-
return fs.realpathSync(p);
|
|
1000
|
-
} catch {
|
|
1001
|
-
return null;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
function resolveWorkspaceDirs(cwd) {
|
|
1006
|
-
const logical = cwd;
|
|
1007
|
-
const real = safeRealpath(cwd);
|
|
1008
|
-
const dirs = [];
|
|
1009
|
-
for (const value of [logical, real]) {
|
|
1010
|
-
if (!value || typeof value !== 'string') continue;
|
|
1011
|
-
dirs.push(path.join(SESSIONS_ROOT, sanitizeWorkspaceDirName(value)));
|
|
1012
|
-
}
|
|
1013
|
-
return Array.from(new Set(dirs));
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
function resolveSessionSettings(workspaceDir, sessionId) {
|
|
1017
|
-
const settingsPath = path.join(workspaceDir, sessionId + '.settings.json');
|
|
1018
|
-
const settings = readJsonFile(settingsPath) || {};
|
|
1019
|
-
return { settingsPath, settings };
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
function resolveGlobalSettingsModel() {
|
|
1023
|
-
const global = readJsonFile(GLOBAL_SETTINGS_PATH);
|
|
1024
|
-
return global && typeof global.model === 'string' ? global.model : null;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
function resolveCustomModelIndex(modelId) {
|
|
1028
|
-
if (typeof modelId !== 'string') return null;
|
|
1029
|
-
if (!modelId.startsWith('custom:')) return null;
|
|
1030
|
-
const m = modelId.match(/-(\\d+)$/);
|
|
1031
|
-
if (!m) return null;
|
|
1032
|
-
const idx = Number(m[1]);
|
|
1033
|
-
return Number.isFinite(idx) ? idx : null;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
function resolveUnderlyingModelId(modelId, factoryConfig) {
|
|
1037
|
-
const idx = resolveCustomModelIndex(modelId);
|
|
1038
|
-
if (idx == null) return modelId;
|
|
1039
|
-
const entry = factoryConfig?.custom_models?.[idx];
|
|
1040
|
-
if (entry && typeof entry.model === 'string') return entry.model;
|
|
1041
|
-
return modelId;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
function resolveProvider(modelId, factoryConfig) {
|
|
1045
|
-
const idx = resolveCustomModelIndex(modelId);
|
|
1046
|
-
if (idx != null) {
|
|
1047
|
-
const entry = factoryConfig?.custom_models?.[idx];
|
|
1048
|
-
if (entry && typeof entry.provider === 'string') return entry.provider;
|
|
1049
|
-
}
|
|
1050
|
-
if (typeof modelId === 'string' && modelId.startsWith('claude-')) return 'anthropic';
|
|
1051
|
-
return '';
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
function formatInt(n) {
|
|
1055
|
-
if (!Number.isFinite(n)) return '0';
|
|
1056
|
-
return Math.round(n).toString();
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
function formatTokens(n) {
|
|
1060
|
-
if (!Number.isFinite(n)) return '0';
|
|
1061
|
-
const sign = n < 0 ? '-' : '';
|
|
1062
|
-
const abs = Math.abs(n);
|
|
1063
|
-
if (abs >= 1_000_000) {
|
|
1064
|
-
const v = abs / 1_000_000;
|
|
1065
|
-
const s = v >= 10 ? v.toFixed(0) : v.toFixed(1);
|
|
1066
|
-
return sign + s.replace(/\\.0$/, '') + 'M';
|
|
1067
|
-
}
|
|
1068
|
-
if (abs >= 10_000) {
|
|
1069
|
-
const v = abs / 1_000;
|
|
1070
|
-
const s = v >= 100 ? v.toFixed(0) : v.toFixed(1);
|
|
1071
|
-
return sign + s.replace(/\\.0$/, '') + 'k';
|
|
1072
|
-
}
|
|
1073
|
-
return sign + Math.round(abs).toString();
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
function emitFrame(line) {
|
|
1077
|
-
try {
|
|
1078
|
-
process.stdout.write(String(line || '') + '\\n');
|
|
1079
|
-
} catch {
|
|
1080
|
-
// ignore
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
function seg(bg, fg, text) {
|
|
1085
|
-
if (!text) return '';
|
|
1086
|
-
return '\\x1b[48;5;' + bg + 'm' + '\\x1b[38;5;' + fg + 'm' + ' ' + text + ' ' + '\\x1b[0m';
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
function resolveGitBranch(cwd) {
|
|
1090
|
-
try {
|
|
1091
|
-
const res = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
1092
|
-
cwd,
|
|
1093
|
-
encoding: 'utf8',
|
|
1094
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1095
|
-
timeout: 800,
|
|
1096
|
-
});
|
|
1097
|
-
if (res && res.status === 0) {
|
|
1098
|
-
const branch = String(res.stdout || '').trim();
|
|
1099
|
-
if (branch && branch !== 'HEAD') return branch;
|
|
1100
|
-
}
|
|
1101
|
-
} catch {}
|
|
1102
|
-
try {
|
|
1103
|
-
const headPath = path.join(cwd, '.git', 'HEAD');
|
|
1104
|
-
const head = safeReadFile(headPath);
|
|
1105
|
-
if (head && head.startsWith('ref: ')) {
|
|
1106
|
-
const ref = head.slice('ref: '.length).trim();
|
|
1107
|
-
const m = ref.match(/refs\\/heads\\/(.+)$/);
|
|
1108
|
-
if (m) return m[1];
|
|
1109
|
-
}
|
|
1110
|
-
} catch {}
|
|
1111
|
-
return '';
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
function resolveGitDiffSummary(cwd) {
|
|
1115
|
-
try {
|
|
1116
|
-
const res = spawnSync('git', ['diff', '--shortstat'], {
|
|
1117
|
-
cwd,
|
|
1118
|
-
encoding: 'utf8',
|
|
1119
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1120
|
-
timeout: 800,
|
|
1121
|
-
});
|
|
1122
|
-
if (!res || res.status !== 0) return '';
|
|
1123
|
-
const text = String(res.stdout || '').trim();
|
|
1124
|
-
if (!text) return '';
|
|
1125
|
-
const ins = (text.match(/(\\d+)\\sinsertions?\\(\\+\\)/) || [])[1];
|
|
1126
|
-
const del = (text.match(/(\\d+)\\sdeletions?\\(-\\)/) || [])[1];
|
|
1127
|
-
const i = ins ? Number(ins) : 0;
|
|
1128
|
-
const d = del ? Number(del) : 0;
|
|
1129
|
-
if (!Number.isFinite(i) && !Number.isFinite(d)) return '';
|
|
1130
|
-
if (i === 0 && d === 0) return '';
|
|
1131
|
-
return '(+' + formatInt(i) + ',-' + formatInt(d) + ')';
|
|
1132
|
-
} catch {
|
|
1133
|
-
return '';
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
function buildLine(params) {
|
|
1138
|
-
const {
|
|
1139
|
-
provider,
|
|
1140
|
-
model,
|
|
1141
|
-
cwdBase,
|
|
1142
|
-
gitBranch,
|
|
1143
|
-
gitDiff,
|
|
1144
|
-
usedTokens,
|
|
1145
|
-
cacheRead,
|
|
1146
|
-
deltaInput,
|
|
1147
|
-
lastOutputTokens,
|
|
1148
|
-
sessionUsage,
|
|
1149
|
-
compacting,
|
|
1150
|
-
ctxAvailable,
|
|
1151
|
-
ctxApprox,
|
|
1152
|
-
ctxOverflow,
|
|
1153
|
-
} = params;
|
|
1154
|
-
|
|
1155
|
-
const ctxValue = !ctxAvailable
|
|
1156
|
-
? '--'
|
|
1157
|
-
: (ctxApprox ? '~' : '') + formatTokens(usedTokens) + (ctxOverflow ? '+' : '');
|
|
1158
|
-
let ctxPart = 'Ctx: ' + ctxValue;
|
|
1159
|
-
|
|
1160
|
-
const cachePart =
|
|
1161
|
-
ctxAvailable && !ctxApprox && !ctxOverflow && (cacheRead > 0 || deltaInput > 0)
|
|
1162
|
-
? ' c' + formatTokens(cacheRead) + '+n' + formatTokens(deltaInput)
|
|
1163
|
-
: '';
|
|
1164
|
-
|
|
1165
|
-
const compactPart = compacting ? ' COMPACT' : '';
|
|
1166
|
-
|
|
1167
|
-
const usagePart = (() => {
|
|
1168
|
-
const u = sessionUsage || {};
|
|
1169
|
-
const input = Number(u.inputTokens ?? 0);
|
|
1170
|
-
const output = Number(u.outputTokens ?? 0);
|
|
1171
|
-
const cacheCreation = Number(u.cacheCreationTokens ?? 0);
|
|
1172
|
-
const cacheReadTotal = Number(u.cacheReadTokens ?? 0);
|
|
1173
|
-
const thinking = Number(u.thinkingTokens ?? 0);
|
|
1174
|
-
if (!(input || output || cacheCreation || cacheReadTotal || thinking)) return '';
|
|
1175
|
-
const parts = [];
|
|
1176
|
-
if (input) parts.push('In:' + formatTokens(input));
|
|
1177
|
-
if (output) parts.push('Out:' + formatTokens(output));
|
|
1178
|
-
if (cacheCreation) parts.push('Cre:' + formatTokens(cacheCreation));
|
|
1179
|
-
if (cacheReadTotal) parts.push('Read:' + formatTokens(cacheReadTotal));
|
|
1180
|
-
if (thinking) parts.push('Think:' + formatTokens(thinking));
|
|
1181
|
-
if (lastOutputTokens > 0) parts.push('LastOut:' + formatTokens(lastOutputTokens));
|
|
1182
|
-
return parts.join(' ');
|
|
1183
|
-
})();
|
|
1184
|
-
|
|
1185
|
-
const modelPart = model ? 'Model: ' + model : '';
|
|
1186
|
-
const providerPart = provider ? 'Prov: ' + provider : '';
|
|
1187
|
-
const cwdPart = cwdBase ? 'cwd: ' + cwdBase : '';
|
|
1188
|
-
const branchPart = gitBranch ? '\\uE0A0 ' + gitBranch : '';
|
|
1189
|
-
const diffPart = gitDiff || '';
|
|
1190
|
-
|
|
1191
|
-
// Background segments (powerline-like blocks)
|
|
1192
|
-
const sModel = seg(88, 15, modelPart); // dark red
|
|
1193
|
-
const sProvider = seg(160, 15, providerPart); // red
|
|
1194
|
-
const sCtx = seg(220, 0, ctxPart + (cachePart ? ' (' + cachePart.trim() + ')' : '')); // yellow
|
|
1195
|
-
const sUsage = seg(173, 0, usagePart); // orange-ish
|
|
1196
|
-
const sBranch = seg(24, 15, branchPart); // blue
|
|
1197
|
-
const sDiff = seg(34, 0, diffPart); // green
|
|
1198
|
-
const sCwd = seg(238, 15, cwdPart); // gray
|
|
1199
|
-
const sExtra = seg(99, 15, compactPart.trim()); // purple-ish
|
|
1200
|
-
|
|
1201
|
-
return [sModel, sProvider, sCtx, sUsage, sBranch, sDiff, sCwd, sExtra].filter(Boolean).join('');
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
async function main() {
|
|
1205
|
-
let factoryConfig = readJsonFile(CONFIG_PATH) || {};
|
|
1206
|
-
|
|
1207
|
-
const cwd = process.cwd();
|
|
1208
|
-
const cwdBase = path.basename(cwd) || cwd;
|
|
1209
|
-
const workspaceDirs = resolveWorkspaceDirs(cwd);
|
|
1210
|
-
const knownIdsByWorkspace = new Map();
|
|
1211
|
-
for (const dir of workspaceDirs) {
|
|
1212
|
-
const set = new Set();
|
|
1213
|
-
for (const c of listSessionCandidates(dir)) set.add(c.id);
|
|
1214
|
-
knownIdsByWorkspace.set(dir, set);
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const { resumeFlag, resumeId } = parseResume(ARGS);
|
|
1218
|
-
|
|
1219
|
-
let sessionId = null;
|
|
1220
|
-
let workspaceDir = null;
|
|
1221
|
-
if (resumeId) {
|
|
1222
|
-
sessionId = resumeId;
|
|
1223
|
-
workspaceDir = findWorkspaceDirForSessionId(workspaceDirs, sessionId) || workspaceDirs[0] || null;
|
|
1224
|
-
} else {
|
|
1225
|
-
let abortResolve = false;
|
|
1226
|
-
const shouldAbort = () => abortResolve;
|
|
1227
|
-
|
|
1228
|
-
const byProcPromise = resolveSessionFromProcessGroup(shouldAbort, 20);
|
|
1229
|
-
|
|
1230
|
-
let picked = null;
|
|
1231
|
-
if (resumeFlag) {
|
|
1232
|
-
// For --resume without an explicit id, don't block startup too long on ps/lsof.
|
|
1233
|
-
// Prefer process-group resolution when it is fast; otherwise fall back to latest.
|
|
1234
|
-
picked = await Promise.race([
|
|
1235
|
-
byProcPromise,
|
|
1236
|
-
sleep(400).then(() => null),
|
|
1237
|
-
]);
|
|
1238
|
-
if (!picked) picked = pickLatestSessionAcross(workspaceDirs);
|
|
1239
|
-
} else {
|
|
1240
|
-
const freshPromise = waitForNewSessionAcross(workspaceDirs, knownIdsByWorkspace, START_MS, shouldAbort);
|
|
1241
|
-
picked = await firstNonNull([byProcPromise, freshPromise]);
|
|
1242
|
-
if (!picked) picked = pickLatestSessionAcross(workspaceDirs);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
abortResolve = true;
|
|
1246
|
-
|
|
1247
|
-
sessionId = picked?.id || null;
|
|
1248
|
-
workspaceDir = picked?.workspaceDir || workspaceDirs[0] || null;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
if (!sessionId || !workspaceDir) return;
|
|
1252
|
-
let sessionIdLower = String(sessionId).toLowerCase();
|
|
1253
|
-
|
|
1254
|
-
let settingsPath = '';
|
|
1255
|
-
let sessionSettings = {};
|
|
1256
|
-
({ settingsPath, settings: sessionSettings } = resolveSessionSettings(workspaceDir, sessionId));
|
|
1257
|
-
|
|
1258
|
-
let configMtimeMs = safeStatMtimeMs(CONFIG_PATH);
|
|
1259
|
-
let globalSettingsMtimeMs = safeStatMtimeMs(GLOBAL_SETTINGS_PATH);
|
|
1260
|
-
let globalSettingsModel = resolveGlobalSettingsModel();
|
|
1261
|
-
|
|
1262
|
-
let modelIdFromLog = null;
|
|
1263
|
-
|
|
1264
|
-
function resolveActiveModelId() {
|
|
1265
|
-
const fromSession =
|
|
1266
|
-
sessionSettings && typeof sessionSettings.model === 'string' ? sessionSettings.model : null;
|
|
1267
|
-
if (fromSession && String(fromSession).startsWith('custom:')) return fromSession;
|
|
1268
|
-
const fromLog = typeof modelIdFromLog === 'string' ? modelIdFromLog : null;
|
|
1269
|
-
if (fromLog) return fromLog;
|
|
1270
|
-
return fromSession || globalSettingsModel || null;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
let modelId = resolveActiveModelId();
|
|
1274
|
-
|
|
1275
|
-
let provider =
|
|
1276
|
-
sessionSettings && typeof sessionSettings.providerLock === 'string'
|
|
1277
|
-
? sessionSettings.providerLock
|
|
1278
|
-
: resolveProvider(modelId, factoryConfig);
|
|
1279
|
-
let underlyingModel = resolveUnderlyingModelId(modelId, factoryConfig) || modelId || 'unknown';
|
|
1280
|
-
|
|
1281
|
-
function refreshModel() {
|
|
1282
|
-
const nextModelId = resolveActiveModelId();
|
|
1283
|
-
|
|
1284
|
-
// Use providerLock if set, otherwise resolve from model/config (same logic as initialization)
|
|
1285
|
-
const nextProvider =
|
|
1286
|
-
sessionSettings && typeof sessionSettings.providerLock === 'string'
|
|
1287
|
-
? sessionSettings.providerLock
|
|
1288
|
-
: resolveProvider(nextModelId, factoryConfig);
|
|
1289
|
-
const nextUnderlying = resolveUnderlyingModelId(nextModelId, factoryConfig) || nextModelId || 'unknown';
|
|
1290
|
-
|
|
1291
|
-
let changed = false;
|
|
1292
|
-
if (nextModelId !== modelId) {
|
|
1293
|
-
modelId = nextModelId;
|
|
1294
|
-
changed = true;
|
|
1295
|
-
}
|
|
1296
|
-
if (nextProvider !== provider) {
|
|
1297
|
-
provider = nextProvider;
|
|
1298
|
-
changed = true;
|
|
1299
|
-
}
|
|
1300
|
-
if (nextUnderlying !== underlyingModel) {
|
|
1301
|
-
underlyingModel = nextUnderlying;
|
|
1302
|
-
changed = true;
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
if (changed) renderNow();
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
let last = { cacheReadInputTokens: 0, contextCount: 0, outputTokens: 0 };
|
|
1309
|
-
let sessionUsage =
|
|
1310
|
-
sessionSettings && typeof sessionSettings.tokenUsage === 'object' && sessionSettings.tokenUsage
|
|
1311
|
-
? sessionSettings.tokenUsage
|
|
1312
|
-
: {};
|
|
1313
|
-
let compacting = false;
|
|
1314
|
-
let lastRenderAt = 0;
|
|
1315
|
-
let lastRenderedLine = '';
|
|
1316
|
-
let gitBranch = '';
|
|
1317
|
-
let gitDiff = '';
|
|
1318
|
-
let lastContextMs = 0;
|
|
1319
|
-
let ctxAvailable = false;
|
|
1320
|
-
let ctxApprox = false;
|
|
1321
|
-
let ctxOverflow = false;
|
|
1322
|
-
let ctxOverrideUsedTokens = null;
|
|
1323
|
-
|
|
1324
|
-
let baselineCacheReadInputTokens = 0;
|
|
1325
|
-
let knownContextMaxTokens = 0;
|
|
1326
|
-
let pendingCompactionSuffixTokens = null;
|
|
1327
|
-
let pendingCompactionSummaryOutputTokens = null;
|
|
1328
|
-
let pendingCompactionSummaryTsMs = null;
|
|
1329
|
-
|
|
1330
|
-
function renderNow() {
|
|
1331
|
-
const override = Number.isFinite(ctxOverrideUsedTokens) && ctxOverrideUsedTokens > 0 ? ctxOverrideUsedTokens : null;
|
|
1332
|
-
const usedTokens = override != null ? override : (last.cacheReadInputTokens || 0) + (last.contextCount || 0);
|
|
1333
|
-
const cacheRead = override != null ? 0 : last.cacheReadInputTokens || 0;
|
|
1334
|
-
const deltaInput = override != null ? 0 : last.contextCount || 0;
|
|
1335
|
-
const line = buildLine({
|
|
1336
|
-
provider,
|
|
1337
|
-
model: underlyingModel,
|
|
1338
|
-
cwdBase,
|
|
1339
|
-
gitBranch,
|
|
1340
|
-
gitDiff,
|
|
1341
|
-
usedTokens,
|
|
1342
|
-
cacheRead,
|
|
1343
|
-
deltaInput,
|
|
1344
|
-
lastOutputTokens: last.outputTokens || 0,
|
|
1345
|
-
sessionUsage,
|
|
1346
|
-
compacting,
|
|
1347
|
-
ctxAvailable: override != null ? true : ctxAvailable,
|
|
1348
|
-
ctxApprox,
|
|
1349
|
-
ctxOverflow,
|
|
1350
|
-
});
|
|
1351
|
-
if (line !== lastRenderedLine) {
|
|
1352
|
-
lastRenderedLine = line;
|
|
1353
|
-
emitFrame(line);
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
// Initial render.
|
|
1358
|
-
renderNow();
|
|
1359
|
-
|
|
1360
|
-
// Resolve git info asynchronously so startup isn't blocked on large repos.
|
|
1361
|
-
setTimeout(() => {
|
|
1362
|
-
try {
|
|
1363
|
-
gitBranch = resolveGitBranch(cwd);
|
|
1364
|
-
gitDiff = resolveGitDiffSummary(cwd);
|
|
1365
|
-
renderNow();
|
|
1366
|
-
} catch {}
|
|
1367
|
-
}, 0).unref();
|
|
1368
|
-
|
|
1369
|
-
// Seed known context max tokens from recent log failures (some providers omit explicit counts).
|
|
1370
|
-
setTimeout(() => {
|
|
1371
|
-
try {
|
|
1372
|
-
seedKnownContextMaxTokensFromLog(8 * 1024 * 1024);
|
|
1373
|
-
} catch {}
|
|
1374
|
-
}, 0).unref();
|
|
1375
|
-
|
|
1376
|
-
let reseedInProgress = false;
|
|
1377
|
-
let reseedQueued = false;
|
|
1378
|
-
|
|
1379
|
-
function extractModelIdFromContext(ctx) {
|
|
1380
|
-
const tagged = ctx?.tags?.modelId;
|
|
1381
|
-
if (typeof tagged === 'string') return tagged;
|
|
1382
|
-
const direct = ctx?.modelId;
|
|
1383
|
-
return typeof direct === 'string' ? direct : null;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
function updateLastFromContext(ctx, updateOutputTokens, tsMs) {
|
|
1387
|
-
const ts = Number.isFinite(tsMs) ? tsMs : null;
|
|
1388
|
-
if (ts != null && lastContextMs && ts < lastContextMs) return false;
|
|
1389
|
-
const cacheRead = Number(ctx?.cacheReadInputTokens);
|
|
1390
|
-
const contextCount = Number(ctx?.contextCount);
|
|
1391
|
-
const out = Number(ctx?.outputTokens);
|
|
1392
|
-
const hasTokens =
|
|
1393
|
-
(Number.isFinite(cacheRead) && cacheRead > 0) ||
|
|
1394
|
-
(Number.isFinite(contextCount) && contextCount > 0);
|
|
1395
|
-
if (hasTokens) {
|
|
1396
|
-
// Treat 0/0 as "not reported" (some providers log zeros even when prompt exists).
|
|
1397
|
-
// If at least one field is >0, accept both fields (including zero) as reliable.
|
|
1398
|
-
if (Number.isFinite(cacheRead)) last.cacheReadInputTokens = cacheRead;
|
|
1399
|
-
if (Number.isFinite(contextCount)) last.contextCount = contextCount;
|
|
1400
|
-
ctxAvailable = true;
|
|
1401
|
-
ctxOverrideUsedTokens = null;
|
|
1402
|
-
ctxApprox = false;
|
|
1403
|
-
ctxOverflow = false;
|
|
1404
|
-
if (Number.isFinite(cacheRead) && cacheRead > 0) {
|
|
1405
|
-
baselineCacheReadInputTokens = baselineCacheReadInputTokens
|
|
1406
|
-
? Math.min(baselineCacheReadInputTokens, cacheRead)
|
|
1407
|
-
: cacheRead;
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
if (updateOutputTokens && Number.isFinite(out)) last.outputTokens = out;
|
|
1411
|
-
if (hasTokens && ts != null) lastContextMs = ts;
|
|
1412
|
-
|
|
1413
|
-
const nextModelIdFromLog = extractModelIdFromContext(ctx);
|
|
1414
|
-
if (nextModelIdFromLog && nextModelIdFromLog !== modelIdFromLog) {
|
|
1415
|
-
modelIdFromLog = nextModelIdFromLog;
|
|
1416
|
-
refreshModel();
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
return true;
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
function setCtxOverride(usedTokens, options) {
|
|
1423
|
-
const opts = options || {};
|
|
1424
|
-
const v = Number(usedTokens);
|
|
1425
|
-
if (!Number.isFinite(v) || v <= 0) return false;
|
|
1426
|
-
const prevUsed = ctxOverrideUsedTokens;
|
|
1427
|
-
const prevApprox = ctxApprox;
|
|
1428
|
-
const prevOverflow = ctxOverflow;
|
|
1429
|
-
ctxOverrideUsedTokens = v;
|
|
1430
|
-
ctxAvailable = true;
|
|
1431
|
-
ctxApprox = !!opts.approx;
|
|
1432
|
-
ctxOverflow = !!opts.overflow;
|
|
1433
|
-
const ts = Number.isFinite(opts.tsMs) ? opts.tsMs : null;
|
|
1434
|
-
if (ts != null && (!lastContextMs || ts > lastContextMs)) lastContextMs = ts;
|
|
1435
|
-
if (prevUsed !== ctxOverrideUsedTokens || prevApprox !== ctxApprox || prevOverflow !== ctxOverflow) {
|
|
1436
|
-
renderNow();
|
|
1437
|
-
}
|
|
1438
|
-
return true;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
function parseContextLimitFromMessage(message) {
|
|
1442
|
-
const s = String(message || '');
|
|
1443
|
-
let promptTokens = null;
|
|
1444
|
-
let maxTokens = null;
|
|
1445
|
-
|
|
1446
|
-
const pair = s.match(/(\\d+)\\s*tokens\\s*>\\s*(\\d+)\\s*maximum/i);
|
|
1447
|
-
if (pair) {
|
|
1448
|
-
const prompt = Number(pair[1]);
|
|
1449
|
-
const max = Number(pair[2]);
|
|
1450
|
-
if (Number.isFinite(prompt)) promptTokens = prompt;
|
|
1451
|
-
if (Number.isFinite(max)) maxTokens = max;
|
|
1452
|
-
return { promptTokens, maxTokens };
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
const promptMatch = s.match(/prompt\\s+is\\s+too\\s+long:\\s*(\\d+)\\s*tokens/i);
|
|
1456
|
-
if (promptMatch) {
|
|
1457
|
-
const prompt = Number(promptMatch[1]);
|
|
1458
|
-
if (Number.isFinite(prompt)) promptTokens = prompt;
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
const maxMatch = s.match(/>\\s*(\\d+)\\s*maximum/i);
|
|
1462
|
-
if (maxMatch) {
|
|
1463
|
-
const max = Number(maxMatch[1]);
|
|
1464
|
-
if (Number.isFinite(max)) maxTokens = max;
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
return { promptTokens, maxTokens };
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
function seedKnownContextMaxTokensFromLog(maxScanBytes = 4 * 1024 * 1024) {
|
|
1471
|
-
try {
|
|
1472
|
-
const stat = fs.statSync(LOG_PATH);
|
|
1473
|
-
const size = Number(stat?.size ?? 0);
|
|
1474
|
-
if (!(size > 0)) return;
|
|
1475
|
-
|
|
1476
|
-
const scan = Math.max(256 * 1024, maxScanBytes);
|
|
1477
|
-
const readSize = Math.min(size, scan);
|
|
1478
|
-
const start = Math.max(0, size - readSize);
|
|
1479
|
-
|
|
1480
|
-
const buf = Buffer.alloc(readSize);
|
|
1481
|
-
const fd = fs.openSync(LOG_PATH, 'r');
|
|
1482
|
-
try {
|
|
1483
|
-
fs.readSync(fd, buf, 0, readSize, start);
|
|
1484
|
-
} finally {
|
|
1485
|
-
try {
|
|
1486
|
-
fs.closeSync(fd);
|
|
1487
|
-
} catch {}
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
const text = buf.toString('utf8');
|
|
1491
|
-
const re = /(\\d+)\\s*tokens\\s*>\\s*(\\d+)\\s*maximum/gi;
|
|
1492
|
-
let m;
|
|
1493
|
-
while ((m = re.exec(text))) {
|
|
1494
|
-
const max = Number(m[2]);
|
|
1495
|
-
if (Number.isFinite(max) && max > 0) knownContextMaxTokens = max;
|
|
1496
|
-
}
|
|
1497
|
-
} catch {}
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
function maybeUpdateCtxFromContextLimitFailure(line, ctx, tsMs) {
|
|
1501
|
-
if (!line || !ctx) return false;
|
|
1502
|
-
if (!String(line).includes('[Chat route failure]')) return false;
|
|
1503
|
-
const reason = ctx?.reason;
|
|
1504
|
-
if (reason !== 'llmContextExceeded') return false;
|
|
1505
|
-
|
|
1506
|
-
const msg = ctx?.error?.message;
|
|
1507
|
-
if (typeof msg !== 'string' || !msg) return false;
|
|
1508
|
-
const parsed = parseContextLimitFromMessage(msg);
|
|
1509
|
-
const max = Number(parsed?.maxTokens);
|
|
1510
|
-
if (Number.isFinite(max) && max > 0) knownContextMaxTokens = max;
|
|
1511
|
-
|
|
1512
|
-
const prompt = Number(parsed?.promptTokens);
|
|
1513
|
-
if (Number.isFinite(prompt) && prompt > 0) {
|
|
1514
|
-
return setCtxOverride(prompt, { tsMs, approx: false, overflow: false });
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
if (Number.isFinite(max) && max > 0) {
|
|
1518
|
-
return setCtxOverride(max, { tsMs, approx: false, overflow: true });
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
if (knownContextMaxTokens > 0) {
|
|
1522
|
-
return setCtxOverride(knownContextMaxTokens, { tsMs, approx: false, overflow: true });
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
return false;
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
function maybeCaptureCompactionSuffix(line, ctx) {
|
|
1529
|
-
if (!line || !ctx) return;
|
|
1530
|
-
if (!String(line).includes('[Compaction] Suffix selection')) return;
|
|
1531
|
-
const suffix = Number(ctx?.suffixTokens);
|
|
1532
|
-
if (Number.isFinite(suffix) && suffix >= 0) pendingCompactionSuffixTokens = suffix;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
function maybeApplyPostCompactionEstimate(line, ctx, tsMs) {
|
|
1536
|
-
if (!line || !ctx) return false;
|
|
1537
|
-
if (!String(line).includes('[Compaction] End')) return false;
|
|
1538
|
-
if (ctx?.eventType !== 'compaction' || ctx?.state !== 'end') return false;
|
|
1539
|
-
const reason = ctx?.reason || ctx?.tags?.compactionReason || null;
|
|
1540
|
-
if (!(reason === 'context_limit' || reason === 'manual')) return false;
|
|
1541
|
-
|
|
1542
|
-
const summaryOut = Number(ctx?.summaryOutputTokens);
|
|
1543
|
-
if (!Number.isFinite(summaryOut) || summaryOut < 0) return false;
|
|
1544
|
-
|
|
1545
|
-
const prefix = baselineCacheReadInputTokens > 0 ? baselineCacheReadInputTokens : 0;
|
|
1546
|
-
const suffix = Number.isFinite(pendingCompactionSuffixTokens) ? pendingCompactionSuffixTokens : 0;
|
|
1547
|
-
pendingCompactionSuffixTokens = null;
|
|
1548
|
-
|
|
1549
|
-
const est = prefix + suffix + summaryOut;
|
|
1550
|
-
if (est <= 0) return false;
|
|
1551
|
-
return setCtxOverride(est, { tsMs, approx: true, overflow: false });
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
function maybeApplyCompactSessionEstimate(sessionIdToEstimate, options, attempt = 0) {
|
|
1555
|
-
const opts = options && typeof options === 'object' ? options : {};
|
|
1556
|
-
const id = String(sessionIdToEstimate || '');
|
|
1557
|
-
if (!isUuid(id)) return;
|
|
1558
|
-
if (!workspaceDir) return;
|
|
1559
|
-
|
|
1560
|
-
// forceApply allows overriding even if ctx is already set (useful for manual /compress)
|
|
1561
|
-
const forceApply = !!opts?.forceApply;
|
|
1562
|
-
if (!forceApply) {
|
|
1563
|
-
if (ctxAvailable) return;
|
|
1564
|
-
if (Number.isFinite(ctxOverrideUsedTokens) && ctxOverrideUsedTokens > 0) return;
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
const suffixVal = opts?.suffixTokens;
|
|
1568
|
-
const suffixTokens =
|
|
1569
|
-
typeof suffixVal === 'number' && Number.isFinite(suffixVal) && suffixVal >= 0 ? suffixVal : 0;
|
|
1570
|
-
const tsVal = opts?.tsMs;
|
|
1571
|
-
const ts = typeof tsVal === 'number' && Number.isFinite(tsVal) ? tsVal : null;
|
|
1572
|
-
|
|
1573
|
-
const jsonlPath = path.join(workspaceDir, id + '.jsonl');
|
|
1574
|
-
let head = null;
|
|
1575
|
-
try {
|
|
1576
|
-
const fd = fs.openSync(jsonlPath, 'r');
|
|
1577
|
-
try {
|
|
1578
|
-
const maxBytes = 2 * 1024 * 1024;
|
|
1579
|
-
const buf = Buffer.alloc(maxBytes);
|
|
1580
|
-
const bytes = fs.readSync(fd, buf, 0, maxBytes, 0);
|
|
1581
|
-
head = buf.slice(0, Math.max(0, bytes)).toString('utf8');
|
|
1582
|
-
} finally {
|
|
1583
|
-
try {
|
|
1584
|
-
fs.closeSync(fd);
|
|
1585
|
-
} catch {}
|
|
1586
|
-
}
|
|
1587
|
-
} catch {
|
|
1588
|
-
head = null;
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
if (!head) {
|
|
1592
|
-
if (attempt < 40) {
|
|
1593
|
-
setTimeout(() => {
|
|
1594
|
-
maybeApplyCompactSessionEstimate(id, opts, attempt + 1);
|
|
1595
|
-
}, 150).unref();
|
|
1596
|
-
}
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
let summaryText = null;
|
|
1601
|
-
for (const raw of head.split('\\n')) {
|
|
1602
|
-
if (!raw) continue;
|
|
1603
|
-
let obj;
|
|
1604
|
-
try {
|
|
1605
|
-
obj = JSON.parse(raw);
|
|
1606
|
-
} catch {
|
|
1607
|
-
continue;
|
|
1608
|
-
}
|
|
1609
|
-
if (obj && obj.type === 'compaction_state' && typeof obj.summaryText === 'string') {
|
|
1610
|
-
summaryText = obj.summaryText;
|
|
1611
|
-
break;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
if (!summaryText) {
|
|
1615
|
-
if (attempt < 40) {
|
|
1616
|
-
setTimeout(() => {
|
|
1617
|
-
maybeApplyCompactSessionEstimate(id, opts, attempt + 1);
|
|
1618
|
-
}, 150).unref();
|
|
1619
|
-
}
|
|
1620
|
-
return;
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
// Rough token estimate (no tokenizer deps): English-like text averages ~4 chars/token.
|
|
1624
|
-
// Non-ASCII tends to be denser; use a smaller divisor.
|
|
1625
|
-
let ascii = 0;
|
|
1626
|
-
let other = 0;
|
|
1627
|
-
for (let i = 0; i < summaryText.length; i++) {
|
|
1628
|
-
const code = summaryText.charCodeAt(i);
|
|
1629
|
-
if (code <= 0x7f) ascii += 1;
|
|
1630
|
-
else other += 1;
|
|
1631
|
-
}
|
|
1632
|
-
const summaryTokens = Math.max(1, Math.ceil(ascii / 4 + other / 1.5));
|
|
1633
|
-
const prefix = baselineCacheReadInputTokens > 0 ? baselineCacheReadInputTokens : 0;
|
|
1634
|
-
const est = prefix + suffixTokens + summaryTokens;
|
|
1635
|
-
if (est > 0) setCtxOverride(est, { tsMs: ts, approx: true, overflow: false });
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
function seedLastContextFromLog(options) {
|
|
1639
|
-
const opts = options || {};
|
|
1640
|
-
const maxScanBytes = Number.isFinite(opts.maxScanBytes) ? opts.maxScanBytes : 64 * 1024 * 1024;
|
|
1641
|
-
const preferStreaming = !!opts.preferStreaming;
|
|
1642
|
-
const minTimestampMs = Number.isFinite(lastContextMs) && lastContextMs > 0 ? lastContextMs : 0;
|
|
1643
|
-
const earlyStopAfterBestBytes = Math.min(2 * 1024 * 1024, Math.max(256 * 1024, maxScanBytes));
|
|
1644
|
-
|
|
1645
|
-
if (reseedInProgress) {
|
|
1646
|
-
reseedQueued = true;
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1649
|
-
reseedInProgress = true;
|
|
1650
|
-
|
|
1651
|
-
setTimeout(() => {
|
|
1652
|
-
try {
|
|
1653
|
-
// Backward scan to find the most recent context entry for this session.
|
|
1654
|
-
// Prefer streaming context if requested; otherwise accept any context line
|
|
1655
|
-
// that includes cacheReadInputTokens/contextCount fields.
|
|
1656
|
-
const CHUNK_BYTES = 1024 * 1024; // 1 MiB
|
|
1657
|
-
|
|
1658
|
-
const fd = fs.openSync(LOG_PATH, 'r');
|
|
1659
|
-
try {
|
|
1660
|
-
const stat = fs.fstatSync(fd);
|
|
1661
|
-
const size = Number(stat?.size ?? 0);
|
|
1662
|
-
let pos = size;
|
|
1663
|
-
let scanned = 0;
|
|
1664
|
-
let remainder = '';
|
|
1665
|
-
let bestCtx = null;
|
|
1666
|
-
let bestIsStreaming = false;
|
|
1667
|
-
let bestTs = null;
|
|
1668
|
-
let bestHasTs = false;
|
|
1669
|
-
let bytesSinceBest = 0;
|
|
1670
|
-
|
|
1671
|
-
while (pos > 0 && scanned < maxScanBytes && (!bestHasTs || bytesSinceBest < earlyStopAfterBestBytes)) {
|
|
1672
|
-
const readSize = Math.min(CHUNK_BYTES, pos);
|
|
1673
|
-
const start = pos - readSize;
|
|
1674
|
-
const buf = Buffer.alloc(readSize);
|
|
1675
|
-
fs.readSync(fd, buf, 0, readSize, start);
|
|
1676
|
-
pos = start;
|
|
1677
|
-
scanned += readSize;
|
|
1678
|
-
bytesSinceBest += readSize;
|
|
1679
|
-
|
|
1680
|
-
let text = buf.toString('utf8') + remainder;
|
|
1681
|
-
let lines = String(text).split('\\n');
|
|
1682
|
-
remainder = lines.shift() || '';
|
|
1683
|
-
if (pos === 0 && remainder) {
|
|
1684
|
-
lines.unshift(remainder);
|
|
1685
|
-
remainder = '';
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1689
|
-
const line = String(lines[i] || '').trimEnd();
|
|
1690
|
-
if (!line) continue;
|
|
1691
|
-
if (!line.includes('Context:')) continue;
|
|
1692
|
-
const sid = extractSessionIdFromLine(line);
|
|
1693
|
-
if (!sid || String(sid).toLowerCase() !== sessionIdLower) continue;
|
|
1694
|
-
|
|
1695
|
-
const isStreaming = line.includes('[Agent] Streaming result');
|
|
1696
|
-
if (preferStreaming && !isStreaming) continue;
|
|
1697
|
-
|
|
1698
|
-
const ctxIndex = line.indexOf('Context: ');
|
|
1699
|
-
if (ctxIndex === -1) continue;
|
|
1700
|
-
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
1701
|
-
let ctx;
|
|
1702
|
-
try {
|
|
1703
|
-
ctx = JSON.parse(jsonStr);
|
|
1704
|
-
} catch {
|
|
1705
|
-
continue;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
const cacheRead = Number(ctx?.cacheReadInputTokens);
|
|
1709
|
-
const contextCount = Number(ctx?.contextCount);
|
|
1710
|
-
const hasUsage = Number.isFinite(cacheRead) || Number.isFinite(contextCount);
|
|
1711
|
-
if (!hasUsage) continue;
|
|
1712
|
-
|
|
1713
|
-
const ts = parseLineTimestampMs(line);
|
|
1714
|
-
if (ts != null && minTimestampMs && ts < minTimestampMs) {
|
|
1715
|
-
continue;
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
if (ts != null) {
|
|
1719
|
-
if (!bestHasTs || ts > bestTs) {
|
|
1720
|
-
bestCtx = ctx;
|
|
1721
|
-
bestIsStreaming = isStreaming;
|
|
1722
|
-
bestTs = ts;
|
|
1723
|
-
bestHasTs = true;
|
|
1724
|
-
bytesSinceBest = 0;
|
|
1725
|
-
}
|
|
1726
|
-
} else if (!bestHasTs && !bestCtx) {
|
|
1727
|
-
// No timestamps available yet: the first match when scanning backward
|
|
1728
|
-
// is the most recent in file order.
|
|
1729
|
-
bestCtx = ctx;
|
|
1730
|
-
bestIsStreaming = isStreaming;
|
|
1731
|
-
bestTs = null;
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
if (remainder.length > 8192) remainder = remainder.slice(-8192);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
if (bestCtx) {
|
|
1739
|
-
updateLastFromContext(bestCtx, bestIsStreaming, bestTs);
|
|
1740
|
-
}
|
|
1741
|
-
} finally {
|
|
1742
|
-
try {
|
|
1743
|
-
fs.closeSync(fd);
|
|
1744
|
-
} catch {}
|
|
1745
|
-
}
|
|
1746
|
-
} catch {
|
|
1747
|
-
// ignore
|
|
1748
|
-
} finally {
|
|
1749
|
-
reseedInProgress = false;
|
|
1750
|
-
if (reseedQueued) {
|
|
1751
|
-
reseedQueued = false;
|
|
1752
|
-
seedLastContextFromLog({ maxScanBytes, preferStreaming });
|
|
1753
|
-
return;
|
|
1754
|
-
}
|
|
1755
|
-
renderNow();
|
|
1756
|
-
}
|
|
1757
|
-
}, 0).unref();
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
// Seed prompt-context usage from existing logs (important for resumed sessions).
|
|
1761
|
-
// Do this asynchronously to avoid delaying the first statusline frame.
|
|
1762
|
-
let initialSeedDone = false;
|
|
1763
|
-
if (resumeFlag || resumeId) {
|
|
1764
|
-
initialSeedDone = true;
|
|
1765
|
-
seedLastContextFromLog({ maxScanBytes: 64 * 1024 * 1024, preferStreaming: true });
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
// Watch session settings for autonomy/reasoning changes (cheap polling with mtime).
|
|
1769
|
-
let settingsMtimeMs = 0;
|
|
1770
|
-
let lastCtxPollMs = 0;
|
|
1771
|
-
setInterval(() => {
|
|
1772
|
-
// Refresh config/global settings if they changed (model display depends on these).
|
|
1773
|
-
const configMtime = safeStatMtimeMs(CONFIG_PATH);
|
|
1774
|
-
if (configMtime && configMtime !== configMtimeMs) {
|
|
1775
|
-
configMtimeMs = configMtime;
|
|
1776
|
-
factoryConfig = readJsonFile(CONFIG_PATH) || {};
|
|
1777
|
-
refreshModel();
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
const globalMtime = safeStatMtimeMs(GLOBAL_SETTINGS_PATH);
|
|
1781
|
-
if (globalMtime && globalMtime !== globalSettingsMtimeMs) {
|
|
1782
|
-
globalSettingsMtimeMs = globalMtime;
|
|
1783
|
-
globalSettingsModel = resolveGlobalSettingsModel();
|
|
1784
|
-
refreshModel();
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
try {
|
|
1788
|
-
const stat = fs.statSync(settingsPath);
|
|
1789
|
-
if (stat.mtimeMs === settingsMtimeMs) return;
|
|
1790
|
-
settingsMtimeMs = stat.mtimeMs;
|
|
1791
|
-
const next = readJsonFile(settingsPath) || {};
|
|
1792
|
-
sessionSettings = next;
|
|
1793
|
-
|
|
1794
|
-
// Keep session token usage in sync (used by /status).
|
|
1795
|
-
if (next && typeof next.tokenUsage === 'object' && next.tokenUsage) {
|
|
1796
|
-
sessionUsage = next.tokenUsage;
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
// Keep model/provider in sync (model can change during a running session).
|
|
1800
|
-
refreshModel();
|
|
1801
|
-
|
|
1802
|
-
const now = Date.now();
|
|
1803
|
-
if (now - lastRenderAt >= MIN_RENDER_INTERVAL_MS) {
|
|
1804
|
-
lastRenderAt = now;
|
|
1805
|
-
renderNow();
|
|
1806
|
-
}
|
|
1807
|
-
} catch {
|
|
1808
|
-
// ignore
|
|
1809
|
-
}
|
|
1810
|
-
}, 750).unref();
|
|
1811
|
-
|
|
1812
|
-
// Fallback: periodically rescan log if context is still zero after startup.
|
|
1813
|
-
// This handles cases where tail misses early log entries.
|
|
1814
|
-
setInterval(() => {
|
|
1815
|
-
const now = Date.now();
|
|
1816
|
-
if (now - START_MS < 3000) return; // wait 3s after startup
|
|
1817
|
-
if (last.contextCount > 0 || last.cacheReadInputTokens > 0) return; // already have data
|
|
1818
|
-
if (now - lastCtxPollMs < 5000) return; // throttle to every 5s
|
|
1819
|
-
lastCtxPollMs = now;
|
|
1820
|
-
seedLastContextFromLog({ maxScanBytes: 4 * 1024 * 1024, preferStreaming: false });
|
|
1821
|
-
}, 2000).unref();
|
|
1822
|
-
|
|
1823
|
-
function switchToSession(nextSessionId) {
|
|
1824
|
-
if (!nextSessionId || !isUuid(nextSessionId)) return;
|
|
1825
|
-
const nextLower = String(nextSessionId).toLowerCase();
|
|
1826
|
-
if (nextLower === sessionIdLower) return;
|
|
1827
|
-
|
|
1828
|
-
sessionId = nextSessionId;
|
|
1829
|
-
sessionIdLower = nextLower;
|
|
1830
|
-
|
|
1831
|
-
const resolved = resolveSessionSettings(workspaceDir, nextSessionId);
|
|
1832
|
-
settingsPath = resolved.settingsPath;
|
|
1833
|
-
sessionSettings = resolved.settings || {};
|
|
1834
|
-
|
|
1835
|
-
sessionUsage =
|
|
1836
|
-
sessionSettings && typeof sessionSettings.tokenUsage === 'object' && sessionSettings.tokenUsage
|
|
1837
|
-
? sessionSettings.tokenUsage
|
|
1838
|
-
: {};
|
|
1839
|
-
|
|
1840
|
-
// Reset cached state for the new session.
|
|
1841
|
-
last = { cacheReadInputTokens: 0, contextCount: 0, outputTokens: 0 };
|
|
1842
|
-
lastContextMs = 0;
|
|
1843
|
-
ctxAvailable = false;
|
|
1844
|
-
ctxApprox = false;
|
|
1845
|
-
ctxOverflow = false;
|
|
1846
|
-
ctxOverrideUsedTokens = null;
|
|
1847
|
-
pendingCompactionSuffixTokens = null;
|
|
1848
|
-
pendingCompactionSummaryOutputTokens = null;
|
|
1849
|
-
pendingCompactionSummaryTsMs = null;
|
|
1850
|
-
modelIdFromLog = null;
|
|
1851
|
-
compacting = false;
|
|
1852
|
-
settingsMtimeMs = 0;
|
|
1853
|
-
lastCtxPollMs = 0;
|
|
1854
|
-
|
|
1855
|
-
refreshModel();
|
|
1856
|
-
renderNow();
|
|
1857
|
-
|
|
1858
|
-
// Best-effort: if the new session already has Context lines in the log, seed quickly.
|
|
1859
|
-
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
// Follow the Factory log and update based on session-scoped events.
|
|
1863
|
-
const tail = spawn('tail', ['-n', '0', '-F', LOG_PATH], {
|
|
1864
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1865
|
-
});
|
|
1866
|
-
|
|
1867
|
-
let buffer = '';
|
|
1868
|
-
tail.stdout.on('data', (chunk) => {
|
|
1869
|
-
buffer += String(chunk);
|
|
1870
|
-
while (true) {
|
|
1871
|
-
const idx = buffer.indexOf('\\n');
|
|
1872
|
-
if (idx === -1) break;
|
|
1873
|
-
const line = buffer.slice(0, idx).trimEnd();
|
|
1874
|
-
buffer = buffer.slice(idx + 1);
|
|
1875
|
-
|
|
1876
|
-
const tsMs = parseLineTimestampMs(line);
|
|
1877
|
-
const lineSessionId = extractSessionIdFromLine(line);
|
|
1878
|
-
const isSessionLine =
|
|
1879
|
-
lineSessionId && String(lineSessionId).toLowerCase() === sessionIdLower;
|
|
1880
|
-
|
|
1881
|
-
if (compacting && line.includes('[Compaction] End') && line.includes('Context:')) {
|
|
1882
|
-
const ctxIndex = line.indexOf('Context: ');
|
|
1883
|
-
if (ctxIndex !== -1) {
|
|
1884
|
-
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
1885
|
-
try {
|
|
1886
|
-
const meta = JSON.parse(jsonStr);
|
|
1887
|
-
const summaryOut = Number(meta?.summaryOutputTokens);
|
|
1888
|
-
if (
|
|
1889
|
-
meta?.eventType === 'compaction' &&
|
|
1890
|
-
meta?.state === 'end' &&
|
|
1891
|
-
Number.isFinite(summaryOut) &&
|
|
1892
|
-
summaryOut >= 0
|
|
1893
|
-
) {
|
|
1894
|
-
pendingCompactionSummaryOutputTokens = summaryOut;
|
|
1895
|
-
if (tsMs != null) pendingCompactionSummaryTsMs = tsMs;
|
|
1896
|
-
}
|
|
1897
|
-
} catch {
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
// /compress (aka /compact) can create a new session ID. Follow it so ctx/model keep updating.
|
|
1903
|
-
if (line.includes('oldSessionId') && line.includes('newSessionId') && line.includes('Context:')) {
|
|
1904
|
-
const ctxIndex = line.indexOf('Context: ');
|
|
1905
|
-
if (ctxIndex !== -1) {
|
|
1906
|
-
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
1907
|
-
try {
|
|
1908
|
-
const meta = JSON.parse(jsonStr);
|
|
1909
|
-
const oldId = meta?.oldSessionId;
|
|
1910
|
-
const newId = meta?.newSessionId;
|
|
1911
|
-
if (
|
|
1912
|
-
isUuid(oldId) &&
|
|
1913
|
-
isUuid(newId) &&
|
|
1914
|
-
String(oldId).toLowerCase() === sessionIdLower &&
|
|
1915
|
-
String(newId).toLowerCase() !== sessionIdLower
|
|
1916
|
-
) {
|
|
1917
|
-
const suffixTokens = Number.isFinite(pendingCompactionSuffixTokens)
|
|
1918
|
-
? pendingCompactionSuffixTokens
|
|
1919
|
-
: 0;
|
|
1920
|
-
const summaryOutTokens = Number.isFinite(pendingCompactionSummaryOutputTokens)
|
|
1921
|
-
? pendingCompactionSummaryOutputTokens
|
|
1922
|
-
: null;
|
|
1923
|
-
const summaryTsMs = Number.isFinite(pendingCompactionSummaryTsMs) ? pendingCompactionSummaryTsMs : null;
|
|
1924
|
-
|
|
1925
|
-
// Save baseline before switching session (it persists across sessions)
|
|
1926
|
-
const savedBaseline = baselineCacheReadInputTokens;
|
|
1927
|
-
|
|
1928
|
-
switchToSession(String(newId));
|
|
1929
|
-
|
|
1930
|
-
// For manual /compress, immediately set an estimated ctx value
|
|
1931
|
-
// This ensures the statusline shows a reasonable value right after compression
|
|
1932
|
-
if (summaryOutTokens != null && summaryOutTokens > 0) {
|
|
1933
|
-
const prefix = savedBaseline > 0 ? savedBaseline : 0;
|
|
1934
|
-
const est = prefix + suffixTokens + summaryOutTokens;
|
|
1935
|
-
if (est > 0) {
|
|
1936
|
-
setCtxOverride(est, { tsMs: summaryTsMs != null ? summaryTsMs : tsMs, approx: true, overflow: false });
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
// Always attempt to get a more accurate estimate from the new session's jsonl
|
|
1941
|
-
// This will read the compaction_state and estimate tokens from summaryText
|
|
1942
|
-
// Note: we pass forceApply=true to override even if ctxOverrideUsedTokens is set,
|
|
1943
|
-
// because the jsonl-based estimate may be more accurate
|
|
1944
|
-
maybeApplyCompactSessionEstimate(String(newId), {
|
|
1945
|
-
suffixTokens,
|
|
1946
|
-
tsMs: summaryTsMs != null ? summaryTsMs : tsMs,
|
|
1947
|
-
forceApply: true,
|
|
1948
|
-
});
|
|
1949
|
-
continue;
|
|
1950
|
-
}
|
|
1951
|
-
} catch {
|
|
1952
|
-
// ignore
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
let compactionChanged = false;
|
|
1958
|
-
let compactionEnded = false;
|
|
1959
|
-
if (line.includes('[Compaction]')) {
|
|
1960
|
-
// Accept session-scoped compaction lines; allow end markers to clear even
|
|
1961
|
-
// if the line lacks a session id (some builds omit Context on end lines).
|
|
1962
|
-
// For manual /compress, [Compaction] End uses the NEW session ID, so we need
|
|
1963
|
-
// to also accept End markers when compacting is true and it's an End line.
|
|
1964
|
-
const isManualCompactionEnd = compacting &&
|
|
1965
|
-
line.includes('[Compaction] End') &&
|
|
1966
|
-
lineSessionId &&
|
|
1967
|
-
String(lineSessionId).toLowerCase() !== sessionIdLower;
|
|
1968
|
-
if (isSessionLine || (compacting && !lineSessionId) || isManualCompactionEnd) {
|
|
1969
|
-
const next = nextCompactionState(line, compacting);
|
|
1970
|
-
if (next !== compacting) {
|
|
1971
|
-
compacting = next;
|
|
1972
|
-
compactionChanged = true;
|
|
1973
|
-
if (!compacting) compactionEnded = true;
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
if (compactionChanged && compacting) {
|
|
1979
|
-
pendingCompactionSuffixTokens = null;
|
|
1980
|
-
pendingCompactionSummaryOutputTokens = null;
|
|
1981
|
-
pendingCompactionSummaryTsMs = null;
|
|
1982
|
-
// Compaction can start after a context-limit error. Ensure we display the latest
|
|
1983
|
-
// pre-compaction ctx by reseeding from log (tail can miss bursts).
|
|
1984
|
-
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: true });
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
if (compactionEnded) {
|
|
1988
|
-
// ctx usage changes dramatically after compaction, but the next Context line
|
|
1989
|
-
// can be delayed. Clear displayed ctx immediately to avoid showing stale numbers.
|
|
1990
|
-
last.cacheReadInputTokens = 0;
|
|
1991
|
-
last.contextCount = 0;
|
|
1992
|
-
ctxAvailable = false;
|
|
1993
|
-
ctxOverrideUsedTokens = null;
|
|
1994
|
-
ctxApprox = false;
|
|
1995
|
-
ctxOverflow = false;
|
|
1996
|
-
if (tsMs != null) lastContextMs = tsMs;
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
if (!line.includes('Context:')) {
|
|
2000
|
-
if (compactionChanged) {
|
|
2001
|
-
lastRenderAt = Date.now();
|
|
2002
|
-
renderNow();
|
|
2003
|
-
}
|
|
2004
|
-
if (compactionEnded) {
|
|
2005
|
-
// Compaction often completes between turns. Refresh ctx numbers promptly
|
|
2006
|
-
// by rescanning the most recent Context entry for this session.
|
|
2007
|
-
setTimeout(() => {
|
|
2008
|
-
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
2009
|
-
}, 250).unref();
|
|
2010
|
-
}
|
|
2011
|
-
continue;
|
|
2012
|
-
}
|
|
2013
|
-
if (!isSessionLine) {
|
|
2014
|
-
if (compactionChanged) {
|
|
2015
|
-
lastRenderAt = Date.now();
|
|
2016
|
-
renderNow();
|
|
2017
|
-
}
|
|
2018
|
-
if (compactionEnded) {
|
|
2019
|
-
setTimeout(() => {
|
|
2020
|
-
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
2021
|
-
}, 250).unref();
|
|
2022
|
-
}
|
|
2023
|
-
continue;
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
const ctxIndex = line.indexOf('Context: ');
|
|
2027
|
-
if (ctxIndex === -1) continue;
|
|
2028
|
-
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
2029
|
-
let ctx;
|
|
2030
|
-
try {
|
|
2031
|
-
ctx = JSON.parse(jsonStr);
|
|
2032
|
-
} catch {
|
|
2033
|
-
if (compactionChanged) {
|
|
2034
|
-
lastRenderAt = Date.now();
|
|
2035
|
-
renderNow();
|
|
2036
|
-
}
|
|
2037
|
-
continue;
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
// Context usage can appear on multiple session-scoped log lines; update whenever present.
|
|
2041
|
-
// (Streaming is still the best source for outputTokens / LastOut.)
|
|
2042
|
-
updateLastFromContext(ctx, false, tsMs);
|
|
2043
|
-
|
|
2044
|
-
maybeCaptureCompactionSuffix(line, ctx);
|
|
2045
|
-
maybeUpdateCtxFromContextLimitFailure(line, ctx, tsMs);
|
|
2046
|
-
maybeApplyPostCompactionEstimate(line, ctx, tsMs);
|
|
2047
|
-
|
|
2048
|
-
// For new sessions: if this is the first valid Context line and ctx is still 0,
|
|
2049
|
-
// trigger a reseed to catch any earlier log entries we might have missed.
|
|
2050
|
-
if (!initialSeedDone && last.contextCount === 0) {
|
|
2051
|
-
initialSeedDone = true;
|
|
2052
|
-
setTimeout(() => {
|
|
2053
|
-
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
2054
|
-
}, 100).unref();
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
if (line.includes('[Agent] Streaming result')) {
|
|
2058
|
-
updateLastFromContext(ctx, true, tsMs);
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
const now = Date.now();
|
|
2062
|
-
if (compactionChanged || now - lastRenderAt >= MIN_RENDER_INTERVAL_MS) {
|
|
2063
|
-
lastRenderAt = now;
|
|
2064
|
-
renderNow();
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
if (compactionEnded) {
|
|
2068
|
-
setTimeout(() => {
|
|
2069
|
-
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
2070
|
-
}, 250).unref();
|
|
2071
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
});
|
|
2074
|
-
|
|
2075
|
-
const stop = () => {
|
|
2076
|
-
try { tail.kill('SIGTERM'); } catch {}
|
|
2077
|
-
process.exit(0);
|
|
2078
|
-
};
|
|
2079
|
-
|
|
2080
|
-
process.on('SIGTERM', stop);
|
|
2081
|
-
process.on('SIGINT', stop);
|
|
2082
|
-
process.on('SIGHUP', stop);
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
main().catch(() => {});
|
|
2086
|
-
`;
|
|
2087
|
-
}
|
|
2088
|
-
function generateStatuslineWrapperScript(execTargetPath, monitorScriptPath, sessionsScriptPath) {
|
|
2089
|
-
return generateStatuslineWrapperScriptBun(execTargetPath, monitorScriptPath, sessionsScriptPath);
|
|
2090
|
-
}
|
|
2091
|
-
function generateStatuslineWrapperScriptBun(execTargetPath, monitorScriptPath, sessionsScriptPath) {
|
|
2092
|
-
return `#!/usr/bin/env bun
|
|
2093
|
-
// Droid with Statusline (Bun PTY proxy)
|
|
2094
|
-
// Auto-generated by droid-patch --statusline
|
|
2095
|
-
|
|
2096
|
-
const EXEC_TARGET = ${JSON.stringify(execTargetPath)};
|
|
2097
|
-
const STATUSLINE_MONITOR = ${JSON.stringify(monitorScriptPath)};
|
|
2098
|
-
const SESSIONS_SCRIPT = ${sessionsScriptPath ? JSON.stringify(sessionsScriptPath) : "null"};
|
|
2099
|
-
|
|
2100
|
-
const IS_APPLE_TERMINAL = process.env.TERM_PROGRAM === "Apple_Terminal";
|
|
2101
|
-
const MIN_RENDER_INTERVAL_MS = IS_APPLE_TERMINAL ? 800 : 400;
|
|
2102
|
-
const QUIET_MS = 50;
|
|
2103
|
-
const FORCE_REPAINT_INTERVAL_MS = 2000;
|
|
2104
|
-
const RESERVED_ROWS = 1;
|
|
2105
|
-
|
|
2106
|
-
const BYPASS_FLAGS = new Set(["--help", "-h", "--version", "-V"]);
|
|
2107
|
-
const BYPASS_COMMANDS = new Set(["help", "version", "completion", "completions", "exec"]);
|
|
2108
|
-
|
|
2109
|
-
function shouldPassthrough(argv) {
|
|
2110
|
-
for (const a of argv) {
|
|
2111
|
-
if (a === "--") break;
|
|
2112
|
-
if (BYPASS_FLAGS.has(a)) return true;
|
|
2113
|
-
}
|
|
2114
|
-
let endOpts = false;
|
|
2115
|
-
let cmd = null;
|
|
2116
|
-
for (const a of argv) {
|
|
2117
|
-
if (a === "--") {
|
|
2118
|
-
endOpts = true;
|
|
2119
|
-
continue;
|
|
2120
|
-
}
|
|
2121
|
-
if (!endOpts && a.startsWith("-")) continue;
|
|
2122
|
-
cmd = a;
|
|
2123
|
-
break;
|
|
2124
|
-
}
|
|
2125
|
-
return cmd && BYPASS_COMMANDS.has(cmd);
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
function isSessionsCommand(argv) {
|
|
2129
|
-
for (const a of argv) {
|
|
2130
|
-
if (a === "--") return false;
|
|
2131
|
-
if (a === "--sessions") return true;
|
|
2132
|
-
}
|
|
2133
|
-
return false;
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
async function execPassthrough(argv) {
|
|
2137
|
-
const proc = Bun.spawn([EXEC_TARGET, ...argv], {
|
|
2138
|
-
stdin: "inherit",
|
|
2139
|
-
stdout: "inherit",
|
|
2140
|
-
stderr: "inherit",
|
|
2141
|
-
});
|
|
2142
|
-
const code = await proc.exited;
|
|
2143
|
-
process.exit(code ?? 0);
|
|
2144
|
-
}
|
|
2145
|
-
|
|
2146
|
-
async function runSessions() {
|
|
2147
|
-
if (SESSIONS_SCRIPT) {
|
|
2148
|
-
const proc = Bun.spawn(["node", String(SESSIONS_SCRIPT)], {
|
|
2149
|
-
stdin: "inherit",
|
|
2150
|
-
stdout: "inherit",
|
|
2151
|
-
stderr: "inherit",
|
|
2152
|
-
});
|
|
2153
|
-
const code = await proc.exited;
|
|
2154
|
-
process.exit(code ?? 0);
|
|
2155
|
-
}
|
|
2156
|
-
process.stderr.write("[statusline] sessions script not found\\n");
|
|
2157
|
-
process.exit(1);
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
function writeStdout(s) {
|
|
2161
|
-
try {
|
|
2162
|
-
process.stdout.write(s);
|
|
2163
|
-
} catch {
|
|
2164
|
-
// ignore
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
function termSize() {
|
|
2169
|
-
const rows = Number(process.stdout.rows || 24);
|
|
2170
|
-
const cols = Number(process.stdout.columns || 80);
|
|
2171
|
-
return { rows: Number.isFinite(rows) ? rows : 24, cols: Number.isFinite(cols) ? cols : 80 };
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
const ANSI_RE = /\\x1b\\[[0-9;]*m/g;
|
|
2175
|
-
const RESET_SGR = "\\x1b[0m";
|
|
2176
|
-
|
|
2177
|
-
function visibleWidth(text) {
|
|
2178
|
-
return String(text || "").replace(ANSI_RE, "").length;
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
function clampAnsi(text, cols) {
|
|
2182
|
-
if (!cols || cols <= 0) return String(text || "");
|
|
2183
|
-
cols = cols > 1 ? cols - 1 : cols; // avoid last-column wrap
|
|
2184
|
-
if (cols < 10) return String(text || "");
|
|
2185
|
-
const s = String(text || "");
|
|
2186
|
-
let visible = 0;
|
|
2187
|
-
let i = 0;
|
|
2188
|
-
const out = [];
|
|
2189
|
-
while (i < s.length) {
|
|
2190
|
-
const ch = s[i];
|
|
2191
|
-
if (ch === "\\x1b") {
|
|
2192
|
-
const m = s.indexOf("m", i);
|
|
2193
|
-
if (m !== -1) {
|
|
2194
|
-
out.push(s.slice(i, m + 1));
|
|
2195
|
-
i = m + 1;
|
|
2196
|
-
continue;
|
|
2197
|
-
}
|
|
2198
|
-
out.push(ch);
|
|
2199
|
-
i += 1;
|
|
2200
|
-
continue;
|
|
2201
|
-
}
|
|
2202
|
-
if (visible >= cols) break;
|
|
2203
|
-
out.push(ch);
|
|
2204
|
-
i += 1;
|
|
2205
|
-
visible += 1;
|
|
2206
|
-
}
|
|
2207
|
-
if (i < s.length && cols >= 1) {
|
|
2208
|
-
if (visible >= cols) {
|
|
2209
|
-
if (out.length) out[out.length - 1] = "…";
|
|
2210
|
-
else out.push("…");
|
|
2211
|
-
} else {
|
|
2212
|
-
out.push("…");
|
|
2213
|
-
}
|
|
2214
|
-
out.push(RESET_SGR);
|
|
2215
|
-
}
|
|
2216
|
-
return out.join("");
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
function splitSegments(text) {
|
|
2220
|
-
if (!text) return [];
|
|
2221
|
-
const s = String(text);
|
|
2222
|
-
const segments = [];
|
|
2223
|
-
let start = 0;
|
|
2224
|
-
while (true) {
|
|
2225
|
-
const idx = s.indexOf(RESET_SGR, start);
|
|
2226
|
-
if (idx === -1) {
|
|
2227
|
-
const tail = s.slice(start);
|
|
2228
|
-
if (tail) segments.push(tail);
|
|
2229
|
-
break;
|
|
2230
|
-
}
|
|
2231
|
-
const seg = s.slice(start, idx + RESET_SGR.length);
|
|
2232
|
-
if (seg) segments.push(seg);
|
|
2233
|
-
start = idx + RESET_SGR.length;
|
|
2234
|
-
}
|
|
2235
|
-
return segments;
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
function wrapSegments(segments, cols) {
|
|
2239
|
-
if (!segments || segments.length === 0) return [""];
|
|
2240
|
-
if (!cols || cols <= 0) return [segments.join("")];
|
|
2241
|
-
|
|
2242
|
-
const lines = [];
|
|
2243
|
-
let cur = [];
|
|
2244
|
-
let curW = 0;
|
|
2245
|
-
|
|
2246
|
-
for (let seg of segments) {
|
|
2247
|
-
let segW = visibleWidth(seg);
|
|
2248
|
-
if (segW <= 0) continue;
|
|
2249
|
-
|
|
2250
|
-
if (cur.length === 0) {
|
|
2251
|
-
if (segW > cols) {
|
|
2252
|
-
seg = clampAnsi(seg, cols);
|
|
2253
|
-
segW = visibleWidth(seg);
|
|
2254
|
-
}
|
|
2255
|
-
cur = [seg];
|
|
2256
|
-
curW = segW;
|
|
2257
|
-
continue;
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
if (curW + segW <= cols) {
|
|
2261
|
-
cur.push(seg);
|
|
2262
|
-
curW += segW;
|
|
2263
|
-
} else {
|
|
2264
|
-
lines.push(cur.join(""));
|
|
2265
|
-
if (segW > cols) {
|
|
2266
|
-
seg = clampAnsi(seg, cols);
|
|
2267
|
-
segW = visibleWidth(seg);
|
|
2268
|
-
}
|
|
2269
|
-
cur = [seg];
|
|
2270
|
-
curW = segW;
|
|
2271
|
-
}
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
if (cur.length) lines.push(cur.join(""));
|
|
2275
|
-
return lines.length ? lines : [""];
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
class StatusRenderer {
|
|
2279
|
-
constructor() {
|
|
2280
|
-
this.raw = "";
|
|
2281
|
-
this.segments = [];
|
|
2282
|
-
this.lines = [""];
|
|
2283
|
-
this.activeReservedRows = RESERVED_ROWS;
|
|
2284
|
-
this.force = false;
|
|
2285
|
-
this.urgent = false;
|
|
2286
|
-
this.lastRenderMs = 0;
|
|
2287
|
-
this.lastChildOutMs = 0;
|
|
2288
|
-
this.cursorVisible = true;
|
|
2289
|
-
}
|
|
2290
|
-
noteChildOutput() {
|
|
2291
|
-
this.lastChildOutMs = Date.now();
|
|
2292
|
-
}
|
|
2293
|
-
setCursorVisible(v) {
|
|
2294
|
-
this.cursorVisible = !!v;
|
|
2295
|
-
}
|
|
2296
|
-
forceRepaint(urgent = false) {
|
|
2297
|
-
this.force = true;
|
|
2298
|
-
if (urgent) this.urgent = true;
|
|
2299
|
-
}
|
|
2300
|
-
setActiveReservedRows(n) {
|
|
2301
|
-
const v = Number(n || 1);
|
|
2302
|
-
this.activeReservedRows = Number.isFinite(v) ? Math.max(1, Math.trunc(v)) : 1;
|
|
2303
|
-
}
|
|
2304
|
-
setLine(line) {
|
|
2305
|
-
const next = String(line || "");
|
|
2306
|
-
if (next !== this.raw) {
|
|
2307
|
-
this.raw = next;
|
|
2308
|
-
this.segments = splitSegments(next);
|
|
2309
|
-
this.force = true;
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
desiredReservedRows(physicalRows, cols, minReserved) {
|
|
2313
|
-
let rows = Number(physicalRows || 24);
|
|
2314
|
-
rows = Number.isFinite(rows) ? rows : 24;
|
|
2315
|
-
cols = Number(cols || 80);
|
|
2316
|
-
cols = Number.isFinite(cols) ? cols : 80;
|
|
2317
|
-
|
|
2318
|
-
const maxReserved = Math.max(1, rows - 4);
|
|
2319
|
-
const segs = this.segments.length ? this.segments : (this.raw ? [this.raw] : []);
|
|
2320
|
-
let lines = segs.length ? wrapSegments(segs, cols) : [""];
|
|
2321
|
-
|
|
2322
|
-
const needed = Math.min(lines.length, maxReserved);
|
|
2323
|
-
let desired = Math.max(Number(minReserved || 1), needed);
|
|
2324
|
-
desired = Math.min(desired, maxReserved);
|
|
2325
|
-
|
|
2326
|
-
if (lines.length < desired) lines = new Array(desired - lines.length).fill("").concat(lines);
|
|
2327
|
-
if (lines.length > desired) lines = lines.slice(-desired);
|
|
2328
|
-
|
|
2329
|
-
this.lines = lines;
|
|
2330
|
-
return desired;
|
|
2331
|
-
}
|
|
2332
|
-
clearReservedArea(physicalRows, cols, reservedRows, restoreRow = 1, restoreCol = 1) {
|
|
2333
|
-
let rows = Number(physicalRows || 24);
|
|
2334
|
-
rows = Number.isFinite(rows) ? rows : 24;
|
|
2335
|
-
cols = Number(cols || 80);
|
|
2336
|
-
cols = Number.isFinite(cols) ? cols : 80;
|
|
2337
|
-
let reserved = Number(reservedRows || 1);
|
|
2338
|
-
reserved = Number.isFinite(reserved) ? Math.max(1, Math.trunc(reserved)) : 1;
|
|
2339
|
-
|
|
2340
|
-
reserved = Math.min(reserved, rows);
|
|
2341
|
-
const startRow = rows - reserved + 1;
|
|
2342
|
-
const parts = ["\\x1b[?2026h", "\\x1b[?25l", RESET_SGR];
|
|
2343
|
-
for (let i = 0; i < reserved; i++) parts.push("\\x1b[" + (startRow + i) + ";1H\\x1b[2K");
|
|
2344
|
-
parts.push("\\x1b[" + restoreRow + ";" + restoreCol + "H");
|
|
2345
|
-
parts.push(this.cursorVisible ? "\\x1b[?25h" : "\\x1b[?25l");
|
|
2346
|
-
parts.push("\\x1b[?2026l");
|
|
2347
|
-
writeStdout(parts.join(""));
|
|
2348
|
-
}
|
|
2349
|
-
render(physicalRows, cols, restoreRow = 1, restoreCol = 1) {
|
|
2350
|
-
if (!this.force) return;
|
|
2351
|
-
if (!this.raw) {
|
|
2352
|
-
this.force = false;
|
|
2353
|
-
this.urgent = false;
|
|
2354
|
-
return;
|
|
2355
|
-
}
|
|
2356
|
-
const now = Date.now();
|
|
2357
|
-
if (!this.urgent && now - this.lastRenderMs < MIN_RENDER_INTERVAL_MS) return;
|
|
2358
|
-
if (!this.urgent && QUIET_MS > 0 && now - this.lastChildOutMs < QUIET_MS) return;
|
|
2359
|
-
|
|
2360
|
-
let rows = Number(physicalRows || 24);
|
|
2361
|
-
rows = Number.isFinite(rows) ? rows : 24;
|
|
2362
|
-
cols = Number(cols || 80);
|
|
2363
|
-
cols = Number.isFinite(cols) ? cols : 80;
|
|
2364
|
-
if (cols <= 0) cols = 80;
|
|
2365
|
-
|
|
2366
|
-
const reserved = Math.max(1, Math.min(this.activeReservedRows, Math.max(1, rows - 4)));
|
|
2367
|
-
const startRow = rows - reserved + 1;
|
|
2368
|
-
const childRows = rows - reserved;
|
|
2369
|
-
|
|
2370
|
-
let lines = this.lines.length ? this.lines.slice() : [""];
|
|
2371
|
-
if (lines.length < reserved) lines = new Array(reserved - lines.length).fill("").concat(lines);
|
|
2372
|
-
if (lines.length > reserved) lines = lines.slice(-reserved);
|
|
2373
|
-
|
|
2374
|
-
const parts = ["\\x1b[?2026h", "\\x1b[?25l"];
|
|
2375
|
-
parts.push("\\x1b[1;" + childRows + "r");
|
|
2376
|
-
for (let i = 0; i < reserved; i++) {
|
|
2377
|
-
const row = startRow + i;
|
|
2378
|
-
const text = clampAnsi(lines[i], cols);
|
|
2379
|
-
parts.push("\\x1b[" + row + ";1H" + RESET_SGR + "\\x1b[2K");
|
|
2380
|
-
parts.push("\\x1b[" + row + ";1H" + text + RESET_SGR);
|
|
2381
|
-
}
|
|
2382
|
-
parts.push("\\x1b[" + restoreRow + ";" + restoreCol + "H");
|
|
2383
|
-
parts.push(this.cursorVisible ? "\\x1b[?25h" : "\\x1b[?25l");
|
|
2384
|
-
parts.push("\\x1b[?2026l");
|
|
2385
|
-
writeStdout(parts.join(""));
|
|
2386
|
-
|
|
2387
|
-
this.lastRenderMs = now;
|
|
2388
|
-
this.force = false;
|
|
2389
|
-
this.urgent = false;
|
|
2390
|
-
}
|
|
2391
|
-
clear() {
|
|
2392
|
-
const { rows, cols } = termSize();
|
|
2393
|
-
this.clearReservedArea(rows, cols, Math.max(this.activeReservedRows, RESERVED_ROWS));
|
|
2394
|
-
}
|
|
2395
|
-
}
|
|
2396
|
-
|
|
2397
|
-
class OutputRewriter {
|
|
2398
|
-
constructor() {
|
|
2399
|
-
this.buf = new Uint8Array(0);
|
|
2400
|
-
}
|
|
2401
|
-
feed(chunk, maxRow) {
|
|
2402
|
-
if (!chunk || chunk.length === 0) return chunk;
|
|
2403
|
-
const merged = new Uint8Array(this.buf.length + chunk.length);
|
|
2404
|
-
merged.set(this.buf, 0);
|
|
2405
|
-
merged.set(chunk, this.buf.length);
|
|
2406
|
-
this.buf = new Uint8Array(0);
|
|
2407
|
-
|
|
2408
|
-
const out = [];
|
|
2409
|
-
let i = 0;
|
|
2410
|
-
|
|
2411
|
-
const isFinal = (v) => v >= 0x40 && v <= 0x7e;
|
|
2412
|
-
|
|
2413
|
-
while (i < merged.length) {
|
|
2414
|
-
const b = merged[i];
|
|
2415
|
-
if (b !== 0x1b) {
|
|
2416
|
-
out.push(b);
|
|
2417
|
-
i += 1;
|
|
2418
|
-
continue;
|
|
2419
|
-
}
|
|
2420
|
-
if (i + 1 >= merged.length) {
|
|
2421
|
-
this.buf = merged.slice(i);
|
|
2422
|
-
break;
|
|
2423
|
-
}
|
|
2424
|
-
const nxt = merged[i + 1];
|
|
2425
|
-
if (nxt !== 0x5b) {
|
|
2426
|
-
out.push(b);
|
|
2427
|
-
i += 1;
|
|
2428
|
-
continue;
|
|
2429
|
-
}
|
|
2430
|
-
|
|
2431
|
-
let j = i + 2;
|
|
2432
|
-
while (j < merged.length && !isFinal(merged[j])) j += 1;
|
|
2433
|
-
if (j >= merged.length) {
|
|
2434
|
-
this.buf = merged.slice(i);
|
|
2435
|
-
break;
|
|
2436
|
-
}
|
|
2437
|
-
const final = merged[j];
|
|
2438
|
-
let seq = merged.slice(i, j + 1);
|
|
2439
|
-
|
|
2440
|
-
if ((final === 0x48 || final === 0x66) && maxRow > 0) {
|
|
2441
|
-
const params = merged.slice(i + 2, j);
|
|
2442
|
-
const s = new TextDecoder().decode(params);
|
|
2443
|
-
if (!s || /^[0-9;]/.test(s)) {
|
|
2444
|
-
const parts = s ? s.split(";") : [];
|
|
2445
|
-
const row = Number(parts[0] || 1);
|
|
2446
|
-
const col = Number(parts[1] || 1);
|
|
2447
|
-
let r = Number.isFinite(row) ? row : 1;
|
|
2448
|
-
let c = Number.isFinite(col) ? col : 1;
|
|
2449
|
-
if (r === 999 || r > maxRow) r = maxRow;
|
|
2450
|
-
if (r < 1) r = 1;
|
|
2451
|
-
if (c < 1) c = 1;
|
|
2452
|
-
const newParams = new TextEncoder().encode(String(r) + ";" + String(c));
|
|
2453
|
-
const ns = new Uint8Array(2 + newParams.length + 1);
|
|
2454
|
-
ns[0] = 0x1b;
|
|
2455
|
-
ns[1] = 0x5b;
|
|
2456
|
-
ns.set(newParams, 2);
|
|
2457
|
-
ns[ns.length - 1] = final;
|
|
2458
|
-
seq = ns;
|
|
2459
|
-
}
|
|
2460
|
-
} else if (final === 0x72 && maxRow > 0) {
|
|
2461
|
-
const params = merged.slice(i + 2, j);
|
|
2462
|
-
const s = new TextDecoder().decode(params);
|
|
2463
|
-
if (!s || /^[0-9;]/.test(s)) {
|
|
2464
|
-
const parts = s ? s.split(";") : [];
|
|
2465
|
-
const top = Number(parts[0] || 1);
|
|
2466
|
-
const bottom = Number(parts[1] || maxRow);
|
|
2467
|
-
let t = Number.isFinite(top) ? top : 1;
|
|
2468
|
-
let btm = Number.isFinite(bottom) ? bottom : maxRow;
|
|
2469
|
-
if (t <= 0) t = 1;
|
|
2470
|
-
if (btm <= 0 || btm === 999 || btm > maxRow) btm = maxRow;
|
|
2471
|
-
if (t > btm) t = 1;
|
|
2472
|
-
const str = "\\x1b[" + String(t) + ";" + String(btm) + "r";
|
|
2473
|
-
seq = new TextEncoder().encode(str);
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
|
|
2477
|
-
for (const bb of seq) out.push(bb);
|
|
2478
|
-
i = j + 1;
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
return new Uint8Array(out);
|
|
2482
|
-
}
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
class CursorTracker {
|
|
2486
|
-
constructor() {
|
|
2487
|
-
this.row = 1;
|
|
2488
|
-
this.col = 1;
|
|
2489
|
-
this.savedRow = 1;
|
|
2490
|
-
this.savedCol = 1;
|
|
2491
|
-
this.buf = new Uint8Array(0);
|
|
2492
|
-
this.inOsc = false;
|
|
2493
|
-
this.utf8Cont = 0;
|
|
2494
|
-
this.wrapPending = false;
|
|
2495
|
-
}
|
|
2496
|
-
position() {
|
|
2497
|
-
return { row: this.row, col: this.col };
|
|
2498
|
-
}
|
|
2499
|
-
feed(chunk, maxRow, maxCol) {
|
|
2500
|
-
if (!chunk || chunk.length === 0) return;
|
|
2501
|
-
maxRow = Math.max(1, Number(maxRow || 1));
|
|
2502
|
-
maxCol = Math.max(1, Number(maxCol || 1));
|
|
2503
|
-
|
|
2504
|
-
const merged = new Uint8Array(this.buf.length + chunk.length);
|
|
2505
|
-
merged.set(this.buf, 0);
|
|
2506
|
-
merged.set(chunk, this.buf.length);
|
|
2507
|
-
this.buf = new Uint8Array(0);
|
|
2508
|
-
|
|
2509
|
-
const clamp = () => {
|
|
2510
|
-
if (this.row < 1) this.row = 1;
|
|
2511
|
-
else if (this.row > maxRow) this.row = maxRow;
|
|
2512
|
-
if (this.col < 1) this.col = 1;
|
|
2513
|
-
else if (this.col > maxCol) this.col = maxCol;
|
|
2514
|
-
};
|
|
2515
|
-
|
|
2516
|
-
const parseIntDefault = (v, d) => {
|
|
2517
|
-
const n = Number(v);
|
|
2518
|
-
return Number.isFinite(n) && n > 0 ? Math.trunc(n) : d;
|
|
2519
|
-
};
|
|
2520
|
-
|
|
2521
|
-
let i = 0;
|
|
2522
|
-
const isFinal = (v) => v >= 0x40 && v <= 0x7e;
|
|
2523
|
-
|
|
2524
|
-
while (i < merged.length) {
|
|
2525
|
-
const b = merged[i];
|
|
2526
|
-
|
|
2527
|
-
if (this.inOsc) {
|
|
2528
|
-
if (b === 0x07) {
|
|
2529
|
-
this.inOsc = false;
|
|
2530
|
-
i += 1;
|
|
2531
|
-
continue;
|
|
2532
|
-
}
|
|
2533
|
-
if (b === 0x1b) {
|
|
2534
|
-
if (i + 1 >= merged.length) {
|
|
2535
|
-
this.buf = merged.slice(i);
|
|
2536
|
-
break;
|
|
2537
|
-
}
|
|
2538
|
-
if (merged[i + 1] === 0x5c) {
|
|
2539
|
-
this.inOsc = false;
|
|
2540
|
-
i += 2;
|
|
2541
|
-
continue;
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
i += 1;
|
|
2545
|
-
continue;
|
|
2546
|
-
}
|
|
2547
|
-
|
|
2548
|
-
if (this.utf8Cont > 0) {
|
|
2549
|
-
if (b >= 0x80 && b <= 0xbf) {
|
|
2550
|
-
this.utf8Cont -= 1;
|
|
2551
|
-
i += 1;
|
|
2552
|
-
continue;
|
|
2553
|
-
}
|
|
2554
|
-
this.utf8Cont = 0;
|
|
2555
|
-
}
|
|
2556
|
-
|
|
2557
|
-
if (b === 0x1b) {
|
|
2558
|
-
this.wrapPending = false;
|
|
2559
|
-
if (i + 1 >= merged.length) {
|
|
2560
|
-
this.buf = merged.slice(i);
|
|
2561
|
-
break;
|
|
2562
|
-
}
|
|
2563
|
-
const nxt = merged[i + 1];
|
|
2564
|
-
|
|
2565
|
-
if (nxt === 0x5b) {
|
|
2566
|
-
let j = i + 2;
|
|
2567
|
-
while (j < merged.length && !isFinal(merged[j])) j += 1;
|
|
2568
|
-
if (j >= merged.length) {
|
|
2569
|
-
this.buf = merged.slice(i);
|
|
2570
|
-
break;
|
|
2571
|
-
}
|
|
2572
|
-
const final = merged[j];
|
|
2573
|
-
const params = merged.slice(i + 2, j);
|
|
2574
|
-
const s = new TextDecoder().decode(params);
|
|
2575
|
-
if (s && !/^[0-9;]/.test(s)) {
|
|
2576
|
-
i = j + 1;
|
|
2577
|
-
continue;
|
|
2578
|
-
}
|
|
2579
|
-
const parts = s ? s.split(";") : [];
|
|
2580
|
-
const p0 = parseIntDefault(parts[0] || "", 1);
|
|
2581
|
-
const p1 = parseIntDefault(parts[1] || "", 1);
|
|
2582
|
-
|
|
2583
|
-
if (final === 0x48 || final === 0x66) {
|
|
2584
|
-
this.row = p0;
|
|
2585
|
-
this.col = p1;
|
|
2586
|
-
clamp();
|
|
2587
|
-
} else if (final === 0x41) {
|
|
2588
|
-
this.row = Math.max(1, this.row - p0);
|
|
2589
|
-
} else if (final === 0x42) {
|
|
2590
|
-
this.row = Math.min(maxRow, this.row + p0);
|
|
2591
|
-
} else if (final === 0x43) {
|
|
2592
|
-
this.col = Math.min(maxCol, this.col + p0);
|
|
2593
|
-
} else if (final === 0x44) {
|
|
2594
|
-
this.col = Math.max(1, this.col - p0);
|
|
2595
|
-
} else if (final === 0x45) {
|
|
2596
|
-
this.row = Math.min(maxRow, this.row + p0);
|
|
2597
|
-
this.col = 1;
|
|
2598
|
-
} else if (final === 0x46) {
|
|
2599
|
-
this.row = Math.max(1, this.row - p0);
|
|
2600
|
-
this.col = 1;
|
|
2601
|
-
} else if (final === 0x47) {
|
|
2602
|
-
this.col = p0;
|
|
2603
|
-
clamp();
|
|
2604
|
-
} else if (final === 0x64) {
|
|
2605
|
-
this.row = p0;
|
|
2606
|
-
clamp();
|
|
2607
|
-
} else if (final === 0x72) {
|
|
2608
|
-
this.row = 1;
|
|
2609
|
-
this.col = 1;
|
|
2610
|
-
} else if (final === 0x73) {
|
|
2611
|
-
this.savedRow = this.row;
|
|
2612
|
-
this.savedCol = this.col;
|
|
2613
|
-
} else if (final === 0x75) {
|
|
2614
|
-
this.row = this.savedRow;
|
|
2615
|
-
this.col = this.savedCol;
|
|
2616
|
-
clamp();
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
|
-
i = j + 1;
|
|
2620
|
-
continue;
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
if (nxt === 0x5d || nxt === 0x50 || nxt === 0x5e || nxt === 0x5f || nxt === 0x58) {
|
|
2624
|
-
this.inOsc = true;
|
|
2625
|
-
i += 2;
|
|
2626
|
-
continue;
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
if (nxt === 0x37) {
|
|
2630
|
-
this.savedRow = this.row;
|
|
2631
|
-
this.savedCol = this.col;
|
|
2632
|
-
i += 2;
|
|
2633
|
-
continue;
|
|
2634
|
-
}
|
|
2635
|
-
if (nxt === 0x38) {
|
|
2636
|
-
this.row = this.savedRow;
|
|
2637
|
-
this.col = this.savedCol;
|
|
2638
|
-
clamp();
|
|
2639
|
-
i += 2;
|
|
2640
|
-
continue;
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
i += 2;
|
|
2644
|
-
continue;
|
|
2645
|
-
}
|
|
2646
|
-
|
|
2647
|
-
if (b === 0x0d) {
|
|
2648
|
-
this.col = 1;
|
|
2649
|
-
this.wrapPending = false;
|
|
2650
|
-
i += 1;
|
|
2651
|
-
continue;
|
|
2652
|
-
}
|
|
2653
|
-
if (b === 0x0a || b === 0x0b || b === 0x0c) {
|
|
2654
|
-
this.row = Math.min(maxRow, this.row + 1);
|
|
2655
|
-
this.wrapPending = false;
|
|
2656
|
-
i += 1;
|
|
2657
|
-
continue;
|
|
2658
|
-
}
|
|
2659
|
-
if (b === 0x08) {
|
|
2660
|
-
this.col = Math.max(1, this.col - 1);
|
|
2661
|
-
this.wrapPending = false;
|
|
2662
|
-
i += 1;
|
|
2663
|
-
continue;
|
|
2664
|
-
}
|
|
2665
|
-
if (b === 0x09) {
|
|
2666
|
-
const nextStop = Math.floor((this.col - 1) / 8 + 1) * 8 + 1;
|
|
2667
|
-
this.col = Math.min(maxCol, nextStop);
|
|
2668
|
-
this.wrapPending = false;
|
|
2669
|
-
i += 1;
|
|
2670
|
-
continue;
|
|
2671
|
-
}
|
|
2672
|
-
if (b < 0x20 || b === 0x7f) {
|
|
2673
|
-
i += 1;
|
|
2674
|
-
continue;
|
|
2675
|
-
}
|
|
2676
|
-
|
|
2677
|
-
if (this.wrapPending) {
|
|
2678
|
-
this.row = Math.min(maxRow, this.row + 1);
|
|
2679
|
-
this.col = 1;
|
|
2680
|
-
this.wrapPending = false;
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
|
-
if (b >= 0x80) {
|
|
2684
|
-
if ((b & 0xe0) === 0xc0) this.utf8Cont = 1;
|
|
2685
|
-
else if ((b & 0xf0) === 0xe0) this.utf8Cont = 2;
|
|
2686
|
-
else if ((b & 0xf8) === 0xf0) this.utf8Cont = 3;
|
|
2687
|
-
else this.utf8Cont = 0;
|
|
2688
|
-
}
|
|
2689
|
-
|
|
2690
|
-
if (this.col < maxCol) this.col += 1;
|
|
2691
|
-
else {
|
|
2692
|
-
this.col = maxCol;
|
|
2693
|
-
this.wrapPending = true;
|
|
2694
|
-
}
|
|
2695
|
-
i += 1;
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
|
-
async function main() {
|
|
2701
|
-
const argv = process.argv.slice(2);
|
|
2702
|
-
|
|
2703
|
-
if (isSessionsCommand(argv)) await runSessions();
|
|
2704
|
-
|
|
2705
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY || shouldPassthrough(argv)) {
|
|
2706
|
-
await execPassthrough(argv);
|
|
2707
|
-
return;
|
|
2708
|
-
}
|
|
2709
|
-
|
|
2710
|
-
// Clean viewport.
|
|
2711
|
-
writeStdout("\\x1b[?2026h\\x1b[0m\\x1b[r\\x1b[2J\\x1b[H\\x1b[?2026l");
|
|
2712
|
-
|
|
2713
|
-
const renderer = new StatusRenderer();
|
|
2714
|
-
renderer.setLine("\\x1b[48;5;238m\\x1b[38;5;15m Statusline: starting… \\x1b[0m");
|
|
2715
|
-
renderer.forceRepaint(true);
|
|
2716
|
-
|
|
2717
|
-
let { rows: physicalRows, cols: physicalCols } = termSize();
|
|
2718
|
-
let effectiveReservedRows = renderer.desiredReservedRows(physicalRows, physicalCols, RESERVED_ROWS);
|
|
2719
|
-
renderer.setActiveReservedRows(effectiveReservedRows);
|
|
2720
|
-
let childRows = Math.max(4, physicalRows - effectiveReservedRows);
|
|
2721
|
-
let childCols = Math.max(10, physicalCols);
|
|
2722
|
-
|
|
2723
|
-
// Reserve the bottom rows early, before the child starts writing.
|
|
2724
|
-
writeStdout(
|
|
2725
|
-
"\\x1b[?2026h\\x1b[?25l\\x1b[1;" + childRows + "r\\x1b[1;1H\\x1b[?25h\\x1b[?2026l",
|
|
2726
|
-
);
|
|
2727
|
-
renderer.forceRepaint(true);
|
|
2728
|
-
renderer.render(physicalRows, physicalCols, 1, 1);
|
|
2729
|
-
|
|
2730
|
-
// Spawn child with terminal support.
|
|
2731
|
-
let child;
|
|
2732
|
-
try {
|
|
2733
|
-
child = Bun.spawn([EXEC_TARGET, ...argv], {
|
|
2734
|
-
cwd: process.cwd(),
|
|
2735
|
-
env: process.env,
|
|
2736
|
-
detached: true,
|
|
2737
|
-
terminal: {
|
|
2738
|
-
cols: childCols,
|
|
2739
|
-
rows: childRows,
|
|
2740
|
-
data(_terminal, data) {
|
|
2741
|
-
onChildData(data);
|
|
2742
|
-
},
|
|
2743
|
-
},
|
|
2744
|
-
onExit(_proc, exitCode, signal, _error) {
|
|
2745
|
-
onChildExit(exitCode, signal);
|
|
2746
|
-
},
|
|
2747
|
-
});
|
|
2748
|
-
} catch (e) {
|
|
2749
|
-
process.stderr.write("[statusline] failed to spawn child: " + String(e?.message || e) + "\\n");
|
|
2750
|
-
process.exit(1);
|
|
2751
|
-
}
|
|
2752
|
-
|
|
2753
|
-
const terminal = child.terminal;
|
|
2754
|
-
|
|
2755
|
-
// Best-effort PGID resolution (matches Python wrapper behavior).
|
|
2756
|
-
// This improves session resolution (ps/lsof scanning) and signal forwarding.
|
|
2757
|
-
let pgid = child.pid;
|
|
2758
|
-
try {
|
|
2759
|
-
const res = Bun.spawnSync(["ps", "-o", "pgid=", "-p", String(child.pid)], {
|
|
2760
|
-
stdin: "ignore",
|
|
2761
|
-
stdout: "pipe",
|
|
2762
|
-
stderr: "ignore",
|
|
2763
|
-
});
|
|
2764
|
-
if (res && res.exitCode === 0 && res.stdout) {
|
|
2765
|
-
const text = new TextDecoder().decode(res.stdout).trim();
|
|
2766
|
-
const n = Number(text);
|
|
2767
|
-
if (Number.isFinite(n) && n > 0) pgid = Math.trunc(n);
|
|
2768
|
-
}
|
|
2769
|
-
} catch {}
|
|
2770
|
-
|
|
2771
|
-
// Spawn monitor (Node).
|
|
2772
|
-
const monitorEnv = { ...process.env, DROID_STATUSLINE_PGID: String(pgid) };
|
|
2773
|
-
const monitor = Bun.spawn(["node", STATUSLINE_MONITOR, ...argv], {
|
|
2774
|
-
stdin: "ignore",
|
|
2775
|
-
stdout: "pipe",
|
|
2776
|
-
stderr: "ignore",
|
|
2777
|
-
env: monitorEnv,
|
|
2778
|
-
});
|
|
2779
|
-
|
|
2780
|
-
let shouldStop = false;
|
|
2781
|
-
const rewriter = new OutputRewriter();
|
|
2782
|
-
const cursor = new CursorTracker();
|
|
2783
|
-
|
|
2784
|
-
let detectBuf = new Uint8Array(0);
|
|
2785
|
-
let detectStr = "";
|
|
2786
|
-
let cursorVisible = true;
|
|
2787
|
-
let scrollRegionDirty = true;
|
|
2788
|
-
let lastForceRepaintMs = Date.now();
|
|
2789
|
-
let lastPhysicalRows = 0;
|
|
2790
|
-
let lastPhysicalCols = 0;
|
|
2791
|
-
|
|
2792
|
-
function appendDetect(chunk) {
|
|
2793
|
-
const max = 128;
|
|
2794
|
-
const merged = new Uint8Array(Math.min(max, detectBuf.length + chunk.length));
|
|
2795
|
-
const takePrev = Math.max(0, merged.length - chunk.length);
|
|
2796
|
-
if (takePrev > 0) merged.set(detectBuf.slice(Math.max(0, detectBuf.length - takePrev)), 0);
|
|
2797
|
-
merged.set(chunk.slice(Math.max(0, chunk.length - (merged.length - takePrev))), takePrev);
|
|
2798
|
-
detectBuf = merged;
|
|
2799
|
-
try {
|
|
2800
|
-
detectStr = Buffer.from(detectBuf).toString("latin1");
|
|
2801
|
-
} catch {
|
|
2802
|
-
detectStr = "";
|
|
2803
|
-
}
|
|
2804
|
-
}
|
|
2805
|
-
|
|
2806
|
-
function includesBytes(needle) {
|
|
2807
|
-
return detectStr.includes(needle);
|
|
2808
|
-
}
|
|
2809
|
-
|
|
2810
|
-
function lastIndexOfBytes(needle) {
|
|
2811
|
-
return detectStr.lastIndexOf(needle);
|
|
2812
|
-
}
|
|
2813
|
-
|
|
2814
|
-
function includesScrollRegionCSI() {
|
|
2815
|
-
return /\\x1b\\[[0-9]*;?[0-9]*r/.test(detectStr);
|
|
2816
|
-
}
|
|
2817
|
-
|
|
2818
|
-
function updateCursorVisibility() {
|
|
2819
|
-
const show = includesBytes("\\x1b[?25h");
|
|
2820
|
-
const hide = includesBytes("\\x1b[?25l");
|
|
2821
|
-
if (show || hide) {
|
|
2822
|
-
// best-effort: if both present, whichever appears later "wins"
|
|
2823
|
-
const h = lastIndexOfBytes("\\x1b[?25h");
|
|
2824
|
-
const l = lastIndexOfBytes("\\x1b[?25l");
|
|
2825
|
-
cursorVisible = h > l;
|
|
2826
|
-
renderer.setCursorVisible(cursorVisible);
|
|
2827
|
-
}
|
|
2828
|
-
}
|
|
2829
|
-
|
|
2830
|
-
function needsScrollRegionReset() {
|
|
2831
|
-
return (
|
|
2832
|
-
includesBytes("\\x1b[?1049") ||
|
|
2833
|
-
includesBytes("\\x1b[?1047") ||
|
|
2834
|
-
includesBytes("\\x1b[?47") ||
|
|
2835
|
-
includesBytes("\\x1b[J") ||
|
|
2836
|
-
includesBytes("\\x1b[0J") ||
|
|
2837
|
-
includesBytes("\\x1b[1J") ||
|
|
2838
|
-
includesBytes("\\x1b[2J") ||
|
|
2839
|
-
includesBytes("\\x1b[3J") ||
|
|
2840
|
-
includesBytes("\\x1b[r") ||
|
|
2841
|
-
includesScrollRegionCSI()
|
|
2842
|
-
);
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
function onChildData(data) {
|
|
2846
|
-
if (shouldStop) return;
|
|
2847
|
-
const chunk = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
2848
|
-
appendDetect(chunk);
|
|
2849
|
-
if (needsScrollRegionReset()) scrollRegionDirty = true;
|
|
2850
|
-
updateCursorVisibility();
|
|
2851
|
-
|
|
2852
|
-
renderer.noteChildOutput();
|
|
2853
|
-
const rewritten = rewriter.feed(chunk, childRows);
|
|
2854
|
-
cursor.feed(rewritten, childRows, childCols);
|
|
2855
|
-
writeStdout(Buffer.from(rewritten));
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
|
-
let cleanupCalled = false;
|
|
2859
|
-
function onChildExit(exitCode, signal) {
|
|
2860
|
-
shouldStop = true;
|
|
2861
|
-
if (cleanupCalled) return;
|
|
2862
|
-
cleanupCalled = true;
|
|
2863
|
-
const code = exitCode ?? (signal != null ? 128 + signal : 0);
|
|
2864
|
-
cleanup().finally(() => process.exit(code));
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2867
|
-
async function readMonitor() {
|
|
2868
|
-
if (!monitor.stdout) return;
|
|
2869
|
-
const reader = monitor.stdout.getReader();
|
|
2870
|
-
let buf = "";
|
|
2871
|
-
while (!shouldStop) {
|
|
2872
|
-
const { value, done } = await reader.read();
|
|
2873
|
-
if (done || !value) break;
|
|
2874
|
-
buf += new TextDecoder().decode(value);
|
|
2875
|
-
while (true) {
|
|
2876
|
-
const idx = buf.indexOf("\\n");
|
|
2877
|
-
if (idx === -1) break;
|
|
2878
|
-
const line = buf.slice(0, idx).replace(/\\r$/, "");
|
|
2879
|
-
buf = buf.slice(idx + 1);
|
|
2880
|
-
if (!line) continue;
|
|
2881
|
-
renderer.setLine(line);
|
|
2882
|
-
renderer.forceRepaint(false);
|
|
2883
|
-
}
|
|
2884
|
-
}
|
|
2885
|
-
}
|
|
2886
|
-
readMonitor().catch(() => {});
|
|
2887
|
-
|
|
2888
|
-
function repaintStatusline(forceUrgent = false) {
|
|
2889
|
-
const { row, col } = cursor.position();
|
|
2890
|
-
let r = Math.max(1, Math.min(childRows, row));
|
|
2891
|
-
let c = Math.max(1, Math.min(childCols, col));
|
|
2892
|
-
|
|
2893
|
-
if (scrollRegionDirty) {
|
|
2894
|
-
const seq =
|
|
2895
|
-
"\\x1b[?2026h\\x1b[?25l\\x1b[1;" +
|
|
2896
|
-
childRows +
|
|
2897
|
-
"r\\x1b[" +
|
|
2898
|
-
r +
|
|
2899
|
-
";" +
|
|
2900
|
-
c +
|
|
2901
|
-
"H" +
|
|
2902
|
-
(cursorVisible ? "\\x1b[?25h" : "\\x1b[?25l") +
|
|
2903
|
-
"\\x1b[?2026l";
|
|
2904
|
-
writeStdout(seq);
|
|
2905
|
-
scrollRegionDirty = false;
|
|
2906
|
-
}
|
|
2907
|
-
|
|
2908
|
-
renderer.forceRepaint(forceUrgent);
|
|
2909
|
-
renderer.render(physicalRows, physicalCols, r, c);
|
|
2910
|
-
}
|
|
2911
|
-
|
|
2912
|
-
function handleSizeChange(nextRows, nextCols, forceUrgent = false) {
|
|
2913
|
-
physicalRows = nextRows;
|
|
2914
|
-
physicalCols = nextCols;
|
|
2915
|
-
|
|
2916
|
-
const desired = renderer.desiredReservedRows(physicalRows, physicalCols, RESERVED_ROWS);
|
|
2917
|
-
const { row, col } = cursor.position();
|
|
2918
|
-
if (desired < effectiveReservedRows) {
|
|
2919
|
-
renderer.clearReservedArea(physicalRows, physicalCols, effectiveReservedRows, row, col);
|
|
2920
|
-
}
|
|
2921
|
-
effectiveReservedRows = desired;
|
|
2922
|
-
renderer.setActiveReservedRows(effectiveReservedRows);
|
|
2923
|
-
|
|
2924
|
-
childRows = Math.max(4, physicalRows - effectiveReservedRows);
|
|
2925
|
-
childCols = Math.max(10, physicalCols);
|
|
2926
|
-
try {
|
|
2927
|
-
terminal.resize(childCols, childRows);
|
|
2928
|
-
} catch {}
|
|
2929
|
-
try {
|
|
2930
|
-
process.kill(-child.pid, "SIGWINCH");
|
|
2931
|
-
} catch {
|
|
2932
|
-
try { process.kill(child.pid, "SIGWINCH"); } catch {}
|
|
2933
|
-
}
|
|
2934
|
-
|
|
2935
|
-
scrollRegionDirty = true;
|
|
2936
|
-
renderer.forceRepaint(true);
|
|
2937
|
-
repaintStatusline(forceUrgent);
|
|
174
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
175
|
+
res.end(JSON.stringify({ status: 'ok', mode: 'external-providers' }));
|
|
176
|
+
return;
|
|
2938
177
|
}
|
|
2939
178
|
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
// Forward signals to child's process group when possible.
|
|
2946
|
-
const forward = (sig) => {
|
|
2947
|
-
// Stop processing child output before forwarding signal
|
|
2948
|
-
// This prevents the child's cleanup/clear screen sequences from being written
|
|
2949
|
-
shouldStop = true;
|
|
2950
|
-
try {
|
|
2951
|
-
process.kill(-pgid, sig);
|
|
2952
|
-
} catch {
|
|
179
|
+
if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {
|
|
180
|
+
let body = '';
|
|
181
|
+
req.on('data', function(c) { body += c; });
|
|
182
|
+
req.on('end', async function() {
|
|
2953
183
|
try {
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
184
|
+
const parsed = JSON.parse(body);
|
|
185
|
+
const result = await search(parsed.query, parsed.numResults || 10);
|
|
186
|
+
log('Results:', result.results.length, 'from', result.source);
|
|
187
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
188
|
+
res.end(JSON.stringify({ results: result.results }));
|
|
189
|
+
} catch (e) {
|
|
190
|
+
log('Search error:', e.message);
|
|
191
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
192
|
+
res.end(JSON.stringify({ error: String(e), results: [] }));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
2962
196
|
}
|
|
2963
197
|
|
|
2964
|
-
//
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
// Prefer bytes when supported; fall back to UTF-8 decoding.
|
|
2974
|
-
try {
|
|
2975
|
-
// Bun.Terminal.write may accept Uint8Array in newer versions.
|
|
2976
|
-
terminal.write(buf);
|
|
2977
|
-
} catch {
|
|
2978
|
-
terminal.write(new TextDecoder().decode(buf));
|
|
2979
|
-
}
|
|
2980
|
-
}
|
|
2981
|
-
} catch {}
|
|
198
|
+
// Proxy other requests
|
|
199
|
+
const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);
|
|
200
|
+
const proxyModule = proxyUrl.protocol === 'https:' ? https : http;
|
|
201
|
+
const proxyReq = proxyModule.request(proxyUrl, {
|
|
202
|
+
method: req.method,
|
|
203
|
+
headers: Object.assign({}, req.headers, { host: proxyUrl.host })
|
|
204
|
+
}, function(proxyRes) {
|
|
205
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
206
|
+
proxyRes.pipe(res);
|
|
2982
207
|
});
|
|
2983
208
|
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
const desired = renderer.desiredReservedRows(next.rows, next.cols, RESERVED_ROWS);
|
|
2989
|
-
if (sizeChanged || desired !== effectiveReservedRows) {
|
|
2990
|
-
handleSizeChange(next.rows, next.cols, true);
|
|
2991
|
-
lastPhysicalRows = next.rows;
|
|
2992
|
-
lastPhysicalCols = next.cols;
|
|
2993
|
-
lastForceRepaintMs = Date.now();
|
|
2994
|
-
return;
|
|
2995
|
-
}
|
|
2996
|
-
const now = Date.now();
|
|
2997
|
-
if (now - lastForceRepaintMs >= FORCE_REPAINT_INTERVAL_MS) {
|
|
2998
|
-
repaintStatusline(false);
|
|
2999
|
-
lastForceRepaintMs = now;
|
|
3000
|
-
} else {
|
|
3001
|
-
const { row, col } = cursor.position();
|
|
3002
|
-
renderer.render(physicalRows, physicalCols, row, col);
|
|
3003
|
-
}
|
|
3004
|
-
}, 50);
|
|
209
|
+
proxyReq.on('error', function(e) {
|
|
210
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
211
|
+
res.end(JSON.stringify({ error: 'Proxy failed: ' + e.message }));
|
|
212
|
+
});
|
|
3005
213
|
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
process.stdin.setRawMode(false);
|
|
3010
|
-
} catch {}
|
|
3011
|
-
// Don't clear screen or reset scroll region on exit - preserve session ID and logs
|
|
3012
|
-
// Only reset colors and show cursor
|
|
3013
|
-
try {
|
|
3014
|
-
writeStdout("\\x1b[0m\\x1b[?25h");
|
|
3015
|
-
} catch {}
|
|
3016
|
-
try {
|
|
3017
|
-
monitor.kill();
|
|
3018
|
-
} catch {}
|
|
3019
|
-
try {
|
|
3020
|
-
terminal.close();
|
|
3021
|
-
} catch {}
|
|
3022
|
-
}
|
|
214
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') req.pipe(proxyReq);
|
|
215
|
+
else proxyReq.end();
|
|
216
|
+
});
|
|
3023
217
|
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
218
|
+
server.listen(PORT, '127.0.0.1', function() {
|
|
219
|
+
const actualPort = server.address().port;
|
|
220
|
+
const portFile = process.env.SEARCH_PROXY_PORT_FILE;
|
|
221
|
+
if (portFile) fs.writeFileSync(portFile, String(actualPort));
|
|
222
|
+
console.log('PORT=' + actualPort);
|
|
223
|
+
log('External providers proxy on port', actualPort);
|
|
224
|
+
});
|
|
3028
225
|
|
|
3029
|
-
|
|
226
|
+
process.on('SIGTERM', function() { server.close(); process.exit(0); });
|
|
227
|
+
process.on('SIGINT', function() { server.close(); process.exit(0); });
|
|
3030
228
|
`;
|
|
3031
229
|
}
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
const monitorScriptPath = join(outputDir, `${aliasName}-statusline.js`);
|
|
3035
|
-
const wrapperScriptPath = join(outputDir, aliasName);
|
|
3036
|
-
await writeFile(monitorScriptPath, generateStatuslineMonitorScript());
|
|
3037
|
-
await chmod(monitorScriptPath, 493);
|
|
3038
|
-
await writeFile(wrapperScriptPath, generateStatuslineWrapperScript(execTargetPath, monitorScriptPath, sessionsScriptPath));
|
|
3039
|
-
await chmod(wrapperScriptPath, 493);
|
|
3040
|
-
return {
|
|
3041
|
-
wrapperScript: wrapperScriptPath,
|
|
3042
|
-
monitorScript: monitorScriptPath
|
|
3043
|
-
};
|
|
230
|
+
function generateExternalSearchProxyServer(factoryApiUrl = "https://api.factory.ai") {
|
|
231
|
+
return generateSearchProxyServerCode().replace("const FACTORY_API = 'https://api.factory.ai';", `const FACTORY_API = '${factoryApiUrl}';`);
|
|
3044
232
|
}
|
|
3045
233
|
|
|
3046
234
|
//#endregion
|
|
3047
|
-
//#region src/
|
|
235
|
+
//#region src/websearch-native.ts
|
|
3048
236
|
/**
|
|
3049
|
-
*
|
|
237
|
+
* WebSearch Native Provider Mode (--websearch-proxy)
|
|
238
|
+
*
|
|
239
|
+
* Uses model's native websearch based on ~/.factory/settings.json configuration
|
|
240
|
+
* Requires proxy plugin (anthropic4droid) to handle format conversion
|
|
241
|
+
*
|
|
242
|
+
* Supported providers: Anthropic, OpenAI (extensible)
|
|
3050
243
|
*/
|
|
3051
|
-
function
|
|
244
|
+
function generateNativeSearchProxyServer(factoryApiUrl = "https://api.factory.ai") {
|
|
3052
245
|
return `#!/usr/bin/env node
|
|
3053
|
-
// Droid
|
|
3054
|
-
//
|
|
246
|
+
// Droid WebSearch Proxy Server (Native Provider Mode)
|
|
247
|
+
// Reads ~/.factory/settings.json for model configuration
|
|
248
|
+
// Requires proxy plugin (anthropic4droid) to handle format conversion
|
|
3055
249
|
|
|
250
|
+
const http = require('http');
|
|
251
|
+
const https = require('https');
|
|
252
|
+
const { execSync } = require('child_process');
|
|
3056
253
|
const fs = require('fs');
|
|
3057
254
|
const path = require('path');
|
|
3058
|
-
const
|
|
3059
|
-
|
|
255
|
+
const os = require('os');
|
|
256
|
+
|
|
257
|
+
const DEBUG = process.env.DROID_SEARCH_DEBUG === '1';
|
|
258
|
+
const PORT = parseInt(process.env.SEARCH_PROXY_PORT || '0');
|
|
259
|
+
const FACTORY_API = '${factoryApiUrl}';
|
|
3060
260
|
|
|
3061
|
-
|
|
3062
|
-
const SESSIONS_ROOT = path.join(FACTORY_HOME, 'sessions');
|
|
3063
|
-
const ALIAS_NAME = ${JSON.stringify(aliasName)};
|
|
261
|
+
function log(...args) { if (DEBUG) console.error('[websearch]', ...args); }
|
|
3064
262
|
|
|
3065
|
-
//
|
|
3066
|
-
const CYAN = '\\x1b[36m';
|
|
3067
|
-
const GREEN = '\\x1b[32m';
|
|
3068
|
-
const YELLOW = '\\x1b[33m';
|
|
3069
|
-
const RED = '\\x1b[31m';
|
|
3070
|
-
const DIM = '\\x1b[2m';
|
|
3071
|
-
const RESET = '\\x1b[0m';
|
|
3072
|
-
const BOLD = '\\x1b[1m';
|
|
3073
|
-
const CLEAR = '\\x1b[2J\\x1b[H';
|
|
3074
|
-
const HIDE_CURSOR = '\\x1b[?25l';
|
|
3075
|
-
const SHOW_CURSOR = '\\x1b[?25h';
|
|
263
|
+
// === Settings Configuration ===
|
|
3076
264
|
|
|
3077
|
-
|
|
3078
|
-
|
|
265
|
+
let cachedSettings = null;
|
|
266
|
+
let settingsLastModified = 0;
|
|
267
|
+
|
|
268
|
+
function getFactorySettings() {
|
|
269
|
+
const settingsPath = path.join(os.homedir(), '.factory', 'settings.json');
|
|
270
|
+
try {
|
|
271
|
+
const stats = fs.statSync(settingsPath);
|
|
272
|
+
if (cachedSettings && stats.mtimeMs === settingsLastModified) return cachedSettings;
|
|
273
|
+
cachedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
274
|
+
settingsLastModified = stats.mtimeMs;
|
|
275
|
+
return cachedSettings;
|
|
276
|
+
} catch (e) {
|
|
277
|
+
log('Failed to load settings.json:', e.message);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
3079
280
|
}
|
|
3080
281
|
|
|
3081
|
-
function
|
|
3082
|
-
const
|
|
3083
|
-
|
|
282
|
+
function getCurrentModelConfig() {
|
|
283
|
+
const settings = getFactorySettings();
|
|
284
|
+
if (!settings) return null;
|
|
285
|
+
|
|
286
|
+
const currentModelId = settings.sessionDefaultSettings?.model;
|
|
287
|
+
if (!currentModelId) return null;
|
|
288
|
+
|
|
289
|
+
const customModels = settings.customModels || [];
|
|
290
|
+
const modelConfig = customModels.find(m => m.id === currentModelId);
|
|
291
|
+
|
|
292
|
+
if (modelConfig) {
|
|
293
|
+
log('Model:', modelConfig.displayName, '| Provider:', modelConfig.provider);
|
|
294
|
+
return modelConfig;
|
|
295
|
+
}
|
|
3084
296
|
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
lastUserMsg: '',
|
|
3092
|
-
messageCount: 0,
|
|
3093
|
-
lastTimestamp: '',
|
|
3094
|
-
};
|
|
297
|
+
if (!currentModelId.startsWith('custom:')) return null;
|
|
298
|
+
log('Model not found:', currentModelId);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// === Native Provider WebSearch ===
|
|
3095
303
|
|
|
304
|
+
async function searchAnthropicNative(query, numResults, modelConfig) {
|
|
305
|
+
const { baseUrl, apiKey, model } = modelConfig;
|
|
306
|
+
|
|
3096
307
|
try {
|
|
3097
|
-
const
|
|
3098
|
-
|
|
3099
|
-
|
|
308
|
+
const requestBody = {
|
|
309
|
+
model: model,
|
|
310
|
+
max_tokens: 4096,
|
|
311
|
+
stream: false,
|
|
312
|
+
system: 'You are a web search assistant. Use the web_search tool to find relevant information and return the results.',
|
|
313
|
+
tools: [{ type: 'web_search_20250305', name: 'web_search', max_uses: 1 }],
|
|
314
|
+
tool_choice: { type: 'tool', name: 'web_search' },
|
|
315
|
+
messages: [{ role: 'user', content: 'Search the web for: ' + query + '\\n\\nReturn up to ' + numResults + ' relevant results.' }]
|
|
316
|
+
};
|
|
3100
317
|
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
318
|
+
let endpoint = baseUrl;
|
|
319
|
+
if (!endpoint.endsWith('/v1/messages')) endpoint = endpoint.replace(/\\/$/, '') + '/v1/messages';
|
|
320
|
+
|
|
321
|
+
log('Anthropic search:', query, '→', endpoint);
|
|
322
|
+
|
|
323
|
+
const bodyStr = JSON.stringify(requestBody).replace(/'/g, "'\\\\''");
|
|
324
|
+
const curlCmd = 'curl -s -X POST "' + endpoint + '" -H "Content-Type: application/json" -H "anthropic-version: 2023-06-01" -H "x-api-key: ' + apiKey + '" -d \\'' + bodyStr + "\\'";
|
|
325
|
+
const responseStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 60000 });
|
|
326
|
+
|
|
327
|
+
let response;
|
|
328
|
+
try { response = JSON.parse(responseStr); } catch { return null; }
|
|
329
|
+
if (response.error) { log('API error:', response.error.message); return null; }
|
|
330
|
+
|
|
331
|
+
const results = [];
|
|
332
|
+
for (const block of (response.content || [])) {
|
|
333
|
+
if (block.type === 'web_search_tool_result') {
|
|
334
|
+
for (const result of (block.content || [])) {
|
|
335
|
+
if (result.type === 'web_search_result') {
|
|
336
|
+
results.push({
|
|
337
|
+
title: result.title || '',
|
|
338
|
+
url: result.url || '',
|
|
339
|
+
content: result.snippet || result.page_content || ''
|
|
340
|
+
});
|
|
3118
341
|
}
|
|
3119
342
|
}
|
|
3120
|
-
}
|
|
343
|
+
}
|
|
3121
344
|
}
|
|
3122
345
|
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
if (fs.existsSync(settingsPath)) {
|
|
3130
|
-
try {
|
|
3131
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
3132
|
-
result.model = settings.model || '';
|
|
3133
|
-
} catch {}
|
|
346
|
+
log('Results:', results.length);
|
|
347
|
+
return results.length > 0 ? results.slice(0, numResults) : null;
|
|
348
|
+
} catch (e) {
|
|
349
|
+
log('Anthropic error:', e.message);
|
|
350
|
+
return null;
|
|
3134
351
|
}
|
|
3135
|
-
|
|
3136
|
-
return result;
|
|
3137
352
|
}
|
|
3138
353
|
|
|
3139
|
-
function
|
|
3140
|
-
const
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
354
|
+
async function searchOpenAINative(query, numResults, modelConfig) {
|
|
355
|
+
const { baseUrl, apiKey, model } = modelConfig;
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const requestBody = {
|
|
359
|
+
model: model,
|
|
360
|
+
tools: [{ type: 'web_search' }],
|
|
361
|
+
tool_choice: 'required',
|
|
362
|
+
input: 'Search the web for: ' + query + '\\n\\nReturn up to ' + numResults + ' relevant results.'
|
|
363
|
+
};
|
|
3148
364
|
|
|
3149
|
-
|
|
3150
|
-
if (!
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
365
|
+
let endpoint = baseUrl;
|
|
366
|
+
if (!endpoint.endsWith('/responses')) endpoint = endpoint.replace(/\\/$/, '') + '/responses';
|
|
367
|
+
|
|
368
|
+
log('OpenAI search:', query, '→', endpoint);
|
|
369
|
+
|
|
370
|
+
const bodyStr = JSON.stringify(requestBody).replace(/'/g, "'\\\\''");
|
|
371
|
+
const curlCmd = 'curl -s -X POST "' + endpoint + '" -H "Content-Type: application/json" -H "Authorization: Bearer ' + apiKey + '" -d \\'' + bodyStr + "\\'";
|
|
372
|
+
const responseStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 60000 });
|
|
373
|
+
|
|
374
|
+
let response;
|
|
375
|
+
try { response = JSON.parse(responseStr); } catch { return null; }
|
|
376
|
+
if (response.error) { log('API error:', response.error.message); return null; }
|
|
377
|
+
|
|
378
|
+
const results = [];
|
|
379
|
+
for (const item of (response.output || [])) {
|
|
380
|
+
if (item.type === 'web_search_call' && item.status === 'completed') {
|
|
381
|
+
for (const result of (item.results || [])) {
|
|
382
|
+
results.push({
|
|
383
|
+
title: result.title || '',
|
|
384
|
+
url: result.url || '',
|
|
385
|
+
content: result.snippet || result.content || ''
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
3164
389
|
}
|
|
390
|
+
|
|
391
|
+
log('Results:', results.length);
|
|
392
|
+
return results.length > 0 ? results.slice(0, numResults) : null;
|
|
393
|
+
} catch (e) {
|
|
394
|
+
log('OpenAI error:', e.message);
|
|
395
|
+
return null;
|
|
3165
396
|
}
|
|
3166
|
-
|
|
3167
|
-
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
3168
|
-
return sessions.slice(0, 50);
|
|
3169
397
|
}
|
|
3170
398
|
|
|
3171
|
-
function
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
399
|
+
async function search(query, numResults) {
|
|
400
|
+
numResults = numResults || 10;
|
|
401
|
+
log('Search:', query);
|
|
402
|
+
|
|
403
|
+
const modelConfig = getCurrentModelConfig();
|
|
404
|
+
if (!modelConfig) {
|
|
405
|
+
log('No custom model configured');
|
|
406
|
+
return { results: [], source: 'none' };
|
|
3178
407
|
}
|
|
408
|
+
|
|
409
|
+
const provider = modelConfig.provider;
|
|
410
|
+
let results = null;
|
|
411
|
+
|
|
412
|
+
if (provider === 'anthropic') results = await searchAnthropicNative(query, numResults, modelConfig);
|
|
413
|
+
else if (provider === 'openai') results = await searchOpenAINative(query, numResults, modelConfig);
|
|
414
|
+
else log('Unsupported provider:', provider);
|
|
415
|
+
|
|
416
|
+
if (results && results.length > 0) return { results: results, source: 'native-' + provider };
|
|
417
|
+
return { results: [], source: 'none' };
|
|
3179
418
|
}
|
|
3180
419
|
|
|
3181
|
-
|
|
3182
|
-
if (!s) return '';
|
|
3183
|
-
s = s.replace(/\\n/g, ' ');
|
|
3184
|
-
return s.length > len ? s.slice(0, len - 3) + '...' : s;
|
|
3185
|
-
}
|
|
420
|
+
// === HTTP Proxy Server ===
|
|
3186
421
|
|
|
3187
|
-
|
|
3188
|
-
const
|
|
3189
|
-
const pageSize = rows - 6;
|
|
3190
|
-
const visible = sessions.slice(offset, offset + pageSize);
|
|
3191
|
-
|
|
3192
|
-
let out = CLEAR;
|
|
3193
|
-
out += BOLD + 'Sessions: ' + RESET + DIM + cwd + RESET + '\\n';
|
|
3194
|
-
out += DIM + '[↑/↓] Select [Enter] Resume [q] Quit' + RESET + '\\n\\n';
|
|
422
|
+
const server = http.createServer(async (req, res) => {
|
|
423
|
+
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
3195
424
|
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
const prefix = isSelected ? GREEN + '▶ ' + RESET : ' ';
|
|
3201
|
-
|
|
3202
|
-
const title = truncate(s.title || '(no title)', 35);
|
|
3203
|
-
const time = formatTime(s.lastTimestamp);
|
|
3204
|
-
const model = truncate(s.model, 20);
|
|
3205
|
-
|
|
3206
|
-
if (isSelected) {
|
|
3207
|
-
out += prefix + YELLOW + title + RESET + '\\n';
|
|
3208
|
-
out += ' ' + DIM + 'ID: ' + RESET + CYAN + s.id + RESET + '\\n';
|
|
3209
|
-
out += ' ' + DIM + 'Last: ' + time + ' | Model: ' + model + ' | ' + s.messageCount + ' msgs' + RESET + '\\n';
|
|
3210
|
-
out += ' ' + DIM + 'First input: ' + RESET + truncate(s.firstUserMsg, 60) + '\\n';
|
|
3211
|
-
if (s.lastUserMsg && s.lastUserMsg !== s.firstUserMsg) {
|
|
3212
|
-
out += ' ' + DIM + 'Last input: ' + RESET + truncate(s.lastUserMsg, 60) + '\\n';
|
|
3213
|
-
}
|
|
3214
|
-
} else {
|
|
3215
|
-
out += prefix + title + DIM + ' (' + time + ')' + RESET + '\\n';
|
|
3216
|
-
}
|
|
425
|
+
if (url.pathname === '/health') {
|
|
426
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
427
|
+
res.end(JSON.stringify({ status: 'ok', mode: 'native-provider' }));
|
|
428
|
+
return;
|
|
3217
429
|
}
|
|
3218
430
|
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
431
|
+
if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {
|
|
432
|
+
let body = '';
|
|
433
|
+
req.on('data', function(c) { body += c; });
|
|
434
|
+
req.on('end', async function() {
|
|
435
|
+
try {
|
|
436
|
+
const parsed = JSON.parse(body);
|
|
437
|
+
const result = await search(parsed.query, parsed.numResults || 10);
|
|
438
|
+
log('Results:', result.results.length, 'from', result.source);
|
|
439
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
440
|
+
res.end(JSON.stringify({ results: result.results }));
|
|
441
|
+
} catch (e) {
|
|
442
|
+
log('Search error:', e.message);
|
|
443
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
444
|
+
res.end(JSON.stringify({ error: String(e), results: [] }));
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
3230
448
|
}
|
|
3231
449
|
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
450
|
+
// Standalone mode: mock non-LLM APIs
|
|
451
|
+
if (process.env.STANDALONE_MODE === '1') {
|
|
452
|
+
const pathname = url.pathname;
|
|
453
|
+
const isCoreLLMApi = pathname.startsWith('/api/llm/a/') || pathname.startsWith('/api/llm/o/');
|
|
454
|
+
|
|
455
|
+
if (!isCoreLLMApi) {
|
|
456
|
+
if (pathname === '/api/sessions/create') {
|
|
457
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
458
|
+
res.end(JSON.stringify({ id: 'local-' + Date.now() + '-' + Math.random().toString(36).slice(2, 10) }));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (pathname === '/api/cli/whoami') {
|
|
462
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
463
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
467
|
+
res.end(JSON.stringify({}));
|
|
468
|
+
return;
|
|
3235
469
|
}
|
|
3236
|
-
process.exit(0);
|
|
3237
470
|
}
|
|
3238
471
|
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
472
|
+
// Simple proxy - no SSE transformation (handled by proxy plugin)
|
|
473
|
+
log('Proxy:', req.method, url.pathname);
|
|
474
|
+
const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);
|
|
475
|
+
const proxyModule = proxyUrl.protocol === 'https:' ? https : http;
|
|
476
|
+
const proxyReq = proxyModule.request(proxyUrl, {
|
|
477
|
+
method: req.method,
|
|
478
|
+
headers: Object.assign({}, req.headers, { host: proxyUrl.host })
|
|
479
|
+
}, function(proxyRes) {
|
|
480
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
481
|
+
proxyRes.pipe(res);
|
|
482
|
+
});
|
|
3243
483
|
|
|
3244
|
-
function
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
}
|
|
484
|
+
proxyReq.on('error', function(e) {
|
|
485
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
486
|
+
res.end(JSON.stringify({ error: 'Proxy failed: ' + e.message }));
|
|
487
|
+
});
|
|
3249
488
|
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
489
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') req.pipe(proxyReq);
|
|
490
|
+
else proxyReq.end();
|
|
491
|
+
});
|
|
3253
492
|
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
process.
|
|
3257
|
-
|
|
3258
|
-
|
|
493
|
+
server.listen(PORT, '127.0.0.1', function() {
|
|
494
|
+
const actualPort = server.address().port;
|
|
495
|
+
const portFile = process.env.SEARCH_PROXY_PORT_FILE;
|
|
496
|
+
if (portFile) fs.writeFileSync(portFile, String(actualPort));
|
|
497
|
+
console.log('PORT=' + actualPort);
|
|
498
|
+
log('Native provider proxy on port', actualPort);
|
|
499
|
+
});
|
|
3259
500
|
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
console.log(GREEN + 'Resuming session: ' + session.id + RESET);
|
|
3276
|
-
console.log(DIM + 'Using: ' + ALIAS_NAME + ' --resume ' + session.id + RESET + '\\n');
|
|
501
|
+
process.on('SIGTERM', function() { server.close(); process.exit(0); });
|
|
502
|
+
process.on('SIGINT', function() { server.close(); process.exit(0); });
|
|
503
|
+
`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/websearch-patch.ts
|
|
508
|
+
/**
|
|
509
|
+
* Generate unified wrapper script
|
|
510
|
+
*/
|
|
511
|
+
function generateUnifiedWrapper(droidPath, proxyScriptPath, standalone = false) {
|
|
512
|
+
const standaloneEnv = standalone ? "STANDALONE_MODE=1 " : "";
|
|
513
|
+
return `#!/bin/bash
|
|
514
|
+
# Droid with WebSearch
|
|
515
|
+
# Auto-generated by droid-patch --websearch
|
|
3277
516
|
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
517
|
+
PROXY_SCRIPT="${proxyScriptPath}"
|
|
518
|
+
DROID_BIN="${droidPath}"
|
|
519
|
+
PROXY_PID=""
|
|
520
|
+
PORT_FILE="/tmp/droid-websearch-$$.port"
|
|
521
|
+
STANDALONE="${standalone ? "1" : "0"}"
|
|
3283
522
|
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
if (selected >= offset + pageSize) offset++;
|
|
3299
|
-
}
|
|
3300
|
-
} else if (k === '\\x1b[5~') { // Page Up
|
|
3301
|
-
selected = Math.max(0, selected - pageSize);
|
|
3302
|
-
offset = Math.max(0, offset - pageSize);
|
|
3303
|
-
} else if (k === '\\x1b[6~') { // Page Down
|
|
3304
|
-
selected = Math.min(sessions.length - 1, selected + pageSize);
|
|
3305
|
-
offset = Math.min(Math.max(0, sessions.length - pageSize), offset + pageSize);
|
|
3306
|
-
}
|
|
3307
|
-
|
|
3308
|
-
render(sessions, selected, offset, rows);
|
|
3309
|
-
};
|
|
523
|
+
should_passthrough() {
|
|
524
|
+
for arg in "$@"; do
|
|
525
|
+
if [ "$arg" = "--" ]; then break; fi
|
|
526
|
+
case "$arg" in --help|-h|--version|-V) return 0 ;; esac
|
|
527
|
+
done
|
|
528
|
+
local end_opts=0
|
|
529
|
+
for arg in "$@"; do
|
|
530
|
+
if [ "$arg" = "--" ]; then end_opts=1; continue; fi
|
|
531
|
+
if [ "$end_opts" -eq 0 ] && [[ "$arg" == -* ]]; then continue; fi
|
|
532
|
+
case "$arg" in help|version|completion|completions|exec) return 0 ;; esac
|
|
533
|
+
break
|
|
534
|
+
done
|
|
535
|
+
return 1
|
|
536
|
+
}
|
|
3310
537
|
|
|
3311
|
-
|
|
538
|
+
if should_passthrough "$@"; then exec "$DROID_BIN" "$@"; fi
|
|
3312
539
|
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
540
|
+
cleanup() {
|
|
541
|
+
if [ -n "$PROXY_PID" ] && kill -0 "$PROXY_PID" 2>/dev/null; then
|
|
542
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Stopping proxy (PID: $PROXY_PID)" >&2
|
|
543
|
+
kill "$PROXY_PID" 2>/dev/null
|
|
544
|
+
wait "$PROXY_PID" 2>/dev/null
|
|
545
|
+
fi
|
|
546
|
+
rm -f "$PORT_FILE"
|
|
3318
547
|
}
|
|
548
|
+
trap cleanup EXIT INT TERM
|
|
549
|
+
|
|
550
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Starting proxy..." >&2
|
|
551
|
+
[ "$STANDALONE" = "1" ] && [ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Standalone mode enabled" >&2
|
|
552
|
+
|
|
553
|
+
if [ -n "$DROID_SEARCH_DEBUG" ]; then
|
|
554
|
+
${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="$PORT_FILE" node "$PROXY_SCRIPT" 2>&1 &
|
|
555
|
+
else
|
|
556
|
+
${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="$PORT_FILE" node "$PROXY_SCRIPT" >/dev/null 2>&1 &
|
|
557
|
+
fi
|
|
558
|
+
PROXY_PID=$!
|
|
559
|
+
|
|
560
|
+
for i in {1..50}; do
|
|
561
|
+
if ! kill -0 "$PROXY_PID" 2>/dev/null; then
|
|
562
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy process died" >&2
|
|
563
|
+
break
|
|
564
|
+
fi
|
|
565
|
+
if [ -f "$PORT_FILE" ]; then
|
|
566
|
+
ACTUAL_PORT=$(cat "$PORT_FILE" 2>/dev/null)
|
|
567
|
+
if [ -n "$ACTUAL_PORT" ] && curl -s "http://127.0.0.1:$ACTUAL_PORT/health" > /dev/null 2>&1; then
|
|
568
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy ready on port $ACTUAL_PORT (PID: $PROXY_PID)" >&2
|
|
569
|
+
break
|
|
570
|
+
fi
|
|
571
|
+
fi
|
|
572
|
+
sleep 0.1
|
|
573
|
+
done
|
|
574
|
+
|
|
575
|
+
if [ ! -f "$PORT_FILE" ] || [ -z "$(cat "$PORT_FILE" 2>/dev/null)" ]; then
|
|
576
|
+
echo "[websearch] Failed to start proxy, running without websearch" >&2
|
|
577
|
+
cleanup
|
|
578
|
+
exec "$DROID_BIN" "$@"
|
|
579
|
+
fi
|
|
580
|
+
|
|
581
|
+
ACTUAL_PORT=$(cat "$PORT_FILE")
|
|
582
|
+
rm -f "$PORT_FILE"
|
|
3319
583
|
|
|
3320
|
-
|
|
584
|
+
export FACTORY_API_BASE_URL_OVERRIDE="http://127.0.0.1:$ACTUAL_PORT"
|
|
585
|
+
"$DROID_BIN" "$@"
|
|
586
|
+
DROID_EXIT_CODE=$?
|
|
587
|
+
exit $DROID_EXIT_CODE
|
|
3321
588
|
`;
|
|
3322
589
|
}
|
|
3323
590
|
/**
|
|
3324
|
-
* Create
|
|
591
|
+
* Create unified WebSearch files
|
|
592
|
+
*
|
|
593
|
+
* @param outputDir - Directory to write files to
|
|
594
|
+
* @param droidPath - Path to droid binary
|
|
595
|
+
* @param aliasName - Alias name for the wrapper
|
|
596
|
+
* @param apiBase - Custom API base URL for proxy to forward requests to
|
|
597
|
+
* @param standalone - Standalone mode: mock non-LLM Factory APIs
|
|
598
|
+
* @param useNativeProvider - Use native provider websearch (--websearch-proxy mode)
|
|
3325
599
|
*/
|
|
3326
|
-
async function
|
|
600
|
+
async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName, apiBase, standalone = false, useNativeProvider = false) {
|
|
3327
601
|
if (!existsSync(outputDir)) await mkdir(outputDir, { recursive: true });
|
|
3328
|
-
const
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
602
|
+
const proxyScriptPath = join(outputDir, `${aliasName}-proxy.js`);
|
|
603
|
+
const wrapperScriptPath = join(outputDir, aliasName);
|
|
604
|
+
const factoryApiUrl = apiBase || "https://api.factory.ai";
|
|
605
|
+
await writeFile(proxyScriptPath, useNativeProvider ? generateNativeSearchProxyServer(factoryApiUrl) : generateExternalSearchProxyServer(factoryApiUrl));
|
|
606
|
+
console.log(`[*] Created proxy script: ${proxyScriptPath}`);
|
|
607
|
+
console.log(`[*] Mode: ${useNativeProvider ? "native provider (requires proxy plugin)" : "external providers"}`);
|
|
608
|
+
await writeFile(wrapperScriptPath, generateUnifiedWrapper(droidPath, proxyScriptPath, standalone));
|
|
609
|
+
await chmod(wrapperScriptPath, 493);
|
|
610
|
+
console.log(`[*] Created wrapper: ${wrapperScriptPath}`);
|
|
611
|
+
if (standalone) console.log(`[*] Standalone mode enabled`);
|
|
612
|
+
return {
|
|
613
|
+
wrapperScript: wrapperScriptPath,
|
|
614
|
+
preloadScript: proxyScriptPath
|
|
615
|
+
};
|
|
3332
616
|
}
|
|
3333
617
|
|
|
3334
618
|
//#endregion
|
|
@@ -3383,14 +667,13 @@ function findDefaultDroidPath() {
|
|
|
3383
667
|
for (const p of paths) if (existsSync(p)) return p;
|
|
3384
668
|
return join(home, ".droid", "bin", "droid");
|
|
3385
669
|
}
|
|
3386
|
-
bin("droid-patch", "CLI tool to patch droid binary with various modifications").package("droid-patch", version).option("--is-custom", "Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)").option("--skip-login", "Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)").option("--api-base <url>", "Replace API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)").option("--websearch", "Enable local WebSearch proxy
|
|
670
|
+
bin("droid-patch", "CLI tool to patch droid binary with various modifications").package("droid-patch", version).option("--is-custom", "Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)").option("--skip-login", "Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)").option("--api-base <url>", "Replace API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)").option("--websearch", "Enable local WebSearch proxy with external providers (Smithery, Google PSE, etc.)").option("--websearch-proxy", "Enable native provider websearch (requires proxy plugin, reads ~/.factory/settings.json)").option("--standalone", "Standalone mode: mock non-LLM Factory APIs (use with --websearch)").option("--reasoning-effort", "Enable reasoning effort for custom models (set to high, enable UI selector)").option("--disable-telemetry", "Disable telemetry and Sentry error reporting (block data uploads)").option("--auto-high", "Set default autonomy mode to auto-high (bypass settings.json race condition)").option("--dry-run", "Verify patches without actually modifying the binary").option("-p, --path <path>", "Path to the droid binary").option("-o, --output <dir>", "Output directory for patched binary").option("--no-backup", "Do not create backup of original binary").option("-v, --verbose", "Enable verbose output").argument("[alias]", "Alias name for the patched binary").action(async (options, args) => {
|
|
3387
671
|
const alias = args?.[0];
|
|
3388
672
|
const isCustom = options["is-custom"];
|
|
3389
673
|
const skipLogin = options["skip-login"];
|
|
3390
674
|
const apiBase = options["api-base"];
|
|
3391
675
|
const websearch = options["websearch"];
|
|
3392
|
-
const
|
|
3393
|
-
const sessions = options["sessions"];
|
|
676
|
+
const websearchProxy = options["websearch-proxy"];
|
|
3394
677
|
const standalone = options["standalone"];
|
|
3395
678
|
const websearchTarget = websearch ? apiBase || "https://api.factory.ai" : void 0;
|
|
3396
679
|
const reasoningEffort = options["reasoning-effort"];
|
|
@@ -3402,38 +685,38 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3402
685
|
const backup = options.backup !== false;
|
|
3403
686
|
const verbose = options.verbose;
|
|
3404
687
|
const outputPath = outputDir && alias ? join(outputDir, alias) : void 0;
|
|
3405
|
-
const needsBinaryPatch = !!isCustom || !!skipLogin || !!reasoningEffort || !!noTelemetry || !!autoHigh || !!apiBase && !websearch;
|
|
3406
|
-
|
|
3407
|
-
|
|
688
|
+
const needsBinaryPatch = !!isCustom || !!skipLogin || !!reasoningEffort || !!noTelemetry || !!autoHigh || !!apiBase && !websearch && !websearchProxy;
|
|
689
|
+
if (websearch && websearchProxy) {
|
|
690
|
+
console.log(styleText("red", "Error: Cannot use --websearch and --websearch-proxy together"));
|
|
691
|
+
console.log(styleText("gray", "Choose one:"));
|
|
692
|
+
console.log(styleText("gray", " --websearch External providers (Smithery, Google PSE, etc.)"));
|
|
693
|
+
console.log(styleText("gray", " --websearch-proxy Native provider (requires proxy plugin)"));
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
if (!needsBinaryPatch && (websearch || websearchProxy)) {
|
|
3408
697
|
if (!alias) {
|
|
3409
|
-
|
|
3410
|
-
console.log(styleText("
|
|
3411
|
-
console.log(styleText("gray",
|
|
698
|
+
const flag = websearchProxy ? "--websearch-proxy" : "--websearch";
|
|
699
|
+
console.log(styleText("red", `Error: Alias name required for ${flag}`));
|
|
700
|
+
console.log(styleText("gray", `Usage: npx droid-patch ${flag} <alias>`));
|
|
3412
701
|
process.exit(1);
|
|
3413
702
|
}
|
|
3414
703
|
console.log(styleText("cyan", "═".repeat(60)));
|
|
3415
704
|
console.log(styleText(["cyan", "bold"], " Droid Wrapper Setup"));
|
|
3416
705
|
console.log(styleText("cyan", "═".repeat(60)));
|
|
3417
706
|
console.log();
|
|
3418
|
-
if (
|
|
3419
|
-
console.log(styleText("white", `WebSearch:
|
|
707
|
+
if (websearchProxy) {
|
|
708
|
+
console.log(styleText("white", `WebSearch: native provider mode`));
|
|
709
|
+
console.log(styleText("gray", ` Requires proxy plugin (anthropic4droid)`));
|
|
710
|
+
console.log(styleText("gray", ` Reads model config from ~/.factory/settings.json`));
|
|
711
|
+
} else if (websearch) {
|
|
712
|
+
console.log(styleText("white", `WebSearch: external providers mode`));
|
|
3420
713
|
console.log(styleText("white", `Forward target: ${websearchTarget}`));
|
|
3421
|
-
if (standalone) console.log(styleText("white", `Standalone mode: enabled`));
|
|
3422
714
|
}
|
|
3423
|
-
if (
|
|
715
|
+
if (standalone) console.log(styleText("white", `Standalone mode: enabled`));
|
|
3424
716
|
console.log();
|
|
3425
717
|
let execTargetPath = path;
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
execTargetPath = wrapperScript;
|
|
3429
|
-
}
|
|
3430
|
-
if (statuslineEnabled) {
|
|
3431
|
-
const statuslineDir = join(homedir(), ".droid-patch", "statusline");
|
|
3432
|
-
let sessionsScript;
|
|
3433
|
-
if (sessions) sessionsScript = (await createSessionsScript(statuslineDir, alias)).sessionsScript;
|
|
3434
|
-
const { wrapperScript } = await createStatuslineFiles(statuslineDir, execTargetPath, alias, sessionsScript);
|
|
3435
|
-
execTargetPath = wrapperScript;
|
|
3436
|
-
}
|
|
718
|
+
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, alias, websearchProxy ? void 0 : websearchTarget, standalone, websearchProxy);
|
|
719
|
+
execTargetPath = wrapperScript;
|
|
3437
720
|
const aliasResult = await createAliasForWrapper(execTargetPath, alias, verbose);
|
|
3438
721
|
const droidVersion = getDroidVersion(path);
|
|
3439
722
|
await saveAliasMetadata(createMetadata(alias, path, {
|
|
@@ -3441,8 +724,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3441
724
|
skipLogin: false,
|
|
3442
725
|
apiBase: apiBase || null,
|
|
3443
726
|
websearch: !!websearch,
|
|
3444
|
-
|
|
3445
|
-
sessions: !!sessions,
|
|
727
|
+
websearchProxy: !!websearchProxy,
|
|
3446
728
|
reasoningEffort: false,
|
|
3447
729
|
noTelemetry: false,
|
|
3448
730
|
standalone
|
|
@@ -3459,10 +741,24 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3459
741
|
console.log("Run directly:");
|
|
3460
742
|
console.log(styleText("yellow", ` ${alias}`));
|
|
3461
743
|
console.log();
|
|
3462
|
-
if (
|
|
3463
|
-
console.log(styleText("cyan", "
|
|
3464
|
-
console.log(styleText("gray", "
|
|
3465
|
-
console.log(styleText("gray", "
|
|
744
|
+
if (websearchProxy) {
|
|
745
|
+
console.log(styleText("cyan", "Native Provider WebSearch (--websearch-proxy):"));
|
|
746
|
+
console.log(styleText("gray", " Uses model's built-in websearch via proxy plugin"));
|
|
747
|
+
console.log(styleText("gray", " Reads model config from ~/.factory/settings.json"));
|
|
748
|
+
console.log();
|
|
749
|
+
console.log(styleText("yellow", "IMPORTANT: Requires proxy plugin (anthropic4droid)"));
|
|
750
|
+
console.log();
|
|
751
|
+
console.log("Supported providers:");
|
|
752
|
+
console.log(styleText("yellow", " - anthropic: Claude web_search_20250305 server tool"));
|
|
753
|
+
console.log(styleText("yellow", " - openai: OpenAI web_search tool"));
|
|
754
|
+
console.log(styleText("gray", " - generic-chat-completion-api: Not supported"));
|
|
755
|
+
console.log();
|
|
756
|
+
console.log("Debug mode:");
|
|
757
|
+
console.log(styleText("gray", " export DROID_SEARCH_DEBUG=1 # Basic logs"));
|
|
758
|
+
console.log(styleText("gray", " export DROID_SEARCH_VERBOSE=1 # Full request/response"));
|
|
759
|
+
} else if (websearch) {
|
|
760
|
+
console.log(styleText("cyan", "External Providers WebSearch (--websearch):"));
|
|
761
|
+
console.log(styleText("gray", " Uses external search providers (Smithery, Google PSE, etc.)"));
|
|
3466
762
|
console.log();
|
|
3467
763
|
console.log("Search providers (in priority order):");
|
|
3468
764
|
console.log(styleText("yellow", " 1. Smithery Exa (best quality):"));
|
|
@@ -3478,13 +774,12 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3478
774
|
}
|
|
3479
775
|
return;
|
|
3480
776
|
}
|
|
3481
|
-
if (!isCustom && !skipLogin && !apiBase && !websearch && !
|
|
777
|
+
if (!isCustom && !skipLogin && !apiBase && !websearch && !reasoningEffort && !noTelemetry && !autoHigh) {
|
|
3482
778
|
console.log(styleText("yellow", "No patch flags specified. Available patches:"));
|
|
3483
779
|
console.log(styleText("gray", " --is-custom Patch isCustom for custom models"));
|
|
3484
780
|
console.log(styleText("gray", " --skip-login Bypass login by injecting a fake API key"));
|
|
3485
781
|
console.log(styleText("gray", " --api-base Replace API URL (standalone: max 22 chars; with --websearch: no limit)"));
|
|
3486
782
|
console.log(styleText("gray", " --websearch Enable local WebSearch proxy"));
|
|
3487
|
-
console.log(styleText("gray", " --statusline Enable Claude-style statusline"));
|
|
3488
783
|
console.log(styleText("gray", " --reasoning-effort Set reasoning effort level for custom models"));
|
|
3489
784
|
console.log(styleText("gray", " --disable-telemetry Disable telemetry and Sentry error reporting"));
|
|
3490
785
|
console.log(styleText("gray", " --auto-high Set default autonomy mode to auto-high"));
|
|
@@ -3496,8 +791,6 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3496
791
|
console.log(styleText("cyan", " npx droid-patch --is-custom --skip-login droid-patched"));
|
|
3497
792
|
console.log(styleText("cyan", " npx droid-patch --websearch droid-search"));
|
|
3498
793
|
console.log(styleText("cyan", " npx droid-patch --websearch --standalone droid-local"));
|
|
3499
|
-
console.log(styleText("cyan", " npx droid-patch --statusline droid-status"));
|
|
3500
|
-
console.log(styleText("cyan", " npx droid-patch --websearch --statusline droid-search-ui"));
|
|
3501
794
|
console.log(styleText("cyan", " npx droid-patch --disable-telemetry droid-private"));
|
|
3502
795
|
console.log(styleText("cyan", " npx droid-patch --websearch --api-base=http://127.0.0.1:20002 my-droid"));
|
|
3503
796
|
process.exit(1);
|
|
@@ -3636,25 +929,22 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3636
929
|
if (result.success && result.outputPath && alias) {
|
|
3637
930
|
console.log();
|
|
3638
931
|
let execTargetPath = result.outputPath;
|
|
3639
|
-
if (websearch) {
|
|
3640
|
-
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, alias, websearchTarget, standalone);
|
|
932
|
+
if (websearch || websearchProxy) {
|
|
933
|
+
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, alias, websearchProxy ? void 0 : websearchTarget, standalone, websearchProxy);
|
|
3641
934
|
execTargetPath = wrapperScript;
|
|
3642
935
|
console.log();
|
|
3643
|
-
|
|
3644
|
-
|
|
936
|
+
if (websearchProxy) {
|
|
937
|
+
console.log(styleText("cyan", "WebSearch enabled (native provider mode)"));
|
|
938
|
+
console.log(styleText("gray", " Requires proxy plugin (anthropic4droid)"));
|
|
939
|
+
console.log(styleText("gray", " Reads model config from ~/.factory/settings.json"));
|
|
940
|
+
} else {
|
|
941
|
+
console.log(styleText("cyan", "WebSearch enabled (external providers mode)"));
|
|
942
|
+
console.log(styleText("white", ` Forward target: ${websearchTarget}`));
|
|
943
|
+
}
|
|
3645
944
|
if (standalone) console.log(styleText("white", ` Standalone mode: enabled`));
|
|
3646
945
|
}
|
|
3647
|
-
if (statuslineEnabled) {
|
|
3648
|
-
const statuslineDir = join(homedir(), ".droid-patch", "statusline");
|
|
3649
|
-
let sessionsScript;
|
|
3650
|
-
if (sessions) sessionsScript = (await createSessionsScript(statuslineDir, alias)).sessionsScript;
|
|
3651
|
-
const { wrapperScript } = await createStatuslineFiles(statuslineDir, execTargetPath, alias, sessionsScript);
|
|
3652
|
-
execTargetPath = wrapperScript;
|
|
3653
|
-
console.log();
|
|
3654
|
-
console.log(styleText("cyan", "Statusline enabled"));
|
|
3655
|
-
}
|
|
3656
946
|
let aliasResult;
|
|
3657
|
-
if (websearch ||
|
|
947
|
+
if (websearch || websearchProxy) aliasResult = await createAliasForWrapper(execTargetPath, alias, verbose);
|
|
3658
948
|
else aliasResult = await createAlias(result.outputPath, alias, verbose);
|
|
3659
949
|
const droidVersion = getDroidVersion(path);
|
|
3660
950
|
await saveAliasMetadata(createMetadata(alias, path, {
|
|
@@ -3662,8 +952,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3662
952
|
skipLogin: !!skipLogin,
|
|
3663
953
|
apiBase: apiBase || null,
|
|
3664
954
|
websearch: !!websearch,
|
|
3665
|
-
|
|
3666
|
-
sessions: !!sessions,
|
|
955
|
+
websearchProxy: !!websearchProxy,
|
|
3667
956
|
reasoningEffort: !!reasoningEffort,
|
|
3668
957
|
noTelemetry: !!noTelemetry,
|
|
3669
958
|
standalone: !!standalone,
|
|
@@ -3688,11 +977,29 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3688
977
|
}
|
|
3689
978
|
}).command("list", "List all droid-patch aliases").action(async () => {
|
|
3690
979
|
await listAliases();
|
|
3691
|
-
}).command("remove", "Remove alias(es) by name or filter").argument("[alias-or-path]", "Alias name or file path to remove").option("--patch-version <version>", "Remove aliases created by this droid-patch version").option("--droid-version <version>", "Remove aliases for this droid version").option("--flag <flag>", "Remove aliases with this flag (is-custom, skip-login, websearch,
|
|
980
|
+
}).command("remove", "Remove alias(es) by name or filter").argument("[alias-or-path]", "Alias name or file path to remove").option("--patch-version <version>", "Remove aliases created by this droid-patch version").option("--droid-version <version>", "Remove aliases for this droid version").option("--flag <flag>", "Remove aliases with this flag (is-custom, skip-login, websearch, api-base, reasoning-effort, disable-telemetry, standalone)").action(async (options, args) => {
|
|
3692
981
|
const target = args?.[0];
|
|
3693
982
|
const patchVersion = options["patch-version"];
|
|
3694
983
|
const droidVersion = options["droid-version"];
|
|
3695
|
-
const
|
|
984
|
+
const flagRaw = options.flag;
|
|
985
|
+
let flag;
|
|
986
|
+
if (flagRaw) {
|
|
987
|
+
const allowedFlags = [
|
|
988
|
+
"is-custom",
|
|
989
|
+
"skip-login",
|
|
990
|
+
"websearch",
|
|
991
|
+
"api-base",
|
|
992
|
+
"reasoning-effort",
|
|
993
|
+
"disable-telemetry",
|
|
994
|
+
"standalone"
|
|
995
|
+
];
|
|
996
|
+
if (!allowedFlags.includes(flagRaw)) {
|
|
997
|
+
console.error(styleText("red", `Error: Invalid --flag value: ${flagRaw}`));
|
|
998
|
+
console.error(styleText("gray", `Allowed: ${allowedFlags.join(", ")}`));
|
|
999
|
+
process.exit(1);
|
|
1000
|
+
}
|
|
1001
|
+
flag = flagRaw;
|
|
1002
|
+
}
|
|
3696
1003
|
if (patchVersion || droidVersion || flag) {
|
|
3697
1004
|
await removeAliasesByFilter({
|
|
3698
1005
|
patchVersion,
|
|
@@ -3886,14 +1193,8 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3886
1193
|
delete meta.patches.proxy;
|
|
3887
1194
|
}
|
|
3888
1195
|
}
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
let sessionsScript;
|
|
3892
|
-
if (meta.patches.sessions) sessionsScript = (await createSessionsScript(statuslineDir, meta.name)).sessionsScript;
|
|
3893
|
-
const { wrapperScript } = await createStatuslineFiles(statuslineDir, execTargetPath, meta.name, sessionsScript);
|
|
3894
|
-
execTargetPath = wrapperScript;
|
|
3895
|
-
if (verbose) console.log(styleText("gray", ` Regenerated statusline wrapper`));
|
|
3896
|
-
}
|
|
1196
|
+
delete meta.patches.statusline;
|
|
1197
|
+
delete meta.patches.sessions;
|
|
3897
1198
|
const { symlink: symlink$1, unlink: unlink$1, readlink: readlink$1, lstat } = await import("node:fs/promises");
|
|
3898
1199
|
let aliasPath = meta.aliasPath;
|
|
3899
1200
|
if (!aliasPath) {
|