@ahpd/server 0.1.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 +88 -81
- package/dist/config.d.ts +72 -0
- package/dist/config.js +75 -0
- package/dist/daemon.d.ts +31 -0
- package/dist/daemon.js +162 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +335 -0
- package/dist/pty.d.ts +12 -0
- package/dist/pty.js +56 -0
- package/package.json +15 -25
- package/dist/automations.d.ts +0 -4
- package/dist/automations.d.ts.map +0 -1
- package/dist/automations.js +0 -191
- package/dist/automations.js.map +0 -1
- package/dist/catalog.d.ts +0 -48
- package/dist/catalog.d.ts.map +0 -1
- package/dist/catalog.js +0 -51
- package/dist/catalog.js.map +0 -1
- package/dist/changes.d.ts +0 -4
- package/dist/changes.d.ts.map +0 -1
- package/dist/changes.js +0 -601
- package/dist/changes.js.map +0 -1
- package/dist/cron.d.ts +0 -43
- package/dist/cron.d.ts.map +0 -1
- package/dist/cron.js +0 -211
- package/dist/cron.js.map +0 -1
- package/dist/git.d.ts +0 -16
- package/dist/git.d.ts.map +0 -1
- package/dist/git.js +0 -126
- package/dist/git.js.map +0 -1
- package/dist/host.d.ts +0 -24
- package/dist/host.d.ts.map +0 -1
- package/dist/host.js +0 -5850
- package/dist/host.js.map +0 -1
- package/dist/index.d.ts +0 -36
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -34
- package/dist/index.js.map +0 -1
- package/dist/listen.d.ts +0 -3
- package/dist/listen.d.ts.map +0 -1
- package/dist/listen.js +0 -183
- package/dist/listen.js.map +0 -1
- package/dist/paging.d.ts +0 -38
- package/dist/paging.d.ts.map +0 -1
- package/dist/paging.js +0 -53
- package/dist/paging.js.map +0 -1
- package/dist/paths.d.ts +0 -15
- package/dist/paths.d.ts.map +0 -1
- package/dist/paths.js +0 -19
- package/dist/paths.js.map +0 -1
- package/dist/resources.d.ts +0 -122
- package/dist/resources.d.ts.map +0 -1
- package/dist/resources.js +0 -494
- package/dist/resources.js.map +0 -1
- package/dist/rpc.d.ts +0 -63
- package/dist/rpc.d.ts.map +0 -1
- package/dist/rpc.js +0 -208
- package/dist/rpc.js.map +0 -1
- package/dist/scheduled.d.ts +0 -48
- package/dist/scheduled.d.ts.map +0 -1
- package/dist/scheduled.js +0 -325
- package/dist/scheduled.js.map +0 -1
- package/dist/terminals.d.ts +0 -29
- package/dist/terminals.d.ts.map +0 -1
- package/dist/terminals.js +0 -346
- package/dist/terminals.js.map +0 -1
- package/dist/tools.d.ts +0 -18
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js +0 -65
- package/dist/tools.js.map +0 -1
- package/dist/types/agent.d.ts +0 -245
- package/dist/types/agent.d.ts.map +0 -1
- package/dist/types/agent.js +0 -3
- package/dist/types/agent.js.map +0 -1
- package/dist/types/automations.d.ts +0 -164
- package/dist/types/automations.d.ts.map +0 -1
- package/dist/types/automations.js +0 -3
- package/dist/types/automations.js.map +0 -1
- package/dist/types/catalog.d.ts +0 -30
- package/dist/types/catalog.d.ts.map +0 -1
- package/dist/types/catalog.js +0 -3
- package/dist/types/catalog.js.map +0 -1
- package/dist/types/changes.d.ts +0 -272
- package/dist/types/changes.d.ts.map +0 -1
- package/dist/types/changes.js +0 -3
- package/dist/types/changes.js.map +0 -1
- package/dist/types/common.d.ts +0 -9
- package/dist/types/common.d.ts.map +0 -1
- package/dist/types/common.js +0 -3
- package/dist/types/common.js.map +0 -1
- package/dist/types/host.d.ts +0 -372
- package/dist/types/host.d.ts.map +0 -1
- package/dist/types/host.js +0 -3
- package/dist/types/host.js.map +0 -1
- package/dist/types/index.d.ts +0 -20
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -8
- package/dist/types/index.js.map +0 -1
- package/dist/types/listen.d.ts +0 -48
- package/dist/types/listen.d.ts.map +0 -1
- package/dist/types/listen.js +0 -3
- package/dist/types/listen.js.map +0 -1
- package/dist/types/paging.d.ts +0 -10
- package/dist/types/paging.d.ts.map +0 -1
- package/dist/types/paging.js +0 -3
- package/dist/types/paging.js.map +0 -1
- package/dist/types/probe.d.ts +0 -46
- package/dist/types/probe.d.ts.map +0 -1
- package/dist/types/probe.js +0 -3
- package/dist/types/probe.js.map +0 -1
- package/dist/types/resources.d.ts +0 -83
- package/dist/types/resources.d.ts.map +0 -1
- package/dist/types/resources.js +0 -3
- package/dist/types/resources.js.map +0 -1
- package/dist/types/rpc.d.ts +0 -65
- package/dist/types/rpc.d.ts.map +0 -1
- package/dist/types/rpc.js +0 -3
- package/dist/types/rpc.js.map +0 -1
- package/dist/types/session.d.ts +0 -395
- package/dist/types/session.d.ts.map +0 -1
- package/dist/types/session.js +0 -3
- package/dist/types/session.js.map +0 -1
- package/dist/types/terminals.d.ts +0 -131
- package/dist/types/terminals.d.ts.map +0 -1
- package/dist/types/terminals.js +0 -3
- package/dist/types/terminals.js.map +0 -1
- package/dist/types/wire.d.ts +0 -62
- package/dist/types/wire.d.ts.map +0 -1
- package/dist/types/wire.js +0 -13
- package/dist/types/wire.js.map +0 -1
- package/dist/types/worktrees.d.ts +0 -86
- package/dist/types/worktrees.d.ts.map +0 -1
- package/dist/types/worktrees.js +0 -3
- package/dist/types/worktrees.js.map +0 -1
- package/dist/worktrees.d.ts +0 -27
- package/dist/worktrees.d.ts.map +0 -1
- package/dist/worktrees.js +0 -152
- package/dist/worktrees.js.map +0 -1
- package/src/automations.ts +0 -204
- package/src/catalog.ts +0 -53
- package/src/changes.ts +0 -629
- package/src/cron.ts +0 -251
- package/src/git.ts +0 -132
- package/src/host.ts +0 -5947
- package/src/index.ts +0 -40
- package/src/listen.ts +0 -240
- package/src/paging.ts +0 -56
- package/src/paths.ts +0 -19
- package/src/resources.ts +0 -513
- package/src/rpc.ts +0 -222
- package/src/scheduled.ts +0 -369
- package/src/terminals.ts +0 -353
- package/src/tools.ts +0 -64
- package/src/types/agent.ts +0 -255
- package/src/types/automations.ts +0 -164
- package/src/types/catalog.ts +0 -26
- package/src/types/changes.ts +0 -266
- package/src/types/common.ts +0 -9
- package/src/types/host.ts +0 -386
- package/src/types/index.ts +0 -27
- package/src/types/listen.ts +0 -53
- package/src/types/paging.ts +0 -11
- package/src/types/probe.ts +0 -42
- package/src/types/resources.ts +0 -107
- package/src/types/rpc.ts +0 -68
- package/src/types/session.ts +0 -415
- package/src/types/terminals.ts +0 -133
- package/src/types/wire.ts +0 -69
- package/src/types/worktrees.ts +0 -91
- package/src/worktrees.ts +0 -162
package/src/terminals.ts
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import type { Pty, SpawnPty, Terminal, TerminalOptions } from './types/terminals.js';
|
|
3
|
-
import type { TerminalStore } from './types/host.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* A shell on the host machine, as a terminal channel.
|
|
7
|
-
*
|
|
8
|
-
* Pipes, not a pseudoterminal: a PTY needs a native binding this daemon does
|
|
9
|
-
* not depend on, and the protocol has `isPty: false` for exactly this - "output
|
|
10
|
-
* is plain text and clients do not need to parse VT sequences". So a command
|
|
11
|
-
* runs and its output arrives, and anything that draws itself with cursor
|
|
12
|
-
* movement will not look right. Said in the state rather than discovered.
|
|
13
|
-
*
|
|
14
|
-
* `node:child_process` is used because all three supported runtimes provide it.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** What runs, when nothing else was asked for. */
|
|
18
|
-
const shellOf = (asked?: string): string => asked ?? process.env.SHELL ?? '/bin/sh';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* What a shell says about itself, in the escape sequences it says it with.
|
|
22
|
-
*
|
|
23
|
-
* OSC 133 is the command-boundary convention every shell integration script
|
|
24
|
-
* writes - `A` before the prompt, `B` where the command starts, `C` where its
|
|
25
|
-
* output does, `D;<code>` when it finished - and OSC 7 is the directory. They
|
|
26
|
-
* arrive mixed into the output, so this reads them out and leaves the rest
|
|
27
|
-
* alone: the bytes still go to the client, which is drawing a terminal and
|
|
28
|
-
* needs them.
|
|
29
|
-
*
|
|
30
|
-
* Only under a pseudoterminal, because only then is there a shell running its
|
|
31
|
-
* own prompt to emit them.
|
|
32
|
-
*/
|
|
33
|
-
const MARKS = /\u001b\](133|7);([^\u0007\u001b]*)(?:\u0007|\u001b\\)/g;
|
|
34
|
-
|
|
35
|
-
export function createTerminal(options: TerminalOptions, pty?: SpawnPty): Terminal {
|
|
36
|
-
const { uri, cwd, emit } = options;
|
|
37
|
-
const shell = shellOf(options.shell);
|
|
38
|
-
let title = options.name ?? shell.slice(shell.lastIndexOf('/') + 1);
|
|
39
|
-
let claim = options.claim;
|
|
40
|
-
let cols = options.cols ?? 80;
|
|
41
|
-
let rows = options.rows ?? 24;
|
|
42
|
-
let exitCode: number | undefined;
|
|
43
|
-
/**
|
|
44
|
-
* Everything written so far, so a client that subscribes late sees it.
|
|
45
|
-
*
|
|
46
|
-
* Capped: a terminal left running `tail -f` for a day is a host holding a
|
|
47
|
-
* day of output for a client that may never come back.
|
|
48
|
-
*/
|
|
49
|
-
let buffered = '';
|
|
50
|
-
const KEEP = 200_000;
|
|
51
|
-
|
|
52
|
-
const said = (data: string): void => {
|
|
53
|
-
buffered = (buffered + data).slice(-KEEP);
|
|
54
|
-
emit('terminal', { type: 'terminal/data', data });
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/*
|
|
58
|
-
* `-c` when there is a command, and nothing when there is not.
|
|
59
|
-
*
|
|
60
|
-
* A shell given `-c` runs the one thing and exits. Under pipes that is the
|
|
61
|
-
* only completion signal there is; under a pseudoterminal the shell says so
|
|
62
|
-
* itself, in OSC 133.
|
|
63
|
-
*/
|
|
64
|
-
const args = options.command === undefined ? [] : ['-c', options.command];
|
|
65
|
-
const environment = {
|
|
66
|
-
...process.env,
|
|
67
|
-
// A real terminal under a pty, and an honest `dumb` without one.
|
|
68
|
-
TERM: pty ? (process.env.TERM ?? 'xterm-256color') : 'dumb',
|
|
69
|
-
COLUMNS: String(cols),
|
|
70
|
-
LINES: String(rows),
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/** Where the shell says it is, once it has said. */
|
|
74
|
-
let where = cwd;
|
|
75
|
-
/** The command being run, from `C` until `D`. */
|
|
76
|
-
let command: { id: string; line: string; at: number } | undefined;
|
|
77
|
-
/** What has been typed since the prompt, so the command line can be read back. */
|
|
78
|
-
let typed = '';
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Read the shell's own marks out of a chunk, and say what they meant.
|
|
82
|
-
*
|
|
83
|
-
* The chunk still reaches the client whole: this is a reader, not a filter,
|
|
84
|
-
* and a client drawing a terminal needs the bytes it was sent.
|
|
85
|
-
*/
|
|
86
|
-
const marked = (data: string): void => {
|
|
87
|
-
for (const found of data.matchAll(MARKS)) {
|
|
88
|
-
const [, kind, body = ''] = found;
|
|
89
|
-
if (kind === '7') {
|
|
90
|
-
// `file://host/path`, per the convention. The host part is dropped:
|
|
91
|
-
// the path is on this machine, and that is what a client opens.
|
|
92
|
-
const path = body.replace(/^file:\/\/[^/]*/, '');
|
|
93
|
-
if (path !== '' && path !== where) {
|
|
94
|
-
where = path;
|
|
95
|
-
emit('terminal', { type: 'terminal/cwdChanged', cwd: `file://${path}` });
|
|
96
|
-
}
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
const mark = body.split(';')[0];
|
|
100
|
-
if (mark === 'A') { typed = ''; continue; }
|
|
101
|
-
if (mark === 'C') {
|
|
102
|
-
command = { id: `c${String(Date.now())}`, line: typed.trim(), at: Date.now() };
|
|
103
|
-
emit('terminal', {
|
|
104
|
-
type: 'terminal/commandExecuted',
|
|
105
|
-
commandId: command.id,
|
|
106
|
-
commandLine: command.line,
|
|
107
|
-
timestamp: command.at,
|
|
108
|
-
});
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (mark !== 'D' || command === undefined) continue;
|
|
112
|
-
const code = Number(body.split(';')[1]);
|
|
113
|
-
emit('terminal', {
|
|
114
|
-
type: 'terminal/commandFinished',
|
|
115
|
-
commandId: command.id,
|
|
116
|
-
...(Number.isFinite(code) ? { exitCode: code } : {}),
|
|
117
|
-
durationMs: Date.now() - command.at,
|
|
118
|
-
});
|
|
119
|
-
command = undefined;
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const terminal: Pty | undefined = pty?.(shell, args, {
|
|
124
|
-
...(cwd !== undefined ? { cwd } : {}),
|
|
125
|
-
cols,
|
|
126
|
-
rows,
|
|
127
|
-
env: environment,
|
|
128
|
-
});
|
|
129
|
-
if (terminal !== undefined) {
|
|
130
|
-
terminal.onData((data) => { marked(data); said(data); });
|
|
131
|
-
terminal.onExit(({ exitCode: code }) => { exitCode = code; ended(); });
|
|
132
|
-
// Said once, at the start. A client MUST check this before relying on
|
|
133
|
-
// command boundaries, and the same fact is on the state.
|
|
134
|
-
emit('terminal', { type: 'terminal/commandDetectionAvailable' });
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const child = terminal !== undefined ? undefined : spawn(shell, args, {
|
|
138
|
-
cwd,
|
|
139
|
-
/*
|
|
140
|
-
* Its own process group, so a signal reaches what it started.
|
|
141
|
-
*
|
|
142
|
-
* A shell reading from a pipe runs each command as its own child, and a
|
|
143
|
-
* signal sent to the shell alone leaves the command running. The group is
|
|
144
|
-
* what a terminal driver would have signalled, and this has no driver.
|
|
145
|
-
*/
|
|
146
|
-
detached: true,
|
|
147
|
-
// A shell reading commands from a pipe. Without a pseudoterminal there is
|
|
148
|
-
// no point asking it to be interactive: it would print a prompt nobody
|
|
149
|
-
// can answer the way it expects.
|
|
150
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
151
|
-
env: environment,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
child?.stdout.on('data', (chunk: Buffer) => said(chunk.toString('utf8')));
|
|
155
|
-
child?.stderr.on('data', (chunk: Buffer) => said(chunk.toString('utf8')));
|
|
156
|
-
/**
|
|
157
|
-
* Said once, whichever of the three got here first.
|
|
158
|
-
*
|
|
159
|
-
* `error` and `close` can both fire for one failed spawn, and a terminal
|
|
160
|
-
* that announced its own exit twice would be one every client draws as
|
|
161
|
-
* having died, come back, and died again.
|
|
162
|
-
*/
|
|
163
|
-
let announced = false;
|
|
164
|
-
const ended = (): void => {
|
|
165
|
-
if (announced) return;
|
|
166
|
-
announced = true;
|
|
167
|
-
emit('terminal', { type: 'terminal/exited', exitCode });
|
|
168
|
-
};
|
|
169
|
-
child?.on('error', (error: Error) => {
|
|
170
|
-
said(`${error.message}\n`);
|
|
171
|
-
exitCode = 127;
|
|
172
|
-
ended();
|
|
173
|
-
});
|
|
174
|
-
child?.on('exit', (code: number | null, signal: string | null) => {
|
|
175
|
-
// A signal is not an exit code, and 128+n is the shell's own convention
|
|
176
|
-
// for one - better than reporting nothing, which reads as still running.
|
|
177
|
-
exitCode = code ?? (signal ? 128 : 0);
|
|
178
|
-
});
|
|
179
|
-
/*
|
|
180
|
-
* Announced on `close` rather than on `exit`, which is a race this lost.
|
|
181
|
-
*
|
|
182
|
-
* `exit` fires when the process goes; `close` fires once its pipes are
|
|
183
|
-
* drained. Between the two there is output already written and not yet
|
|
184
|
-
* read, so a host that reported the exit on `exit` reported a command's
|
|
185
|
-
* result before the result had arrived - which is exactly what a `!`
|
|
186
|
-
* command in the composer reads back.
|
|
187
|
-
*/
|
|
188
|
-
child?.on('close', () => {
|
|
189
|
-
// A process that closed without an exit event was killed outright.
|
|
190
|
-
exitCode ??= 0;
|
|
191
|
-
ended();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
uri,
|
|
196
|
-
title: () => title,
|
|
197
|
-
claim: () => claim,
|
|
198
|
-
exitCode: () => exitCode,
|
|
199
|
-
lifecycle: () => (exitCode === undefined
|
|
200
|
-
? { status: 'running' }
|
|
201
|
-
: { status: 'exited', exitCode }),
|
|
202
|
-
|
|
203
|
-
state: () => ({
|
|
204
|
-
title,
|
|
205
|
-
cwd: `file://${cwd}`,
|
|
206
|
-
cols,
|
|
207
|
-
rows,
|
|
208
|
-
// One part, because without command detection there are no boundaries
|
|
209
|
-
// to divide the output at. The protocol's shape, not a flat string.
|
|
210
|
-
content: buffered === '' ? [] : [{ type: 'unclassified', value: buffered }],
|
|
211
|
-
claim,
|
|
212
|
-
// Both true only under a pseudoterminal: without one there is no shell
|
|
213
|
-
// running its own prompt, so there are no boundaries to report and no
|
|
214
|
-
// VT sequences for a client to parse.
|
|
215
|
-
supportsCommandDetection: terminal !== undefined,
|
|
216
|
-
isPty: terminal !== undefined,
|
|
217
|
-
/*
|
|
218
|
-
* Both spellings, because this host speaks five versions.
|
|
219
|
-
*
|
|
220
|
-
* `lifecycle` is what 0.9.0 requires and is not optional there - a
|
|
221
|
-
* client reading `lifecycle.status` on a 0.8.0-shaped terminal gets
|
|
222
|
-
* `undefined`, which reads as a process that never exits. The flat
|
|
223
|
-
* `exitCode` is what every version before it reads, and this daemon
|
|
224
|
-
* negotiates down to 0.5.1.
|
|
225
|
-
*/
|
|
226
|
-
lifecycle: exitCode === undefined
|
|
227
|
-
? { status: 'running' }
|
|
228
|
-
: { status: 'exited', exitCode },
|
|
229
|
-
...(exitCode !== undefined ? { exitCode } : {}),
|
|
230
|
-
}),
|
|
231
|
-
|
|
232
|
-
write: (data) => {
|
|
233
|
-
if (exitCode !== undefined)
|
|
234
|
-
return;
|
|
235
|
-
/*
|
|
236
|
-
* `^C` is a signal, and there is nothing here to turn it into one.
|
|
237
|
-
*
|
|
238
|
-
* A pseudoterminal has a line discipline that sees the byte and sends
|
|
239
|
-
* SIGINT to the foreground group. Pipes have none, so the byte arrives
|
|
240
|
-
* as input and the command runs on - which is a terminal a runaway
|
|
241
|
-
* command cannot be stopped in. Sending the signal is what the driver
|
|
242
|
-
* would have done.
|
|
243
|
-
*/
|
|
244
|
-
/*
|
|
245
|
-
* Under a pseudoterminal the byte is the signal.
|
|
246
|
-
*
|
|
247
|
-
* A pty has a line discipline: `^C` reaches it as input and it sends
|
|
248
|
-
* SIGINT to the foreground group itself, which is the whole point of
|
|
249
|
-
* having one. So this writes it through and does nothing clever.
|
|
250
|
-
*/
|
|
251
|
-
if (terminal !== undefined) {
|
|
252
|
-
terminal.write(data);
|
|
253
|
-
// Kept so a command line can be read back at the next `C` mark; the
|
|
254
|
-
// shell echoes what was typed, but the echo arrives as output and
|
|
255
|
-
// this is the only place the input itself is seen.
|
|
256
|
-
typed += data;
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
const at = data.indexOf('\u0003');
|
|
260
|
-
if (at !== -1) {
|
|
261
|
-
const rest = data.slice(0, at) + data.slice(at + 1);
|
|
262
|
-
if (rest !== '' && child?.stdin.writable) child.stdin.write(rest);
|
|
263
|
-
/*
|
|
264
|
-
* The group, named by the child's own pid, and only when there is one.
|
|
265
|
-
*
|
|
266
|
-
* A spawn that failed leaves no pid, and `0` is not a safe stand-in:
|
|
267
|
-
* to `kill` it means every process in *this* process group, so a
|
|
268
|
-
* terminal whose shell never started would signal the host and
|
|
269
|
-
* whatever started the host.
|
|
270
|
-
*/
|
|
271
|
-
const group = child?.pid;
|
|
272
|
-
if (group !== undefined) {
|
|
273
|
-
try { process.kill(-group, 'SIGINT'); }
|
|
274
|
-
// The group is gone, which is the outcome asked for.
|
|
275
|
-
catch { /* nothing left to interrupt */ }
|
|
276
|
-
}
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (child?.stdin.writable) child.stdin.write(data);
|
|
280
|
-
},
|
|
281
|
-
|
|
282
|
-
// Told, when there is something to tell: a pseudoterminal gets the new
|
|
283
|
-
// size and sends SIGWINCH itself. Without one these are kept because the
|
|
284
|
-
// state reports them and a client draws to them.
|
|
285
|
-
resize: (nextCols, nextRows) => {
|
|
286
|
-
terminal?.resize(nextCols, nextRows);
|
|
287
|
-
cols = nextCols;
|
|
288
|
-
rows = nextRows;
|
|
289
|
-
emit('terminal', { type: 'terminal/resized', cols, rows });
|
|
290
|
-
},
|
|
291
|
-
|
|
292
|
-
// The scrollback and nothing else. `cols`, `rows`, `title` and `claim` are
|
|
293
|
-
// untouched, which is what the reference suite checks for.
|
|
294
|
-
clear: () => {
|
|
295
|
-
buffered = '';
|
|
296
|
-
emit('terminal', { type: 'terminal/cleared' });
|
|
297
|
-
},
|
|
298
|
-
|
|
299
|
-
setTitle: (next) => {
|
|
300
|
-
if (next === title) return;
|
|
301
|
-
title = next;
|
|
302
|
-
emit('terminal', { type: 'terminal/titleChanged', title });
|
|
303
|
-
},
|
|
304
|
-
|
|
305
|
-
setClaim: (next) => {
|
|
306
|
-
claim = next;
|
|
307
|
-
emit('terminal', { type: 'terminal/claimed', claim });
|
|
308
|
-
},
|
|
309
|
-
|
|
310
|
-
close: () => {
|
|
311
|
-
if (terminal !== undefined) {
|
|
312
|
-
terminal.kill();
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
child?.stdin.end();
|
|
316
|
-
// The group, not the shell: detached, its children outlive it otherwise.
|
|
317
|
-
// Only when there is a pid to name it - see the interrupt above.
|
|
318
|
-
const group = child?.pid;
|
|
319
|
-
if (group === undefined) {
|
|
320
|
-
child?.kill();
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
try { process.kill(-group, 'SIGKILL'); }
|
|
324
|
-
catch { child?.kill(); }
|
|
325
|
-
},
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* A shell on this machine, as a host's `TerminalStore`.
|
|
331
|
-
*
|
|
332
|
-
* Kept out of `createHost` for the reason above: spawning is the runtime's
|
|
333
|
-
* business, and a host that opens no terminal should not have to have one.
|
|
334
|
-
*
|
|
335
|
-
* ```ts
|
|
336
|
-
* createHost({ path, agents, terminals: shellTerminals() });
|
|
337
|
-
* ```
|
|
338
|
-
*
|
|
339
|
-
* Given a `pty` it runs shells under a pseudoterminal instead, which is what
|
|
340
|
-
* makes shell integration possible: the shell prints its own OSC 133 marks, so
|
|
341
|
-
* command boundaries and the working directory become facts rather than
|
|
342
|
-
* guesses. The binding is handed in because it is native code - `node-pty` is
|
|
343
|
-
* the daemon's dependency and never this library's, and a host on another
|
|
344
|
-
* runtime passes whatever it has.
|
|
345
|
-
*
|
|
346
|
-
* ```ts
|
|
347
|
-
* import { spawn } from 'node-pty';
|
|
348
|
-
* createHost({ path, agents, terminals: shellTerminals({ pty: spawn }) });
|
|
349
|
-
* ```
|
|
350
|
-
*/
|
|
351
|
-
export const shellTerminals = (options: { pty?: SpawnPty } = {}): TerminalStore => ({
|
|
352
|
-
create: (asked) => createTerminal(asked, options.pty),
|
|
353
|
-
});
|
package/src/tools.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import type { HostTool } from './types/host.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The tools this package contributes to every session, as a host's `tools`.
|
|
5
|
-
*
|
|
6
|
-
* The protocol's `serverTools` are the *host's* own - not a backend's and not
|
|
7
|
-
* a client's - and what makes one worth contributing is that the host knows
|
|
8
|
-
* something the agent inside a session cannot: the other sessions running
|
|
9
|
-
* beside it, and the terminals the person is watching. Both are read-only.
|
|
10
|
-
*
|
|
11
|
-
* ```ts
|
|
12
|
-
* createHost({ path, agents, tools: hostTools() });
|
|
13
|
-
* ```
|
|
14
|
-
*
|
|
15
|
-
* A host that wants its own passes its own; a host that passes none
|
|
16
|
-
* contributes none, and its sessions report no `serverTools` at all.
|
|
17
|
-
*/
|
|
18
|
-
export const hostTools = (): HostTool[] => [
|
|
19
|
-
{
|
|
20
|
-
definition: {
|
|
21
|
-
name: 'ahp_sessions',
|
|
22
|
-
title: 'Sessions on this host',
|
|
23
|
-
description: 'The other agent sessions running on this host, with the directories each works in. '
|
|
24
|
-
+ 'Use it before touching a file to find out whether another agent is already working there.',
|
|
25
|
-
inputSchema: { type: 'object', properties: {} },
|
|
26
|
-
annotations: { title: 'Sessions on this host', readOnlyHint: true, openWorldHint: false },
|
|
27
|
-
},
|
|
28
|
-
run: (_input, at) => {
|
|
29
|
-
const others = at.sessions().filter((one) => one.uri !== at.session);
|
|
30
|
-
if (others.length === 0) return 'No other session is running on this host.';
|
|
31
|
-
return others
|
|
32
|
-
.map((one) => `${one.uri}\t${one.provider}\t${one.title}\t${one.workingDirectories.join(' ')}`)
|
|
33
|
-
.join('\n');
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
definition: {
|
|
38
|
-
name: 'ahp_resource',
|
|
39
|
-
title: 'Read a resource this host serves',
|
|
40
|
-
description: 'Read a resource by URI, including one a connected client publishes and this machine has no copy of '
|
|
41
|
-
+ '- a plugin\u2019s virtual files, an editor\u2019s unsaved buffers. Addressed as <scheme>://<client>/<path>.',
|
|
42
|
-
inputSchema: { type: 'object', properties: { uri: { type: 'string' } }, required: ['uri'] },
|
|
43
|
-
annotations: { title: 'Read a resource this host serves', readOnlyHint: true },
|
|
44
|
-
},
|
|
45
|
-
run: async (input, at) => await at.read(String(input.uri ?? '')),
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
definition: {
|
|
49
|
-
name: 'ahp_terminals',
|
|
50
|
-
title: 'Terminals on this host',
|
|
51
|
-
description: 'The terminals this host has open, with what each is running and where. '
|
|
52
|
-
+ 'These are the terminals the person can see, which is not the same as a shell run from a tool.',
|
|
53
|
-
inputSchema: { type: 'object', properties: {} },
|
|
54
|
-
annotations: { title: 'Terminals on this host', readOnlyHint: true, openWorldHint: false },
|
|
55
|
-
},
|
|
56
|
-
run: (_input, at) => {
|
|
57
|
-
const open = at.terminals();
|
|
58
|
-
if (open.length === 0) return 'This host has no terminal open.';
|
|
59
|
-
return open
|
|
60
|
-
.map((one) => `${one.uri}\t${one.title}\t${one.cwd}\t${one.running ? 'running' : 'exited'}`)
|
|
61
|
-
.join('\n');
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
];
|
package/src/types/agent.ts
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
/** An agent backend, and everything the host asks one for. */
|
|
2
|
-
|
|
3
|
-
import type { Turn } from '@microsoft/agent-host-protocol';
|
|
4
|
-
import type { Bag } from './common.js';
|
|
5
|
-
import type { WireTurn } from './wire.js';
|
|
6
|
-
import type { Emit, Session } from './session.js';
|
|
7
|
-
import type { ToolDefinition } from '@microsoft/agent-host-protocol';
|
|
8
|
-
import type { Offered } from './probe.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* A tool the host contributes, with the session it was contributed to
|
|
12
|
-
* already bound.
|
|
13
|
-
*
|
|
14
|
-
* The host's `HostTool` takes a `ToolCall` saying where it was called from;
|
|
15
|
-
* by the time a backend sees one that is answered, so what is left is a
|
|
16
|
-
* definition to offer the model and a function to call.
|
|
17
|
-
*/
|
|
18
|
-
export interface BoundTool {
|
|
19
|
-
/** What the model is offered. `name` is the id it calls. */
|
|
20
|
-
definition: ToolDefinition;
|
|
21
|
-
/**
|
|
22
|
-
* What running it does. Absent for a tool a client runs - see `owner`.
|
|
23
|
-
*/
|
|
24
|
-
run?(input: Record<string, unknown>): Promise<string> | string;
|
|
25
|
-
/**
|
|
26
|
-
* The client that runs this one, when it is a client's rather than the host's.
|
|
27
|
-
*
|
|
28
|
-
* A client announces what it provides on `SessionActiveClient.tools`, and
|
|
29
|
-
* the protocol makes that client responsible for executing the call and
|
|
30
|
-
* dispatching its result. So there is nothing to run here: the backend
|
|
31
|
-
* offers the tool to the model, reports the call against that client, and
|
|
32
|
-
* waits for it to say what happened.
|
|
33
|
-
*/
|
|
34
|
-
owner?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* One session a backend already has, before the host has named it.
|
|
39
|
-
*
|
|
40
|
-
* Deliberately not a `Summary`: the resource URI, the provider and the status
|
|
41
|
-
* bits are the host's to decide - `IsRead` and `IsArchived` in particular are
|
|
42
|
-
* kept per client and a backend has never heard of them.
|
|
43
|
-
*/
|
|
44
|
-
export interface Listed {
|
|
45
|
-
/** The backend's own id for it. The host serves it as `<provider>:/<id>`. */
|
|
46
|
-
id: string;
|
|
47
|
-
/** Display title. */
|
|
48
|
-
title: string;
|
|
49
|
-
/** ISO 8601 timestamp of creation. */
|
|
50
|
-
createdAt: string;
|
|
51
|
-
/** ISO 8601 timestamp of the last change. */
|
|
52
|
-
modifiedAt: string;
|
|
53
|
-
/** Directories the agent has tool access to, as `file://` URIs. */
|
|
54
|
-
workingDirectories: string[];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** How the host asks a backend to start a session. */
|
|
58
|
-
export interface Start {
|
|
59
|
-
/** The session channel URI the client chose. */
|
|
60
|
-
uri: string;
|
|
61
|
-
/** The chat channel URI beneath it. */
|
|
62
|
-
chatUri: string;
|
|
63
|
-
/** Config values in force, by key: this agent's defaults with the client's on top. */
|
|
64
|
-
settings: Record<string, unknown>;
|
|
65
|
-
/**
|
|
66
|
-
* The directory the client asked the agent to work in, if it named one.
|
|
67
|
-
*
|
|
68
|
-
* A path, not a `file://` URI. Absent means the client named none and the
|
|
69
|
-
* backend picks. A backend that will not work there should throw saying so:
|
|
70
|
-
* a directory accepted and then ignored is a session running somewhere
|
|
71
|
-
* nobody asked for, and nothing on screen says which.
|
|
72
|
-
*/
|
|
73
|
-
workingDirectory?: string;
|
|
74
|
-
/** Directories beside it the agent may also work in. */
|
|
75
|
-
additional?: string[];
|
|
76
|
-
/**
|
|
77
|
-
* Tools the host contributes to this session, for the backend to offer.
|
|
78
|
-
*
|
|
79
|
-
* The host's own, not this backend's: a backend that cannot take tools from
|
|
80
|
-
* anywhere ignores them, and the host still reports them on the session so
|
|
81
|
-
* a client knows they exist.
|
|
82
|
-
*/
|
|
83
|
-
tools?: BoundTool[];
|
|
84
|
-
/** The config schema to report on the session channel. This agent's own. */
|
|
85
|
-
schema(): Bag;
|
|
86
|
-
/** What to report as customizations until the backend reports its own. */
|
|
87
|
-
seedCustomizations?: Bag[];
|
|
88
|
-
/** Where state actions go. The host routes them to the right channel. */
|
|
89
|
-
emit: Emit;
|
|
90
|
-
/** A session of this backend's to continue, rather than starting a new one. */
|
|
91
|
-
resume?: string;
|
|
92
|
-
/** The prompt to resume *at*, so a fork leaves the turns after it behind. */
|
|
93
|
-
forkAt?: string;
|
|
94
|
-
/**
|
|
95
|
-
* The chain entry to resume *at*, keeping the id the session already had.
|
|
96
|
-
*
|
|
97
|
-
* A truncation rather than a fork: the turns after that point are dropped
|
|
98
|
-
* and the conversation carries on as itself, which is what `chat/truncated`
|
|
99
|
-
* asks for.
|
|
100
|
-
*/
|
|
101
|
-
rewindAt?: string;
|
|
102
|
-
/** Context the first turn carries to the backend without showing it. */
|
|
103
|
-
context?: string;
|
|
104
|
-
/** Turns already known, so a resumed session does not open empty. */
|
|
105
|
-
seed?: Bag[];
|
|
106
|
-
/**
|
|
107
|
-
* A file a tool is about to change, and the same file once it has.
|
|
108
|
-
*
|
|
109
|
-
* Optional both ways: a backend that cannot see its own tools does not call
|
|
110
|
-
* it, and a host with no changeset source does not pass one.
|
|
111
|
-
*/
|
|
112
|
-
onFileEdit?(turnId: string, path: string, phase: 'before' | 'after'): void;
|
|
113
|
-
/** Called once the backend has reported what it can do. */
|
|
114
|
-
onHandshake?(): void;
|
|
115
|
-
/**
|
|
116
|
-
* Tokens for this backend's protected resources, by resource identifier.
|
|
117
|
-
*
|
|
118
|
-
* Only resources this agent advertised, and only what the connection
|
|
119
|
-
* asking for the session pushed - authentication is per connection, so a
|
|
120
|
-
* token one client offered is never spent on another's session. Absent for
|
|
121
|
-
* a session nobody asked for: an automation firing at nine in the morning
|
|
122
|
-
* has no connection behind it and runs on the daemon's own credentials.
|
|
123
|
-
*
|
|
124
|
-
* What to *do* with one is the backend's business. The host knows a token
|
|
125
|
-
* belongs to a resource and nothing else about it.
|
|
126
|
-
*/
|
|
127
|
-
credentials?: Record<string, string>;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* A backend the host can run sessions on.
|
|
132
|
-
*
|
|
133
|
-
* One host serves several. `provider` is what a client names in
|
|
134
|
-
* `createSession` and what every session of this kind reports, so it has to
|
|
135
|
-
* be unique across the agents a host was given.
|
|
136
|
-
*
|
|
137
|
-
* Everything but `provider`, `displayName`, `schema`, `defaults` and `create`
|
|
138
|
-
* is optional, and what is left out is a real answer rather than a gap: a
|
|
139
|
-
* backend with no `list` has no sessions to browse, one with no `probe`
|
|
140
|
-
* offers no models and no commands until a session of its own reports them.
|
|
141
|
-
*/
|
|
142
|
-
export interface Agent {
|
|
143
|
-
/** The id clients name. Unique among a host's agents. */
|
|
144
|
-
provider: string;
|
|
145
|
-
/** What a person reads instead of the id. */
|
|
146
|
-
displayName: string;
|
|
147
|
-
/** One line about what this backend is. */
|
|
148
|
-
description?: string;
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* What a second chat in one session can be made from.
|
|
152
|
-
*
|
|
153
|
-
* Multi-chat itself is the host's doing - a second chat is `create` called
|
|
154
|
-
* twice - but these two are the backend's: a fork continues a conversation
|
|
155
|
-
* from one of its turns, and a side chat starts a fresh one that knows what
|
|
156
|
-
* a turn elsewhere said. A backend that declares neither still gets several
|
|
157
|
-
* chats; it just cannot be asked to make one out of another.
|
|
158
|
-
*/
|
|
159
|
-
chats?: { fork?: boolean; sideChat?: boolean };
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Whether a session of this backend can work in more than one directory.
|
|
163
|
-
*
|
|
164
|
-
* The first is the process root and never moves; the rest are its peers.
|
|
165
|
-
* A backend that leaves this out gets one directory per session, which is
|
|
166
|
-
* what a client is told when the capability is absent.
|
|
167
|
-
*/
|
|
168
|
-
multipleDirectories?: boolean;
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* OAuth protected resources this backend can be given a token for.
|
|
172
|
-
*
|
|
173
|
-
* RFC 9728 metadata, served on `AgentInfo.protectedResources`, and the only
|
|
174
|
-
* thing that makes `authenticate` callable: the protocol says a client's
|
|
175
|
-
* `resource` MUST match one the server has itself advertised, so a host that
|
|
176
|
-
* advertises none can be sent no token at all.
|
|
177
|
-
*
|
|
178
|
-
* `required: false` on an entry is a backend saying it works without one -
|
|
179
|
-
* this daemon runs as whoever started it and inherits their credentials, so
|
|
180
|
-
* a token is an override rather than a precondition.
|
|
181
|
-
*/
|
|
182
|
-
protectedResources?: Bag[];
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* What a session of this kind can be told to do differently.
|
|
186
|
-
*
|
|
187
|
-
* A JSON-Schema-shaped `{ properties }`. Each property may carry `title`,
|
|
188
|
-
* `description`, `enum`, `enumLabels`, `enumDescriptions`, `default`,
|
|
189
|
-
* `sessionMutable` and `scope`.
|
|
190
|
-
*
|
|
191
|
-
* The last two are what the *host* reads, and they are the only two things
|
|
192
|
-
* it needs to know about a key it otherwise knows nothing about.
|
|
193
|
-
* `sessionMutable: false` is refused on a running session rather than
|
|
194
|
-
* accepted and dropped. `scope` is `'session'` - the default - for a key the
|
|
195
|
-
* chats of one session share, and `'chat'` for one each chat answers for
|
|
196
|
-
* itself: a permission mode is the session's, and a model is the chat's.
|
|
197
|
-
* `scope` is this library's, not the protocol's, whose config schema is
|
|
198
|
-
* deliberately generic and says nothing about either.
|
|
199
|
-
*
|
|
200
|
-
* Everything else is between the backend and whatever client draws it.
|
|
201
|
-
*
|
|
202
|
-
* One schema, used before a session exists and by every session that does.
|
|
203
|
-
* Two copies drift, and a composer then offers one set of controls on the
|
|
204
|
-
* new-session screen and a different set the moment a session opens.
|
|
205
|
-
*/
|
|
206
|
-
schema(): Bag;
|
|
207
|
-
/** What each schema key sits at when nothing has been chosen. */
|
|
208
|
-
defaults(): Record<string, unknown>;
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* What the backend offers, asked once at startup.
|
|
212
|
-
*
|
|
213
|
-
* Before any session exists, because that is when a client asks: the models
|
|
214
|
-
* to pick from and the commands behind a slash are what a composer needs to
|
|
215
|
-
* draw itself, and waiting for the first session means offering them only
|
|
216
|
-
* once the conversation has started.
|
|
217
|
-
*/
|
|
218
|
-
probe?(): Promise<Offered>;
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* The directories this backend will work in.
|
|
222
|
-
*
|
|
223
|
-
* The host's answer to "may this client read that file": a resource outside
|
|
224
|
-
* every backend's directories is refused, because a host that served the
|
|
225
|
-
* whole filesystem is one that anybody who can reach the port can read
|
|
226
|
-
* `~/.ssh` through.
|
|
227
|
-
*
|
|
228
|
-
* Left out means this backend claims no directories, and contributes
|
|
229
|
-
* nothing to what may be browsed.
|
|
230
|
-
*/
|
|
231
|
-
directories?(): string[];
|
|
232
|
-
|
|
233
|
-
/** Sessions this backend already has. Ordering is the host's business. */
|
|
234
|
-
list?(): Promise<Listed[]>;
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* One past session's turns, read without starting anything.
|
|
238
|
-
*
|
|
239
|
-
* What makes a catalogue row openable: the host serves it from here, and
|
|
240
|
-
* starts nothing until somebody sends a turn to it. Undefined means this
|
|
241
|
-
* backend has no such session.
|
|
242
|
-
*
|
|
243
|
-
* A `WireTurn` rather than a `Bag[]`: everything around the parts is checked
|
|
244
|
-
* here, and each part is checked where it is built - a part is assembled by
|
|
245
|
-
* mutation as an agent talks, so the literal is what can be held to a shape
|
|
246
|
-
* and the variable after it cannot. This was a `Bag[]`, and inside it a
|
|
247
|
-
* rebuilt transcript wrote a tool-call `status` that is not one of the
|
|
248
|
-
* seven, left off three fields the completed state requires, and gave its
|
|
249
|
-
* content blocks no `type`.
|
|
250
|
-
*/
|
|
251
|
-
transcript?(id: string): Promise<WireTurn<Turn>[] | undefined>;
|
|
252
|
-
|
|
253
|
-
/** Start one. */
|
|
254
|
-
create(start: Start): Session;
|
|
255
|
-
}
|