@bytetrue/byspace 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/commands/agent/open.js +3 -3
  2. package/dist/commands/onboard.js +2 -3
  3. package/dist/commands/open.d.ts +17 -2
  4. package/dist/commands/open.js +91 -68
  5. package/dist/run.js +2 -2
  6. package/node_modules/@bytetrue/client/package.json +3 -3
  7. package/node_modules/@bytetrue/highlight/package.json +1 -1
  8. package/node_modules/@bytetrue/protocol/dist/agent-deep-link.d.ts +0 -2
  9. package/node_modules/@bytetrue/protocol/dist/agent-deep-link.js +0 -33
  10. package/node_modules/@bytetrue/protocol/package.json +1 -1
  11. package/node_modules/@bytetrue/relay/package.json +1 -1
  12. package/node_modules/@bytetrue/server/dist/server/server/byspace-env.d.ts +1 -0
  13. package/node_modules/@bytetrue/server/dist/server/server/byspace-env.js +9 -0
  14. package/node_modules/@bytetrue/server/dist/server/terminal/agent-hooks/pi/pi-extension.d.ts +1 -1
  15. package/node_modules/@bytetrue/server/dist/server/terminal/agent-hooks/pi/pi-extension.js +16 -1
  16. package/node_modules/@bytetrue/server/dist/server/terminal/terminal.js +6 -0
  17. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/{index-1e1ff7510a612ab12641e5e07e0a7208.js → index-3e1fce95f28be7888ddabb4aad3c337a.js} +527 -526
  18. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/index-3e1fce95f28be7888ddabb4aad3c337a.js.br +0 -0
  19. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/index-3e1fce95f28be7888ddabb4aad3c337a.js.gz +0 -0
  20. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-1e67472b9640c869a55ff6915d1f7afb.js → indexeddb-attachment-store-a2e8491be0374dc2a52abfe002ecabe9.js} +1 -1
  21. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-1e67472b9640c869a55ff6915d1f7afb.js.br → indexeddb-attachment-store-a2e8491be0374dc2a52abfe002ecabe9.js.br} +0 -0
  22. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-a2e8491be0374dc2a52abfe002ecabe9.js.gz +0 -0
  23. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/{native-file-attachment-store-63659fda781a1cc594f259795f1cc29f.js → native-file-attachment-store-6ba28513708667bbad7dd5434a0f8d5b.js} +3 -3
  24. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-6ba28513708667bbad7dd5434a0f8d5b.js.br +0 -0
  25. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-6ba28513708667bbad7dd5434a0f8d5b.js.gz +0 -0
  26. package/node_modules/@bytetrue/server/dist/server/web-ui/apple-touch-icon.png +0 -0
  27. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-dark-attention.9c2d51faf93468f68840811c0d9a8c6f.png +0 -0
  28. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-dark-running.a52155010955dca55546513b60bb2e2b.png +0 -0
  29. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-dark.ee7787d5c6bd1ad67c31b9949a832f7a.png +0 -0
  30. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-light-attention.9c2d51faf93468f68840811c0d9a8c6f.png +0 -0
  31. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-light-running.a52155010955dca55546513b60bb2e2b.png +0 -0
  32. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-light.ee7787d5c6bd1ad67c31b9949a832f7a.png +0 -0
  33. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/notification-icon.ba5a7dce236a7febe5d803b16f027b70.png +0 -0
  34. package/node_modules/@bytetrue/server/dist/server/web-ui/favicon.ico +0 -0
  35. package/node_modules/@bytetrue/server/dist/server/web-ui/index.html +1 -1
  36. package/node_modules/@bytetrue/server/dist/server/web-ui/index.html.br +0 -0
  37. package/node_modules/@bytetrue/server/dist/server/web-ui/index.html.gz +0 -0
  38. package/node_modules/@bytetrue/server/dist/server/web-ui/manifest.json +14 -2
  39. package/node_modules/@bytetrue/server/dist/server/web-ui/manifest.json.br +0 -0
  40. package/node_modules/@bytetrue/server/dist/server/web-ui/manifest.json.gz +0 -0
  41. package/node_modules/@bytetrue/server/dist/server/web-ui/pwa-icon-192.png +0 -0
  42. package/node_modules/@bytetrue/server/dist/server/web-ui/pwa-icon-512.png +0 -0
  43. package/node_modules/@bytetrue/server/dist/server/web-ui/pwa-maskable-192.png +0 -0
  44. package/node_modules/@bytetrue/server/dist/server/web-ui/pwa-maskable-512.png +0 -0
  45. package/node_modules/@bytetrue/server/dist/src/server/byspace-env.js +9 -0
  46. package/node_modules/@bytetrue/server/dist/src/terminal/terminal.js +6 -0
  47. package/node_modules/@bytetrue/server/package.json +5 -5
  48. package/package.json +7 -7
  49. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/index-1e1ff7510a612ab12641e5e07e0a7208.js.br +0 -0
  50. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/index-1e1ff7510a612ab12641e5e07e0a7208.js.gz +0 -0
  51. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-1e67472b9640c869a55ff6915d1f7afb.js.gz +0 -0
  52. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-63659fda781a1cc594f259795f1cc29f.js.br +0 -0
  53. package/node_modules/@bytetrue/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-63659fda781a1cc594f259795f1cc29f.js.gz +0 -0
  54. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-dark-attention.882b3a27dcb2073e9e31b334f9ed9728.png +0 -0
  55. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-dark-running.8112342ff0d39e047a7f8d4fad9402f3.png +0 -0
  56. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-dark.8005ed36ac07a5a7c60de25780897bd4.png +0 -0
  57. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-light-attention.882b3a27dcb2073e9e31b334f9ed9728.png +0 -0
  58. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-light-running.8112342ff0d39e047a7f8d4fad9402f3.png +0 -0
  59. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/favicon-light.8005ed36ac07a5a7c60de25780897bd4.png +0 -0
  60. package/node_modules/@bytetrue/server/dist/server/web-ui/assets/assets/images/notification-icon.3bf81d33ddbf380606bdd248ba83e158.png +0 -0
