@haywoodmarx/agent-pager 0.1.0 → 0.1.1

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Haywood-Marx
3
+ Copyright (c) 2026 Paul Haywood-Marx
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -17,14 +17,22 @@ A send commits before its wake is published. Every session registers the address
17
17
 
18
18
  Implementation metadata is open and opaque. The neutral runtime discovers provider manifests by convention, and shared launch or notification code resolves capabilities through `ImplementationRegistry` rather than switching on provider ids.
19
19
 
20
- Pager runs on Bun, at or above the release named by `packageManager` in `package.json`, and requires it on `PATH`. Embedded GUI hosts that do not inherit the interactive shell environment require `PAGER_BUN_PATH` to name the validated absolute Bun executable. The store directory and every path derived from it are resolved for macOS only; the CLI refuses to start on another platform.
20
+ Pager runs on Bun, at or above the release named by `packageManager` in `package.json`, and requires it on `PATH`. Embedded GUI hosts that do not inherit the interactive shell environment require `PAGER_BUN_PATH` to name the validated absolute Bun executable. An installer that keeps each version in its own directory sets `PAGER_PACKAGE_ROOT` to a path of the package root that survives its upgrades, and Pager names its entry points through that path in the files it writes for a host to run. The store directory and every path derived from it are resolved for macOS only; the CLI refuses to start on another platform.
21
21
 
22
22
  ## Install
23
23
 
24
+ ```sh
25
+ brew install haywoodmarx/tap/agent-pager
26
+ ```
27
+
28
+ Or with npm:
29
+
24
30
  ```sh
25
31
  npm install -g @haywoodmarx/agent-pager
26
32
  ```
27
33
 
34
+ Some harnesses need a plugin before sessions you start without `pager` can join the broker. To install one, open `pager`, press `m`, and choose it under Install plugins.
35
+
28
36
  ## Install from a local checkout
29
37
 
