bridge-agent 0.2.11 → 0.2.13
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/dist/__tests__/pty-manager.test.d.ts +1 -0
- package/dist/__tests__/pty-manager.test.js +75 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +88 -0
- package/dist/commands/start.d.ts +1 -0
- package/dist/commands/start.js +258 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +85 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +49 -30
- package/dist/metrics.d.ts +11 -0
- package/dist/metrics.js +123 -0
- package/dist/pty/agents.d.ts +30 -0
- package/dist/pty/agents.js +228 -0
- package/dist/pty/claude-quota.d.ts +12 -0
- package/dist/pty/claude-quota.js +114 -0
- package/dist/pty/claude-usage.d.ts +5 -0
- package/dist/pty/claude-usage.js +92 -0
- package/dist/pty/manager.d.ts +22 -0
- package/dist/pty/manager.js +144 -0
- package/dist/pty/spawn-helper-health.d.ts +13 -0
- package/dist/pty/spawn-helper-health.js +54 -0
- package/dist/shared/types.d.ts +87 -0
- package/dist/shared/types.js +4 -0
- package/dist/ws/client.d.ts +3 -0
- package/dist/ws/client.js +989 -0
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import * as pty from 'node-pty';
|
|
2
|
+
import { AGENT_SPECS } from './agents.js';
|
|
3
|
+
export class PtyManager {
|
|
4
|
+
handles = new Map();
|
|
5
|
+
nextInstanceId = 1;
|
|
6
|
+
lastErrors = new Map();
|
|
7
|
+
spawn(agentId, agentKey, binary, args, cols, rows, onData, onExit, ctx) {
|
|
8
|
+
// Same logical panel may be re-spawned after reconnect/remount while the old PTY
|
|
9
|
+
// is still registered locally. This happens when a new spawn is processed before
|
|
10
|
+
// a prior kill message reaches the daemon. Replace the old handle in-process
|
|
11
|
+
// instead of surfacing SPAWN_DUPLICATE back to the server.
|
|
12
|
+
const existing = this.handles.get(agentId);
|
|
13
|
+
if (existing) {
|
|
14
|
+
console.warn('[daemon] pty.spawn.replace_existing', { agentId, oldPid: existing.pid, newAgentKey: agentKey });
|
|
15
|
+
this.kill(agentId, true);
|
|
16
|
+
}
|
|
17
|
+
const clampedCols = Math.max(1, Math.min(500, cols));
|
|
18
|
+
const clampedRows = Math.max(1, Math.min(500, rows));
|
|
19
|
+
const env = {
|
|
20
|
+
...process.env,
|
|
21
|
+
TERM: 'xterm-256color',
|
|
22
|
+
COLORTERM: 'truecolor',
|
|
23
|
+
};
|
|
24
|
+
if (ctx) {
|
|
25
|
+
env['BRIDGE_SERVER_URL'] = ctx.serverUrl;
|
|
26
|
+
env['BRIDGE_TOKEN'] = ctx.token;
|
|
27
|
+
env['BRIDGE_WORKSPACE_ID'] = ctx.workspaceId;
|
|
28
|
+
env['BRIDGE_PROJECT_ID'] = ctx.projectId;
|
|
29
|
+
// Merge project-scoped env vars — these override process.env
|
|
30
|
+
if (ctx.projectEnv)
|
|
31
|
+
Object.assign(env, ctx.projectEnv);
|
|
32
|
+
}
|
|
33
|
+
const bridgeMcpUrl = process.env['BRIDGE_MCP_URL'];
|
|
34
|
+
if (bridgeMcpUrl) {
|
|
35
|
+
env['BRIDGE_MCP_URL'] = bridgeMcpUrl;
|
|
36
|
+
}
|
|
37
|
+
let proc;
|
|
38
|
+
try {
|
|
39
|
+
proc = pty.spawn(binary, args, {
|
|
40
|
+
name: 'xterm-256color',
|
|
41
|
+
cols: clampedCols,
|
|
42
|
+
rows: clampedRows,
|
|
43
|
+
cwd: ctx?.cwd,
|
|
44
|
+
env,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
49
|
+
this.lastErrors.set(agentId, msg);
|
|
50
|
+
console.error('[daemon] pty.spawn.failed', { agentId, agentKey, error: msg });
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const instanceId = this.nextInstanceId++;
|
|
54
|
+
const handle = { agentId, agentKey, process: proc, pid: proc.pid, killed: false, instanceId };
|
|
55
|
+
proc.onData(data => {
|
|
56
|
+
const current = this.handles.get(agentId);
|
|
57
|
+
if (!current || current.instanceId !== instanceId || handle.killed)
|
|
58
|
+
return;
|
|
59
|
+
onData(Buffer.from(data).toString('base64'));
|
|
60
|
+
});
|
|
61
|
+
proc.onExit(({ exitCode, signal }) => {
|
|
62
|
+
const current = this.handles.get(agentId);
|
|
63
|
+
if (!current || current.instanceId !== instanceId || handle.killed)
|
|
64
|
+
return;
|
|
65
|
+
if (handle.killed)
|
|
66
|
+
return;
|
|
67
|
+
this.handles.delete(agentId);
|
|
68
|
+
console.log('[daemon] pty.exit', { agentId, exitCode, signal });
|
|
69
|
+
onExit(exitCode ?? null, signal ? String(signal) : null);
|
|
70
|
+
});
|
|
71
|
+
this.lastErrors.delete(agentId);
|
|
72
|
+
this.handles.set(agentId, handle);
|
|
73
|
+
console.log('[daemon] pty.spawn.success', { agentId, agentKey, args, cwd: ctx?.cwd });
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
write(agentId, data, source) {
|
|
77
|
+
const handle = this.handles.get(agentId);
|
|
78
|
+
if (!handle)
|
|
79
|
+
return;
|
|
80
|
+
const decoded = Buffer.from(data, 'base64').toString();
|
|
81
|
+
// Only apply agent-specific formatInput for orchestrator injections.
|
|
82
|
+
// User keystrokes from xterm already carry the correct terminators —
|
|
83
|
+
// appending \n/\r to every character would submit each keystroke immediately.
|
|
84
|
+
const spec = AGENT_SPECS.find(s => s.key === handle.agentKey);
|
|
85
|
+
const formatted = (source === 'orchestrator' && spec?.formatInput)
|
|
86
|
+
? spec.formatInput(decoded)
|
|
87
|
+
: decoded;
|
|
88
|
+
handle.process.write(formatted);
|
|
89
|
+
}
|
|
90
|
+
kill(agentId, force = false) {
|
|
91
|
+
const handle = this.handles.get(agentId);
|
|
92
|
+
if (!handle)
|
|
93
|
+
return;
|
|
94
|
+
handle.killed = true;
|
|
95
|
+
this.handles.delete(agentId);
|
|
96
|
+
const pid = handle.pid;
|
|
97
|
+
if (force) {
|
|
98
|
+
// Kill entire process group: SIGTERM first, then SIGKILL after 2s
|
|
99
|
+
try {
|
|
100
|
+
process.kill(-pid, 'SIGTERM');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
handle.process.kill();
|
|
104
|
+
}
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
try {
|
|
107
|
+
process.kill(-pid, 'SIGKILL');
|
|
108
|
+
}
|
|
109
|
+
catch { /* already dead */ }
|
|
110
|
+
}, 2000);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
try {
|
|
114
|
+
process.kill(-pid, 'SIGTERM');
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
handle.process.kill();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
console.log('[daemon] pty.kill', { agentId, force });
|
|
121
|
+
}
|
|
122
|
+
resize(agentId, cols, rows) {
|
|
123
|
+
const handle = this.handles.get(agentId);
|
|
124
|
+
if (!handle)
|
|
125
|
+
return;
|
|
126
|
+
const clampedCols = Math.max(1, Math.min(500, cols));
|
|
127
|
+
const clampedRows = Math.max(1, Math.min(500, rows));
|
|
128
|
+
handle.process.resize(clampedCols, clampedRows);
|
|
129
|
+
}
|
|
130
|
+
getLastError(agentId) {
|
|
131
|
+
return this.lastErrors.get(agentId);
|
|
132
|
+
}
|
|
133
|
+
killAll() {
|
|
134
|
+
for (const handle of this.handles.values()) {
|
|
135
|
+
try {
|
|
136
|
+
process.kill(-handle.pid, 'SIGTERM');
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
handle.process.kill();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
this.handles.clear();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the spawn-helper binary path for the currently loaded node-pty module
|
|
3
|
+
* and active platform/architecture.
|
|
4
|
+
*/
|
|
5
|
+
export declare function resolveSpawnHelperPath(): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Check whether the node-pty spawn-helper is executable by file permission
|
|
8
|
+
* AND by performing a silent test spawn.
|
|
9
|
+
*
|
|
10
|
+
* We test-spawn because npm tarballs can strip +x from the helper, which
|
|
11
|
+
* causes `posix_spawnp failed` on macOS even when the file looks okay.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isSpawnHelperHealthy(): boolean;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import * as pty from 'node-pty';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the spawn-helper binary path for the currently loaded node-pty module
|
|
6
|
+
* and active platform/architecture.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveSpawnHelperPath() {
|
|
9
|
+
try {
|
|
10
|
+
const nodePtyEntry = require.resolve('node-pty');
|
|
11
|
+
// require.resolve('node-pty') returns .../node-pty/lib/index.js
|
|
12
|
+
// The prebuilds folder is at the package root, one level above lib/
|
|
13
|
+
const nodePtyDir = path.resolve(path.dirname(nodePtyEntry), '..');
|
|
14
|
+
const helperPath = path.join(nodePtyDir, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper');
|
|
15
|
+
return helperPath;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check whether the node-pty spawn-helper is executable by file permission
|
|
23
|
+
* AND by performing a silent test spawn.
|
|
24
|
+
*
|
|
25
|
+
* We test-spawn because npm tarballs can strip +x from the helper, which
|
|
26
|
+
* causes `posix_spawnp failed` on macOS even when the file looks okay.
|
|
27
|
+
*/
|
|
28
|
+
export function isSpawnHelperHealthy() {
|
|
29
|
+
const helperPath = resolveSpawnHelperPath();
|
|
30
|
+
if (!helperPath || !fs.existsSync(helperPath)) {
|
|
31
|
+
return true; // If node-pty doesn't ship a helper, we assume it's fine (e.g. Windows)
|
|
32
|
+
}
|
|
33
|
+
// 1. Permission check
|
|
34
|
+
try {
|
|
35
|
+
fs.accessSync(helperPath, fs.constants.X_OK);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
// 2. Functional test: silently spawn /bin/sh and immediately kill it.
|
|
41
|
+
// We intentionally do NOT register onData/onExit to avoid log noise.
|
|
42
|
+
try {
|
|
43
|
+
const proc = pty.spawn('/bin/sh', [], {
|
|
44
|
+
name: 'xterm-256color',
|
|
45
|
+
cols: 80,
|
|
46
|
+
rows: 24,
|
|
47
|
+
});
|
|
48
|
+
proc.kill();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export * from '@jerico/shared';
|
|
2
|
+
/** Messages sent from server to daemon (client from daemon's perspective) */
|
|
3
|
+
export type ClientMessage = {
|
|
4
|
+
type: 'spawn';
|
|
5
|
+
agentId: string;
|
|
6
|
+
daemonId: string;
|
|
7
|
+
agentKey: string;
|
|
8
|
+
cols: number;
|
|
9
|
+
rows: number;
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
projectId?: string;
|
|
12
|
+
workspaceId?: string;
|
|
13
|
+
cwd?: string;
|
|
14
|
+
role?: string;
|
|
15
|
+
runnerCmd?: string;
|
|
16
|
+
orchestratorOwned?: boolean;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'input';
|
|
19
|
+
agentId: string;
|
|
20
|
+
daemonId: string;
|
|
21
|
+
data: string;
|
|
22
|
+
source?: 'user' | 'orchestrator';
|
|
23
|
+
} | {
|
|
24
|
+
type: 'kill';
|
|
25
|
+
agentId: string;
|
|
26
|
+
daemonId: string;
|
|
27
|
+
force: boolean;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'resize';
|
|
30
|
+
agentId: string;
|
|
31
|
+
daemonId: string;
|
|
32
|
+
cols: number;
|
|
33
|
+
rows: number;
|
|
34
|
+
} | {
|
|
35
|
+
type: 'detect_agents';
|
|
36
|
+
daemonId?: string;
|
|
37
|
+
} | {
|
|
38
|
+
type: 'dir_list';
|
|
39
|
+
daemonId: string;
|
|
40
|
+
requestId: string;
|
|
41
|
+
path: string;
|
|
42
|
+
};
|
|
43
|
+
/** Messages sent from daemon to server */
|
|
44
|
+
export type ServerMessage = {
|
|
45
|
+
type: 'output';
|
|
46
|
+
agentId: string;
|
|
47
|
+
data: string;
|
|
48
|
+
} | {
|
|
49
|
+
type: 'exit';
|
|
50
|
+
agentId: string;
|
|
51
|
+
exitCode: number | null;
|
|
52
|
+
signal: string | null;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'agents';
|
|
55
|
+
daemonId: string;
|
|
56
|
+
list: import('@jerico/shared').AgentInfo[];
|
|
57
|
+
} | {
|
|
58
|
+
type: 'ready';
|
|
59
|
+
version: string;
|
|
60
|
+
name?: string;
|
|
61
|
+
spawnHelperBroken?: boolean;
|
|
62
|
+
} | {
|
|
63
|
+
type: 'error';
|
|
64
|
+
code: string;
|
|
65
|
+
message: string;
|
|
66
|
+
} | {
|
|
67
|
+
type: 'session_started';
|
|
68
|
+
agentId: string;
|
|
69
|
+
sessionId: string;
|
|
70
|
+
} | {
|
|
71
|
+
type: 'dir_list_result';
|
|
72
|
+
requestId: string;
|
|
73
|
+
path: string;
|
|
74
|
+
entries: import('@jerico/shared').DirEntry[];
|
|
75
|
+
} | {
|
|
76
|
+
type: 'panel_token_usage';
|
|
77
|
+
agentId: string;
|
|
78
|
+
contextWindowPct: number;
|
|
79
|
+
quota5hPct: number;
|
|
80
|
+
quota5hResetInSec: number;
|
|
81
|
+
} | {
|
|
82
|
+
type: 'system_metrics';
|
|
83
|
+
daemonId: string;
|
|
84
|
+
cpu: number;
|
|
85
|
+
ramUsedMb: number;
|
|
86
|
+
ramTotalMb: number;
|
|
87
|
+
};
|