@ahpd/server 0.4.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/dist/config.d.ts CHANGED
@@ -25,6 +25,8 @@ export interface Config {
25
25
  * beside this configuration, or `memory` until the process ends.
26
26
  */
27
27
  sessions?: 'file' | 'memory';
28
+ /** A file every frame is appended to, both directions, as JSON lines. */
29
+ wire?: string;
28
30
  }
29
31
  /**
30
32
  * Where this tool's files live.
package/dist/main.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { automationsPath, configPath, loadConfig, sessionsPath } from './config.js';
2
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { automationsPath, configPath, daemonLog, loadConfig, sessionsPath } from './config.js';
4
4
  import { version } from './version.js';
5
5
  import { running, start, stop as stopDaemon } from './daemon.js';
6
6
  import { pty } from './pty.js';
7
7
  import { claude } from '@ahpd/agent-claude';
8
- import { createHost, fileResources, gitBranches, gitChanges, gitWorktrees, hostTools, listen, fileSessions, memoryAutomations, memorySessions, scheduledAutomations, shellTerminals } from '@ahpd/sdk';
8
+ import { createHost, fileResources, gitBranches, gitChanges, gitWorktrees, githubPullRequests, hostTools, listen, fileSessions, memoryAutomations, memorySessions, scheduledAutomations, shellTerminals } from '@ahpd/sdk';
9
9
  const USAGE = `ahpd - an Agent Host Protocol server, with a Claude backend
10
10
 
11
11
  ahpd [options] run it here, in this terminal
@@ -35,12 +35,16 @@ const USAGE = `ahpd - an Agent Host Protocol server, with a Claude backend
35
35
  session's settings go. file, the default,
36
36
  keeps them beside the configuration; memory
37
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.
38
42
  --version, -v What version this is
39
43
  --help, -h This
40
44
 
41
45
  Every option above can be a key in the configuration file instead, spelled the
42
46
  way it is here without the dashes: port, host, paths, connectionToken,
43
- connectionTokenFile, withoutConnectionToken, automations, sessions. A flag beats the file, because a
47
+ connectionTokenFile, withoutConnectionToken, automations, sessions, wire. A flag beats the file, because a
44
48
  flag is this run and a file is every run until somebody edits it.
45
49
 
46
50
  Clients present the token as ?tkn=<secret> on the URL, or as an
@@ -104,6 +108,9 @@ function parse(argv) {
104
108
  stop(`--sessions takes file or memory, not ${said}.`);
105
109
  break;
106
110
  }
111
+ case '--wire':
112
+ options.wire = String(argv[++i]);
113
+ break;
107
114
  case '--help':
108
115
  case '-h':
109
116
  options.help = true;
@@ -147,6 +154,8 @@ function parse(argv) {
147
154
  if (!argv.includes('--sessions') && (file.sessions === 'file' || file.sessions === 'memory')) {
148
155
  options.sessions = file.sessions;
149
156
  }
157
+ if (!argv.includes('--wire') && typeof file.wire === 'string')
158
+ options.wire = file.wire;
150
159
  if (options.paths.length === 0)
151
160
  options.paths.push(process.cwd());
152
161
  return options;
@@ -298,6 +307,7 @@ const host = createHost({
298
307
  directories: gitBranches(),
299
308
  changes: gitChanges(),
300
309
  worktrees: gitWorktrees(),
310
+ github: githubPullRequests(),
301
311
  /*
302
312
  * The host's own tools, offered to every session's model.
303
313
  *
@@ -357,18 +367,52 @@ const host = createHost({
357
367
  * `onEvent` hands it the message to do that with.
358
368
  */
359
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
+ },
360
382
  });
361
383
  // Whichever runtime this is. `listen` is the only file that knows, and it
362
384
  // says which one it found - a daemon that silently ran somewhere unexpected
363
385
  // would be a daemon nobody could tell apart from the one they meant to start.
364
- const listener = await listen({ port: options.port, host: options.host, ...(token !== undefined ? { token } : {}) }, (peer) => host.accept(peer));
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));
365
408
  process.stdout.write(`ahpd on ws://${listener.host}:${listener.port} (${listener.runtime}), sessions in ${options.paths.join(', ')}\n`
366
409
  // Its own line rather than the end of the one above, which `daemon.ts`
367
410
  // reads the session directories off with a regular expression.
368
411
  + `automations ${memory ? 'in memory, schedules do not fire' : `in ${automationsPath()}, schedules fire`}\n`
369
412
  // Where the secret came from, never the secret: stdout is a log, and a log
370
413
  // is the one place a credential should not end up.
371
- + `${from}\n`);
414
+ + `${from}\n`
415
+ + (options.wire === undefined ? '' : `wire to ${options.wire}\n`));
372
416
  const shutdown = () => {
373
417
  void Promise.resolve(listener.close()).finally(() => process.exit(0));
374
418
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahpd/server",
3
- "version": "0.4.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.4.0",
45
- "@ahpd/sdk": "^0.4.0"
44
+ "@ahpd/agent-claude": "^0.5.0",
45
+ "@ahpd/sdk": "^0.5.0"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"