@ahpd/server 0.2.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 CHANGED
@@ -52,6 +52,7 @@ ahpd config print the config file path and its contents
52
52
  | `--connection-token-file <p>` | Require the secret in this file. Writes a new one if the file is missing |
53
53
  | `--without-connection-token` | Accept any connection |
54
54
  | `--config-file <p>` | Use this config file instead of the default |
55
+ | `--automations <where>` | `file`, the default, keeps them beside the config and fires their schedules. `memory` keeps them until the process ends and fires nothing |
55
56
  | `--help`, `-h` | |
56
57
 
57
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.
package/dist/config.d.ts CHANGED
@@ -13,6 +13,13 @@ export interface Config {
13
13
  connectionTokenFile?: string;
14
14
  /** Accept any connection, with no secret at all. */
15
15
  withoutConnectionToken?: boolean;
16
+ /**
17
+ * Where automations are kept: `file` beside this configuration, or `memory`.
18
+ *
19
+ * `file` is the default and is the one with a clock in it. `memory` holds
20
+ * definitions for the life of the process and fires nothing.
21
+ */
22
+ automations?: 'file' | 'memory';
16
23
  }
17
24
  /**
18
25
  * Where this tool's files live.
package/dist/daemon.d.ts CHANGED
@@ -5,6 +5,11 @@ export interface Running {
5
5
  url: string;
6
6
  paths: string[];
7
7
  startedAt: string;
8
+ /**
9
+ * Where automations are kept and whether their schedules fire, in the
10
+ * daemon's own words. Absent from a record an older daemon wrote.
11
+ */
12
+ automations?: string;
8
13
  }
