@talotrace/connect 0.4.0 → 0.6.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 ADDED
@@ -0,0 +1,45 @@
1
+ # @talotrace/connect
2
+
3
+ Lets TaloTrace's cloud browser and phones test the app running on your laptop, and gives your coding agent the
4
+ TaloTrace MCP tools. Node 18 or newer; npm installs the build for your OS (macOS, Linux arm64/x64, Windows x64).
5
+
6
+ ```
7
+ npx @talotrace/connect login # once: signs in through your browser
8
+ npx @talotrace/connect 3000 # serve http://localhost:3000 to TaloTrace runs until Ctrl-C
9
+ npx @talotrace/connect mcp # the MCP server for Claude Code, Cursor, Codex
10
+ npx @talotrace/connect --help # every command
11
+ ```
12
+
13
+ Set `TALOTRACE_URL` to your TaloTrace API (pilot: `https://dev.api.talotrace.ai`) or pass `--url`.
14
+
15
+ ## A page and its API on two ports
16
+
17
+ `talotrace-connect 5173 8787` serves both ports in one session, but a run opens your page at its own https address,
18
+ not at `localhost:5173`. A `fetch("http://localhost:8787/...")` from the page therefore stays inside the cloud browser
19
+ and is refused there. Until the cloud browser maps `localhost` ports itself, call the API with a relative URL and let
20
+ your dev server proxy it:
21
+
22
+ - Vite: `server: { proxy: { '/api': 'http://localhost:8787' } }`
23
+ - Next.js: `rewrites()` returning `{ source: '/api/:path*', destination: 'http://localhost:8787/api/:path*' }`
24
+
25
+ Then `talotrace-connect 5173` is enough.
26
+
27
+ ## Several apps at once
28
+
29
+ Each `talotrace-connect` holds a session of its own, so an app and its API (or two apps) can run in two terminals at
30
+ the same time. One person may hold up to five sessions open at once; past that, a new one is refused with a message
31
+ saying so. Ctrl-C ends a session at once; a killed one counts until its two-minute grace window ends.
32
+
33
+ ## Stopping
34
+
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.
37
+
38
+ If the binary stops responding (for example, the disk holding npm's cache was unplugged, which leaves a program unable
39
+ to run its own code), the launcher stops it after 5 seconds and says so.
40
+
41
+ ## Exit codes
42
+
43
+ `0` done or stopped, `1` failure, `2` usage, `3` TaloTrace ended the session, `4` you ended it, `5` the session expired,
44
+ `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.
@@ -3,6 +3,14 @@
3
3
 
4
4
  const { spawn } = require('node:child_process');
5
5
  const { binaryPath } = require('../lib/resolve');
6
+ const {
7
+ ALIVE_ENV,
8
+ ALIVE_FD,
9
+ ALIVE_INTERVAL_MS,
10
+ EXIT_UNRESPONSIVE,
11
+ UNRESPONSIVE_MESSAGE,
12
+ aliveWatch,
13
+ } = require('../lib/alive');
6
14
 
7
15
  let binary;
8
16
  try {
@@ -12,17 +20,49 @@ try {
12
20
  process.exit(1);
13
21
  }
14
22
 
15
- // stdio is inherited: `mcp` speaks the protocol on this process's stdin and stdout.
16
- const child = spawn(binary, process.argv.slice(2), { stdio: 'inherit' });
23
+ // stdio is inherited: `mcp` speaks the protocol on this process's stdin and stdout. Off Windows the
24
+ // child also gets the alive pipe (lib/alive.js); Windows passes handles, not descriptors.
25
+ const watched = process.platform !== 'win32';
26
+ const stdio = watched ? ['inherit', 'inherit', 'inherit', 'pipe'] : 'inherit';
27
+ const env = watched ? { ...process.env, [ALIVE_ENV]: String(ALIVE_FD) } : process.env;
28
+ const child = spawn(binary, process.argv.slice(2), { stdio, env });
29
+
30
+ // The first signal is passed on and the binary stops within its own shutdown budget; a second one
31
+ // kills it now.
32
+ let stopping = false;
17
33
  for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
18
- process.on(signal, () => child.kill(signal));
34
+ process.on(signal, () => {
35
+ child.kill(stopping ? 'SIGKILL' : signal);
36
+ stopping = true;
37
+ });
38
+ }
39
+
40
+ let unresponsive = false;
41
+ let looking = null;
42
+ if (watched) {
43
+ const watch = aliveWatch(Date.now);
44
+ child.stdio[ALIVE_FD].on('data', () => watch.beat());
45
+ child.stdio[ALIVE_FD].on('error', () => {}); // the child exiting closes it; 'exit' reports that
46
+ looking = setInterval(() => {
47
+ if (watch.look()) {
48
+ clearInterval(looking);
49
+ unresponsive = true;
50
+ console.error(UNRESPONSIVE_MESSAGE);
51
+ child.kill('SIGKILL');
52
+ }
53
+ }, ALIVE_INTERVAL_MS);
19
54
  }
55
+
20
56
  child.on('error', (error) => {
21
57
  console.error(`could not start ${binary}: ${error.message}`);
22
58
  process.exit(1);
23
59
  });
24
60
  child.on('exit', (code, signal) => {
25
- if (signal) {
61
+ clearInterval(looking);
62
+ if (unresponsive) {
63
+ process.exit(EXIT_UNRESPONSIVE);
64
+ } else if (signal) {
65
+ process.removeAllListeners(signal); // so the signal ends this process as it ended the child
26
66
  process.kill(process.pid, signal);
27
67
  } else {
28
68
  process.exit(code ?? 1);
package/lib/alive.js ADDED
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ // The launcher's watch on the Go binary. The binary writes one byte to an extra pipe every second
4
+ // (cmd/talotrace-connect/shutdown.go; keep ALIVE_ENV and ALIVE_INTERVAL_MS equal to envAliveFD and
5
+ // aliveInterval there). A binary whose program file sat on a disk that went away cannot run its own
6
+ // code any more: it spins at full CPU and never handles a signal, so only SIGKILL from outside stops it
7
+ // (31 Oct 2026: two clients spun for 8 hours after an external disk dropped out). The launcher runs from
8
+ // node, which is on another disk, so it notices the silence and does that.
9
+
10
+ /** The environment variable that tells the binary which file descriptor its alive pipe is. */
11
+ const ALIVE_ENV = 'TALOTRACE_CONNECT_ALIVE_FD';
12
+ /** The alive pipe's file descriptor in the child (after stdin, stdout and stderr). */
13
+ const ALIVE_FD = 3;
14
+ /** How often the binary writes, and how often the launcher looks. */
15
+ const ALIVE_INTERVAL_MS = 1000;
16
+ /** A binary silent this long is stopped. */
17
+ const SILENT_LIMIT_MS = 5000;
18
+ /** The launcher's exit code after it stopped a silent binary (the binary's own codes are 0-8). */
19
+ const EXIT_UNRESPONSIVE = 9;
20
+ /** What the launcher prints when it does. */
21
+ const UNRESPONSIVE_MESSAGE =
22
+ 'talotrace-connect stopped responding (was its program file on a disk that went away?); stopped it';
23
+
24
+ /**
25
+ * Tracks the binary's beats. `look()` is called every ALIVE_INTERVAL_MS and says whether the binary
26
+ * has been silent for longer than SILENT_LIMIT_MS. When the looks themselves were late (the laptop
27
+ * slept, so the binary could not write either), the silence starts again from now.
28
+ */
29
+ function aliveWatch(now) {
30
+ let lastBeat = now();
31
+ let lastLook = lastBeat;
32
+ return {
33
+ beat() {
34
+ lastBeat = now();
35
+ },
36
+ look() {
37
+ const at = now();
38
+ if (at - lastLook > 2 * ALIVE_INTERVAL_MS) {
39
+ lastBeat = at;
40
+ }
41
+ lastLook = at;
42
+ return at - lastBeat > SILENT_LIMIT_MS;
43
+ },
44
+ };
45
+ }
46
+
47
+ module.exports = {
48
+ ALIVE_ENV,
49
+ ALIVE_FD,
50
+ ALIVE_INTERVAL_MS,
51
+ SILENT_LIMIT_MS,
52
+ EXIT_UNRESPONSIVE,
53
+ UNRESPONSIVE_MESSAGE,
54
+ aliveWatch,
55
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talotrace/connect",
3
- "version": "0.4.0",
3
+ "version": "0.6.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": {
@@ -18,10 +18,10 @@
18
18
  "access": "public"
19
19
  },
20
20
  "optionalDependencies": {
21
- "@talotrace/connect-darwin-arm64": "0.4.0",
22
- "@talotrace/connect-darwin-x64": "0.4.0",
23
- "@talotrace/connect-linux-arm64": "0.4.0",
24
- "@talotrace/connect-linux-x64": "0.4.0",
25
- "@talotrace/connect-win32-x64": "0.4.0"
21
+ "@talotrace/connect-darwin-arm64": "0.6.0",
22
+ "@talotrace/connect-darwin-x64": "0.6.0",
23
+ "@talotrace/connect-linux-arm64": "0.6.0",
24
+ "@talotrace/connect-linux-x64": "0.6.0",
25
+ "@talotrace/connect-win32-x64": "0.6.0"
26
26
  }
27
27
  }