@ahpd/server 0.3.0 → 0.4.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @ahpd/server
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/%40ahpd%2Fserver)](https://www.npmjs.com/package/@ahpd/server)
4
+ [![CI](https://github.com/softov/ahpd/actions/workflows/ci.yml/badge.svg)](https://github.com/softov/ahpd/actions/workflows/ci.yml)
5
+ ![license MIT](https://img.shields.io/badge/license-MIT-blue)
6
+ ![node >=22](https://img.shields.io/badge/node-%3E%3D22-5fa04e)
7
+ ![Agent Host Protocol 0.9.0](https://img.shields.io/badge/AHP-0.9.0-0b7285)
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
- Every 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.
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,11 @@ 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';
23
28
  }
24
29
  /**
25
30
  * Where this tool's files live.
@@ -59,6 +64,15 @@ export declare const daemonLog: () => string;
59
64
  * and the ordering somebody put there.
60
65
  */
61
66
  export declare const automationsPath: () => string;
67
+ /**
68
+ * Where what this host adds on top of a backend is kept.
69
+ *
70
+ * The `IsRead` and `IsArchived` bits every client shares, and the settings a
71
+ * session is running under. Beside the automations for the same reason: this
72
+ * one is written whenever somebody archives a row, and `config.json` is a file
73
+ * a person edits.
74
+ */
75
+ export declare const sessionsPath: () => string;
62
76
  /** Make sure the directory is there, so a write into it can succeed. */
63
77
  export declare const ensureConfigDir: () => void;
64
78
  /**
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
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { automationsPath, configPath, loadConfig } from './config.js';
3
+ import { automationsPath, configPath, 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, 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,16 @@ 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
+ --version, -v What version this is
33
39
  --help, -h This
34
40
 
35
41
  Every option above can be a key in the configuration file instead, spelled the
36
42
  way it is here without the dashes: port, host, paths, connectionToken,
37
- connectionTokenFile, withoutConnectionToken, automations. A flag beats the file, because a
43
+ connectionTokenFile, withoutConnectionToken, automations, sessions. A flag beats the file, because a
38
44
  flag is this run and a file is every run until somebody edits it.
39
45
 
40
46
  Clients present the token as ?tkn=<secret> on the URL, or as an
@@ -49,8 +55,10 @@ function parse(argv) {
49
55
  host: '127.0.0.1',
50
56
  paths: [],
51
57
  automations: 'file',
58
+ sessions: 'file',
52
59
  open: false,
53
60
  help: false,
61
+ version: false,
54
62
  };
55
63
  for (let i = 0; i < argv.length; i++) {
56
64
  switch (argv[i]) {
@@ -88,10 +96,22 @@ function parse(argv) {
88
96
  stop(`--automations takes file or memory, not ${said}.`);
89
97
  break;
90
98
  }
99
+ case '--sessions': {
100
+ const said = String(argv[++i]);
101
+ if (said === 'file' || said === 'memory')
102
+ options.sessions = said;
103
+ else
104
+ stop(`--sessions takes file or memory, not ${said}.`);
105
+ break;
106
+ }
91
107
  case '--help':
92
108
  case '-h':
93
109
  options.help = true;
94
110
  break;
111
+ case '--version':
112
+ case '-v':
113
+ options.version = true;
114
+ break;
95
115
  default:
96
116
  if (argv[i]?.startsWith('-')) {
97
117
  process.stderr.write(`Unknown option ${argv[i]}. Try --help.\n`);
@@ -124,6 +144,9 @@ function parse(argv) {
124
144
  if (!argv.includes('--automations') && (file.automations === 'file' || file.automations === 'memory')) {
125
145
  options.automations = file.automations;
126
146
  }
147
+ if (!argv.includes('--sessions') && (file.sessions === 'file' || file.sessions === 'memory')) {
148
+ options.sessions = file.sessions;
149
+ }
127
150
  if (options.paths.length === 0)
128
151
  options.paths.push(process.cwd());
129
152
  return options;
@@ -238,6 +261,10 @@ if (verb !== undefined) {
238
261
  process.exit(2);
239
262
  }
240
263
  const options = parse(argv);
264
+ if (options.version) {
265
+ process.stdout.write(`${version()}\n`);
266
+ process.exit(0);
267
+ }
241
268
  if (options.help) {
242
269
  process.stdout.write(USAGE);
243
270
  process.exit(0);
@@ -293,6 +320,20 @@ const host = createHost({
293
320
  * not told which it was given - a host embedded in something that already
294
321
  * schedules passes a third of its own.
295
322
  */
323
+ /*
324
+ * What this host adds on top of a backend, kept between restarts.
325
+ *
326
+ * The bits every client shares and the settings a session runs under. A
327
+ * daemon is exactly the case the port was written for: it is restarted for
328
+ * an upgrade, and without this every archived session comes back into the
329
+ * catalogue and every read one is unread, for everybody, with nothing said.
330
+ */
331
+ sessions: options.sessions === 'memory'
332
+ ? memorySessions()
333
+ : fileSessions({
334
+ file: sessionsPath(),
335
+ onProblem: (message) => process.stdout.write(`${message}\n`),
336
+ }),
296
337
  automations: memory
297
338
  ? memoryAutomations()
298
339
  : scheduledAutomations({
@@ -0,0 +1,2 @@
1
+ /** The version in the nearest `package.json`, or `unknown` where there is none. */
2
+ export declare const version: () => string;
@@ -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.0",
3
+ "version": "0.4.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.3.0",
45
- "@ahpd/sdk": "^0.3.0"
44
+ "@ahpd/agent-claude": "^0.4.0",
45
+ "@ahpd/sdk": "^0.4.0"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"