9
14
  /**
10
15
  * The daemon this user has running, if the record names one that still is.
package/dist/daemon.js CHANGED
@@ -130,6 +130,7 @@ export async function start(argv, self) {
130
130
  // rather than for the case, and `0` must never reach the record.
131
131
  if (child.pid === undefined)
132
132
  throw new Error('it started but has no process id');
133
+ const automations = /^automations (.+)$/m.exec(announced)?.[1]?.trim();
133
134
  const record = {
134
135
  pid: child.pid,
135
136
  url,
@@ -139,6 +140,7 @@ export async function start(argv, self) {
139
140
  paths: (/sessions in (.+)/.exec(announced)?.[1] ?? '')
140
141
  .trim().split(',').map((one) => one.trim()).filter((one) => one !== ''),
141
142
  startedAt: new Date().toISOString(),
143
+ ...(automations !== undefined ? { automations } : {}),
142
144
  };
143
145
  writeFileSync(daemonPath(), `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
144
146
  return record;
package/dist/main.js CHANGED
@@ -4,7 +4,7 @@ import { automationsPath, configPath, loadConfig } from './config.js';
4
4
  import { running, start, stop as stopDaemon } from './daemon.js';
5
5
  import { pty } from './pty.js';
6
6
  import { claude } from '@ahpd/agent-claude';
7
- import { createHost, fileResources, gitBranches, gitChanges, gitWorktrees, hostTools, listen, scheduledAutomations, shellTerminals } from '@ahpd/sdk';
7
+ import { createHost, fileResources, gitBranches, gitChanges, gitWorktrees, hostTools, listen, memoryAutomations, scheduledAutomations, shellTerminals } from '@ahpd/sdk';
8
8
  const USAGE = `ahpd - an Agent Host Protocol server, with a Claude backend
9
9
 
10
10
  ahpd [options] run it here, in this terminal
@@ -26,11 +26,15 @@ const USAGE = `ahpd - an Agent Host Protocol server, with a Claude backend
26
26
  --without-connection-token Accept any connection. Only when the port is
27
27
  already reachable by nobody else.
28
28
  --config-file <p> Read this instead of the file below.
29
+ --automations <where> file, the default, keeps them beside the
30
+ configuration and fires their schedules;
31
+ memory keeps them until this process ends and
32
+ fires nothing.
29
33
  --help, -h This
30
34
 
31
35
  Every option above can be a key in the configuration file instead, spelled the
32
36
  way it is here without the dashes: port, host, paths, connectionToken,
33
- connectionTokenFile, withoutConnectionToken. A flag beats the file, because a
37
+ connectionTokenFile, withoutConnectionToken, automations. A flag beats the file, because a
34
38
  flag is this run and a file is every run until somebody edits it.
35
39
 
36
40
  Clients present the token as ?tkn=<secret> on the URL, or as an
@@ -44,6 +48,7 @@ function parse(argv) {
44
48
  port: 9187,
45
49
  host: '127.0.0.1',
46
50
  paths: [],
51
+ automations: 'file',
47
52
  open: false,
48
53
  help: false,
49
54
  };
@@ -75,6 +80,14 @@ function parse(argv) {
75
80
  case '--config-file':
76
81
  options.configFile = String(argv[++i]);
77
82
  break;
83
+ case '--automations': {
84
+ const said = String(argv[++i]);
85
+ if (said === 'file' || said === 'memory')
86
+ options.automations = said;
87
+ else
88
+ stop(`--automations takes file or memory, not ${said}.`);
89
+ break;
90
+ }
78
91
  case '--help':
79
92
  case '-h':
80
93
  options.help = true;
@@ -108,6 +121,9 @@ function parse(argv) {
108
121
  options.tokenFile = file.connectionTokenFile;
109
122
  if (!options.open && file.withoutConnectionToken === true)
110
123
  options.open = true;
124
+ if (!argv.includes('--automations') && (file.automations === 'file' || file.automations === 'memory')) {
125
+ options.automations = file.automations;
126
+ }
111
127
  if (options.paths.length === 0)
112
128
  options.paths.push(process.cwd());
113
129
  return options;
@@ -179,6 +195,8 @@ if (verb !== undefined) {
179
195
  try {
180
196
  const begun = await start(rest, process.argv[1]);
181
197
  process.stdout.write(`ahpd on ${begun.url} (pid ${String(begun.pid)}), sessions in ${begun.paths.join(', ') || process.cwd()}\n`);
198
+ if (begun.automations !== undefined)
199
+ process.stdout.write(`automations ${begun.automations}\n`);
182
200
  process.exit(0);
183
201
  }
184
202
  catch (error) {
@@ -200,6 +218,10 @@ if (verb !== undefined) {
200
218
  process.stdout.write(`ahpd on ${found.url} (pid ${String(found.pid)}), started ${found.startedAt}\n`);
201
219
  if (found.paths.length > 0)
202
220
  process.stdout.write(`sessions in ${found.paths.join(', ')}\n`);
221
+ // Absent from a record written by an older daemon, which is the one case
222
+ // where saying nothing is better than guessing which store it was given.
223
+ if (found.automations !== undefined)
224
+ process.stdout.write(`automations ${found.automations}\n`);
203
225
  process.exit(0);
204
226
  }
205
227
  if (verb === 'config') {
@@ -221,6 +243,15 @@ if (options.help) {
221
243
  process.exit(0);
222
244
  }
223
245
  const { token, from } = secret(options);
246
+ /*
247
+ * Whether a clock is running, decided once and then said out loud.
248
+ *
249
+ * Both stores take a schedule trigger and only one of them ever fires it, and
250
+ * what tells a client which it got is a `nextRunAt` that is simply absent.
251
+ * That is too quiet for somebody who has just written a schedule, so the
252
+ * startup line says it in words and `ahpd status` repeats it.
253
+ */
254
+ const memory = options.automations === 'memory';
224
255
  const host = createHost({
225
256
  path: options.paths[0],
226
257
  // The daemon serves Claude Code. The host serves whatever it is given -
@@ -249,7 +280,7 @@ const host = createHost({
249
280
  */
250
281
  tools: hostTools(),
251
282
  /*
252
- * Automations, with a clock.
283
+ * Automations, with a clock unless asked otherwise.
253
284
  *
254
285
  * A daemon is the case the port was written for: it is already running at
255
286
  * nine in the morning, which is the only way an automation fires with
@@ -257,15 +288,19 @@ const host = createHost({
257
288
  * come back on a restart; the runs do not, because they name sessions that
258
289
  * went when the process did.
259
290
  *
260
- * A host embedded in something that already schedules passes its own store
261
- * instead, and one that should fire nothing passes `memoryAutomations()`.
291
+ * `--automations memory` is the same store without either half: nothing is
292
+ * written and nothing fires. Both are an `AutomationStore`, so the host is
293
+ * not told which it was given - a host embedded in something that already
294
+ * schedules passes a third of its own.
262
295
  */
263
- automations: scheduledAutomations({
264
- // Beside the configuration, which is this daemon's decision to make and
265
- // not the store's - see `ScheduledOptions.file`.
266
- file: automationsPath(),
267
- onProblem: (message) => process.stdout.write(`${message}\n`),
268
- }),
296
+ automations: memory
297
+ ? memoryAutomations()
298
+ : scheduledAutomations({
299
+ // Beside the configuration, which is this daemon's decision to make and
300
+ // not the store's - see `ScheduledOptions.file`.
301
+ file: automationsPath(),
302
+ onProblem: (message) => process.stdout.write(`${message}\n`),
303
+ }),
269
304
  /*
270
305
  * When, as well as what.
271
306
  *
@@ -287,6 +322,9 @@ const host = createHost({
287
322
  // would be a daemon nobody could tell apart from the one they meant to start.
288
323
  const listener = await listen({ port: options.port, host: options.host, ...(token !== undefined ? { token } : {}) }, (peer) => host.accept(peer));
289
324
  process.stdout.write(`ahpd on ws://${listener.host}:${listener.port} (${listener.runtime}), sessions in ${options.paths.join(', ')}\n`
325
+ // Its own line rather than the end of the one above, which `daemon.ts`
326
+ // reads the session directories off with a regular expression.
327
+ + `automations ${memory ? 'in memory, schedules do not fire' : `in ${automationsPath()}, schedules fire`}\n`
290
328
  // Where the secret came from, never the secret: stdout is a log, and a log
291
329
  // is the one place a credential should not end up.
292
330
  + `${from}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahpd/server",
3
- "version": "0.2.0",
3
+ "version": "0.3.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.2.0",
45
- "@ahpd/sdk": "^0.2.0"
44
+ "@ahpd/agent-claude": "^0.3.0",
45
+ "@ahpd/sdk": "^0.3.0"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"