30
38
  ```sh
@@ -58,6 +66,6 @@ The broker starts on demand: `pager` with no command, and every harness connecti
58
66
 
59
67
  Callers adopt a running service only when its database-adjacent lock and `/health` agree on the service identity; concurrent starters race for that lock and every caller adopts the winner. The adoption rules are declared in [`src/launch/on-demand.ts`](src/launch/on-demand.ts) and [`src/launch/broker-lock.ts`](src/launch/broker-lock.ts).
60
68
 
61
- Back up the database and active service plist before an upgrade if their current state matters. Schema changes are destructive: a database whose schema marker or required columns do not match the running build is recreated, discarding every identity and message it held, and the recreation is reported with the counts it deleted on the standard error of the command that started the service, whether that is `pager serve` or an on-demand start. The marker is part of the service adoption identity, so a build that would recreate the database cannot displace a running service that owns it. Deleting the database and starting the service also creates an empty broker. After `uninstall`, delete the retained `.previous` plist and database only when rollback and message recovery are no longer required.
69
+ Back up the database before an upgrade if its current state matters. Schema changes are destructive: a database whose schema marker or required columns do not match the running build is recreated, discarding every identity and message it held, and the recreation is reported with the counts it deleted on the standard error of the command that started the service, whether that is `pager serve` or an on-demand start. The marker is part of the service adoption identity, so a build that would recreate the database cannot displace a running service that owns it. Deleting the database and starting the service also creates an empty broker.
62
70
 
63
71
  Per-implementation installation and conformance documentation is indexed under [`docs/implementations/acceptance/`](docs/implementations/acceptance/README.md).
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { resolvePagerStoreDirectory } from "../../configuration.js";
7
7
  import { packagedSkillPath } from "../../launch/skill.js";
8
- import { resolveBunExecutable, stableBunExecutable } from "../../launch/runtime.js";
8
+ import { resolveBunExecutable, stableBunExecutable, stablePackagePath } from "../../launch/runtime.js";
9
9
  import { implementationId } from "../registry.js";
10
10
  import { CLAUDE_BROKER_URL_ENVIRONMENT, CLAUDE_MESSAGING_RESERVATION_ENVIRONMENT } from "./environment.js";
11
11
  import { claudePluginRegistrationCommand, createClaudePluginPort } from "./plugin-directory.js";
@@ -85,7 +85,7 @@ function systemPluginPort() {
85
85
  // Discovery imports this manifest on every platform and before Bun is necessarily resolvable, so
86
86
  // the store path and runtime are resolved on first use rather than at module load.
87
87
  let resolved;
88
- const port = () => (resolved ??= createClaudePluginPort(join(resolvePagerStoreDirectory(process.platform, homedir()), "claude-plugin"), stableBunExecutable(resolveBunExecutable()), packagedSkillPath));
88
+ const port = () => (resolved ??= createClaudePluginPort(join(resolvePagerStoreDirectory(process.platform, homedir()), "claude-plugin"), stableBunExecutable(resolveBunExecutable()), (entryPath) => stablePackagePath(entryPath), packagedSkillPath));
89
89
  return {
90
90
  ensurePlugin: () => port().ensurePlugin(),
91
91
  };
@@ -33,11 +33,13 @@ export type ClaudePluginFileSystem = {
33
33
  * validated Bun executable and this installation's entry points by absolute path.
34
34
  * @param root - Directory the plugin is written into.
35
35
  * @param runtimePath - Absolute Bun executable that runs the generated entry points.
36
+ * @param stablePath - Maps one of this installation's entry points to the absolute path the plugin
37
+ * names it by, which must outlive an upgrade of the installation.
36
38
  * @param skillPath - Locates the packaged skill directory whose `SKILL.md` the plugin ships.
37
39
  * @param files - Filesystem seam used for the module's own isolation.
38
40
  * @returns - Port that writes the generated plugin directory.
39
41
  */
40
- export declare function createClaudePluginPort(root: string, runtimePath: string, skillPath: () => string, files?: ClaudePluginFileSystem): ClaudePluginPort;
42
+ export declare function createClaudePluginPort(root: string, runtimePath: string, stablePath: (entryPath: string) => string, skillPath: () => string, files?: ClaudePluginFileSystem): ClaudePluginPort;
41
43
  /**
42
44
  * Build the plugin manifest Claude reads to name this integration and reach its channel tools.
43
45
  *
@@ -75,6 +77,7 @@ export declare function claudePluginMarketplace(): Record<string, unknown>;
75
77
  * The `hooks` wrapper is required. A document that omits it is accepted silently and no hook ever
76
78
  * runs, which presents as a session that looks healthy and never wakes.
77
79
  * @param runtimePath - Absolute Bun executable that runs the hook entry point.
80
+ * @param sessionStartPath - Absolute session start entry point the hook runs.
78
81
  * @returns - Hook document for the generated plugin.
79
82
  */
80
- export declare function claudePluginHooks(runtimePath: string): Record<string, unknown>;
83
+ export declare function claudePluginHooks(runtimePath: string, sessionStartPath: string): Record<string, unknown>;
@@ -24,11 +24,13 @@ const sessionStartEntryPath = fileURLToPath(new URL("./session-start-cli.js", im
24
24
  * validated Bun executable and this installation's entry points by absolute path.
25
25
  * @param root - Directory the plugin is written into.
26
26
  * @param runtimePath - Absolute Bun executable that runs the generated entry points.
27
+ * @param stablePath - Maps one of this installation's entry points to the absolute path the plugin
28
+ * names it by, which must outlive an upgrade of the installation.
27
29
  * @param skillPath - Locates the packaged skill directory whose `SKILL.md` the plugin ships.
28
30
  * @param files - Filesystem seam used for the module's own isolation.
29
31
  * @returns - Port that writes the generated plugin directory.
30
32
  */
31
- export function createClaudePluginPort(root, runtimePath, skillPath, files = systemFileSystem()) {
33
+ export function createClaudePluginPort(root, runtimePath, stablePath, skillPath, files = systemFileSystem()) {
32
34
  const manifestPath = join(root, ".claude-plugin", "plugin.json");
33
35
  const marketplacePath = join(root, ".claude-plugin", "marketplace.json");
34
36
  const hooksPath = join(root, "hooks", "hooks.json");
@@ -41,9 +43,11 @@ export function createClaudePluginPort(root, runtimePath, skillPath, files = sys
41
43
  // Remove a stale monitors document: Claude loads whatever the directory holds, and one left
42
44
  // over would arm a monitor that fails at every session start.
43
45
  files.removeFile(retiredMonitorsPath);
44
- files.writeFile(manifestPath, `${JSON.stringify(claudePluginManifest(runtimePath, gatewayPath), null, 2)}\n`);
46
+ const manifest = claudePluginManifest(runtimePath, stablePath(gatewayPath));
47
+ const hooks = claudePluginHooks(runtimePath, stablePath(sessionStartEntryPath));
48
+ files.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
45
49
  files.writeFile(marketplacePath, `${JSON.stringify(claudePluginMarketplace(), null, 2)}\n`);
46
- files.writeFile(hooksPath, `${JSON.stringify(claudePluginHooks(runtimePath), null, 2)}\n`);
50
+ files.writeFile(hooksPath, `${JSON.stringify(hooks, null, 2)}\n`);
47
51
  files.writeFile(skillDocumentPath, files.readFile(join(skillPath(), "SKILL.md")));
48
52
  return root;
49
53
  },
@@ -109,12 +113,13 @@ export function claudePluginMarketplace() {
109
113
  * The `hooks` wrapper is required. A document that omits it is accepted silently and no hook ever
110
114
  * runs, which presents as a session that looks healthy and never wakes.
111
115
  * @param runtimePath - Absolute Bun executable that runs the hook entry point.
116
+ * @param sessionStartPath - Absolute session start entry point the hook runs.
112
117
  * @returns - Hook document for the generated plugin.
113
118
  */
114
- export function claudePluginHooks(runtimePath) {
119
+ export function claudePluginHooks(runtimePath, sessionStartPath) {
115
120
  return {
116
121
  hooks: {
117
- SessionStart: [{ hooks: [{ type: "command", command: shellCommand(runtimePath, sessionStartEntryPath) }] }],
122
+ SessionStart: [{ hooks: [{ type: "command", command: shellCommand(runtimePath, sessionStartPath) }] }],
118
123
  },
119
124
  };
120
125
  }
@@ -9,7 +9,7 @@ import { satisfiesBunVersionFloor } from "./runtime.js";
9
9
  /** Identifies the ownership schema understood by this release. */
10
10
  export const SERVICE_PROTOCOL_VERSION = 1;
11
11
  /** Identifies the package build that owns production service state. */
12
- export const SERVICE_BUILD_ID = `agent-pager@${PAGER_VERSION}`;
12
+ export const SERVICE_BUILD_ID = `@haywoodmarx/agent-pager@${PAGER_VERSION}`;
13
13
  /** Identifies the production runtime required to own SQLite. */
14
14
  export const SERVICE_RUNTIME = "bun";
15
15
  /**
@@ -3,6 +3,8 @@ import { type RuntimeEnvironment } from "./environment.js";
3
3
  export declare const MINIMUM_BUN_VERSION = "1.4.2";
4
4
  /** Names the environment variable that pins an explicit Bun executable for spawned services. */
5
5
  export declare const BUN_PATH_ENVIRONMENT = "PAGER_BUN_PATH";
6
+ /** Names the environment variable an installer sets to a path of this package's root that outlives its upgrades. */
7
+ export declare const PACKAGE_ROOT_ENVIRONMENT = "PAGER_PACKAGE_ROOT";
6
8
  /** Names the Node runtime override variable; `resolveBunExecutable` rejects it whenever it holds a non-empty value. */
7
9
  export declare const RETIRED_NODE_PATH_ENVIRONMENT = "PAGER_NODE_PATH";
8
10
  /** Raised when no executable satisfying the Bun runtime policy can be selected. */
@@ -64,6 +66,19 @@ export declare function resolveBunExecutable(environment?: NodeJS.ProcessEnv, ov
64
66
  * @returns - First `bun` on the search path that resolves to the same file, or the executable itself.
65
67
  */
66
68
  export declare function stableBunExecutable(executable: string, searchPath?: readonly string[], realPath?: (path: string) => string): string;
69
+ /**
70
+ * Prefer the package root an installer keeps at a stable path over the versioned directory it links to.
71
+ *
72
+ * A path written into a long-lived file must outlive an upgrade. Homebrew, for one, keeps each
73
+ * version in a directory the next upgrade deletes, and Bun runs a module from its resolved path, so
74
+ * the module's own location names that directory even when it was started through the stable link.
75
+ * @param entryPath - Absolute path of a file in this package, as the module that owns it sees it.
76
+ * @param environment - Process environment naming the stable package root.
77
+ * @param realPath - Resolves symbolic links; overridden by isolated verification.
78
+ * @returns - The same file under the stable root when that root resolves to a directory holding it,
79
+ * or the entry path itself.
80
+ */
81
+ export declare function stablePackagePath(entryPath: string, environment?: NodeJS.ProcessEnv, realPath?: (path: string) => string): string;
67
82
  /**
68
83
  * Resolve and pin the standalone Bun executable before an embedded host starts a service.
69
84
  * @param processEnvironment - Environment inherited by the editor host.
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { accessSync, constants, realpathSync } from "node:fs";
3
- import { delimiter, isAbsolute, resolve } from "node:path";
3
+ import { delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { readRuntimeEnvironment } from "./environment.js";
5
5
  /** Bounds one probe of an external runtime executable. */
6
6
  const RUNTIME_PROBE_TIMEOUT_MS = 10_000;
@@ -8,6 +8,8 @@ const RUNTIME_PROBE_TIMEOUT_MS = 10_000;
8
8
  export const MINIMUM_BUN_VERSION = "1.4.2";
9
9
  /** Names the environment variable that pins an explicit Bun executable for spawned services. */
10
10
  export const BUN_PATH_ENVIRONMENT = "PAGER_BUN_PATH";
11
+ /** Names the environment variable an installer sets to a path of this package's root that outlives its upgrades. */
12
+ export const PACKAGE_ROOT_ENVIRONMENT = "PAGER_PACKAGE_ROOT";
11
13
  /** Names the Node runtime override variable; `resolveBunExecutable` rejects it whenever it holds a non-empty value. */
12
14
  export const RETIRED_NODE_PATH_ENVIRONMENT = "PAGER_NODE_PATH";
13
15
  /** Raised when no executable satisfying the Bun runtime policy can be selected. */
@@ -119,6 +121,34 @@ export function stableBunExecutable(executable, searchPath = defaultProbe().sear
119
121
  }
120
122
  return executable;
121
123
  }
124
+ /**
125
+ * Prefer the package root an installer keeps at a stable path over the versioned directory it links to.
126
+ *
127
+ * A path written into a long-lived file must outlive an upgrade. Homebrew, for one, keeps each
128
+ * version in a directory the next upgrade deletes, and Bun runs a module from its resolved path, so
129
+ * the module's own location names that directory even when it was started through the stable link.
130
+ * @param entryPath - Absolute path of a file in this package, as the module that owns it sees it.
131
+ * @param environment - Process environment naming the stable package root.
132
+ * @param realPath - Resolves symbolic links; overridden by isolated verification.
133
+ * @returns - The same file under the stable root when that root resolves to a directory holding it,
134
+ * or the entry path itself.
135
+ */
136
+ export function stablePackagePath(entryPath, environment = process.env, realPath = realpathSync) {
137
+ const stableRoot = environment[PACKAGE_ROOT_ENVIRONMENT];
138
+ if (!stableRoot)
139
+ return entryPath;
140
+ if (!isAbsolute(stableRoot))
141
+ throw new Error(`${PACKAGE_ROOT_ENVIRONMENT} must name an absolute directory`);
142
+ const resolvedRoot = tryRealPath(stableRoot, realPath);
143
+ const resolvedEntry = tryRealPath(entryPath, realPath);
144
+ if (resolvedRoot === undefined || resolvedEntry === undefined)
145
+ return entryPath;
146
+ const entryWithinRoot = relative(resolvedRoot, resolvedEntry);
147
+ if (entryWithinRoot === ".." || entryWithinRoot.startsWith(`..${sep}`) || isAbsolute(entryWithinRoot)) {
148
+ return entryPath;
149
+ }
150
+ return join(stableRoot, entryWithinRoot);
151
+ }
122
152
  function tryRealPath(path, realPath) {
123
153
  try {
124
154
  return realPath(path);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Read the version a package manifest declares.
3
3
  *
4
- * Throws for a manifest without one, which would otherwise build `agent-pager@undefined`.
4
+ * Throws for a manifest without one.
5
5
  * @param contents - Manifest text as read from disk.
6
6
  * @param source - Manifest path named in the rejection.
7
7
  * @returns - The declared version.
@@ -5,7 +5,7 @@ import { isPlainObject } from "./shared/guards.js";
5
5
  /**
6
6
  * Read the version a package manifest declares.
7
7
  *
8
- * Throws for a manifest without one, which would otherwise build `agent-pager@undefined`.
8
+ * Throws for a manifest without one.
9
9
  * @param contents - Manifest text as read from disk.
10
10
  * @param source - Manifest path named in the rejection.
11
11
  * @returns - The declared version.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haywoodmarx/agent-pager",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Durable local messaging between agent implementations.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -38,6 +38,7 @@
38
38
  "scripts": {
39
39
  "prepare": "bun install --frozen-lockfile --ignore-scripts && bun run build && husky",
40
40
  "build": "bun scripts/tooling/build.mjs",
41
+ "release": "bun scripts/tooling/release.mjs",
41
42
  "prepack": "bun run build",
42
43
  "check:client-boundary": "bun scripts/tooling/client-boundary-self-test.mjs && bun scripts/tooling/client-boundary.mjs",
43
44
  "check:broker-writes": "bun scripts/tooling/broker-writes-self-test.mjs && bun scripts/tooling/broker-writes.mjs",
@@ -99,7 +100,7 @@
99
100
  "orchestration",
100
101
  "pi-package"
101
102
  ],
102
- "author": "Haywood-Marx",
103
+ "author": "Paul Haywood-Marx",
103
104
  "license": "MIT",
104
105
  "devDependencies": {
105
106
  "@earendil-works/pi-coding-agent": "*",