@@ -1,5 +1,5 @@
1
1
  import { buildDaemonConnectionCommandError, connectToDaemon } from "../../utils/client.js";
2
- import { openDesktopWithAgent } from "../open.js";
2
+ import { openAgentInWebApp } from "../open.js";
3
3
  const openAgentSchema = {
4
4
  idField: "agentId",
5
5
  columns: [
@@ -10,7 +10,7 @@ const openAgentSchema = {
10
10
  };
11
11
  export function addOpenOptions(command) {
12
12
  return command
13
- .description("Open an existing agent in BySpace Desktop")
13
+ .description("Open an existing agent in the BySpace web app")
14
14
  .argument("<agent-id>", "Existing agent ID")
15
15
  .option("--server <server-id>", "Server ID (defaults to the local daemon)");
16
16
  }
@@ -51,7 +51,7 @@ export async function runOpenCommand(agentIdArg, options, _command) {
51
51
  throw error;
52
52
  }
53
53
  const serverId = await resolveServerId(options);
54
- await openDesktopWithAgent({ serverId, agentId });
54
+ await openAgentInWebApp({ serverId, agentId }, { host: options.host });
55
55
  return {
56
56
  type: "single",
57
57
  data: { agentId, serverId, status: "opened" },
@@ -106,9 +106,8 @@ function printNextSteps(pairingUrl, byspaceHome, richUi) {
106
106
  ? "1. Open BySpace and scan the QR code above, or paste the pairing link."
107
107
  : "1. Open BySpace and connect to your daemon.",
108
108
  `2. Web app: ${appBaseUrl}`,
109
- "3. Desktop app: https://github.com/ByteTrue/byspace/releases/latest",
110
- "4. Docs: https://github.com/ByteTrue/byspace",
111
- '5. Example: byspace run --output-schema schema.json "extract fields"',
109
+ "3. Docs: https://github.com/ByteTrue/byspace",
110
+ '4. Example: byspace run --output-schema schema.json "extract fields"',
112
111
  ];
113
112
  const quickReferenceLines = [
114
113
  "1. byspace --help",
@@ -1,4 +1,19 @@
1
1
  import { type AgentDeepLinkTarget } from "@bytetrue/protocol/agent-deep-link";
2
- export declare function openDesktopWithProject(projectPath: string): Promise<void>;
3
- export declare function openDesktopWithAgent(target: AgentDeepLinkTarget): Promise<void>;
2
+ /**
3
+ * The daemon serves its bundled web UI on the same TCP port as `/ws`, so a TCP
4
+ * daemon host doubles as its HTTP origin. Unix sockets, SSH tunnels, and relay
5
+ * offers have no address a browser on this machine can reach.
6
+ */
7
+ export declare function resolveWebUiOrigin(rawHost: string): string | null;
8
+ /**
9
+ * Mirrors `buildHostWorkspaceRoute` in `packages/app/src/utils/host-routes.ts`.
10
+ * Workspace IDs are generated as `wks_<hex>` (packages/server/src/server/workspace-registry-model.ts),
11
+ * so percent-encoding always suffices; the app's base64 path segment form only
12
+ * exists for pre-v0.1.95 path-shaped IDs, which this fork does not carry.
13
+ */
14
+ export declare function buildWorkspaceWebRoute(serverId: string, workspaceId: string): string;
15
+ export declare function openProjectInWebApp(projectPath: string): Promise<void>;
16
+ export declare function openAgentInWebApp(target: AgentDeepLinkTarget, options?: {
17
+ host?: string;
18
+ }): Promise<void>;
4
19
  //# sourceMappingURL=open.d.ts.map
@@ -1,89 +1,112 @@
1
- import { existsSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import path from "node:path";
4
1
  import { spawnProcess } from "@bytetrue/server";
5
- import { buildAgentDeepLink } from "@bytetrue/protocol/agent-deep-link";
6
- function findDesktopApp() {
7
- if (process.platform === "darwin") {
8
- const candidates = [
9
- "/Applications/BySpace.app",
10
- path.join(homedir(), "Applications", "BySpace.app"),
11
- ];
12
- for (const candidate of candidates) {
13
- if (existsSync(candidate)) {
14
- return candidate;
15
- }
16
- }
2
+ import { buildAgentDeepLinkRoute, } from "@bytetrue/protocol/agent-deep-link";
3
+ import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./daemon/local-daemon.js";
4
+ import { buildDaemonConnectionCommandError, connectToDaemon, getDaemonHost, getExplicitDaemonHost, } from "../utils/client.js";
5
+ /**
6
+ * The daemon serves its bundled web UI on the same TCP port as `/ws`, so a TCP
7
+ * daemon host doubles as its HTTP origin. Unix sockets, SSH tunnels, and relay
8
+ * offers have no address a browser on this machine can reach.
9
+ */
10
+ export function resolveWebUiOrigin(rawHost) {
11
+ const trimmed = rawHost.trim();
12
+ if (!trimmed || trimmed.startsWith("ssh://")) {
17
13
  return null;
18
14
  }
19
- if (process.platform === "linux") {
20
- const candidates = [
21
- "/usr/bin/BySpace",
22
- "/opt/BySpace/BySpace",
23
- path.join(homedir(), "Applications", "BySpace.AppImage"),
24
- ];
25
- for (const candidate of candidates) {
26
- if (existsSync(candidate)) {
27
- return candidate;
28
- }
29
- }
15
+ const endpoint = trimmed.startsWith("tcp://") ? trimmed.slice("tcp://".length) : trimmed;
16
+ const match = /^\[?([^\]/]+?)]?:(\d+)$/.exec(endpoint);
17
+ if (!match) {
30
18
  return null;
31
19
  }
32
- if (process.platform === "win32") {
33
- const localAppData = process.env.LOCALAPPDATA;
34
- if (!localAppData) {
35
- return null;
36
- }
37
- const candidate = path.join(localAppData, "Programs", "BySpace", "BySpace.exe");
38
- return existsSync(candidate) ? candidate : null;
20
+ const [, rawHostname, port] = match;
21
+ const hostname = rawHostname === "0.0.0.0" ? "127.0.0.1" : rawHostname;
22
+ const hostPart = hostname.includes(":") ? `[${hostname}]` : hostname;
23
+ return `http://${hostPart}:${port}`;
24
+ }
25
+ function resolveLocalWebUiOrigin() {
26
+ const listen = resolveLocalDaemonState().listen;
27
+ const host = resolveTcpHostFromListen(listen) ?? listen;
28
+ return { origin: resolveWebUiOrigin(host), host };
29
+ }
30
+ function resolveWebUiOriginForHost(host) {
31
+ const explicitHost = getExplicitDaemonHost(host);
32
+ if (!explicitHost) {
33
+ return resolveLocalWebUiOrigin();
39
34
  }
40
- return null;
35
+ return { origin: resolveWebUiOrigin(explicitHost), host: explicitHost };
41
36
  }
42
- function cleanEnvForDesktopLaunch() {
43
- const env = { ...process.env };
44
- // The CLI runs via ELECTRON_RUN_AS_NODE=1. On Linux/Windows the spawned
45
- // desktop process inherits the env directly, so we must strip it or the
46
- // desktop app would start as a bare Node process instead of Electron.
47
- delete env.ELECTRON_RUN_AS_NODE;
48
- delete env.ELECTRON_NO_ATTACH_CONSOLE;
49
- delete env.BYSPACE_NODE_ENV;
50
- return env;
37
+ /**
38
+ * Mirrors `buildHostWorkspaceRoute` in `packages/app/src/utils/host-routes.ts`.
39
+ * Workspace IDs are generated as `wks_<hex>` (packages/server/src/server/workspace-registry-model.ts),
40
+ * so percent-encoding always suffices; the app's base64 path segment form only
41
+ * exists for pre-v0.1.95 path-shaped IDs, which this fork does not carry.
42
+ */
43
+ export function buildWorkspaceWebRoute(serverId, workspaceId) {
44
+ return `/h/${encodeURIComponent(serverId)}/workspace/${encodeURIComponent(workspaceId)}`;
45
+ }
46
+ function openInBrowser(url) {
47
+ let command;
48
+ let args;
49
+ if (process.platform === "darwin") {
50
+ command = "open";
51
+ args = [url];
52
+ }
53
+ else if (process.platform === "win32") {
54
+ command = process.env.ComSpec ?? "cmd";
55
+ args = ["/c", "start", "", url];
56
+ }
57
+ else {
58
+ command = "xdg-open";
59
+ args = [url];
60
+ }
61
+ spawnProcess(command, args, { detached: true, stdio: "ignore", shell: false }).unref();
62
+ process.stderr.write(`${url}\n`);
51
63
  }
52
- function spawnDetached(command, args) {
53
- spawnProcess(command, args, {
54
- detached: true,
55
- stdio: "ignore",
56
- env: cleanEnvForDesktopLaunch(),
57
- }).unref();
64
+ function reportWebUiUnavailable(host) {
65
+ process.stderr.write(`Cannot open the web app for ${host}: the daemon has no HTTP endpoint reachable from here.\n` +
66
+ "Start it with `byspace daemon start` and open the Web UI URL it prints.\n");
67
+ process.exitCode = 1;
58
68
  }
59
- function launchDesktop(args) {
60
- if (process.env.BYSPACE_DESKTOP_CLI === "1") {
61
- throw new Error("Cannot open BySpace Desktop while running in desktop CLI passthrough mode.");
69
+ export async function openProjectInWebApp(projectPath) {
70
+ const { origin, host } = resolveLocalWebUiOrigin();
71
+ if (!origin) {
72
+ reportWebUiUnavailable(host);
73
+ return;
62
74
  }
63
- const desktopApp = findDesktopApp();
64
- if (!desktopApp) {
65
- throw new Error("BySpace desktop app not found. Install it from https://github.com/ByteTrue/byspace/releases");
75
+ let client;
76
+ try {
77
+ client = await connectToDaemon();
66
78
  }
67
- if (process.platform === "darwin") {
68
- // -n forces a new instance even if the app is already running. The new
69
- // instance relays its argv to the existing one through Electron's
70
- // single-instance lock. -g keeps the terminal in the foreground.
71
- spawnDetached("open", ["-n", "-g", "-a", desktopApp, "--args", ...args]);
79
+ catch (error) {
80
+ const failure = buildDaemonConnectionCommandError({ error });
81
+ process.stderr.write(`${failure.message}\n${failure.details}\n`);
82
+ process.exitCode = 1;
72
83
  return;
73
84
  }
74
- spawnDetached(desktopApp, args);
75
- }
76
- export async function openDesktopWithProject(projectPath) {
77
85
  try {
78
- launchDesktop([projectPath]);
86
+ const serverId = client.getLastServerInfoMessage()?.serverId.trim();
87
+ if (!serverId) {
88
+ throw new Error("The daemon did not report a server ID.");
89
+ }
90
+ const result = await client.openProject(projectPath);
91
+ if (result.error || !result.workspace) {
92
+ throw new Error(result.error ?? `The daemon did not open a workspace for ${projectPath}`);
93
+ }
94
+ openInBrowser(`${origin}${buildWorkspaceWebRoute(serverId, result.workspace.id)}`);
79
95
  }
80
96
  catch (error) {
81
- const message = error instanceof Error ? error.message : String(error);
82
- process.stderr.write(`${message}\n`);
97
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
83
98
  process.exitCode = 1;
84
99
  }
100
+ finally {
101
+ await client.close().catch(() => { });
102
+ }
85
103
  }
86
- export async function openDesktopWithAgent(target) {
87
- launchDesktop([buildAgentDeepLink(target)]);
104
+ export async function openAgentInWebApp(target, options = {}) {
105
+ const { origin, host } = resolveWebUiOriginForHost(options.host);
106
+ if (!origin) {
107
+ reportWebUiUnavailable(host || getDaemonHost());
108
+ return;
109
+ }
110
+ openInBrowser(`${origin}${buildAgentDeepLinkRoute(target)}`);
88
111
  }
89
112
  //# sourceMappingURL=open.js.map
package/dist/run.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createCli } from "./cli.js";
2
2
  import { classifyInvocation } from "./classify.js";
3
- import { openDesktopWithProject } from "./commands/open.js";
3
+ import { openProjectInWebApp } from "./commands/open.js";
4
4
  export function createCliParseArgv(input) {
5
5
  const program = createCli();
6
6
  const knownCommands = new Set(program.commands.map((command) => command.name()));
@@ -30,7 +30,7 @@ export async function runCli(argv, options = {}) {
30
30
  nodeArgv: options.nodeArgv,
31
31
  });
32
32
  if (!Array.isArray(parseArgv)) {
33
- await openDesktopWithProject(parseArgv.resolvedPath);
33
+ await openProjectInWebApp(parseArgv.resolvedPath);
34
34
  return typeof process.exitCode === "number" ? process.exitCode : 0;
35
35
  }
36
36
  const program = createCli();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytetrue/client",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "BySpace client SDK package",
5
5
  "files": [
6
6
  "dist",
@@ -39,8 +39,8 @@
39
39
  "test": "vitest run"
40
40
  },
41
41
  "dependencies": {
42
- "@bytetrue/protocol": "0.15.0",
43
- "@bytetrue/relay": "0.15.0"
42
+ "@bytetrue/protocol": "0.16.1",
43
+ "@bytetrue/relay": "0.16.1"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^20.9.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytetrue/highlight",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.map",
@@ -3,6 +3,4 @@ export interface AgentDeepLinkTarget {
3
3
  agentId: string;
4
4
  }
5
5
  export declare function buildAgentDeepLinkRoute(target: AgentDeepLinkTarget): `/h/${string}/agent/${string}`;
6
- export declare function buildAgentDeepLink(target: AgentDeepLinkTarget): string;
7
- export declare function parseAgentDeepLink(input: string): AgentDeepLinkTarget | null;
8
6
  //# sourceMappingURL=agent-deep-link.d.ts.map
@@ -13,37 +13,4 @@ export function buildAgentDeepLinkRoute(target) {
13
13
  const { serverId, agentId } = normalizeAgentDeepLinkTarget(target);
14
14
  return `/h/${encodeURIComponent(serverId)}/agent/${encodeURIComponent(agentId)}`;
15
15
  }
16
- export function buildAgentDeepLink(target) {
17
- return `byspace:/${buildAgentDeepLinkRoute(target)}`;
18
- }
19
- export function parseAgentDeepLink(input) {
20
- let url;
21
- try {
22
- url = new URL(input);
23
- }
24
- catch {
25
- return null;
26
- }
27
- if (url.protocol !== "byspace:" ||
28
- url.hostname !== "h" ||
29
- url.username ||
30
- url.password ||
31
- url.port ||
32
- url.search ||
33
- url.hash) {
34
- return null;
35
- }
36
- const segments = url.pathname.split("/").filter(Boolean);
37
- if (segments.length !== 3 || segments[1] !== "agent") {
38
- return null;
39
- }
40
- try {
41
- const serverId = normalizeSegment(decodeURIComponent(segments[0] ?? ""));
42
- const agentId = normalizeSegment(decodeURIComponent(segments[2] ?? ""));
43
- return serverId && agentId ? { serverId, agentId } : null;
44
- }
45
- catch {
46
- return null;
47
- }
48
- }
49
16
  //# sourceMappingURL=agent-deep-link.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytetrue/protocol",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "BySpace shared protocol schemas and wire types",
5
5
  "files": [
6
6
  "dist",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytetrue/relay",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "BySpace relay for bridging daemon and client connections",
5
5
  "files": [
6
6
  "dist",
@@ -1,3 +1,4 @@
1
+ export declare const PI_TERMINAL_HOOK_OWNER_PID_ENV_KEY = "BYSPACE_PI_TERMINAL_HOOK_OWNER_PID";
1
2
  export type BySpaceNodeEnv = "development" | "production" | "test";
2
3
  export type ProcessEnvRecord = Record<string, string | undefined>;
3
4
  export type ExternalProcessEnv = NodeJS.ProcessEnv & Record<string, string>;
@@ -1,5 +1,7 @@
1
1
  const BYSPACE_NODE_ENV = "BYSPACE_NODE_ENV";
2
2
  const ELECTRON_RUN_AS_NODE = "ELECTRON_RUN_AS_NODE";
3
+ // Shared with the installed Pi extension (which cannot import from here).
4
+ export const PI_TERMINAL_HOOK_OWNER_PID_ENV_KEY = "BYSPACE_PI_TERMINAL_HOOK_OWNER_PID";
3
5
  const RUNTIME_CONTROL_ENV_KEYS = [
4
6
  BYSPACE_NODE_ENV,
5
7
  "BYSPACE_DESKTOP_MANAGED",
@@ -7,6 +9,13 @@ const RUNTIME_CONTROL_ENV_KEYS = [
7
9
  ELECTRON_RUN_AS_NODE,
8
10
  "ELECTRON_NO_ATTACH_CONSOLE",
9
11
  "ESBUILD_BINARY_PATH",
12
+ // Written into `process.env` by the Pi terminal-activity extension so child Pi
13
+ // processes do not register a second reporter for the same terminal. If it
14
+ // survives into a fresh terminal or agent, that Pi process sees a marker whose
15
+ // value is a stale pid, concludes it is an inherited child, and registers no
16
+ // handlers at all — the terminal then never reports running/idle. It is
17
+ // per-process state, never inherited. See docs/terminal-activity.md.
18
+ PI_TERMINAL_HOOK_OWNER_PID_ENV_KEY,
10
19
  ];
11
20
  function buildInternalProcessEnv(baseEnv) {
12
21
  return { ...baseEnv };
@@ -1,2 +1,2 @@
1
- export declare const PI_TERMINAL_ACTIVITY_EXTENSION_SOURCE = "// byspace.pi-terminal-activity\nconst ownerPidKey = \"BYSPACE_PI_TERMINAL_HOOK_OWNER_PID\";\n\n// Keep one request in flight and only the latest state waiting behind it.\nexport function createReportQueue(sendReport) {\n let pendingReport;\n let reporting;\n\n async function flushReports() {\n while (pendingReport) {\n const next = pendingReport;\n pendingReport = undefined;\n try {\n await sendReport(next);\n } catch {}\n }\n reporting = undefined;\n }\n\n return {\n enqueue(next) {\n pendingReport = next;\n reporting ??= Promise.resolve().then(flushReports);\n return reporting;\n },\n wait() {\n return reporting ?? Promise.resolve();\n },\n };\n}\n\nasync function sendReport(next) {\n await fetch(next.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n terminalId: next.terminalId,\n token: next.token,\n state: next.state,\n }),\n signal: AbortSignal.timeout(1_000),\n });\n}\n\nconst reportQueue = createReportQueue(sendReport);\n\nfunction report(state) {\n const url =\n process.env.BYSPACE_TERMINAL_ACTIVITY_URL;\n const terminalId = process.env.BYSPACE_TERMINAL_ID;\n const token = process.env.BYSPACE_ACTIVITY_TOKEN;\n if (!url || !terminalId || !token) return reportQueue.wait();\n\n return reportQueue.enqueue({ url, terminalId, token, state });\n}\n\nexport default function byspaceTerminalActivity(pi) {\n const ownerPid = process.env[ownerPidKey];\n if (ownerPid && ownerPid !== String(process.pid)) return;\n process.env[ownerPidKey] = String(process.pid);\n\n pi.on(\"agent_start\", () => void report(\"running\"));\n pi.on(\"ui_prompt_start\", () => void report(\"needs-input\"));\n pi.on(\"ui_prompt_end\", () => void report(\"running\"));\n pi.on(\"agent_settled\", () => void report(\"idle\"));\n pi.on(\"session_shutdown\", () => report(\"idle\"));\n}\n";
1
+ export declare const PI_TERMINAL_ACTIVITY_EXTENSION_SOURCE = "// byspace.pi-terminal-activity\nconst ownerPidKey = \"BYSPACE_PI_TERMINAL_HOOK_OWNER_PID\";\n\n// Keep one request in flight and only the latest state waiting behind it.\nexport function createReportQueue(sendReport) {\n let pendingReport;\n let reporting;\n\n async function flushReports() {\n while (pendingReport) {\n const next = pendingReport;\n pendingReport = undefined;\n try {\n await sendReport(next);\n } catch {}\n }\n reporting = undefined;\n }\n\n return {\n enqueue(next) {\n pendingReport = next;\n reporting ??= Promise.resolve().then(flushReports);\n return reporting;\n },\n wait() {\n return reporting ?? Promise.resolve();\n },\n };\n}\n\nasync function sendReport(next) {\n await fetch(next.url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n terminalId: next.terminalId,\n token: next.token,\n state: next.state,\n }),\n signal: AbortSignal.timeout(1_000),\n });\n}\n\nconst reportQueue = createReportQueue(sendReport);\n\n// The marker is inherited by Pi child processes so they do not register a second\n// reporter for the same terminal. A marker left by a process that is gone is\n// stale, not an owner: treating it as an owner makes this Pi report nothing at\n// all, which looks exactly like \"the terminal stopped showing running\".\nfunction ownerIsLive(ownerPid) {\n const pid = Number(ownerPid);\n if (!Number.isInteger(pid) || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return error?.code === \"EPERM\";\n }\n}\n\nfunction report(state) {\n const url =\n process.env.BYSPACE_TERMINAL_ACTIVITY_URL;\n const terminalId = process.env.BYSPACE_TERMINAL_ID;\n const token = process.env.BYSPACE_ACTIVITY_TOKEN;\n if (!url || !terminalId || !token) return reportQueue.wait();\n\n return reportQueue.enqueue({ url, terminalId, token, state });\n}\n\nexport default function byspaceTerminalActivity(pi) {\n const ownerPid = process.env[ownerPidKey];\n if (ownerPid && ownerPid !== String(process.pid) && ownerIsLive(ownerPid)) return;\n process.env[ownerPidKey] = String(process.pid);\n\n pi.on(\"agent_start\", () => void report(\"running\"));\n pi.on(\"ui_prompt_start\", () => void report(\"needs-input\"));\n pi.on(\"ui_prompt_end\", () => void report(\"running\"));\n pi.on(\"agent_settled\", () => void report(\"idle\"));\n pi.on(\"session_shutdown\", () => report(\"idle\"));\n}\n";
2
2
  //# sourceMappingURL=pi-extension.d.ts.map
@@ -44,6 +44,21 @@ async function sendReport(next) {
44
44
 
45
45
  const reportQueue = createReportQueue(sendReport);
46
46
 
47
+ // The marker is inherited by Pi child processes so they do not register a second
48
+ // reporter for the same terminal. A marker left by a process that is gone is
49
+ // stale, not an owner: treating it as an owner makes this Pi report nothing at
50
+ // all, which looks exactly like "the terminal stopped showing running".
51
+ function ownerIsLive(ownerPid) {
52
+ const pid = Number(ownerPid);
53
+ if (!Number.isInteger(pid) || pid <= 0) return false;
54
+ try {
55
+ process.kill(pid, 0);
56
+ return true;
57
+ } catch (error) {
58
+ return error?.code === "EPERM";
59
+ }
60
+ }
61
+
47
62
  function report(state) {
48
63
  const url =
49
64
  process.env.BYSPACE_TERMINAL_ACTIVITY_URL;
@@ -56,7 +71,7 @@ function report(state) {
56
71
 
57
72
  export default function byspaceTerminalActivity(pi) {
58
73
  const ownerPid = process.env[ownerPidKey];
59
- if (ownerPid && ownerPid !== String(process.pid)) return;
74
+ if (ownerPid && ownerPid !== String(process.pid) && ownerIsLive(ownerPid)) return;
60
75
  process.env[ownerPidKey] = String(process.pid);
61
76
 
62
77
  pi.on("agent_start", () => void report("running"));
@@ -786,6 +786,12 @@ export async function createTerminal(options) {
786
786
  if (!commandFinished) {
787
787
  return true;
788
788
  }
789
+ // The interactive shell only emits this when its foreground job ends. A
790
+ // terminal agent that exits without reporting idle (SIGKILL, crash, a
791
+ // suspend that never resumes) leaves `working` set forever, so treat the
792
+ // shell regaining the prompt as proof that the agent is gone. `interrupt`
793
+ // only clears `working`; a finished-attention idle state is untouched.
794
+ activityTracker.interrupt();
789
795
  for (const listener of Array.from(commandFinishedListeners)) {
790
796
  try {
791
797
  listener(commandFinished);