@haywoodmarx/agent-pager 0.1.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.
Files changed (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/dist/src/broker/address.d.ts +49 -0
  4. package/dist/src/broker/address.js +110 -0
  5. package/dist/src/broker/broker.d.ts +665 -0
  6. package/dist/src/broker/broker.js +2270 -0
  7. package/dist/src/broker/database.d.ts +81 -0
  8. package/dist/src/broker/database.js +69 -0
  9. package/dist/src/broker/operator-projection.d.ts +291 -0
  10. package/dist/src/broker/operator-projection.js +722 -0
  11. package/dist/src/broker/protocol.d.ts +237 -0
  12. package/dist/src/broker/protocol.js +311 -0
  13. package/dist/src/broker/schema.d.ts +94 -0
  14. package/dist/src/broker/schema.js +454 -0
  15. package/dist/src/broker/snapshot.d.ts +171 -0
  16. package/dist/src/broker/snapshot.js +78 -0
  17. package/dist/src/configuration-defaults.d.ts +64 -0
  18. package/dist/src/configuration-defaults.js +48 -0
  19. package/dist/src/configuration.d.ts +76 -0
  20. package/dist/src/configuration.js +156 -0
  21. package/dist/src/implementations/claude/environment.d.ts +8 -0
  22. package/dist/src/implementations/claude/environment.js +9 -0
  23. package/dist/src/implementations/claude/gateway-cli.d.ts +2 -0
  24. package/dist/src/implementations/claude/gateway-cli.js +136 -0
  25. package/dist/src/implementations/claude/gateway-identity.d.ts +134 -0
  26. package/dist/src/implementations/claude/gateway-identity.js +103 -0
  27. package/dist/src/implementations/claude/gateway.d.ts +148 -0
  28. package/dist/src/implementations/claude/gateway.js +236 -0
  29. package/dist/src/implementations/claude/instructions.d.ts +33 -0
  30. package/dist/src/implementations/claude/instructions.js +64 -0
  31. package/dist/src/implementations/claude/manifest.d.ts +33 -0
  32. package/dist/src/implementations/claude/manifest.js +108 -0
  33. package/dist/src/implementations/claude/plugin-directory.d.ts +80 -0
  34. package/dist/src/implementations/claude/plugin-directory.js +137 -0
  35. package/dist/src/implementations/claude/session-continuation.d.ts +25 -0
  36. package/dist/src/implementations/claude/session-continuation.js +125 -0
  37. package/dist/src/implementations/claude/session-identity.d.ts +25 -0
  38. package/dist/src/implementations/claude/session-identity.js +31 -0
  39. package/dist/src/implementations/claude/session-start-cli.d.ts +2 -0
  40. package/dist/src/implementations/claude/session-start-cli.js +16 -0
  41. package/dist/src/implementations/claude/socket-delivery.d.ts +52 -0
  42. package/dist/src/implementations/claude/socket-delivery.js +159 -0
  43. package/dist/src/implementations/model.d.ts +162 -0
  44. package/dist/src/implementations/model.js +1 -0
  45. package/dist/src/implementations/opencode/adapter.d.ts +71 -0
  46. package/dist/src/implementations/opencode/adapter.js +94 -0
  47. package/dist/src/implementations/opencode/environment.d.ts +8 -0
  48. package/dist/src/implementations/opencode/environment.js +9 -0
  49. package/dist/src/implementations/opencode/manifest.d.ts +30 -0
  50. package/dist/src/implementations/opencode/manifest.js +93 -0
  51. package/dist/src/implementations/opencode/plugin-api.d.ts +120 -0
  52. package/dist/src/implementations/opencode/plugin-api.js +1 -0
  53. package/dist/src/implementations/opencode/plugin.d.ts +38 -0
  54. package/dist/src/implementations/opencode/plugin.js +303 -0
  55. package/dist/src/implementations/opencode/self-close-tool.d.ts +9 -0
  56. package/dist/src/implementations/opencode/self-close-tool.js +31 -0
  57. package/dist/src/implementations/orca/terminal.d.ts +48 -0
  58. package/dist/src/implementations/orca/terminal.js +154 -0
  59. package/dist/src/implementations/pi/broker-client.d.ts +56 -0
  60. package/dist/src/implementations/pi/broker-client.js +156 -0
  61. package/dist/src/implementations/pi/environment.d.ts +8 -0
  62. package/dist/src/implementations/pi/environment.js +9 -0
  63. package/dist/src/implementations/pi/extension.d.ts +57 -0
  64. package/dist/src/implementations/pi/extension.js +253 -0
  65. package/dist/src/implementations/pi/host-slots.d.ts +18 -0
  66. package/dist/src/implementations/pi/host-slots.js +27 -0
  67. package/dist/src/implementations/pi/manifest.d.ts +11 -0
  68. package/dist/src/implementations/pi/manifest.js +61 -0
  69. package/dist/src/implementations/pi/self-close-tool.d.ts +7 -0
  70. package/dist/src/implementations/pi/self-close-tool.js +27 -0
  71. package/dist/src/implementations/registry.d.ts +47 -0
  72. package/dist/src/implementations/registry.js +157 -0
  73. package/dist/src/implementations/session-key.d.ts +13 -0
  74. package/dist/src/implementations/session-key.js +19 -0
  75. package/dist/src/index.d.ts +27 -0
  76. package/dist/src/index.js +26 -0
  77. package/dist/src/launch/broker-lock.d.ts +257 -0
  78. package/dist/src/launch/broker-lock.js +644 -0
  79. package/dist/src/launch/cli.d.ts +37 -0
  80. package/dist/src/launch/cli.js +317 -0
  81. package/dist/src/launch/environment.d.ts +45 -0
  82. package/dist/src/launch/environment.js +48 -0
  83. package/dist/src/launch/launch.d.ts +235 -0
  84. package/dist/src/launch/launch.js +223 -0
  85. package/dist/src/launch/on-demand.d.ts +202 -0
  86. package/dist/src/launch/on-demand.js +435 -0
  87. package/dist/src/launch/runtime-composition.d.ts +88 -0
  88. package/dist/src/launch/runtime-composition.js +253 -0
  89. package/dist/src/launch/runtime.d.ts +79 -0
  90. package/dist/src/launch/runtime.js +196 -0
  91. package/dist/src/launch/service-system.d.ts +11 -0
  92. package/dist/src/launch/service-system.js +39 -0
  93. package/dist/src/launch/service.d.ts +173 -0
  94. package/dist/src/launch/service.js +492 -0
  95. package/dist/src/launch/skill.d.ts +57 -0
  96. package/dist/src/launch/skill.js +81 -0
  97. package/dist/src/launch/terminal.d.ts +26 -0
  98. package/dist/src/launch/terminal.js +43 -0
  99. package/dist/src/launch/workspace-branch.d.ts +10 -0
  100. package/dist/src/launch/workspace-branch.js +29 -0
  101. package/dist/src/messaging/delivery-client.d.ts +144 -0
  102. package/dist/src/messaging/delivery-client.js +261 -0
  103. package/dist/src/messaging/delivery.d.ts +75 -0
  104. package/dist/src/messaging/delivery.js +22 -0
  105. package/dist/src/messaging/inbox-socket.d.ts +98 -0
  106. package/dist/src/messaging/inbox-socket.js +278 -0
  107. package/dist/src/messaging/mcp.d.ts +53 -0
  108. package/dist/src/messaging/mcp.js +501 -0
  109. package/dist/src/messaging/notifications.d.ts +231 -0
  110. package/dist/src/messaging/notifications.js +489 -0
  111. package/dist/src/messaging/push-delivery.d.ts +77 -0
  112. package/dist/src/messaging/push-delivery.js +187 -0
  113. package/dist/src/messaging/tools.d.ts +11 -0
  114. package/dist/src/messaging/tools.js +102 -0
  115. package/dist/src/messaging/wake.d.ts +23 -0
  116. package/dist/src/messaging/wake.js +39 -0
  117. package/dist/src/operator/agent-projection.d.ts +102 -0
  118. package/dist/src/operator/agent-projection.js +249 -0
  119. package/dist/src/operator/commands.d.ts +50 -0
  120. package/dist/src/operator/commands.js +199 -0
  121. package/dist/src/operator/diagnostics.d.ts +21 -0
  122. package/dist/src/operator/diagnostics.js +22 -0
  123. package/dist/src/operator/protocol.d.ts +511 -0
  124. package/dist/src/operator/protocol.js +49 -0
  125. package/dist/src/operator/repository-system.d.ts +14 -0
  126. package/dist/src/operator/repository-system.js +150 -0
  127. package/dist/src/operator/repository.d.ts +79 -0
  128. package/dist/src/operator/repository.js +72 -0
  129. package/dist/src/operator/traffic-projection.d.ts +19 -0
  130. package/dist/src/operator/traffic-projection.js +229 -0
  131. package/dist/src/operator/transport.d.ts +87 -0
  132. package/dist/src/operator/transport.js +549 -0
  133. package/dist/src/operator/update-store.d.ts +53 -0
  134. package/dist/src/operator/update-store.js +162 -0
  135. package/dist/src/operator/updates.d.ts +94 -0
  136. package/dist/src/operator/updates.js +280 -0
  137. package/dist/src/operator/validation.d.ts +114 -0
  138. package/dist/src/operator/validation.js +916 -0
  139. package/dist/src/shared/guards.d.ts +59 -0
  140. package/dist/src/shared/guards.js +119 -0
  141. package/dist/src/shared/http.d.ts +30 -0
  142. package/dist/src/shared/http.js +50 -0
  143. package/dist/src/shared/row.d.ts +2 -0
  144. package/dist/src/shared/row.js +1 -0
  145. package/dist/src/shared/shell.d.ts +6 -0
  146. package/dist/src/shared/shell.js +8 -0
  147. package/dist/src/tui/actions.d.ts +166 -0
  148. package/dist/src/tui/actions.js +253 -0
  149. package/dist/src/tui/agent-tree.d.ts +125 -0
  150. package/dist/src/tui/agent-tree.js +663 -0
  151. package/dist/src/tui/app.d.ts +38 -0
  152. package/dist/src/tui/app.js +378 -0
  153. package/dist/src/tui/chrome.d.ts +86 -0
  154. package/dist/src/tui/chrome.js +85 -0
  155. package/dist/src/tui/client.d.ts +41 -0
  156. package/dist/src/tui/client.js +279 -0
  157. package/dist/src/tui/composition.d.ts +41 -0
  158. package/dist/src/tui/composition.js +146 -0
  159. package/dist/src/tui/message-views.d.ts +94 -0
  160. package/dist/src/tui/message-views.js +515 -0
  161. package/dist/src/tui/model.d.ts +414 -0
  162. package/dist/src/tui/model.js +758 -0
  163. package/dist/src/tui/presentation.d.ts +350 -0
  164. package/dist/src/tui/presentation.js +541 -0
  165. package/dist/src/tui/production.d.ts +79 -0
  166. package/dist/src/tui/production.js +253 -0
  167. package/dist/src/tui/run-terminal.d.ts +32 -0
  168. package/dist/src/tui/run-terminal.js +54 -0
  169. package/dist/src/tui/runtime.d.ts +113 -0
  170. package/dist/src/tui/runtime.js +189 -0
  171. package/dist/src/tui/theme.d.ts +49 -0
  172. package/dist/src/tui/theme.js +55 -0
  173. package/dist/src/tui/transport.d.ts +31 -0
  174. package/dist/src/tui/transport.js +255 -0
  175. package/dist/src/version.d.ts +11 -0
  176. package/dist/src/version.js +52 -0
  177. package/package.json +142 -0
  178. package/skills/pager/SKILL.md +112 -0
@@ -0,0 +1,78 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { LOOPBACK_HOST } from "../configuration-defaults.js";
4
+ import { closeServer, listenServer, urlHost, writeJson } from "../shared/http.js";
5
+ /**
6
+ * Start the loopback-only snapshot HTTP endpoint.
7
+ * @param broker - Durable broker port that owns message state.
8
+ * @param port - Requested loopback port, with zero selecting an ephemeral port.
9
+ * @param host - Network interface to bind and advertise.
10
+ * @returns - Handle carrying the bound base URL and a `close` that resolves once the server has stopped.
11
+ */
12
+ export async function startSnapshotServer(broker, port = 0, host = LOOPBACK_HOST) {
13
+ const server = createServer((request, response) => {
14
+ void handleSnapshotRequest(broker, request, response);
15
+ });
16
+ await listenServer(server, port, host);
17
+ const address = server.address();
18
+ return {
19
+ url: `http://${urlHost(address.address)}:${String(address.port)}`,
20
+ close: () => closeServer(server),
21
+ };
22
+ }
23
+ async function handleSnapshotRequest(broker, request, response) {
24
+ try {
25
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
26
+ if (request.method !== "GET" &&
27
+ !(request.method === "POST" && /^\/snapshot\/messages\/[^/]+\/expire$/u.test(url.pathname))) {
28
+ throw new Error("GET required");
29
+ }
30
+ const body = request.method === "POST" ? await mutateSnapshotPath(broker, url.pathname) : selectSnapshotPath(broker, url);
31
+ writeJson(response, 200, body, { "cache-control": "no-store" });
32
+ }
33
+ catch (error) {
34
+ writeJson(response, 400, { error: error instanceof Error ? error.message : "Snapshot failed" }, {
35
+ "cache-control": "no-store",
36
+ });
37
+ }
38
+ }
39
+ function selectSnapshotPath(broker, url) {
40
+ const messageBody = /^\/snapshot\/messages\/([^/]+)\/body$/u.exec(url.pathname);
41
+ const messageId = messageBody?.[1];
42
+ if (messageId)
43
+ return broker.readMessageBody(decodeURIComponent(messageId));
44
+ // Health answers a liveness deadline, so it must not pay for a projection it does not read.
45
+ if (url.pathname === "/snapshot/health")
46
+ return broker.snapshotHealth();
47
+ const snapshot = broker.snapshot(url.searchParams.get("workspace") ?? undefined);
48
+ switch (url.pathname) {
49
+ case "/snapshot":
50
+ return snapshot;
51
+ case "/snapshot/workspaces":
52
+ return { workspaces: snapshot.workspaces };
53
+ case "/snapshot/agents":
54
+ return { agents: snapshot.agents, connections: snapshot.connections };
55
+ case "/snapshot/deliveries":
56
+ return { deliveries: snapshot.deliveries };
57
+ case "/snapshot/diagnostics":
58
+ return snapshot.diagnostics;
59
+ default:
60
+ throw new Error("Unknown snapshot endpoint");
61
+ }
62
+ }
63
+ function mutateSnapshotPath(broker, pathname) {
64
+ const expire = /^\/snapshot\/messages\/([^/]+)\/expire$/u.exec(pathname);
65
+ const messageId = expire?.[1];
66
+ if (!messageId)
67
+ throw new Error("Unknown snapshot endpoint");
68
+ broker.expireMessage(decodeURIComponent(messageId), "operator");
69
+ return broker.status(decodeURIComponent(messageId));
70
+ }
71
+ /**
72
+ * Return a non-secret stable marker for restart detection without exposing process-binding tokens.
73
+ * @param epoch - Broker generation used to fence stale sessions.
74
+ * @returns - Non-secret epoch fingerprint suitable for public diagnostics.
75
+ */
76
+ export function publicEpoch(epoch) {
77
+ return createHash("sha256").update(epoch).digest("hex").slice(0, 16);
78
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Names the loopback interface every local HTTP server binds and advertises by default.
3
+ *
4
+ * The broker has no authentication and no `Origin` or `Host` validation; the loopback bind is what
5
+ * keeps it unreachable from the network, so `PagerConfig.bindHost` is validated to equal
6
+ * this and nothing else (`configuration.ts`); every bind site reads it (or this constant) rather
7
+ * than restating the address.
8
+ */
9
+ export declare const LOOPBACK_HOST = "127.0.0.1";
10
+ /** Names every user-configurable Pager runtime setting. */
11
+ export type PagerConfig = {
12
+ /** Names the absolute SQLite database path retained across service restarts. */
13
+ databasePath: string;
14
+ /** Names the network interface used by local HTTP servers; rejected unless loopback. */
15
+ bindHost: string;
16
+ /** Selects the MCP TCP port in the inclusive range 0..65535. */
17
+ mcpPort: number;
18
+ /** Selects the snapshot TCP port in the inclusive range 0..65535. */
19
+ snapshotPort: number;
20
+ /** Selects the operator update-stream TCP port in the inclusive range 0..65535. */
21
+ operatorPort: number;
22
+ /** Sets the lifetime in milliseconds of an unacknowledged message claim. */
23
+ visibilityTimeoutMs: number;
24
+ /** Sets the delay in milliseconds before the first redelivery wake for an unclaimed inbox. */
25
+ redeliveryBaseDelayMs: number;
26
+ /** Sets how long in milliseconds an inbox is retried before its messages are dead-lettered. */
27
+ redeliveryGiveUpMs: number;
28
+ /** Sets the lifetime in milliseconds of an unattached launch reservation. */
29
+ reservationTtlMs: number;
30
+ /** Sets the lifetime in milliseconds of an attached agent lease. */
31
+ leaseTtlMs: number;
32
+ /** Sets the maximum non-negative sends allowed for one sender-recipient pair per window. */
33
+ perPairRateLimit: number;
34
+ /** Sets the pair-rate measurement window in milliseconds. */
35
+ perPairRateWindowMs: number;
36
+ /** Sets the maximum non-negative queued or claimed messages retained for one recipient. */
37
+ maxInboxMessages: number;
38
+ /** Sets the maximum non-negative wakes delivered to one agent per window. */
39
+ wakeBudget: number;
40
+ /** Sets the wake-budget measurement window in milliseconds. */
41
+ wakeBudgetWindowMs: number;
42
+ /** Sets the adapter heartbeat interval in milliseconds. */
43
+ adapterHeartbeatIntervalMs: number;
44
+ /** Bounds waiting for an on-demand service to publish endpoints, in milliseconds. */
45
+ serviceStartupTimeoutMs: number;
46
+ };
47
+ /**
48
+ * Supplies the default values used for configurable runtime settings.
49
+ *
50
+ * `databasePath` is never effective: `loadPagerConfiguration` derives the real default from
51
+ * the resolved store directory at load time, since it depends on a path not known here.
52
+ */
53
+ export declare const PAGER_DEFAULTS: Readonly<PagerConfig>;
54
+ /** Names the configured adapter heartbeat cadence published to launched implementation processes. */
55
+ export declare const ADAPTER_HEARTBEAT_INTERVAL_ENVIRONMENT = "PAGER_ADAPTER_HEARTBEAT_INTERVAL_MS";
56
+ /**
57
+ * Resolve the heartbeat cadence one implementation process should renew its attachment on.
58
+ *
59
+ * A heartbeat renews the attachment lease, so a value that is not a positive integer is ignored
60
+ * rather than allowed to stall renewal.
61
+ * @param processEnv - Environment the launching process published to this one.
62
+ * @returns - Milliseconds between attachment heartbeats.
63
+ */
64
+ export declare function adapterHeartbeatIntervalMs(processEnv: Readonly<NodeJS.ProcessEnv>): number;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Names the loopback interface every local HTTP server binds and advertises by default.
3
+ *
4
+ * The broker has no authentication and no `Origin` or `Host` validation; the loopback bind is what
5
+ * keeps it unreachable from the network, so `PagerConfig.bindHost` is validated to equal
6
+ * this and nothing else (`configuration.ts`); every bind site reads it (or this constant) rather
7
+ * than restating the address.
8
+ */
9
+ export const LOOPBACK_HOST = "127.0.0.1";
10
+ /**
11
+ * Supplies the default values used for configurable runtime settings.
12
+ *
13
+ * `databasePath` is never effective: `loadPagerConfiguration` derives the real default from
14
+ * the resolved store directory at load time, since it depends on a path not known here.
15
+ */
16
+ export const PAGER_DEFAULTS = Object.freeze({
17
+ databasePath: "",
18
+ bindHost: LOOPBACK_HOST,
19
+ mcpPort: 43112,
20
+ snapshotPort: 43113,
21
+ operatorPort: 43114,
22
+ visibilityTimeoutMs: 300_000,
23
+ redeliveryBaseDelayMs: 60_000,
24
+ redeliveryGiveUpMs: 86_400_000,
25
+ reservationTtlMs: 300_000,
26
+ leaseTtlMs: 120_000,
27
+ perPairRateLimit: 60,
28
+ perPairRateWindowMs: 60_000,
29
+ maxInboxMessages: 1_000,
30
+ wakeBudget: 60,
31
+ wakeBudgetWindowMs: 60_000,
32
+ adapterHeartbeatIntervalMs: 10_000,
33
+ serviceStartupTimeoutMs: 10_000,
34
+ });
35
+ /** Names the configured adapter heartbeat cadence published to launched implementation processes. */
36
+ export const ADAPTER_HEARTBEAT_INTERVAL_ENVIRONMENT = "PAGER_ADAPTER_HEARTBEAT_INTERVAL_MS";
37
+ /**
38
+ * Resolve the heartbeat cadence one implementation process should renew its attachment on.
39
+ *
40
+ * A heartbeat renews the attachment lease, so a value that is not a positive integer is ignored
41
+ * rather than allowed to stall renewal.
42
+ * @param processEnv - Environment the launching process published to this one.
43
+ * @returns - Milliseconds between attachment heartbeats.
44
+ */
45
+ export function adapterHeartbeatIntervalMs(processEnv) {
46
+ const configured = Number(processEnv[ADAPTER_HEARTBEAT_INTERVAL_ENVIRONMENT]);
47
+ return Number.isSafeInteger(configured) && configured > 0 ? configured : PAGER_DEFAULTS.adapterHeartbeatIntervalMs;
48
+ }
@@ -0,0 +1,76 @@
1
+ import { type PagerConfig } from "./configuration-defaults.js";
2
+ export { PAGER_DEFAULTS, type PagerConfig } from "./configuration-defaults.js";
3
+ /** Reports a rejected configuration boundary value without losing its source location. */
4
+ export declare class ConfigurationError extends Error {
5
+ /** Identifies configuration validation failures to callers. */
6
+ readonly code = "configuration_invalid";
7
+ /** Names the source file containing the rejected value. */
8
+ readonly filePath: string;
9
+ /** Names the rejected key, or `$` for document-level failures. */
10
+ readonly key: string;
11
+ /**
12
+ * Construct a configuration failure.
13
+ * @param filePath - Configuration source containing the rejected value.
14
+ * @param key - Rejected key, or `$` when the whole document is invalid.
15
+ * @param reason - Caller-visible reason the value cannot be used.
16
+ */
17
+ constructor(filePath: string, key: string, reason: string);
18
+ }
19
+ /** Describes resolved configuration and whether a user file supplied values. */
20
+ export type LoadedConfiguration = {
21
+ /** Carries every validated runtime setting after precedence resolution. */
22
+ config: PagerConfig;
23
+ /** Names the user file that supplied values, or null when discovery found none. */
24
+ filePath: string | null;
25
+ /** Names the resolved user store directory, as `resolvePagerStoreDirectory` returns it. */
26
+ storeDirectory: string;
27
+ };
28
+ /** Supplies explicit configuration layers and IO seams to the loader. */
29
+ export type ConfigurationLoadOptions = {
30
+ /** Supplies environment variables, which take precedence over the file. */
31
+ environment?: NodeJS.ProcessEnv;
32
+ /** Supplies the operating system identifier used for path discovery. */
33
+ platform?: NodeJS.Platform;
34
+ /** Supplies the home directory without reading process-global user state. */
35
+ homeDirectory: string;
36
+ /** Reads a discovered file as UTF-8 text for boundary validation. */
37
+ readFile?: (path: string) => string;
38
+ /** Reports whether a discovered file exists. */
39
+ fileExists?: (path: string) => boolean;
40
+ };
41
+ /**
42
+ * Resolve the platform-specific user store directory.
43
+ * @param platform - Operating-system identifier being resolved.
44
+ * @param homeDirectory - Absolute user home directory used as the discovery root.
45
+ * @returns - Absolute directory containing user configuration and durable state.
46
+ */
47
+ export declare function resolvePagerStoreDirectory(platform: NodeJS.Platform, homeDirectory: string): string;
48
+ /** Precedence layers for {@link resolveConfigurationSetting}, ordered from lowest to highest precedence. */
49
+ export type ConfigurationSettingPrecedence<T> = {
50
+ /** Repository-owned fallback value. */
51
+ defaultValue: T;
52
+ /** Validated user-file value, when supplied. */
53
+ fileValue?: T | undefined;
54
+ /** Parsed environment value, when supplied. */
55
+ environmentValue?: T | undefined;
56
+ };
57
+ /**
58
+ * Resolve one setting according to environment, file, then default precedence.
59
+ * @param precedence - The setting's three sources, named by precedence rather than argument order.
60
+ * @returns - Highest-precedence supplied value.
61
+ */
62
+ export declare function resolveConfigurationSetting<T>({ environmentValue, fileValue, defaultValue, }: ConfigurationSettingPrecedence<T>): T;
63
+ /**
64
+ * Load and validate user configuration without creating durable state.
65
+ * @param options - Explicit path inputs, precedence layers, and file IO seams.
66
+ * @returns - Fully resolved settings, source path, and shared store directory.
67
+ */
68
+ export declare function loadPagerConfiguration(options: ConfigurationLoadOptions): LoadedConfiguration;
69
+ /**
70
+ * Create the directory that owns the resolved database before SQLite opens it.
71
+ * @param loaded - Resolved configuration whose database parent must exist.
72
+ * @param createDirectory - Filesystem seam that recursively creates the database parent.
73
+ */
74
+ export declare function initializePagerStore(loaded: LoadedConfiguration, createDirectory?: (path: string, options: {
75
+ recursive: true;
76
+ }) => unknown): void;
@@ -0,0 +1,156 @@
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ import { PAGER_DEFAULTS } from "./configuration-defaults.js";
4
+ export { PAGER_DEFAULTS } from "./configuration-defaults.js";
5
+ /** Reports a rejected configuration boundary value without losing its source location. */
6
+ export class ConfigurationError extends Error {
7
+ /** Identifies configuration validation failures to callers. */
8
+ code = "configuration_invalid";
9
+ /** Names the source file containing the rejected value. */
10
+ filePath;
11
+ /** Names the rejected key, or `$` for document-level failures. */
12
+ key;
13
+ /**
14
+ * Construct a configuration failure.
15
+ * @param filePath - Configuration source containing the rejected value.
16
+ * @param key - Rejected key, or `$` when the whole document is invalid.
17
+ * @param reason - Caller-visible reason the value cannot be used.
18
+ */
19
+ constructor(filePath, key, reason) {
20
+ super(`Invalid Pager configuration at ${filePath}, key ${key}: ${reason}`);
21
+ this.name = "ConfigurationError";
22
+ this.filePath = filePath;
23
+ this.key = key;
24
+ }
25
+ }
26
+ const configKeys = Object.keys(PAGER_DEFAULTS);
27
+ const portKeys = new Set(["mcpPort", "snapshotPort", "operatorPort"]);
28
+ const pathKeys = new Set(["databasePath"]);
29
+ const stringKeys = new Set(["databasePath", "bindHost"]);
30
+ /**
31
+ * Resolve the platform-specific user store directory.
32
+ * @param platform - Operating-system identifier being resolved.
33
+ * @param homeDirectory - Absolute user home directory used as the discovery root.
34
+ * @returns - Absolute directory containing user configuration and durable state.
35
+ */
36
+ export function resolvePagerStoreDirectory(platform, homeDirectory) {
37
+ if (platform !== "darwin")
38
+ throw new Error(`Pager supports macOS only; got platform ${platform}`);
39
+ if (!isAbsolute(homeDirectory))
40
+ throw new Error(`Pager home directory must be absolute: ${homeDirectory}`);
41
+ return join(homeDirectory, "Library", "Application Support", "pager");
42
+ }
43
+ /**
44
+ * Resolve one setting according to environment, file, then default precedence.
45
+ * @param precedence - The setting's three sources, named by precedence rather than argument order.
46
+ * @returns - Highest-precedence supplied value.
47
+ */
48
+ export function resolveConfigurationSetting({ environmentValue, fileValue, defaultValue, }) {
49
+ if (environmentValue !== undefined)
50
+ return environmentValue;
51
+ if (fileValue !== undefined)
52
+ return fileValue;
53
+ return defaultValue;
54
+ }
55
+ /**
56
+ * Load and validate user configuration without creating durable state.
57
+ * @param options - Explicit path inputs, precedence layers, and file IO seams.
58
+ * @returns - Fully resolved settings, source path, and shared store directory.
59
+ */
60
+ export function loadPagerConfiguration(options) {
61
+ const environment = options.environment ?? {};
62
+ const platform = options.platform ?? process.platform;
63
+ const storeDirectory = resolvePagerStoreDirectory(platform, options.homeDirectory);
64
+ const discoveredPath = join(storeDirectory, "config.json");
65
+ const configPath = environment.PAGER_CONFIG ?? discoveredPath;
66
+ if (!isAbsolute(configPath))
67
+ throw new ConfigurationError(configPath, "configPath", "path must be absolute");
68
+ const exists = options.fileExists ?? existsSync;
69
+ const read = options.readFile ?? ((path) => readFileSync(path, "utf8"));
70
+ const filePresent = exists(configPath);
71
+ const file = filePresent ? parseConfigurationFile(configPath, read(configPath)) : {};
72
+ const defaults = { ...PAGER_DEFAULTS, databasePath: join(storeDirectory, "broker.sqlite") };
73
+ const environmentLayer = parseEnvironment(configPath, environment);
74
+ const config = Object.fromEntries(configKeys.map((key) => [
75
+ key,
76
+ resolveConfigurationSetting({
77
+ environmentValue: environmentLayer[key],
78
+ fileValue: file[key],
79
+ defaultValue: defaults[key],
80
+ }),
81
+ ]));
82
+ validateResolvedConfiguration(configPath, config);
83
+ return { config, filePath: filePresent ? configPath : null, storeDirectory };
84
+ }
85
+ /**
86
+ * Create the directory that owns the resolved database before SQLite opens it.
87
+ * @param loaded - Resolved configuration whose database parent must exist.
88
+ * @param createDirectory - Filesystem seam that recursively creates the database parent.
89
+ */
90
+ export function initializePagerStore(loaded, createDirectory = mkdirSync) {
91
+ try {
92
+ createDirectory(resolve(loaded.config.databasePath, ".."), { recursive: true });
93
+ }
94
+ catch (error) {
95
+ const reason = error instanceof Error ? error.message : String(error);
96
+ throw new ConfigurationError(loaded.filePath ?? join(loaded.storeDirectory, "config.json"), "databasePath", reason);
97
+ }
98
+ }
99
+ function parseConfigurationFile(path, text) {
100
+ let parsed;
101
+ try {
102
+ parsed = JSON.parse(text);
103
+ }
104
+ catch (error) {
105
+ throw new ConfigurationError(path, "$", error instanceof Error ? error.message : "malformed JSON");
106
+ }
107
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
108
+ throw new ConfigurationError(path, "$", "expected an object");
109
+ const record = parsed;
110
+ for (const key of Object.keys(record)) {
111
+ if (!configKeys.includes(key))
112
+ throw new ConfigurationError(path, key, "unknown key");
113
+ }
114
+ for (const key of configKeys) {
115
+ const value = record[key];
116
+ if (value !== undefined)
117
+ validateValue(path, key, value);
118
+ }
119
+ return record;
120
+ }
121
+ function parseEnvironment(path, environment) {
122
+ const result = {};
123
+ for (const key of configKeys) {
124
+ const environmentKey = `PAGER_${key.replace(/[A-Z]/gu, (letter) => `_${letter}`).toUpperCase()}`;
125
+ const raw = environment[environmentKey];
126
+ if (raw === undefined)
127
+ continue;
128
+ const value = stringKeys.has(key) ? raw : Number(raw);
129
+ validateValue(path, key, value);
130
+ Object.assign(result, { [key]: value });
131
+ }
132
+ if (environment.PAGER_DB !== undefined) {
133
+ validateValue(path, "databasePath", environment.PAGER_DB);
134
+ result.databasePath = environment.PAGER_DB;
135
+ }
136
+ return result;
137
+ }
138
+ function validateResolvedConfiguration(path, config) {
139
+ for (const key of configKeys)
140
+ validateValue(path, key, config[key]);
141
+ }
142
+ function validateValue(path, key, value) {
143
+ if (stringKeys.has(key)) {
144
+ if (typeof value !== "string" || value.length === 0)
145
+ throw new ConfigurationError(path, key, "expected a non-empty string");
146
+ if (pathKeys.has(key) && !isAbsolute(value))
147
+ throw new ConfigurationError(path, key, "path must be absolute");
148
+ if (key === "bindHost" && value !== PAGER_DEFAULTS.bindHost)
149
+ throw new ConfigurationError(path, key, "v1 services must bind to loopback 127.0.0.1");
150
+ return;
151
+ }
152
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0)
153
+ throw new ConfigurationError(path, key, "expected a non-negative integer");
154
+ if (portKeys.has(key) && value > 65_535)
155
+ throw new ConfigurationError(path, key, "port must be in range 0..65535");
156
+ }
@@ -0,0 +1,8 @@
1
+ /** Names the broker endpoint supplied to launched Claude sessions. */
2
+ export declare const CLAUDE_BROKER_URL_ENVIRONMENT = "PAGER_BROKER_URL";
3
+ /** Names the single-use messaging attachment consumed by the stdio gateway. */
4
+ export declare const CLAUDE_MESSAGING_RESERVATION_ENVIRONMENT = "PAGER_ATTACH_RESERVATION";
5
+ /** Names the inbox socket Claude Code exports to every process it starts for a session. */
6
+ export declare const CLAUDE_INBOX_SOCKET_ENVIRONMENT = "CLAUDE_CODE_MESSAGING_SOCKET";
7
+ /** Names the per-session token Claude Code exports beside the inbox socket. */
8
+ export declare const CLAUDE_INBOX_TOKEN_ENVIRONMENT = "CLAUDE_CODE_MESSAGING_TOKEN";
@@ -0,0 +1,9 @@
1
+ import { PAGER_ATTACH_RESERVATION_ENVIRONMENT, PAGER_BROKER_URL_ENVIRONMENT } from "../../launch/environment.js";
2
+ /** Names the broker endpoint supplied to launched Claude sessions. */
3
+ export const CLAUDE_BROKER_URL_ENVIRONMENT = PAGER_BROKER_URL_ENVIRONMENT;
4
+ /** Names the single-use messaging attachment consumed by the stdio gateway. */
5
+ export const CLAUDE_MESSAGING_RESERVATION_ENVIRONMENT = PAGER_ATTACH_RESERVATION_ENVIRONMENT;
6
+ /** Names the inbox socket Claude Code exports to every process it starts for a session. */
7
+ export const CLAUDE_INBOX_SOCKET_ENVIRONMENT = "CLAUDE_CODE_MESSAGING_SOCKET";
8
+ /** Names the per-session token Claude Code exports beside the inbox socket. */
9
+ export const CLAUDE_INBOX_TOKEN_ENVIRONMENT = "CLAUDE_CODE_MESSAGING_TOKEN";
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env bun
2
+ import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
3
+ import { readRuntimeEnvironment } from "../../launch/environment.js";
4
+ import { ensureBrokerService } from "../../launch/on-demand.js";
5
+ import { assertSupportedBunRuntime } from "../../launch/runtime.js";
6
+ import { ClaudeMcpGateway, createClaudeDiscoveryServer, createClaudeGatewayServer, createClaudeGatewayUpstream, rejoinKeyAfterRefusedAttach, retryBrokerUrlAfterUnreachableLaunch, } from "./gateway.js";
7
+ import { registerClaudeAgent, registerClaudeConversation, resolveClaudeGatewayIdentity, } from "./gateway-identity.js";
8
+ import { unattachedCause } from "./instructions.js";
9
+ import { claudeContinuedSessionKey } from "./session-continuation.js";
10
+ import { claudeSessionKey } from "./session-identity.js";
11
+ import { claudeDeliveryAddress } from "./socket-delivery.js";
12
+ try {
13
+ assertSupportedBunRuntime();
14
+ const workspacePath = process.cwd();
15
+ const identityPorts = {
16
+ environment: process.env,
17
+ workspacePath,
18
+ sessionKey: () => claudeSessionKey(process.env),
19
+ continuedSessionKey: () => claudeContinuedSessionKey(process.env, workspacePath),
20
+ brokerUrl: async () => (await ensureBrokerService(readRuntimeEnvironment())).mcpUrl,
21
+ register: (brokerUrl, request) => registerClaudeAgent(fetch, brokerUrl, request),
22
+ };
23
+ const resolution = await resolveClaudeGatewayIdentity(identityPorts);
24
+ if (resolution.attached) {
25
+ let gateway;
26
+ try {
27
+ gateway = await startAttachedGateway(resolution);
28
+ }
29
+ catch (error) {
30
+ // Claude Code restarts a stdio server with the environment of the original launch, whose
31
+ // reservation the first process already spent. The conversation key is what still identifies
32
+ // this session, so it registers by key and rejoins the identity the launch bound the key to.
33
+ const sessionKey = rejoinKeyAfterRefusedAttach(resolution, error);
34
+ if (sessionKey !== undefined) {
35
+ try {
36
+ const registered = await registerClaudeConversation(identityPorts, resolution.brokerUrl, sessionKey);
37
+ gateway = await startAttachedGateway({
38
+ ...resolution,
39
+ reservation: registered.reservation,
40
+ reservationOrigin: "registration",
41
+ });
42
+ }
43
+ catch (retryError) {
44
+ await serveUnattached({
45
+ attached: false,
46
+ reason: "registration_failed",
47
+ brokerUrl: resolution.brokerUrl,
48
+ detail: retryError instanceof Error ? retryError.message : String(retryError),
49
+ });
50
+ }
51
+ }
52
+ else {
53
+ // A launch broker that is unreachable is brought back and the same reservation retried
54
+ // once, since its database persists across a restart.
55
+ const runtime = readRuntimeEnvironment();
56
+ const brokerUrl = await retryBrokerUrlAfterUnreachableLaunch(resolution, error, {
57
+ configuredBrokerUrl: `http://${runtime.config.bindHost}:${String(runtime.config.mcpPort)}/mcp`,
58
+ ensureBrokerService: () => ensureBrokerService(runtime),
59
+ });
60
+ if (brokerUrl !== undefined) {
61
+ try {
62
+ gateway = await startAttachedGateway({ ...resolution, brokerUrl });
63
+ }
64
+ catch {
65
+ throw error;
66
+ }
67
+ }
68
+ else {
69
+ throw error;
70
+ }
71
+ }
72
+ }
73
+ if (gateway) {
74
+ const attached = gateway;
75
+ const shutdown = () => {
76
+ void attached.close().finally(() => process.exit(0));
77
+ };
78
+ process.once("SIGINT", shutdown);
79
+ process.once("SIGTERM", shutdown);
80
+ }
81
+ }
82
+ else {
83
+ await serveUnattached(resolution);
84
+ }
85
+ }
86
+ catch (error) {
87
+ process.stderr.write(`pager Claude gateway failed: ${error instanceof Error ? error.message : String(error)}\n`);
88
+ process.exitCode = 1;
89
+ }
90
+ /**
91
+ * Serve Claude a tool-less server carrying the cause, and say the same on stderr.
92
+ * @param unattached - Cause this process resolved.
93
+ */
94
+ async function serveUnattached(unattached) {
95
+ process.stderr.write(`pager Claude gateway is serving no tools: ${unattachedCause(unattached)}\n`);
96
+ const discoveryServer = createClaudeDiscoveryServer(unattached);
97
+ const shutdown = () => {
98
+ void discoveryServer.close().finally(() => process.exit(0));
99
+ };
100
+ process.once("SIGINT", shutdown);
101
+ process.once("SIGTERM", shutdown);
102
+ await discoveryServer.connect(new StdioServerTransport());
103
+ }
104
+ /**
105
+ * Attach to the broker with the given credentials and begin serving Claude over stdio.
106
+ * @param identity - Endpoint, reservation, and conversation key to attach with.
107
+ * @returns - Running gateway; rejects, with the upstream released, when the attach is refused.
108
+ */
109
+ async function startAttachedGateway(identity) {
110
+ const deliveryAddress = claudeDeliveryAddress(process.env);
111
+ const upstream = createClaudeGatewayUpstream(identity.brokerUrl, identity.reservation, {
112
+ ...(identity.sessionKey ? { sessionKey: identity.sessionKey } : {}),
113
+ ...(deliveryAddress.known ? { deliveryAddress: deliveryAddress.value } : {}),
114
+ scheduler: {
115
+ schedule: (delayMs, task) => {
116
+ const timeout = setTimeout(() => {
117
+ void task().catch(() => undefined);
118
+ }, delayMs);
119
+ timeout.unref();
120
+ return () => {
121
+ clearTimeout(timeout);
122
+ };
123
+ },
124
+ },
125
+ });
126
+ const server = createClaudeGatewayServer(upstream, identity.brokerUrl);
127
+ const gateway = new ClaudeMcpGateway(upstream, server, new StdioServerTransport());
128
+ try {
129
+ await gateway.start();
130
+ }
131
+ catch (error) {
132
+ await gateway.close().catch(() => undefined);
133
+ throw error;
134
+ }
135
+ return gateway;
136
+ }