@tensor.chat/companion 0.2.0 → 0.3.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 +2 -1
- package/package.json +1 -1
- package/src/daemon.js +6 -1
- package/src/tools/fetch.js +31 -3
package/README.md
CHANGED
|
@@ -38,7 +38,8 @@ It then appears as the folder `Devices/<name>/` in that chat's file tree.
|
|
|
38
38
|
sandboxed — that is the point of a companion device. Git Bash is used on Windows when
|
|
39
39
|
it is installed, PowerShell otherwise; `tensor-companion shell <path>` overrides it.
|
|
40
40
|
A long-running command can be started as a background job and looked at later.
|
|
41
|
-
- **Fetch** downloads a page
|
|
41
|
+
- **Fetch** downloads a page through your connection and hands it to tensor.chat as
|
|
42
|
+
served (up to 2 MB). The AI reads it as Markdown, or as served when it asks for that.
|
|
42
43
|
|
|
43
44
|
## Approval
|
|
44
45
|
|
package/package.json
CHANGED
package/src/daemon.js
CHANGED
|
@@ -169,7 +169,7 @@ function createDaemon({ serverUrl, token, shell, log, approvedRoots, approvedShe
|
|
|
169
169
|
case 'removepath': return await files.removepath(callRoots, a.path, { endgueltig: a.endgueltig === true });
|
|
170
170
|
case 'renamepath': return files.renamepath(callRoots, a.from, a.to);
|
|
171
171
|
case 'statpath': return files.statpath(callRoots, a.path);
|
|
172
|
-
case 'fetch': return fetchUrl(a.url, { signal });
|
|
172
|
+
case 'fetch': return fetchUrl(a.url, { signal, raw: a.raw === true });
|
|
173
173
|
default: return { ok: false, text: `(unknown tool "${tool}" on the companion device)` };
|
|
174
174
|
}
|
|
175
175
|
}
|
|
@@ -274,6 +274,11 @@ function createDaemon({ serverUrl, token, shell, log, approvedRoots, approvedShe
|
|
|
274
274
|
const code = res && res.statusCode;
|
|
275
275
|
say(`server refused the connection (HTTP ${code})`);
|
|
276
276
|
if (code === 401) { state.stopped = true; emitter.emit('unauthorized'); }
|
|
277
|
+
// With this listener in place, ws leaves the refused handshake open: no
|
|
278
|
+
// 'close' ever comes, so no reconnect either. A 502 from nginx while the
|
|
279
|
+
// server restarts left the device offline until the app was restarted
|
|
280
|
+
// (26.09.2026). End it here; 'close' then schedules the next try.
|
|
281
|
+
try { ws.terminate(); } catch (_) {}
|
|
277
282
|
});
|
|
278
283
|
ws.on('error', (e) => { say(`socket error: ${e.message}`); });
|
|
279
284
|
ws.on('close', (code, reason) => {
|
package/src/tools/fetch.js
CHANGED
|
@@ -2,10 +2,36 @@
|
|
|
2
2
|
// Fetches a URL through this machine's own internet connection and returns a text
|
|
3
3
|
// view: HTML is reduced to its visible text, JSON is pretty-printed, everything else
|
|
4
4
|
// is shown as text when it is text at all.
|
|
5
|
+
//
|
|
6
|
+
// With raw, the page goes back as served instead, for the server to convert or show
|
|
7
|
+
// (Fetch modes, tensorclaw/src/deviceFetch.js). A server that does not ask for raw
|
|
8
|
+
// gets the text view as before.
|
|
5
9
|
|
|
6
10
|
const MAX_BYTES = 2 * 1024 * 1024;
|
|
7
11
|
const MAX_CHARS = 50000;
|
|
8
12
|
const TIMEOUT_MS = 20000;
|
|
13
|
+
// The hub takes a message of up to 4 MB, and JSON doubles every quote and backslash
|
|
14
|
+
// in a page: what goes back raw stays below this many bytes as JSON.
|
|
15
|
+
const RAW_JSON_BYTES = 3 * 1024 * 1024;
|
|
16
|
+
|
|
17
|
+
// The text in the charset the server named; utf-8 when it named none or one this
|
|
18
|
+
// runtime does not know.
|
|
19
|
+
function decode(buf, type) {
|
|
20
|
+
const m = /charset\s*=\s*"?([\w.:-]+)/i.exec(type);
|
|
21
|
+
try { return new TextDecoder(m ? m[1] : 'utf-8').decode(buf); } catch (_) { return buf.toString('utf8'); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The page goes in `page`, not `text`: the hub cuts a text at 200,000 characters.
|
|
25
|
+
function rawResult(res, url, type, buf, binary, truncated) {
|
|
26
|
+
const head = { ok: res.ok, raw: true, url: res.url || url.href, httpStatus: res.status, type, bytes: buf.length, text: '' };
|
|
27
|
+
if (binary) return { ...head, binary: true, truncated };
|
|
28
|
+
let page = decode(buf, type);
|
|
29
|
+
while (Buffer.byteLength(JSON.stringify(page)) > RAW_JSON_BYTES) {
|
|
30
|
+
page = page.slice(0, Math.floor(page.length * 0.9));
|
|
31
|
+
truncated = true;
|
|
32
|
+
}
|
|
33
|
+
return { ...head, truncated, page };
|
|
34
|
+
}
|
|
9
35
|
|
|
10
36
|
function htmlToText(html) {
|
|
11
37
|
return String(html)
|
|
@@ -21,7 +47,7 @@ function htmlToText(html) {
|
|
|
21
47
|
.trim();
|
|
22
48
|
}
|
|
23
49
|
|
|
24
|
-
async function fetchUrl(rawUrl, { signal, timeoutMs } = {}) {
|
|
50
|
+
async function fetchUrl(rawUrl, { signal, timeoutMs, raw = false } = {}) {
|
|
25
51
|
let url;
|
|
26
52
|
try { url = new URL(String(rawUrl || '').trim()); } catch (_) { return { ok: false, text: '(invalid URL)' }; }
|
|
27
53
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') return { ok: false, text: '(only http and https URLs can be fetched)' };
|
|
@@ -50,7 +76,9 @@ async function fetchUrl(rawUrl, { signal, timeoutMs } = {}) {
|
|
|
50
76
|
}
|
|
51
77
|
}
|
|
52
78
|
const buf = Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
53
|
-
|
|
79
|
+
const binary = buf.includes(0) && !/text|json|xml|html|javascript/.test(type);
|
|
80
|
+
if (raw) return rawResult(res, url, type, buf, binary, truncated);
|
|
81
|
+
if (binary) {
|
|
54
82
|
return { ok: true, text: `${url.href}\nHTTP ${res.status} ${type || 'unknown type'}\n(binary content, ${size} bytes — not shown)` };
|
|
55
83
|
}
|
|
56
84
|
let body = buf.toString('utf8');
|
|
@@ -67,4 +95,4 @@ async function fetchUrl(rawUrl, { signal, timeoutMs } = {}) {
|
|
|
67
95
|
}
|
|
68
96
|
}
|
|
69
97
|
|
|
70
|
-
module.exports = { fetchUrl, htmlToText, MAX_BYTES, MAX_CHARS };
|
|
98
|
+
module.exports = { fetchUrl, htmlToText, MAX_BYTES, MAX_CHARS, RAW_JSON_BYTES };
|