@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,134 @@
1
+ /** Credentials one gateway process uses to reach and attach to the broker. */
2
+ export type ClaudeGatewayIdentity = {
3
+ /** Reports that this process holds what it needs to attach. */
4
+ attached: true;
5
+ /** Locates the broker's MCP endpoint. */
6
+ brokerUrl: string;
7
+ /** Attaches this process to its agent identity, single-use. */
8
+ reservation: string;
9
+ /**
10
+ * Where the reservation came from. A launch's reservation was minted before this process existed and
11
+ * may already have been spent by a predecessor; one from this process's own registration was not.
12
+ */
13
+ reservationOrigin: "launch" | "registration";
14
+ /** Identifies the conversation, presented at attach so a resume rejoins rather than forks. */
15
+ sessionKey?: string;
16
+ };
17
+ /** Why a gateway process serves Claude no tools, each cause a distinct decision rather than a fallthrough. */
18
+ export type ClaudeGatewayUnattached = {
19
+ attached: false;
20
+ /** Claude Code exported no conversation id, so the broker cannot be told which conversation this is. */
21
+ reason: "conversation_unidentified";
22
+ } | {
23
+ attached: false;
24
+ /** No broker service could be adopted or started. */
25
+ reason: "broker_unavailable";
26
+ /** Failure text from the service start. */
27
+ detail: string;
28
+ } | {
29
+ attached: false;
30
+ /** The broker refused or failed the registration of this conversation. */
31
+ reason: "registration_failed";
32
+ /** Endpoint the registration was sent to. */
33
+ brokerUrl: string;
34
+ /** Failure text from the registration. */
35
+ detail: string;
36
+ };
37
+ /** Outcome of resolving a gateway process's identity: credentials, or the reason it has none. */
38
+ export type ClaudeGatewayResolution = ClaudeGatewayIdentity | ClaudeGatewayUnattached;
39
+ /** Supplies the boundaries the gateway consults before it can attach. */
40
+ export type ClaudeGatewayIdentityPorts = {
41
+ /** Supplies the environment Claude Code exported to this stdio child. */
42
+ environment: Readonly<NodeJS.ProcessEnv>;
43
+ /** Names the workspace this conversation belongs to. */
44
+ workspacePath: string;
45
+ /** Resolves the conversation key, or nothing when this process cannot prove which one it is. */
46
+ sessionKey(): string | undefined;
47
+ /** Resolves the key of the conversation this one continues, or nothing when it continues none. */
48
+ continuedSessionKey(): string | undefined;
49
+ /**
50
+ * Report the broker endpoint, starting the service when none is running.
51
+ * @returns - Endpoint of the adopted or started service; rejects when neither is possible.
52
+ */
53
+ brokerUrl(): Promise<string>;
54
+ /**
55
+ * Register or rejoin an identity through the broker's unauthenticated registration boundary.
56
+ * @param brokerUrl - Endpoint published by the running service.
57
+ * @param request - Workspace and conversation key identifying the agent to register or rejoin, and
58
+ * the key of the conversation it continues, when it continues one.
59
+ * @returns - Single-use messaging attachment for the resolved identity.
60
+ */
61
+ register(brokerUrl: string, request: ClaudeRegistrationRequest): Promise<{
62
+ reservation: string;
63
+ }>;
64
+ };
65
+ /** What one registration tells the broker about the conversation registering. */
66
+ export type ClaudeRegistrationRequest = {
67
+ /** Names the workspace the conversation belongs to. */
68
+ workspacePath: string;
69
+ /** Identifies the conversation registering. */
70
+ sessionKey: string;
71
+ /** Identifies the conversation this one continues, or nothing when it continues none. */
72
+ continuesSessionKey: string | undefined;
73
+ };
74
+ /** What the environment alone says about how a gateway process can attach, before any broker is consulted. */
75
+ export type ClaudeAttachSource = {
76
+ /** A launch exported the endpoint and reservation pair; the key, when present, rides along. */
77
+ source: "launch";
78
+ brokerUrl: string;
79
+ reservation: string;
80
+ sessionKey?: string;
81
+ } | {
82
+ /** Only the conversation key identifies this process, against whatever service the broker port finds. */
83
+ source: "conversation";
84
+ sessionKey: string;
85
+ } | {
86
+ /** Nothing in the environment identifies this process to a broker. */
87
+ source: "none";
88
+ };
89
+ /**
90
+ * Read from the environment how a gateway process can attach, without consulting a broker.
91
+ *
92
+ * Shared by the gateway and the session start hook so both answer "can this session attach" from
93
+ * one rule: a launch counts only as the endpoint and reservation pair, and a conversation id counts
94
+ * only once it qualifies as a broker key.
95
+ * @param environment - Environment Claude Code exported to a process it started for a session.
96
+ * @param sessionKey - Qualified conversation key, or nothing when the environment yields none.
97
+ * @returns - Launch credentials, the key alone, or nothing.
98
+ */
99
+ export declare function claudeAttachSource(environment: Readonly<NodeJS.ProcessEnv>, sessionKey: string | undefined): ClaudeAttachSource;
100
+ /**
101
+ * Resolve how this gateway process reaches its agent identity.
102
+ *
103
+ * A launch publishes an endpoint and a reservation, and the conversation key rides along so the broker
104
+ * can bind it. Anything else — a resumed conversation, a Claude session the person started
105
+ * themselves, or a restarted stdio child whose launch reservation is spent — registers by the
106
+ * conversation key against the service the broker port adopts or starts. A launch endpoint without
107
+ * its reservation is not trusted on its own: the service it named may have stopped since, and the
108
+ * broker port is what knows whether one is running. An unattached outcome names its cause so the
109
+ * process can report it where Claude and the person will see it.
110
+ * @param ports - Environment, workspace, key resolution, discovery, and registration boundaries.
111
+ * @returns - Credentials to attach with, or the reason this process must stay unattached.
112
+ */
113
+ export declare function resolveClaudeGatewayIdentity(ports: ClaudeGatewayIdentityPorts): Promise<ClaudeGatewayResolution>;
114
+ /**
115
+ * Register a conversation by its key, presenting the conversation it continues when there is one.
116
+ * @param ports - Workspace, continuation lookup, and registration boundaries.
117
+ * @param brokerUrl - Endpoint published by the running service.
118
+ * @param sessionKey - Key of the conversation registering.
119
+ * @returns - Single-use messaging attachment for the resolved identity.
120
+ */
121
+ export declare function registerClaudeConversation(ports: Pick<ClaudeGatewayIdentityPorts, "workspacePath" | "continuedSessionKey" | "register">, brokerUrl: string, sessionKey: string): Promise<{
122
+ reservation: string;
123
+ }>;
124
+ /**
125
+ * Register or rejoin a Claude identity over the broker's loopback registration boundary.
126
+ * @param fetchImplementation - HTTP operation used to reach the loopback broker.
127
+ * @param brokerUrl - Endpoint published by the running service.
128
+ * @param request - Workspace and conversation key identifying the agent to register or rejoin, and
129
+ * the key of the conversation it continues, when it continues one.
130
+ * @returns - Single-use messaging attachment for the resolved identity.
131
+ */
132
+ export declare function registerClaudeAgent(fetchImplementation: typeof fetch, brokerUrl: string, request: ClaudeRegistrationRequest): Promise<{
133
+ reservation: string;
134
+ }>;
@@ -0,0 +1,103 @@
1
+ import { registerAgent } from "../../messaging/delivery-client.js";
2
+ import { CLAUDE_BROKER_URL_ENVIRONMENT, CLAUDE_MESSAGING_RESERVATION_ENVIRONMENT } from "./environment.js";
3
+ /**
4
+ * Read from the environment how a gateway process can attach, without consulting a broker.
5
+ *
6
+ * Shared by the gateway and the session start hook so both answer "can this session attach" from
7
+ * one rule: a launch counts only as the endpoint and reservation pair, and a conversation id counts
8
+ * only once it qualifies as a broker key.
9
+ * @param environment - Environment Claude Code exported to a process it started for a session.
10
+ * @param sessionKey - Qualified conversation key, or nothing when the environment yields none.
11
+ * @returns - Launch credentials, the key alone, or nothing.
12
+ */
13
+ export function claudeAttachSource(environment, sessionKey) {
14
+ const brokerUrl = environment[CLAUDE_BROKER_URL_ENVIRONMENT];
15
+ const reservation = environment[CLAUDE_MESSAGING_RESERVATION_ENVIRONMENT];
16
+ if (brokerUrl && reservation)
17
+ return { source: "launch", brokerUrl, reservation, ...(sessionKey ? { sessionKey } : {}) };
18
+ if (sessionKey)
19
+ return { source: "conversation", sessionKey };
20
+ return { source: "none" };
21
+ }
22
+ /**
23
+ * Resolve how this gateway process reaches its agent identity.
24
+ *
25
+ * A launch publishes an endpoint and a reservation, and the conversation key rides along so the broker
26
+ * can bind it. Anything else — a resumed conversation, a Claude session the person started
27
+ * themselves, or a restarted stdio child whose launch reservation is spent — registers by the
28
+ * conversation key against the service the broker port adopts or starts. A launch endpoint without
29
+ * its reservation is not trusted on its own: the service it named may have stopped since, and the
30
+ * broker port is what knows whether one is running. An unattached outcome names its cause so the
31
+ * process can report it where Claude and the person will see it.
32
+ * @param ports - Environment, workspace, key resolution, discovery, and registration boundaries.
33
+ * @returns - Credentials to attach with, or the reason this process must stay unattached.
34
+ */
35
+ export async function resolveClaudeGatewayIdentity(ports) {
36
+ const attach = claudeAttachSource(ports.environment, ports.sessionKey());
37
+ if (attach.source === "launch") {
38
+ return {
39
+ attached: true,
40
+ brokerUrl: attach.brokerUrl,
41
+ reservation: attach.reservation,
42
+ reservationOrigin: "launch",
43
+ ...(attach.sessionKey ? { sessionKey: attach.sessionKey } : {}),
44
+ };
45
+ }
46
+ if (attach.source === "none")
47
+ return { attached: false, reason: "conversation_unidentified" };
48
+ const { sessionKey } = attach;
49
+ let brokerUrl;
50
+ try {
51
+ brokerUrl = await ports.brokerUrl();
52
+ }
53
+ catch (error) {
54
+ return { attached: false, reason: "broker_unavailable", detail: errorText(error) };
55
+ }
56
+ try {
57
+ const registered = await registerClaudeConversation(ports, brokerUrl, sessionKey);
58
+ return {
59
+ attached: true,
60
+ brokerUrl,
61
+ reservation: registered.reservation,
62
+ reservationOrigin: "registration",
63
+ sessionKey,
64
+ };
65
+ }
66
+ catch (error) {
67
+ return { attached: false, reason: "registration_failed", brokerUrl, detail: errorText(error) };
68
+ }
69
+ }
70
+ /**
71
+ * Register a conversation by its key, presenting the conversation it continues when there is one.
72
+ * @param ports - Workspace, continuation lookup, and registration boundaries.
73
+ * @param brokerUrl - Endpoint published by the running service.
74
+ * @param sessionKey - Key of the conversation registering.
75
+ * @returns - Single-use messaging attachment for the resolved identity.
76
+ */
77
+ export function registerClaudeConversation(ports, brokerUrl, sessionKey) {
78
+ return ports.register(brokerUrl, {
79
+ workspacePath: ports.workspacePath,
80
+ sessionKey,
81
+ continuesSessionKey: ports.continuedSessionKey(),
82
+ });
83
+ }
84
+ /**
85
+ * Register or rejoin a Claude identity over the broker's loopback registration boundary.
86
+ * @param fetchImplementation - HTTP operation used to reach the loopback broker.
87
+ * @param brokerUrl - Endpoint published by the running service.
88
+ * @param request - Workspace and conversation key identifying the agent to register or rejoin, and
89
+ * the key of the conversation it continues, when it continues one.
90
+ * @returns - Single-use messaging attachment for the resolved identity.
91
+ */
92
+ export async function registerClaudeAgent(fetchImplementation, brokerUrl, request) {
93
+ const registered = await registerAgent(fetchImplementation, brokerUrl, {
94
+ workspacePath: request.workspacePath,
95
+ implementationId: "claude",
96
+ sessionKey: request.sessionKey,
97
+ ...(request.continuesSessionKey ? { continuesSessionKey: request.continuesSessionKey } : {}),
98
+ });
99
+ return { reservation: registered.messagingReservation };
100
+ }
101
+ function errorText(error) {
102
+ return error instanceof Error ? error.message : String(error);
103
+ }
@@ -0,0 +1,148 @@
1
+ import { Client, type CallToolRequest } from "@modelcontextprotocol/client";
2
+ import { McpServer, type Transport } from "@modelcontextprotocol/server";
3
+ import type { ClaudeGatewayIdentity, ClaudeGatewayUnattached } from "./gateway-identity.js";
4
+ /** Schedules one cancellable gateway lease renewal. */
5
+ export interface ClaudeGatewayScheduler {
6
+ /**
7
+ * Schedule an asynchronous renewal attempt.
8
+ * @param delayMs - Non-negative delay before the attempt begins.
9
+ * @param task - Renewal operation to invoke once after the delay.
10
+ * @returns - Cancellation function that prevents an outstanding attempt.
11
+ */
12
+ schedule(delayMs: number, task: () => Promise<void>): () => void;
13
+ }
14
+ /** Configures authentication for the upstream Streamable HTTP session. */
15
+ export type ClaudeGatewayUpstreamOptions = {
16
+ /**
17
+ * Identifies the Claude conversation this process serves. The broker binds it to the identity the
18
+ * reservation reserves, and attaches an existing owner of the key instead when the conversation
19
+ * already has one, so a relaunched or resumed conversation rejoins rather than forking.
20
+ */
21
+ sessionKey?: string;
22
+ /**
23
+ * Locates this session's inbox for the broker to deliver into. Presented on every request, so a
24
+ * broker that restarted learns it again without this process noticing.
25
+ */
26
+ deliveryAddress?: string;
27
+ /** Owns renewal timing for the lifetime of the upstream connection. */
28
+ scheduler: ClaudeGatewayScheduler;
29
+ };
30
+ /** Supplies the upstream MCP operations forwarded by the Claude stdio server. */
31
+ export interface ClaudeGatewayUpstream {
32
+ /** Establishes the authenticated Streamable HTTP session. */
33
+ connect(): Promise<void>;
34
+ /** Releases the authenticated Streamable HTTP session. */
35
+ close(): Promise<void>;
36
+ /** Lists the tools currently published by the singleton broker. */
37
+ listTools(): ReturnType<Client["listTools"]>;
38
+ /** Calls one broker tool through the authenticated upstream session. */
39
+ callTool(params: CallToolRequest["params"]): ReturnType<Client["callTool"]>;
40
+ }
41
+ /** Connects the Claude-facing stdio server to the singleton Streamable HTTP MCP service. */
42
+ export declare class ClaudeMcpGateway {
43
+ #private;
44
+ /**
45
+ * Construct a gateway from independently testable upstream and downstream transports.
46
+ * @param upstream - Authenticated client for the singleton broker service.
47
+ * @param server - Claude-facing MCP server with forwarding handlers installed.
48
+ * @param transport - Process-owned stdio transport accepted by Claude Code.
49
+ */
50
+ constructor(upstream: ClaudeGatewayUpstream, server: McpServer, transport: Transport);
51
+ /** Establish the upstream session before accepting requests on stdio. */
52
+ start(): Promise<void>;
53
+ /** Close both MCP sessions owned by this gateway process. */
54
+ close(): Promise<void>;
55
+ }
56
+ /**
57
+ * Construct the SDK client that authenticates to the singleton broker over Streamable HTTP.
58
+ * @param brokerUrl - Loopback MCP endpoint published by the singleton service.
59
+ * @param reservation - Single-use attachment credential consumed during first initialization.
60
+ * @param options - Conversation key presented at attach, and the renewal scheduler.
61
+ * @returns - Upstream tool operations for one Claude session.
62
+ */
63
+ export declare function createClaudeGatewayUpstream(brokerUrl: string, reservation: string, options: ClaudeGatewayUpstreamOptions): ClaudeGatewayUpstream;
64
+ /**
65
+ * Construct the Claude-facing MCP server while retaining broker-owned tool discovery and execution.
66
+ * @param upstream - Authenticated operations delegated to the singleton broker.
67
+ * @param brokerUrl - Loopback endpoint named in standing wake instructions.
68
+ * @returns - SDK server ready to connect to a stdio transport.
69
+ */
70
+ export declare function createClaudeGatewayServer(upstream: ClaudeGatewayUpstream, brokerUrl: string): McpServer;
71
+ /**
72
+ * Construct the tool-less server an unattached gateway process answers Claude with.
73
+ *
74
+ * Claude Code expects a working MCP connection from every configured server; a process that exits
75
+ * instead is shown as a failed server with the cause buried in a log, whereas this server's
76
+ * instructions put the cause in front of the session.
77
+ * @param unattached - Cause the gateway process resolved.
78
+ * @returns - SDK server publishing no capabilities and the cause as its instructions.
79
+ */
80
+ export declare function createClaudeDiscoveryServer(unattached: ClaudeGatewayUnattached): McpServer;
81
+ /**
82
+ * Decide whether a refused attach should be retried by registering under the conversation key.
83
+ *
84
+ * Only a launch reservation can have been spent by a predecessor process, and only a refusal that
85
+ * names it as consumed says that is what happened. Every other refusal fails closed: the launch
86
+ * reserved an identity this process cannot reach, and registering another in its place would hide
87
+ * that from the wrapper and the operator.
88
+ * @param identity - Credentials the refused attach was made with.
89
+ * @param error - Rejection from {@link ClaudeGatewayUpstream.connect}.
90
+ * @returns - Conversation key to register under, or nothing when the refusal must stand.
91
+ */
92
+ export declare function rejoinKeyAfterRefusedAttach(identity: ClaudeGatewayIdentity, error: unknown): string | undefined;
93
+ /**
94
+ * Report whether an upstream connect failed because the attach reservation was already spent.
95
+ *
96
+ * The broker answers an attach it refuses with its own error code in the JSON-RPC error data, and
97
+ * `conflict` on an attach means exactly that the reservation was consumed; an unknown or expired one
98
+ * is refused under another code. Matching the code rather than the message keeps this from
99
+ * drifting with wording.
100
+ * @param error - Rejection from {@link ClaudeGatewayUpstream.connect}.
101
+ * @returns - True only when the broker reported the reservation as consumed.
102
+ */
103
+ export declare function attachRefusedAsConsumed(error: unknown): boolean;
104
+ /**
105
+ * Report whether an upstream connect failed because the broker named at launch could not be reached
106
+ * at all, as opposed to reaching it and being refused.
107
+ *
108
+ * The service a launch named can have stopped since without the reservation it minted having gone
109
+ * anywhere: the broker's database persists across a restart, so the same reservation is still good
110
+ * once the service is back. This is the one case the launch endpoint alone cannot answer, and the
111
+ * broker port is what can.
112
+ * @param error - Rejection from {@link ClaudeGatewayUpstream.connect}.
113
+ * @returns - True only when the failure was the transport never reaching the broker at all.
114
+ */
115
+ export declare function attachFailedToReachBroker(error: unknown): boolean;
116
+ /**
117
+ * Report whether a freshly ensured broker is the one a launch actually named.
118
+ *
119
+ * `ensureBrokerService` resolves against the local configuration's own default ports, which need not
120
+ * be the service a launch named: `PAGER_BROKER_URL` can point at a non-default port that has since
121
+ * gone down while the configured default is a different, unrelated broker entirely. Retrying the
122
+ * launch reservation against that other broker would not reach the service that ever minted it, so
123
+ * the CLI's retry only proceeds when the two agree.
124
+ * @param launchBrokerUrl - Endpoint the launch named, from {@link ClaudeGatewayIdentity.brokerUrl}.
125
+ * @param ensuredBrokerUrl - Endpoint {@link ensureBrokerService} resolved to.
126
+ * @returns - True only when the ensured broker is the one the launch named.
127
+ */
128
+ export declare function ensuredBrokerMatchesLaunch(launchBrokerUrl: string, ensuredBrokerUrl: string): boolean;
129
+ /** Ports the unreachable-launch retry needs, kept narrow so a test can inject a spy for either. */
130
+ export type LaunchBrokerRetryPorts = {
131
+ /** Endpoint `ensureBrokerService` would resolve to, built from the local configuration alone. */
132
+ configuredBrokerUrl: string;
133
+ /** Starts or adopts the local broker service; called only once the endpoints are already known to agree. */
134
+ ensureBrokerService: () => Promise<{
135
+ mcpUrl: string;
136
+ }>;
137
+ };
138
+ /**
139
+ * Decide the endpoint to retry a launch attach against, once its broker turned out unreachable.
140
+ *
141
+ * Retries only when the launch named the locally configured broker, and establishes that before
142
+ * `ensureBrokerService`, which starts or adopts a broker process.
143
+ * @param identity - Credentials the refused attach was made with.
144
+ * @param error - Rejection from {@link ClaudeGatewayUpstream.connect}.
145
+ * @param ports - Configured endpoint and ensure operation to retry against.
146
+ * @returns - Endpoint to retry against, or nothing when the retry must not proceed.
147
+ */
148
+ export declare function retryBrokerUrlAfterUnreachableLaunch(identity: ClaudeGatewayIdentity, error: unknown, ports: LaunchBrokerRetryPorts): Promise<string | undefined>;
@@ -0,0 +1,236 @@
1
+ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
2
+ import { McpServer } from "@modelcontextprotocol/server";
3
+ import { LOOPBACK_HOST } from "../../configuration-defaults.js";
4
+ import { ATTACH_HEADER } from "../../broker/protocol.js";
5
+ import { compatibilityToolRequest } from "../../messaging/delivery-client.js";
6
+ import { DELIVERY_ADDRESS_HEADER } from "../../messaging/mcp.js";
7
+ import { PAGER_VERSION } from "../../version.js";
8
+ import { claudeUnattachedInstructions, claudeWakeInstructions } from "./instructions.js";
9
+ /** Connects the Claude-facing stdio server to the singleton Streamable HTTP MCP service. */
10
+ export class ClaudeMcpGateway {
11
+ #upstream;
12
+ #server;
13
+ #transport;
14
+ /**
15
+ * Construct a gateway from independently testable upstream and downstream transports.
16
+ * @param upstream - Authenticated client for the singleton broker service.
17
+ * @param server - Claude-facing MCP server with forwarding handlers installed.
18
+ * @param transport - Process-owned stdio transport accepted by Claude Code.
19
+ */
20
+ constructor(upstream, server, transport) {
21
+ this.#upstream = upstream;
22
+ this.#server = server;
23
+ this.#transport = transport;
24
+ }
25
+ /** Establish the upstream session before accepting requests on stdio. */
26
+ async start() {
27
+ await this.#upstream.connect();
28
+ await this.#server.connect(this.#transport);
29
+ }
30
+ /** Close both MCP sessions owned by this gateway process. */
31
+ async close() {
32
+ await Promise.allSettled([this.#server.close(), this.#upstream.close()]);
33
+ }
34
+ }
35
+ /**
36
+ * Construct the SDK client that authenticates to the singleton broker over Streamable HTTP.
37
+ * @param brokerUrl - Loopback MCP endpoint published by the singleton service.
38
+ * @param reservation - Single-use attachment credential consumed during first initialization.
39
+ * @param options - Conversation key presented at attach, and the renewal scheduler.
40
+ * @returns - Upstream tool operations for one Claude session.
41
+ */
42
+ export function createClaudeGatewayUpstream(brokerUrl, reservation, options) {
43
+ const url = validatedBrokerUrl(brokerUrl);
44
+ if (!reservation)
45
+ throw new Error("Claude gateway authentication is missing");
46
+ const client = new Client({
47
+ name: "pager-claude-gateway",
48
+ version: PAGER_VERSION,
49
+ });
50
+ let session;
51
+ let leaseTtlMs;
52
+ let cancelRenewal;
53
+ let closed = false;
54
+ const scheduleRenewal = () => {
55
+ cancelRenewal?.();
56
+ if (closed || leaseTtlMs === undefined || leaseTtlMs <= 0)
57
+ return;
58
+ cancelRenewal = options.scheduler.schedule(Math.max(1, Math.floor(leaseTtlMs / 2)), async () => {
59
+ try {
60
+ await client.listTools();
61
+ }
62
+ finally {
63
+ scheduleRenewal();
64
+ }
65
+ });
66
+ };
67
+ const brokerFetch = async (input, init) => {
68
+ const headers = new Headers(init?.headers);
69
+ if (session)
70
+ headers.set("mcp-session-id", session.sessionId);
71
+ if (reservation)
72
+ headers.set(ATTACH_HEADER, reservation);
73
+ if (options.sessionKey)
74
+ headers.set("x-pager-session-key", options.sessionKey);
75
+ if (options.deliveryAddress)
76
+ headers.set(DELIVERY_ADDRESS_HEADER, options.deliveryAddress);
77
+ if (session?.resumeToken)
78
+ headers.set("x-pager-resume-token", session.resumeToken);
79
+ if (session?.epoch)
80
+ headers.set("x-pager-epoch", session.epoch);
81
+ let response;
82
+ try {
83
+ response = await fetch(input, { ...init, headers });
84
+ }
85
+ catch (cause) {
86
+ throw new Error(`Cannot reach the Pager broker at ${url.href}. It is not running or not listening there; start it with \`pager\`, then retry.`, { cause });
87
+ }
88
+ const sessionId = response.headers.get("mcp-session-id");
89
+ const resumeToken = response.headers.get("x-pager-resume-token");
90
+ const epoch = response.headers.get("x-pager-epoch");
91
+ const leaseHeader = response.headers.get("x-pager-lease-ttl-ms");
92
+ if (sessionId && resumeToken && epoch)
93
+ session = { sessionId, resumeToken, epoch };
94
+ if (leaseHeader && Number.isSafeInteger(Number(leaseHeader)) && Number(leaseHeader) >= 0) {
95
+ leaseTtlMs = Number(leaseHeader);
96
+ }
97
+ return response;
98
+ };
99
+ const transport = new StreamableHTTPClientTransport(url, { fetch: brokerFetch });
100
+ return {
101
+ connect: async () => {
102
+ await client.connect(transport);
103
+ scheduleRenewal();
104
+ },
105
+ close: async () => {
106
+ closed = true;
107
+ cancelRenewal?.();
108
+ // The model-facing tools/call path refuses connector-only tools, so this travels the
109
+ // compatibility path. A failed notice leaves the lease to lapse, as after a crash.
110
+ if (session) {
111
+ await brokerFetch(url, compatibilityToolRequest("detach", {})).catch(() => undefined);
112
+ }
113
+ await client.close();
114
+ },
115
+ listTools: () => client.listTools(),
116
+ callTool: (params) => client.callTool(params),
117
+ };
118
+ }
119
+ /**
120
+ * Construct the Claude-facing MCP server while retaining broker-owned tool discovery and execution.
121
+ * @param upstream - Authenticated operations delegated to the singleton broker.
122
+ * @param brokerUrl - Loopback endpoint named in standing wake instructions.
123
+ * @returns - SDK server ready to connect to a stdio transport.
124
+ */
125
+ export function createClaudeGatewayServer(upstream, brokerUrl) {
126
+ const server = new McpServer({ name: "pager", version: PAGER_VERSION }, {
127
+ capabilities: { tools: {} },
128
+ instructions: claudeWakeInstructions(validatedBrokerUrl(brokerUrl).toString()),
129
+ });
130
+ server.server.setRequestHandler("tools/list", () => upstream.listTools());
131
+ server.server.setRequestHandler("tools/call", (request) => upstream.callTool(request.params));
132
+ return server;
133
+ }
134
+ /**
135
+ * Construct the tool-less server an unattached gateway process answers Claude with.
136
+ *
137
+ * Claude Code expects a working MCP connection from every configured server; a process that exits
138
+ * instead is shown as a failed server with the cause buried in a log, whereas this server's
139
+ * instructions put the cause in front of the session.
140
+ * @param unattached - Cause the gateway process resolved.
141
+ * @returns - SDK server publishing no capabilities and the cause as its instructions.
142
+ */
143
+ export function createClaudeDiscoveryServer(unattached) {
144
+ return new McpServer({ name: "pager", version: PAGER_VERSION }, { capabilities: {}, instructions: claudeUnattachedInstructions(unattached) });
145
+ }
146
+ /**
147
+ * Decide whether a refused attach should be retried by registering under the conversation key.
148
+ *
149
+ * Only a launch reservation can have been spent by a predecessor process, and only a refusal that
150
+ * names it as consumed says that is what happened. Every other refusal fails closed: the launch
151
+ * reserved an identity this process cannot reach, and registering another in its place would hide
152
+ * that from the wrapper and the operator.
153
+ * @param identity - Credentials the refused attach was made with.
154
+ * @param error - Rejection from {@link ClaudeGatewayUpstream.connect}.
155
+ * @returns - Conversation key to register under, or nothing when the refusal must stand.
156
+ */
157
+ export function rejoinKeyAfterRefusedAttach(identity, error) {
158
+ if (identity.reservationOrigin !== "launch" || !attachRefusedAsConsumed(error))
159
+ return undefined;
160
+ return identity.sessionKey;
161
+ }
162
+ /**
163
+ * Report whether an upstream connect failed because the attach reservation was already spent.
164
+ *
165
+ * The broker answers an attach it refuses with its own error code in the JSON-RPC error data, and
166
+ * `conflict` on an attach means exactly that the reservation was consumed; an unknown or expired one
167
+ * is refused under another code. Matching the code rather than the message keeps this from
168
+ * drifting with wording.
169
+ * @param error - Rejection from {@link ClaudeGatewayUpstream.connect}.
170
+ * @returns - True only when the broker reported the reservation as consumed.
171
+ */
172
+ export function attachRefusedAsConsumed(error) {
173
+ if (typeof error !== "object" || error === null || !("data" in error))
174
+ return false;
175
+ const data = error.data;
176
+ if (typeof data !== "object" || data === null || !("code" in data))
177
+ return false;
178
+ const code = "conflict";
179
+ return data.code === code;
180
+ }
181
+ /**
182
+ * Report whether an upstream connect failed because the broker named at launch could not be reached
183
+ * at all, as opposed to reaching it and being refused.
184
+ *
185
+ * The service a launch named can have stopped since without the reservation it minted having gone
186
+ * anywhere: the broker's database persists across a restart, so the same reservation is still good
187
+ * once the service is back. This is the one case the launch endpoint alone cannot answer, and the
188
+ * broker port is what can.
189
+ * @param error - Rejection from {@link ClaudeGatewayUpstream.connect}.
190
+ * @returns - True only when the failure was the transport never reaching the broker at all.
191
+ */
192
+ export function attachFailedToReachBroker(error) {
193
+ return error instanceof Error && error.message.startsWith("Cannot reach the Pager broker at ");
194
+ }
195
+ /**
196
+ * Report whether a freshly ensured broker is the one a launch actually named.
197
+ *
198
+ * `ensureBrokerService` resolves against the local configuration's own default ports, which need not
199
+ * be the service a launch named: `PAGER_BROKER_URL` can point at a non-default port that has since
200
+ * gone down while the configured default is a different, unrelated broker entirely. Retrying the
201
+ * launch reservation against that other broker would not reach the service that ever minted it, so
202
+ * the CLI's retry only proceeds when the two agree.
203
+ * @param launchBrokerUrl - Endpoint the launch named, from {@link ClaudeGatewayIdentity.brokerUrl}.
204
+ * @param ensuredBrokerUrl - Endpoint {@link ensureBrokerService} resolved to.
205
+ * @returns - True only when the ensured broker is the one the launch named.
206
+ */
207
+ export function ensuredBrokerMatchesLaunch(launchBrokerUrl, ensuredBrokerUrl) {
208
+ return ensuredBrokerUrl === launchBrokerUrl;
209
+ }
210
+ /**
211
+ * Decide the endpoint to retry a launch attach against, once its broker turned out unreachable.
212
+ *
213
+ * Retries only when the launch named the locally configured broker, and establishes that before
214
+ * `ensureBrokerService`, which starts or adopts a broker process.
215
+ * @param identity - Credentials the refused attach was made with.
216
+ * @param error - Rejection from {@link ClaudeGatewayUpstream.connect}.
217
+ * @param ports - Configured endpoint and ensure operation to retry against.
218
+ * @returns - Endpoint to retry against, or nothing when the retry must not proceed.
219
+ */
220
+ export async function retryBrokerUrlAfterUnreachableLaunch(identity, error, ports) {
221
+ if (identity.reservationOrigin !== "launch" || !attachFailedToReachBroker(error))
222
+ return undefined;
223
+ if (!ensuredBrokerMatchesLaunch(identity.brokerUrl, ports.configuredBrokerUrl))
224
+ return undefined;
225
+ const ensured = await ports.ensureBrokerService().catch(() => undefined);
226
+ if (!ensured || !ensuredBrokerMatchesLaunch(identity.brokerUrl, ensured.mcpUrl))
227
+ return undefined;
228
+ return ensured.mcpUrl;
229
+ }
230
+ function validatedBrokerUrl(value) {
231
+ const url = new URL(value);
232
+ if (url.protocol !== "http:" || url.hostname !== LOOPBACK_HOST || url.pathname !== "/mcp") {
233
+ throw new Error("Claude gateway broker URL must identify the loopback MCP endpoint");
234
+ }
235
+ return url;
236
+ }
@@ -0,0 +1,33 @@
1
+ import type { ClaudeGatewayUnattached } from "./gateway-identity.js";
2
+ /**
3
+ * Build the standing guidance a Claude session needs before its first delivery arrives.
4
+ *
5
+ * Two clauses are load-bearing against observed model behaviour rather than stylistic. Without the
6
+ * clause granting the delivery authority, it is declined as an automated event that is not a user
7
+ * instruction. Without the clause suppressing notification, the host's own framing of an inbound
8
+ * message invites a push the operator did not ask for.
9
+ * @param brokerUrl - Loopback MCP endpoint exposed to the harness, or undefined when a directly
10
+ * started session has not yet resolved one; the broker tools reach the session through its MCP
11
+ * server either way, so the endpoint line is informational.
12
+ * @returns - Standing instructions naming both delivery forms, the acknowledgement they require, and
13
+ * the alias to keep in step with the session's display name.
14
+ */
15
+ export declare function claudeWakeInstructions(brokerUrl: string | undefined): string;
16
+ /**
17
+ * Build the guidance a Claude session receives when its gateway holds no broker identity.
18
+ *
19
+ * Served in place of the standing instructions so the session is told, in the same channel that
20
+ * would otherwise announce the tools, that none exist and why, rather than discovering it through a
21
+ * tool that is not there.
22
+ * @param unattached - Cause the gateway resolved, with the detail it carries.
23
+ * @returns - Instructions naming the cause and what the session must not attempt.
24
+ */
25
+ export declare function claudeUnattachedInstructions(unattached: ClaudeGatewayUnattached): string;
26
+ /**
27
+ * State why a gateway process holds no broker identity, in the words the session is given.
28
+ *
29
+ * Shared with the gateway's stderr line so a log entry reads the same as what the model was told.
30
+ * @param unattached - Cause the gateway resolved, with the detail it carries.
31
+ * @returns - One sentence naming the cause.
32
+ */
33
+ export declare function unattachedCause(unattached: ClaudeGatewayUnattached): string;