@ahpd/server 0.3.0 → 0.5.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 +9 -1
- package/dist/config.d.ts +16 -0
- package/dist/config.js +9 -0
- package/dist/main.js +91 -6
- package/dist/version.d.ts +2 -0
- package/dist/version.js +34 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @ahpd/server
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@ahpd/server)
|
|
4
|
+
[](https://github.com/softov/ahpd/actions/workflows/ci.yml)
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
3
9
|
[`@ahpd/server`](https://www.npmjs.com/package/@ahpd/server) is a ready-to-run [Agent Host Protocol](https://microsoft.github.io/agent-host-protocol/) server. It installs the `ahpd` command.
|
|
4
10
|
|
|
5
11
|
It runs agent sessions and serves them over a WebSocket, so several clients can watch and drive the same session at once.
|
|
@@ -53,9 +59,11 @@ ahpd config print the config file path and its contents
|
|
|
53
59
|
| `--without-connection-token` | Accept any connection |
|
|
54
60
|
| `--config-file <p>` | Use this config file instead of the default |
|
|
55
61
|
| `--automations <where>` | `file`, the default, keeps them beside the config and fires their schedules. `memory` keeps them until the process ends and fires nothing |
|
|
62
|
+
| `--sessions <where>` | Where the read and archived bits and a session's settings go. `file`, the default, keeps them beside the config. `memory` forgets them when the process ends |
|
|
63
|
+
| `--version`, `-v` | What version this is |
|
|
56
64
|
| `--help`, `-h` | |
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
`--version` and `--help` are the two that are not configuration; every other flag also has a key in `config.json` under `$XDG_CONFIG_HOME/ahpd`, spelled the same way without the dashes. A flag beats the file. Run `ahpd config` to see the path and the current values.
|
|
59
67
|
|
|
60
68
|
## Directories
|
|
61
69
|
|
package/dist/config.d.ts
CHANGED
|
@@ -20,6 +20,13 @@ export interface Config {
|
|
|
20
20
|
* definitions for the life of the process and fires nothing.
|
|
21
21
|
*/
|
|
22
22
|
automations?: 'file' | 'memory';
|
|
23
|
+
/**
|
|
24
|
+
* Where the read and archived bits and a session's settings are kept: `file`
|
|
25
|
+
* beside this configuration, or `memory` until the process ends.
|
|
26
|
+
*/
|
|
27
|
+
sessions?: 'file' | 'memory';
|
|
28
|
+
/** A file every frame is appended to, both directions, as JSON lines. */
|
|
29
|
+
wire?: string;
|
|
23
30
|
}
|
|
24
31
|
/**
|
|
25
32
|
* Where this tool's files live.
|
|
@@ -59,6 +66,15 @@ export declare const daemonLog: () => string;
|
|
|
59
66
|
* and the ordering somebody put there.
|
|
60
67
|
*/
|
|
61
68
|
export declare const automationsPath: () => string;
|
|
69
|
+
/**
|
|
70
|
+
* Where what this host adds on top of a backend is kept.
|
|
71
|
+
*
|
|
72
|
+
* The `IsRead` and `IsArchived` bits every client shares, and the settings a
|
|
73
|
+
* session is running under. Beside the automations for the same reason: this
|
|
74
|
+
* one is written whenever somebody archives a row, and `config.json` is a file
|
|
75
|
+
* a person edits.
|
|
76
|
+
*/
|
|
77
|
+
export declare const sessionsPath: () => string;
|
|
62
78
|
/** Make sure the directory is there, so a write into it can succeed. */
|
|
63
79
|
export declare const ensureConfigDir: () => void;
|
|
64
80
|
/**
|
package/dist/config.js
CHANGED
|
@@ -40,6 +40,15 @@ export const daemonLog = () => join(configDir(), 'daemon.log');
|
|
|
40
40
|
* and the ordering somebody put there.
|
|
41
41
|
*/
|
|
42
42
|
export const automationsPath = () => join(configDir(), 'automations.json');
|
|
43
|
+
/**
|
|
44
|
+
* Where what this host adds on top of a backend is kept.
|
|
45
|
+
*
|
|
46
|
+
* The `IsRead` and `IsArchived` bits every client shares, and the settings a
|
|
47
|
+
* session is running under. Beside the automations for the same reason: this
|
|
48
|
+
* one is written whenever somebody archives a row, and `config.json` is a file
|
|
49
|
+
* a person edits.
|
|
50
|
+
*/
|
|
51
|
+
export const sessionsPath = () => join(configDir(), 'sessions.json');
|
|
43
52
|
/** Make sure the directory is there, so a write into it can succeed. */
|
|
44
53
|
export const ensureConfigDir = () => { mkdirSync(configDir(), { recursive: true }); };
|
|
45
54
|
/**
|
package/dist/main.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { automationsPath, configPath, loadConfig } from './config.js';
|
|
2
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { automationsPath, configPath, daemonLog, loadConfig, sessionsPath } from './config.js';
|
|
4
|
+
import { version } from './version.js';
|
|
4
5
|
import { running, start, stop as stopDaemon } from './daemon.js';
|
|
5
6
|
import { pty } from './pty.js';
|
|
6
7
|
import { claude } from '@ahpd/agent-claude';
|
|
7
|
-
import { createHost, fileResources, gitBranches, gitChanges, gitWorktrees, hostTools, listen, memoryAutomations, scheduledAutomations, shellTerminals } from '@ahpd/sdk';
|
|
8
|
+
import { createHost, fileResources, gitBranches, gitChanges, gitWorktrees, githubPullRequests, hostTools, listen, fileSessions, memoryAutomations, memorySessions, scheduledAutomations, shellTerminals } from '@ahpd/sdk';
|
|
8
9
|
const USAGE = `ahpd - an Agent Host Protocol server, with a Claude backend
|
|
9
10
|
|
|
10
11
|
ahpd [options] run it here, in this terminal
|
|
@@ -30,11 +31,20 @@ const USAGE = `ahpd - an Agent Host Protocol server, with a Claude backend
|
|
|
30
31
|
configuration and fires their schedules;
|
|
31
32
|
memory keeps them until this process ends and
|
|
32
33
|
fires nothing.
|
|
34
|
+
--sessions <where> Where the read and archived bits and a
|
|
35
|
+
session's settings go. file, the default,
|
|
36
|
+
keeps them beside the configuration; memory
|
|
37
|
+
forgets them when this process ends.
|
|
38
|
+
--wire <file> Append every frame, both directions, to this
|
|
39
|
+
file as JSON lines: { at, from, peer, frame }.
|
|
40
|
+
pnpm wire -- <file> checks it against the
|
|
41
|
+
protocol schema.
|
|
42
|
+
--version, -v What version this is
|
|
33
43
|
--help, -h This
|
|
34
44
|
|
|
35
45
|
Every option above can be a key in the configuration file instead, spelled the
|
|
36
46
|
way it is here without the dashes: port, host, paths, connectionToken,
|
|
37
|
-
connectionTokenFile, withoutConnectionToken, automations. A flag beats the file, because a
|
|
47
|
+
connectionTokenFile, withoutConnectionToken, automations, sessions, wire. A flag beats the file, because a
|
|
38
48
|
flag is this run and a file is every run until somebody edits it.
|
|
39
49
|
|
|
40
50
|
Clients present the token as ?tkn=<secret> on the URL, or as an
|
|
@@ -49,8 +59,10 @@ function parse(argv) {
|
|
|
49
59
|
host: '127.0.0.1',
|
|
50
60
|
paths: [],
|
|
51
61
|
automations: 'file',
|
|
62
|
+
sessions: 'file',
|
|
52
63
|
open: false,
|
|
53
64
|
help: false,
|
|
65
|
+
version: false,
|
|
54
66
|
};
|
|
55
67
|
for (let i = 0; i < argv.length; i++) {
|
|
56
68
|
switch (argv[i]) {
|
|
@@ -88,10 +100,25 @@ function parse(argv) {
|
|
|
88
100
|
stop(`--automations takes file or memory, not ${said}.`);
|
|
89
101
|
break;
|
|
90
102
|
}
|
|
103
|
+
case '--sessions': {
|
|
104
|
+
const said = String(argv[++i]);
|
|
105
|
+
if (said === 'file' || said === 'memory')
|
|
106
|
+
options.sessions = said;
|
|
107
|
+
else
|
|
108
|
+
stop(`--sessions takes file or memory, not ${said}.`);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case '--wire':
|
|
112
|
+
options.wire = String(argv[++i]);
|
|
113
|
+
break;
|
|
91
114
|
case '--help':
|
|
92
115
|
case '-h':
|
|
93
116
|
options.help = true;
|
|
94
117
|
break;
|
|
118
|
+
case '--version':
|
|
119
|
+
case '-v':
|
|
120
|
+
options.version = true;
|
|
121
|
+
break;
|
|
95
122
|
default:
|
|
96
123
|
if (argv[i]?.startsWith('-')) {
|
|
97
124
|
process.stderr.write(`Unknown option ${argv[i]}. Try --help.\n`);
|
|
@@ -124,6 +151,11 @@ function parse(argv) {
|
|
|
124
151
|
if (!argv.includes('--automations') && (file.automations === 'file' || file.automations === 'memory')) {
|
|
125
152
|
options.automations = file.automations;
|
|
126
153
|
}
|
|
154
|
+
if (!argv.includes('--sessions') && (file.sessions === 'file' || file.sessions === 'memory')) {
|
|
155
|
+
options.sessions = file.sessions;
|
|
156
|
+
}
|
|
157
|
+
if (!argv.includes('--wire') && typeof file.wire === 'string')
|
|
158
|
+
options.wire = file.wire;
|
|
127
159
|
if (options.paths.length === 0)
|
|
128
160
|
options.paths.push(process.cwd());
|
|
129
161
|
return options;
|
|
@@ -238,6 +270,10 @@ if (verb !== undefined) {
|
|
|
238
270
|
process.exit(2);
|
|
239
271
|
}
|
|
240
272
|
const options = parse(argv);
|
|
273
|
+
if (options.version) {
|
|
274
|
+
process.stdout.write(`${version()}\n`);
|
|
275
|
+
process.exit(0);
|
|
276
|
+
}
|
|
241
277
|
if (options.help) {
|
|
242
278
|
process.stdout.write(USAGE);
|
|
243
279
|
process.exit(0);
|
|
@@ -271,6 +307,7 @@ const host = createHost({
|
|
|
271
307
|
directories: gitBranches(),
|
|
272
308
|
changes: gitChanges(),
|
|
273
309
|
worktrees: gitWorktrees(),
|
|
310
|
+
github: githubPullRequests(),
|
|
274
311
|
/*
|
|
275
312
|
* The host's own tools, offered to every session's model.
|
|
276
313
|
*
|
|
@@ -293,6 +330,20 @@ const host = createHost({
|
|
|
293
330
|
* not told which it was given - a host embedded in something that already
|
|
294
331
|
* schedules passes a third of its own.
|
|
295
332
|
*/
|
|
333
|
+
/*
|
|
334
|
+
* What this host adds on top of a backend, kept between restarts.
|
|
335
|
+
*
|
|
336
|
+
* The bits every client shares and the settings a session runs under. A
|
|
337
|
+
* daemon is exactly the case the port was written for: it is restarted for
|
|
338
|
+
* an upgrade, and without this every archived session comes back into the
|
|
339
|
+
* catalogue and every read one is unread, for everybody, with nothing said.
|
|
340
|
+
*/
|
|
341
|
+
sessions: options.sessions === 'memory'
|
|
342
|
+
? memorySessions()
|
|
343
|
+
: fileSessions({
|
|
344
|
+
file: sessionsPath(),
|
|
345
|
+
onProblem: (message) => process.stdout.write(`${message}\n`),
|
|
346
|
+
}),
|
|
296
347
|
automations: memory
|
|
297
348
|
? memoryAutomations()
|
|
298
349
|
: scheduledAutomations({
|
|
@@ -316,18 +367,52 @@ const host = createHost({
|
|
|
316
367
|
* `onEvent` hands it the message to do that with.
|
|
317
368
|
*/
|
|
318
369
|
onEvent: (message) => process.stdout.write(`${new Date().toISOString()} ${message}\n`),
|
|
370
|
+
/*
|
|
371
|
+
* What the window's diagnostics get from this daemon.
|
|
372
|
+
*
|
|
373
|
+
* The version out of the manifest, the log a detached daemon writes to and
|
|
374
|
+
* the wire capture when there is one, and a shutdown that is the same
|
|
375
|
+
* signal handler `ahpd stop` reaches through `SIGTERM`.
|
|
376
|
+
*/
|
|
377
|
+
diagnostics: {
|
|
378
|
+
version: version(),
|
|
379
|
+
logs: () => [daemonLog(), ...(options.wire === undefined ? [] : [options.wire])],
|
|
380
|
+
shutdown: () => { process.kill(process.pid, 'SIGTERM'); },
|
|
381
|
+
},
|
|
319
382
|
});
|
|
320
383
|
// Whichever runtime this is. `listen` is the only file that knows, and it
|
|
321
384
|
// says which one it found - a daemon that silently ran somewhere unexpected
|
|
322
385
|
// would be a daemon nobody could tell apart from the one they meant to start.
|
|
323
|
-
|
|
386
|
+
/*
|
|
387
|
+
* The wire, written down as it happens.
|
|
388
|
+
*
|
|
389
|
+
* One line per frame, appended synchronously so the file is whole at the
|
|
390
|
+
* moment anything else is read: a capture that lags the crash it is meant to
|
|
391
|
+
* explain is no capture. `frame` is the message parsed, so `jq` reads the
|
|
392
|
+
* file; a frame that is not JSON is kept as the string it was, because a
|
|
393
|
+
* client that sent one is exactly what a capture is for.
|
|
394
|
+
*/
|
|
395
|
+
const tap = options.wire === undefined ? undefined : (() => {
|
|
396
|
+
const at = options.wire;
|
|
397
|
+
writeFileSync(at, '');
|
|
398
|
+
return (from, text, peer) => {
|
|
399
|
+
let frame = text;
|
|
400
|
+
try {
|
|
401
|
+
frame = JSON.parse(text);
|
|
402
|
+
}
|
|
403
|
+
catch { /* kept as text */ }
|
|
404
|
+
appendFileSync(at, `${JSON.stringify({ at: new Date().toISOString(), from, peer, frame })}\n`);
|
|
405
|
+
};
|
|
406
|
+
})();
|
|
407
|
+
const listener = await listen({ port: options.port, host: options.host, ...(token !== undefined ? { token } : {}), ...(tap ? { tap } : {}) }, (peer) => host.accept(peer));
|
|
324
408
|
process.stdout.write(`ahpd on ws://${listener.host}:${listener.port} (${listener.runtime}), sessions in ${options.paths.join(', ')}\n`
|
|
325
409
|
// Its own line rather than the end of the one above, which `daemon.ts`
|
|
326
410
|
// reads the session directories off with a regular expression.
|
|
327
411
|
+ `automations ${memory ? 'in memory, schedules do not fire' : `in ${automationsPath()}, schedules fire`}\n`
|
|
328
412
|
// Where the secret came from, never the secret: stdout is a log, and a log
|
|
329
413
|
// is the one place a credential should not end up.
|
|
330
|
-
+ `${from}\n`
|
|
414
|
+
+ `${from}\n`
|
|
415
|
+
+ (options.wire === undefined ? '' : `wire to ${options.wire}\n`));
|
|
331
416
|
const shutdown = () => {
|
|
332
417
|
void Promise.resolve(listener.close()).finally(() => process.exit(0));
|
|
333
418
|
};
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* What version this is, read from the manifest rather than written twice.
|
|
3
|
+
*
|
|
4
|
+
* A literal in the source is a literal that drifts, and a `--version` that
|
|
5
|
+
* lies is worse than no `--version` at all.
|
|
6
|
+
*
|
|
7
|
+
* Found by walking up from this module rather than by a fixed relative path,
|
|
8
|
+
* because the depth differs: `src/version.ts` in this checkout and
|
|
9
|
+
* `dist/version.js` in an install, and neither should have to know which it
|
|
10
|
+
* is. `package.json` is always at the package root and npm always ships it,
|
|
11
|
+
* so the first one above this file is the right one - which in a workspace is
|
|
12
|
+
* this package's rather than the repository's.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
/** The version in the nearest `package.json`, or `unknown` where there is none. */
|
|
18
|
+
export const version = () => {
|
|
19
|
+
let at = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
for (;;) {
|
|
21
|
+
try {
|
|
22
|
+
const found = JSON.parse(readFileSync(join(at, 'package.json'), 'utf8'));
|
|
23
|
+
if (typeof found.version === 'string')
|
|
24
|
+
return found.version;
|
|
25
|
+
}
|
|
26
|
+
catch { /* not this directory */ }
|
|
27
|
+
const up = dirname(at);
|
|
28
|
+
// The root of the filesystem, which means there is no manifest anywhere
|
|
29
|
+
// above this file - a bundler inlined it, or something unpacked it wrong.
|
|
30
|
+
if (up === at)
|
|
31
|
+
return 'unknown';
|
|
32
|
+
at = up;
|
|
33
|
+
}
|
|
34
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahpd/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "An Agent Host Protocol server on Node, Bun or Deno. Ships with a Claude backend",
|
|
6
6
|
"keywords": [
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@microsoft/agent-host-protocol": "^0.9.0",
|
|
44
|
-
"@ahpd/agent-claude": "^0.
|
|
45
|
-
"@ahpd/sdk": "^0.
|
|
44
|
+
"@ahpd/agent-claude": "^0.5.0",
|
|
45
|
+
"@ahpd/sdk": "^0.5.0"
|
|
46
46
|
},
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|