@talotrace/connect 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/bin/talotrace-connect.js +50 -9
- package/checksums.json +7 -0
- package/lib/copy.js +63 -0
- package/lib/escalate.js +20 -0
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -33,7 +33,9 @@ saying so. Ctrl-C ends a session at once; a killed one counts until its two-minu
|
|
|
33
33
|
## Stopping
|
|
34
34
|
|
|
35
35
|
Ctrl-C, SIGTERM or a closed terminal stops it within 2 seconds and ends its session (`dev` may take up to 30 seconds:
|
|
36
|
-
it releases the phone and closes a cloud workspace first). A second Ctrl-C stops it at once. The stdio MCP server stops when your agent closes it.
|
|
36
|
+
it releases the phone and closes a cloud workspace first). A second Ctrl-C stops it at once, and the launcher sends SIGKILL if the binary has not stopped 5 seconds after the signal (`dev` announces its longer stop). The stdio MCP server stops when your agent closes it.
|
|
37
|
+
|
|
38
|
+
The launcher runs the binary from a copy under your user cache directory (`~/Library/Caches/talotrace-connect/<version>/` on macOS), checked against the checksum published with the package, so unplugging the disk that holds npm's cache cannot hang it.
|
|
37
39
|
|
|
38
40
|
If the binary stops responding (for example, the disk holding npm's cache was unplugged, which leaves a program unable
|
|
39
41
|
to run its own code), the launcher stops it after 5 seconds and says so.
|
|
@@ -42,4 +44,4 @@ to run its own code), the launcher stops it after 5 seconds and says so.
|
|
|
42
44
|
|
|
43
45
|
`0` done or stopped, `1` failure, `2` usage, `3` TaloTrace ended the session, `4` you ended it, `5` the session expired,
|
|
44
46
|
`6` the relay refused its credential, `7` another `talotrace-connect` took the session over, `8` it did not finish
|
|
45
|
-
stopping within its budget, `9` it stopped responding and the launcher stopped it.
|
|
47
|
+
stopping within its budget, `9` it stopped responding and the launcher stopped it. A binary ended by a signal gives `128` plus the signal number.
|
package/bin/talotrace-connect.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const { spawn } = require('node:child_process');
|
|
5
|
+
const os = require('node:os');
|
|
5
6
|
const { binaryPath } = require('../lib/resolve');
|
|
7
|
+
const { verifiedCopy } = require('../lib/copy');
|
|
8
|
+
const { KILL_AFTER_MS, killDelay } = require('../lib/escalate');
|
|
6
9
|
const {
|
|
7
10
|
ALIVE_ENV,
|
|
8
11
|
ALIVE_FD,
|
|
@@ -14,7 +17,13 @@ const {
|
|
|
14
17
|
|
|
15
18
|
let binary;
|
|
16
19
|
try {
|
|
17
|
-
|
|
20
|
+
const source = binaryPath(process.platform, process.arch, require.resolve);
|
|
21
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
22
|
+
const expected = require('../checksums.json')[platformKey];
|
|
23
|
+
if (!expected) {
|
|
24
|
+
throw new Error(`@talotrace/connect publishes no checksum for ${platformKey}`);
|
|
25
|
+
}
|
|
26
|
+
binary = verifiedCopy({ source, expected, version: require('../package.json').version });
|
|
18
27
|
} catch (error) {
|
|
19
28
|
console.error(error.message);
|
|
20
29
|
process.exit(1);
|
|
@@ -27,13 +36,44 @@ const stdio = watched ? ['inherit', 'inherit', 'inherit', 'pipe'] : 'inherit';
|
|
|
27
36
|
const env = watched ? { ...process.env, [ALIVE_ENV]: String(ALIVE_FD) } : process.env;
|
|
28
37
|
const child = spawn(binary, process.argv.slice(2), { stdio, env });
|
|
29
38
|
|
|
30
|
-
// The first signal is passed on and the binary stops within its own shutdown budget;
|
|
31
|
-
// kills it now.
|
|
39
|
+
// The first signal is passed on and the binary stops within its own shutdown budget; SIGKILL follows
|
|
40
|
+
// when it has not (lib/escalate.js), and a second signal kills it now. A terminal's Ctrl-C and hangup
|
|
41
|
+
// already reach the binary (same process group), so with a terminal on stdin the launcher does not
|
|
42
|
+
// send those two again: a repeat would be the binary's second signal and kill it mid-stop.
|
|
32
43
|
let stopping = false;
|
|
44
|
+
let stoppedAt = 0;
|
|
45
|
+
let stopBudgetMs = 0; // what the binary says its stop may take, from its beats
|
|
46
|
+
// Any of the three standard streams being a terminal means Ctrl-C and hangup reach the binary directly.
|
|
47
|
+
// A binary run with all three redirected under a terminal's process group (a script) can still get its
|
|
48
|
+
// Ctrl-C twice; the binary then takes the repeat as its second signal. That is the known limit.
|
|
49
|
+
const onTerminal = [0, 1, 2].some((fd) => require('node:tty').isatty(fd));
|
|
50
|
+
const reachedByTerminal = new Set(onTerminal ? ['SIGINT', 'SIGHUP'] : []);
|
|
51
|
+
// Only `dev` (releasing a phone and a cloud workspace) may run past the 5 s backstop; any other
|
|
52
|
+
// command is killed at 5 s whatever its beats announce (L41).
|
|
53
|
+
const mayRunLong = process.argv[2] === 'dev';
|
|
54
|
+
function killChild() {
|
|
55
|
+
child.kill('SIGKILL');
|
|
56
|
+
}
|
|
57
|
+
function escalate() {
|
|
58
|
+
const wait = killDelay(Date.now() - stoppedAt, stopBudgetMs);
|
|
59
|
+
if (wait <= 0) {
|
|
60
|
+
killChild();
|
|
61
|
+
} else {
|
|
62
|
+
setTimeout(escalate, wait).unref();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
33
65
|
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
34
66
|
process.on(signal, () => {
|
|
35
|
-
|
|
67
|
+
if (stopping) {
|
|
68
|
+
killChild();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
36
71
|
stopping = true;
|
|
72
|
+
stoppedAt = Date.now();
|
|
73
|
+
if (!reachedByTerminal.has(signal)) {
|
|
74
|
+
child.kill(signal);
|
|
75
|
+
}
|
|
76
|
+
setTimeout(escalate, KILL_AFTER_MS).unref();
|
|
37
77
|
});
|
|
38
78
|
}
|
|
39
79
|
|
|
@@ -41,7 +81,10 @@ let unresponsive = false;
|
|
|
41
81
|
let looking = null;
|
|
42
82
|
if (watched) {
|
|
43
83
|
const watch = aliveWatch(Date.now);
|
|
44
|
-
child.stdio[ALIVE_FD].on('data', () =>
|
|
84
|
+
child.stdio[ALIVE_FD].on('data', (beats) => {
|
|
85
|
+
watch.beat();
|
|
86
|
+
if (mayRunLong) stopBudgetMs = Math.max(stopBudgetMs, beats[beats.length - 1] * 1000); // 0 until a stop is under way
|
|
87
|
+
});
|
|
45
88
|
child.stdio[ALIVE_FD].on('error', () => {}); // the child exiting closes it; 'exit' reports that
|
|
46
89
|
looking = setInterval(() => {
|
|
47
90
|
if (watch.look()) {
|
|
@@ -61,10 +104,8 @@ child.on('exit', (code, signal) => {
|
|
|
61
104
|
clearInterval(looking);
|
|
62
105
|
if (unresponsive) {
|
|
63
106
|
process.exit(EXIT_UNRESPONSIVE);
|
|
64
|
-
} else if (signal) {
|
|
65
|
-
process.removeAllListeners(signal); // so the signal ends this process as it ended the child
|
|
66
|
-
process.kill(process.pid, signal);
|
|
67
107
|
} else {
|
|
68
|
-
|
|
108
|
+
// The child's own code; a child ended by a signal gives the shell's 128 + signal number.
|
|
109
|
+
process.exit(code ?? 128 + (os.constants.signals[signal] ?? 1));
|
|
69
110
|
}
|
|
70
111
|
});
|
package/checksums.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"darwin-arm64": "81eaaa7dc30769b7aca5f34353e5008b355697745cf8518ba542c3afc30acf0e",
|
|
3
|
+
"darwin-x64": "bb33f6c29b3318345ff323b548fe5de95566d250ecdb9ff2d847e1e4c5e80686",
|
|
4
|
+
"linux-arm64": "4b9a07202940f75e1f0fed9f49ac43019261268248df2d21f46f21bfed43f2af",
|
|
5
|
+
"linux-x64": "05e1b04182fcf07d64be493a06c93b6d8b4e6a99fa3c5716ec0cf84ecbf57163",
|
|
6
|
+
"win32-x64": "a46b77206c1ff061c2d71aa0bacd9a42ad8dcbac66095cdbc844f9b02917094c"
|
|
7
|
+
}
|
package/lib/copy.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The binary runs from a copy on the system disk. npx and npm keep packages in a cache that may live on
|
|
4
|
+
// an external disk, and a program whose file sits on a volume that went away spins at full CPU and
|
|
5
|
+
// ignores signals (31 Oct 2026: two clients ran for 8 hours after such a disk dropped out). The copy is
|
|
6
|
+
// checked against the checksum this package was published with, so a half-written or tampered file is
|
|
7
|
+
// never run.
|
|
8
|
+
|
|
9
|
+
const crypto = require('node:crypto');
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
const CACHE_DIR_NAME = 'talotrace-connect';
|
|
15
|
+
|
|
16
|
+
/** The per-user cache directory for this OS, built from the home directory (never npm's cache). */
|
|
17
|
+
function userCacheDir(platform, env, home) {
|
|
18
|
+
if (platform === 'darwin') {
|
|
19
|
+
return path.join(home, 'Library', 'Caches');
|
|
20
|
+
}
|
|
21
|
+
if (platform === 'win32') {
|
|
22
|
+
return env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
23
|
+
}
|
|
24
|
+
return env.XDG_CACHE_HOME || path.join(home, '.cache');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sha256(file) {
|
|
28
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The path of a checksum-verified copy of `source` under `<user cache>/talotrace-connect/<version>/`,
|
|
33
|
+
* made when it is missing or does not match. `expected` is the checksum published for this platform.
|
|
34
|
+
* The copy is written under a temporary name and renamed, so concurrent launchers never see a partial file.
|
|
35
|
+
*/
|
|
36
|
+
function verifiedCopy({ source, expected, version, platform = process.platform, env = process.env, home = os.homedir() }) {
|
|
37
|
+
if (sha256(source) !== expected) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`${source} does not match the checksum published with @talotrace/connect ${version}; ` +
|
|
40
|
+
'reinstall it (npm cache clean --force, then run the command again).',
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const dir = path.join(userCacheDir(platform, env, home), CACHE_DIR_NAME, version);
|
|
44
|
+
const target = path.join(dir, path.basename(source));
|
|
45
|
+
if (fs.existsSync(target) && sha256(target) === expected) {
|
|
46
|
+
return target;
|
|
47
|
+
}
|
|
48
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
49
|
+
const partial = `${target}.${process.pid}.tmp`;
|
|
50
|
+
try {
|
|
51
|
+
fs.copyFileSync(source, partial);
|
|
52
|
+
fs.chmodSync(partial, 0o755);
|
|
53
|
+
if (sha256(partial) !== expected) {
|
|
54
|
+
throw new Error(`the copy of ${source} under ${dir} does not match its checksum; is the disk full?`);
|
|
55
|
+
}
|
|
56
|
+
fs.renameSync(partial, target);
|
|
57
|
+
} finally {
|
|
58
|
+
fs.rmSync(partial, { force: true });
|
|
59
|
+
}
|
|
60
|
+
return target;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { CACHE_DIR_NAME, userCacheDir, verifiedCopy };
|
package/lib/escalate.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// When the launcher gives up waiting for a stopped binary and sends SIGKILL. A binary that ignores
|
|
4
|
+
// SIGTERM (or whose program file went away) must not outlive its launcher's stop by more than this.
|
|
5
|
+
|
|
6
|
+
/** SIGKILL follows the forwarded signal after this long. */
|
|
7
|
+
const KILL_AFTER_MS = 5000;
|
|
8
|
+
/** A binary that announced a longer stop (`dev` releasing a phone) is given that, plus this. */
|
|
9
|
+
const ANNOUNCED_SLACK_MS = 1000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Milliseconds still to wait before SIGKILL, `sinceSignalMs` after the signal, when the binary has
|
|
13
|
+
* announced a stop budget of `announcedMs` (0 when it has not). 0 or less means kill now.
|
|
14
|
+
*/
|
|
15
|
+
function killDelay(sinceSignalMs, announcedMs) {
|
|
16
|
+
const allowed = announcedMs > 0 ? Math.max(KILL_AFTER_MS, announcedMs + ANNOUNCED_SLACK_MS) : KILL_AFTER_MS;
|
|
17
|
+
return allowed - sinceSignalMs;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { KILL_AFTER_MS, ANNOUNCED_SLACK_MS, killDelay };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@talotrace/connect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "TaloTrace Connect: the TaloTrace MCP server for your coding agent, with a tunnel to the app on your laptop.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"bin",
|
|
12
|
-
"lib"
|
|
12
|
+
"lib",
|
|
13
|
+
"checksums.json"
|
|
13
14
|
],
|
|
14
15
|
"engines": {
|
|
15
16
|
"node": ">=18"
|
|
@@ -18,10 +19,10 @@
|
|
|
18
19
|
"access": "public"
|
|
19
20
|
},
|
|
20
21
|
"optionalDependencies": {
|
|
21
|
-
"@talotrace/connect-darwin-arm64": "0.
|
|
22
|
-
"@talotrace/connect-darwin-x64": "0.
|
|
23
|
-
"@talotrace/connect-linux-arm64": "0.
|
|
24
|
-
"@talotrace/connect-linux-x64": "0.
|
|
25
|
-
"@talotrace/connect-win32-x64": "0.
|
|
22
|
+
"@talotrace/connect-darwin-arm64": "0.9.0",
|
|
23
|
+
"@talotrace/connect-darwin-x64": "0.9.0",
|
|
24
|
+
"@talotrace/connect-linux-arm64": "0.9.0",
|
|
25
|
+
"@talotrace/connect-linux-x64": "0.9.0",
|
|
26
|
+
"@talotrace/connect-win32-x64": "0.9.0"
|
|
26
27
|
}
|
|
27
28
|
}
|