@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.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/src/broker/address.d.ts +49 -0
- package/dist/src/broker/address.js +110 -0
- package/dist/src/broker/broker.d.ts +665 -0
- package/dist/src/broker/broker.js +2270 -0
- package/dist/src/broker/database.d.ts +81 -0
- package/dist/src/broker/database.js +69 -0
- package/dist/src/broker/operator-projection.d.ts +291 -0
- package/dist/src/broker/operator-projection.js +722 -0
- package/dist/src/broker/protocol.d.ts +237 -0
- package/dist/src/broker/protocol.js +311 -0
- package/dist/src/broker/schema.d.ts +94 -0
- package/dist/src/broker/schema.js +454 -0
- package/dist/src/broker/snapshot.d.ts +171 -0
- package/dist/src/broker/snapshot.js +78 -0
- package/dist/src/configuration-defaults.d.ts +64 -0
- package/dist/src/configuration-defaults.js +48 -0
- package/dist/src/configuration.d.ts +76 -0
- package/dist/src/configuration.js +156 -0
- package/dist/src/implementations/claude/environment.d.ts +8 -0
- package/dist/src/implementations/claude/environment.js +9 -0
- package/dist/src/implementations/claude/gateway-cli.d.ts +2 -0
- package/dist/src/implementations/claude/gateway-cli.js +136 -0
- package/dist/src/implementations/claude/gateway-identity.d.ts +134 -0
- package/dist/src/implementations/claude/gateway-identity.js +103 -0
- package/dist/src/implementations/claude/gateway.d.ts +148 -0
- package/dist/src/implementations/claude/gateway.js +236 -0
- package/dist/src/implementations/claude/instructions.d.ts +33 -0
- package/dist/src/implementations/claude/instructions.js +64 -0
- package/dist/src/implementations/claude/manifest.d.ts +33 -0
- package/dist/src/implementations/claude/manifest.js +108 -0
- package/dist/src/implementations/claude/plugin-directory.d.ts +80 -0
- package/dist/src/implementations/claude/plugin-directory.js +137 -0
- package/dist/src/implementations/claude/session-continuation.d.ts +25 -0
- package/dist/src/implementations/claude/session-continuation.js +125 -0
- package/dist/src/implementations/claude/session-identity.d.ts +25 -0
- package/dist/src/implementations/claude/session-identity.js +31 -0
- package/dist/src/implementations/claude/session-start-cli.d.ts +2 -0
- package/dist/src/implementations/claude/session-start-cli.js +16 -0
- package/dist/src/implementations/claude/socket-delivery.d.ts +52 -0
- package/dist/src/implementations/claude/socket-delivery.js +159 -0
- package/dist/src/implementations/model.d.ts +162 -0
- package/dist/src/implementations/model.js +1 -0
- package/dist/src/implementations/opencode/adapter.d.ts +71 -0
- package/dist/src/implementations/opencode/adapter.js +94 -0
- package/dist/src/implementations/opencode/environment.d.ts +8 -0
- package/dist/src/implementations/opencode/environment.js +9 -0
- package/dist/src/implementations/opencode/manifest.d.ts +30 -0
- package/dist/src/implementations/opencode/manifest.js +93 -0
- package/dist/src/implementations/opencode/plugin-api.d.ts +120 -0
- package/dist/src/implementations/opencode/plugin-api.js +1 -0
- package/dist/src/implementations/opencode/plugin.d.ts +38 -0
- package/dist/src/implementations/opencode/plugin.js +303 -0
- package/dist/src/implementations/opencode/self-close-tool.d.ts +9 -0
- package/dist/src/implementations/opencode/self-close-tool.js +31 -0
- package/dist/src/implementations/orca/terminal.d.ts +48 -0
- package/dist/src/implementations/orca/terminal.js +154 -0
- package/dist/src/implementations/pi/broker-client.d.ts +56 -0
- package/dist/src/implementations/pi/broker-client.js +156 -0
- package/dist/src/implementations/pi/environment.d.ts +8 -0
- package/dist/src/implementations/pi/environment.js +9 -0
- package/dist/src/implementations/pi/extension.d.ts +57 -0
- package/dist/src/implementations/pi/extension.js +253 -0
- package/dist/src/implementations/pi/host-slots.d.ts +18 -0
- package/dist/src/implementations/pi/host-slots.js +27 -0
- package/dist/src/implementations/pi/manifest.d.ts +11 -0
- package/dist/src/implementations/pi/manifest.js +61 -0
- package/dist/src/implementations/pi/self-close-tool.d.ts +7 -0
- package/dist/src/implementations/pi/self-close-tool.js +27 -0
- package/dist/src/implementations/registry.d.ts +47 -0
- package/dist/src/implementations/registry.js +157 -0
- package/dist/src/implementations/session-key.d.ts +13 -0
- package/dist/src/implementations/session-key.js +19 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.js +26 -0
- package/dist/src/launch/broker-lock.d.ts +257 -0
- package/dist/src/launch/broker-lock.js +644 -0
- package/dist/src/launch/cli.d.ts +37 -0
- package/dist/src/launch/cli.js +317 -0
- package/dist/src/launch/environment.d.ts +45 -0
- package/dist/src/launch/environment.js +48 -0
- package/dist/src/launch/launch.d.ts +235 -0
- package/dist/src/launch/launch.js +223 -0
- package/dist/src/launch/on-demand.d.ts +202 -0
- package/dist/src/launch/on-demand.js +435 -0
- package/dist/src/launch/runtime-composition.d.ts +88 -0
- package/dist/src/launch/runtime-composition.js +253 -0
- package/dist/src/launch/runtime.d.ts +79 -0
- package/dist/src/launch/runtime.js +196 -0
- package/dist/src/launch/service-system.d.ts +11 -0
- package/dist/src/launch/service-system.js +39 -0
- package/dist/src/launch/service.d.ts +173 -0
- package/dist/src/launch/service.js +492 -0
- package/dist/src/launch/skill.d.ts +57 -0
- package/dist/src/launch/skill.js +81 -0
- package/dist/src/launch/terminal.d.ts +26 -0
- package/dist/src/launch/terminal.js +43 -0
- package/dist/src/launch/workspace-branch.d.ts +10 -0
- package/dist/src/launch/workspace-branch.js +29 -0
- package/dist/src/messaging/delivery-client.d.ts +144 -0
- package/dist/src/messaging/delivery-client.js +261 -0
- package/dist/src/messaging/delivery.d.ts +75 -0
- package/dist/src/messaging/delivery.js +22 -0
- package/dist/src/messaging/inbox-socket.d.ts +98 -0
- package/dist/src/messaging/inbox-socket.js +278 -0
- package/dist/src/messaging/mcp.d.ts +53 -0
- package/dist/src/messaging/mcp.js +501 -0
- package/dist/src/messaging/notifications.d.ts +231 -0
- package/dist/src/messaging/notifications.js +489 -0
- package/dist/src/messaging/push-delivery.d.ts +77 -0
- package/dist/src/messaging/push-delivery.js +187 -0
- package/dist/src/messaging/tools.d.ts +11 -0
- package/dist/src/messaging/tools.js +102 -0
- package/dist/src/messaging/wake.d.ts +23 -0
- package/dist/src/messaging/wake.js +39 -0
- package/dist/src/operator/agent-projection.d.ts +102 -0
- package/dist/src/operator/agent-projection.js +249 -0
- package/dist/src/operator/commands.d.ts +50 -0
- package/dist/src/operator/commands.js +199 -0
- package/dist/src/operator/diagnostics.d.ts +21 -0
- package/dist/src/operator/diagnostics.js +22 -0
- package/dist/src/operator/protocol.d.ts +511 -0
- package/dist/src/operator/protocol.js +49 -0
- package/dist/src/operator/repository-system.d.ts +14 -0
- package/dist/src/operator/repository-system.js +150 -0
- package/dist/src/operator/repository.d.ts +79 -0
- package/dist/src/operator/repository.js +72 -0
- package/dist/src/operator/traffic-projection.d.ts +19 -0
- package/dist/src/operator/traffic-projection.js +229 -0
- package/dist/src/operator/transport.d.ts +87 -0
- package/dist/src/operator/transport.js +549 -0
- package/dist/src/operator/update-store.d.ts +53 -0
- package/dist/src/operator/update-store.js +162 -0
- package/dist/src/operator/updates.d.ts +94 -0
- package/dist/src/operator/updates.js +280 -0
- package/dist/src/operator/validation.d.ts +114 -0
- package/dist/src/operator/validation.js +916 -0
- package/dist/src/shared/guards.d.ts +59 -0
- package/dist/src/shared/guards.js +119 -0
- package/dist/src/shared/http.d.ts +30 -0
- package/dist/src/shared/http.js +50 -0
- package/dist/src/shared/row.d.ts +2 -0
- package/dist/src/shared/row.js +1 -0
- package/dist/src/shared/shell.d.ts +6 -0
- package/dist/src/shared/shell.js +8 -0
- package/dist/src/tui/actions.d.ts +166 -0
- package/dist/src/tui/actions.js +253 -0
- package/dist/src/tui/agent-tree.d.ts +125 -0
- package/dist/src/tui/agent-tree.js +663 -0
- package/dist/src/tui/app.d.ts +38 -0
- package/dist/src/tui/app.js +378 -0
- package/dist/src/tui/chrome.d.ts +86 -0
- package/dist/src/tui/chrome.js +85 -0
- package/dist/src/tui/client.d.ts +41 -0
- package/dist/src/tui/client.js +279 -0
- package/dist/src/tui/composition.d.ts +41 -0
- package/dist/src/tui/composition.js +146 -0
- package/dist/src/tui/message-views.d.ts +94 -0
- package/dist/src/tui/message-views.js +515 -0
- package/dist/src/tui/model.d.ts +414 -0
- package/dist/src/tui/model.js +758 -0
- package/dist/src/tui/presentation.d.ts +350 -0
- package/dist/src/tui/presentation.js +541 -0
- package/dist/src/tui/production.d.ts +79 -0
- package/dist/src/tui/production.js +253 -0
- package/dist/src/tui/run-terminal.d.ts +32 -0
- package/dist/src/tui/run-terminal.js +54 -0
- package/dist/src/tui/runtime.d.ts +113 -0
- package/dist/src/tui/runtime.js +189 -0
- package/dist/src/tui/theme.d.ts +49 -0
- package/dist/src/tui/theme.js +55 -0
- package/dist/src/tui/transport.d.ts +31 -0
- package/dist/src/tui/transport.js +255 -0
- package/dist/src/version.d.ts +11 -0
- package/dist/src/version.js +52 -0
- package/package.json +142 -0
- package/skills/pager/SKILL.md +112 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { ATTACH_HEADER } from "../../broker/protocol.js";
|
|
6
|
+
import { deliverToPagerInbox, parsePagerInboxAddress, writePagerInbox, } from "../../messaging/inbox-socket.js";
|
|
7
|
+
import { implementationId } from "../registry.js";
|
|
8
|
+
import { BUN_PATH_ENVIRONMENT, resolveBunExecutable } from "../../launch/runtime.js";
|
|
9
|
+
import { harnessSessionKey } from "../session-key.js";
|
|
10
|
+
import { OPENCODE_AGENT_ID_ENVIRONMENT, OPENCODE_BROKER_URL_ENVIRONMENT, OPENCODE_DELIVERY_RESERVATION_ENVIRONMENT, OPENCODE_MESSAGING_RESERVATION_ENVIRONMENT, } from "./environment.js";
|
|
11
|
+
/**
|
|
12
|
+
* Construct the built-in OpenCode implementation provider.
|
|
13
|
+
* @param ports - Version reader, launch-file, and inbox ports.
|
|
14
|
+
* @returns - Provider owning OpenCode integration behavior.
|
|
15
|
+
*/
|
|
16
|
+
export function createOpenCodeImplementationProvider({ readVersion, launchFiles, writeInbox, }) {
|
|
17
|
+
const version = readVersion();
|
|
18
|
+
return {
|
|
19
|
+
descriptor: {
|
|
20
|
+
id: implementationId("opencode"),
|
|
21
|
+
displayName: "OpenCode",
|
|
22
|
+
version: version ? { known: true, value: version } : { known: false },
|
|
23
|
+
capabilities: ["launch", "notification", "session-identity"],
|
|
24
|
+
},
|
|
25
|
+
launch: {
|
|
26
|
+
supported: true,
|
|
27
|
+
buildPlan: (request) => {
|
|
28
|
+
const bunPath = launchFiles.runtimePath();
|
|
29
|
+
const configPath = launchFiles.writeLaunchConfig(JSON.stringify({
|
|
30
|
+
$schema: "https://opencode.ai/config.json",
|
|
31
|
+
plugin: [new URL("./plugin.js", import.meta.url).href],
|
|
32
|
+
mcp: {
|
|
33
|
+
pager: {
|
|
34
|
+
type: "remote",
|
|
35
|
+
url: request.brokerUrl,
|
|
36
|
+
headers: { [ATTACH_HEADER]: `{env:${OPENCODE_MESSAGING_RESERVATION_ENVIRONMENT}}` },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}, null, 2));
|
|
40
|
+
return Promise.resolve({
|
|
41
|
+
command: "opencode",
|
|
42
|
+
args: [...request.passthroughArgs],
|
|
43
|
+
environment: {
|
|
44
|
+
OPENCODE_CONFIG: configPath,
|
|
45
|
+
[OPENCODE_AGENT_ID_ENVIRONMENT]: request.agentId,
|
|
46
|
+
[OPENCODE_BROKER_URL_ENVIRONMENT]: request.brokerUrl,
|
|
47
|
+
[BUN_PATH_ENVIRONMENT]: bunPath,
|
|
48
|
+
},
|
|
49
|
+
sensitiveEnvironment: {
|
|
50
|
+
[OPENCODE_MESSAGING_RESERVATION_ENVIRONMENT]: request.reservation,
|
|
51
|
+
[OPENCODE_DELIVERY_RESERVATION_ENVIRONMENT]: request.deliveryReservation,
|
|
52
|
+
},
|
|
53
|
+
workspacePath: request.workspacePath,
|
|
54
|
+
title: "Pager — OpenCode",
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
sessionIdentity: {
|
|
59
|
+
supported: true,
|
|
60
|
+
sessionKey: ({ environment, hostSessionId }) => harnessSessionKey("opencode", hostSessionId ?? environment.OPENCODE_SESSION_ID),
|
|
61
|
+
},
|
|
62
|
+
hostRegistration: { supported: false },
|
|
63
|
+
notification: {
|
|
64
|
+
supported: true,
|
|
65
|
+
mode: "host-push",
|
|
66
|
+
parseAddress: parsePagerInboxAddress,
|
|
67
|
+
deliver: (address, wake) => deliverToPagerInbox(writeInbox, address, wake),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function installedVersion() {
|
|
72
|
+
try {
|
|
73
|
+
return execFileSync("opencode", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function systemLaunchFilesPort() {
|
|
80
|
+
return {
|
|
81
|
+
runtimePath: () => resolveBunExecutable(),
|
|
82
|
+
writeLaunchConfig: (content) => {
|
|
83
|
+
const configPath = join(mkdtempSync(join(tmpdir(), "pager-opencode-")), "mcp-config.json");
|
|
84
|
+
writeFileSync(configPath, content);
|
|
85
|
+
return configPath;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export default createOpenCodeImplementationProvider({
|
|
90
|
+
readVersion: installedVersion,
|
|
91
|
+
launchFiles: systemLaunchFilesPort(),
|
|
92
|
+
writeInbox: writePagerInbox,
|
|
93
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Minimal OpenCode plugin API used by the Pager implementation. */
|
|
2
|
+
export type OpenCodePlugin = (context: OpenCodePluginContext) => Promise<OpenCodePluginHooks>;
|
|
3
|
+
/** Supplies the OpenCode client operations used by the plugin. */
|
|
4
|
+
export type OpenCodePluginContext = {
|
|
5
|
+
/** Names the session working directory used for workspace registration. */
|
|
6
|
+
directory: string;
|
|
7
|
+
/** Exposes application logging and session operations. */
|
|
8
|
+
client: {
|
|
9
|
+
/** Exposes application diagnostics. */
|
|
10
|
+
app: {
|
|
11
|
+
/** Writes one structured plugin log entry. */
|
|
12
|
+
log(input: {
|
|
13
|
+
body: {
|
|
14
|
+
service: string;
|
|
15
|
+
level: "info" | "warn";
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
}): Promise<unknown>;
|
|
19
|
+
};
|
|
20
|
+
/** Publishes commands to the active OpenCode TUI. */
|
|
21
|
+
tui: {
|
|
22
|
+
/** Publishes one validated TUI event in the current workspace. */
|
|
23
|
+
publish(input: {
|
|
24
|
+
body: {
|
|
25
|
+
type: "tui.command.execute";
|
|
26
|
+
properties: {
|
|
27
|
+
command: "app.exit";
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
query: {
|
|
31
|
+
directory: string;
|
|
32
|
+
};
|
|
33
|
+
}): Promise<{
|
|
34
|
+
data?: boolean;
|
|
35
|
+
error?: unknown;
|
|
36
|
+
}>;
|
|
37
|
+
};
|
|
38
|
+
/** Adds messaging tools to a running editor session. */
|
|
39
|
+
mcp: {
|
|
40
|
+
/** Registers one remote MCP endpoint for the active workspace. */
|
|
41
|
+
add(input: {
|
|
42
|
+
body: {
|
|
43
|
+
name: string;
|
|
44
|
+
config: {
|
|
45
|
+
type: "remote";
|
|
46
|
+
url: string;
|
|
47
|
+
headers: Record<string, string>;
|
|
48
|
+
oauth: false;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
query: {
|
|
52
|
+
directory: string;
|
|
53
|
+
};
|
|
54
|
+
}): Promise<{
|
|
55
|
+
error?: unknown;
|
|
56
|
+
}>;
|
|
57
|
+
};
|
|
58
|
+
/** Exposes session lookup and asynchronous prompt delivery. */
|
|
59
|
+
session: {
|
|
60
|
+
/** Resolves a session and its optional parent. */
|
|
61
|
+
get(input: {
|
|
62
|
+
path: {
|
|
63
|
+
id: string;
|
|
64
|
+
};
|
|
65
|
+
}): Promise<{
|
|
66
|
+
error?: unknown;
|
|
67
|
+
data?: {
|
|
68
|
+
id: string;
|
|
69
|
+
parentID?: string;
|
|
70
|
+
};
|
|
71
|
+
}>;
|
|
72
|
+
/** Delivers one asynchronous system prompt to a session. */
|
|
73
|
+
promptAsync(input: {
|
|
74
|
+
path: {
|
|
75
|
+
id: string;
|
|
76
|
+
};
|
|
77
|
+
body: {
|
|
78
|
+
system: string;
|
|
79
|
+
parts: Array<{
|
|
80
|
+
type: "text";
|
|
81
|
+
text: string;
|
|
82
|
+
}>;
|
|
83
|
+
};
|
|
84
|
+
}): Promise<{
|
|
85
|
+
error?: unknown;
|
|
86
|
+
}>;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
/** Defines one model-callable tool returned by an OpenCode plugin. */
|
|
91
|
+
export type OpenCodeToolDefinition = {
|
|
92
|
+
/** Explains when and why the model should call the tool. */
|
|
93
|
+
description: string;
|
|
94
|
+
/** Defines the tool's accepted arguments. */
|
|
95
|
+
args: Record<string, never>;
|
|
96
|
+
/** Executes the tool and returns model-visible text. */
|
|
97
|
+
execute(args: Record<string, never>, context: unknown): Promise<string>;
|
|
98
|
+
};
|
|
99
|
+
/** Defines hooks returned to the OpenCode plugin host. */
|
|
100
|
+
export type OpenCodePluginHooks = {
|
|
101
|
+
/** Publishes model-callable tools local to this plugin. */
|
|
102
|
+
tool?: Record<string, OpenCodeToolDefinition>;
|
|
103
|
+
/** Tracks a session after a user chat message. */
|
|
104
|
+
"chat.message"?: (input: {
|
|
105
|
+
sessionID: string;
|
|
106
|
+
}) => Promise<void>;
|
|
107
|
+
/** Observes session lifecycle events. */
|
|
108
|
+
event?: (input: {
|
|
109
|
+
event: {
|
|
110
|
+
type: string;
|
|
111
|
+
properties: {
|
|
112
|
+
info: {
|
|
113
|
+
id: string;
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
}) => Promise<void>;
|
|
118
|
+
/** Releases the inbox, heartbeat, and adapter resources. */
|
|
119
|
+
dispose?: () => Promise<void>;
|
|
120
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RuntimeEnvironment } from "../../launch/environment.js";
|
|
2
|
+
import { type BrokerEndpoints } from "../../launch/on-demand.js";
|
|
3
|
+
import { type PagerInboxOpener } from "../../messaging/inbox-socket.js";
|
|
4
|
+
import type { OpenCodePlugin } from "./plugin-api.js";
|
|
5
|
+
/** Owns repeating background work so a host or a test decides when it runs. */
|
|
6
|
+
export interface PluginScheduler {
|
|
7
|
+
/**
|
|
8
|
+
* Start repeating work until the returned cancellation runs.
|
|
9
|
+
* @param intervalMs - Delay between attempts in milliseconds.
|
|
10
|
+
* @param task - Work invoked on each attempt.
|
|
11
|
+
* @returns - Cancellation that stops further attempts.
|
|
12
|
+
*/
|
|
13
|
+
repeat(intervalMs: number, task: () => void): () => void;
|
|
14
|
+
}
|
|
15
|
+
/** Names every collaborator the editor-hosted plugin uses. */
|
|
16
|
+
export type PluginDependencies = {
|
|
17
|
+
/** Resolves user configuration without depending on the editor's working directory. */
|
|
18
|
+
readEnvironment(): RuntimeEnvironment;
|
|
19
|
+
/** Adopts or starts the shared broker service. */
|
|
20
|
+
ensureService(environment: RuntimeEnvironment): Promise<BrokerEndpoints>;
|
|
21
|
+
/** Supplies the launch identity and heartbeat policy published by the launching process. */
|
|
22
|
+
processEnv: NodeJS.ProcessEnv;
|
|
23
|
+
/** Performs one broker HTTP request. */
|
|
24
|
+
fetch(url: string, init: RequestInit): Promise<Response>;
|
|
25
|
+
/** Owns the attachment heartbeat cadence. */
|
|
26
|
+
scheduler: PluginScheduler;
|
|
27
|
+
/** Opens the inbox the broker pushes this session's deliveries to. */
|
|
28
|
+
openInbox: PagerInboxOpener;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Construct the broker-backed plugin around explicit service discovery dependencies.
|
|
32
|
+
* @param dependencies - Configuration and on-demand service operations used before adapter startup.
|
|
33
|
+
* @returns - OpenCode plugin that prefers launch identity and otherwise self-registers lazily.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createAgentMessagingPlugin(dependencies: PluginDependencies): OpenCodePlugin;
|
|
36
|
+
/** Broker-backed OpenCode implementation plugin. */
|
|
37
|
+
export declare const AgentMessagingPlugin: OpenCodePlugin;
|
|
38
|
+
export default AgentMessagingPlugin;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import { ATTACH_HEADER, isAgentId } from "../../broker/protocol.js";
|
|
3
|
+
import { adapterHeartbeatIntervalMs } from "../../configuration-defaults.js";
|
|
4
|
+
import { implementationId } from "../registry.js";
|
|
5
|
+
import { ensureBrokerService } from "../../launch/on-demand.js";
|
|
6
|
+
import { readEmbeddedRuntimeEnvironment, resolveBunExecutable } from "../../launch/runtime.js";
|
|
7
|
+
import { BrokerDeliveryClient, exactRecord, registerAgent } from "../../messaging/delivery-client.js";
|
|
8
|
+
import { openPagerInbox } from "../../messaging/inbox-socket.js";
|
|
9
|
+
import { OpenCodeInboundAdapter } from "./adapter.js";
|
|
10
|
+
import { createOpenCodeSelfCloseTool } from "./self-close-tool.js";
|
|
11
|
+
import { harnessSessionKey } from "../session-key.js";
|
|
12
|
+
import { OPENCODE_AGENT_ID_ENVIRONMENT, OPENCODE_BROKER_URL_ENVIRONMENT, OPENCODE_DELIVERY_RESERVATION_ENVIRONMENT, } from "./environment.js";
|
|
13
|
+
/**
|
|
14
|
+
* Construct the broker-backed plugin around explicit service discovery dependencies.
|
|
15
|
+
* @param dependencies - Configuration and on-demand service operations used before adapter startup.
|
|
16
|
+
* @returns - OpenCode plugin that prefers launch identity and otherwise self-registers lazily.
|
|
17
|
+
*/
|
|
18
|
+
export function createAgentMessagingPlugin(dependencies) {
|
|
19
|
+
return async (context) => {
|
|
20
|
+
const { client } = context;
|
|
21
|
+
let stopRuntime = () => { };
|
|
22
|
+
const localHooks = {
|
|
23
|
+
tool: {
|
|
24
|
+
close_opencode_session: createOpenCodeSelfCloseTool(client, context.directory, () => {
|
|
25
|
+
stopRuntime();
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const environmentBrokerUrl = dependencies.processEnv[OPENCODE_BROKER_URL_ENVIRONMENT];
|
|
30
|
+
const environmentReservation = dependencies.processEnv[OPENCODE_DELIVERY_RESERVATION_ENVIRONMENT];
|
|
31
|
+
const environmentAgentId = dependencies.processEnv[OPENCODE_AGENT_ID_ENVIRONMENT];
|
|
32
|
+
const launchIdentityPresent = Boolean(environmentBrokerUrl || environmentReservation || environmentAgentId);
|
|
33
|
+
let brokerUrl;
|
|
34
|
+
let reservation;
|
|
35
|
+
let agentId;
|
|
36
|
+
let identityPath;
|
|
37
|
+
if (launchIdentityPresent) {
|
|
38
|
+
if (!environmentBrokerUrl || !environmentReservation || !isAgentId(environmentAgentId)) {
|
|
39
|
+
await log(client, "warn", "Broker adapter is inactive because launch identity is incomplete.");
|
|
40
|
+
return localHooks;
|
|
41
|
+
}
|
|
42
|
+
brokerUrl = environmentBrokerUrl;
|
|
43
|
+
reservation = environmentReservation;
|
|
44
|
+
agentId = environmentAgentId;
|
|
45
|
+
identityPath = "launch";
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
try {
|
|
49
|
+
brokerUrl = (await dependencies.ensureService(dependencies.readEnvironment())).mcpUrl;
|
|
50
|
+
identityPath = "self-registration";
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
await log(client, "warn", error instanceof Error ? error.message : "Broker service could not be started");
|
|
54
|
+
return localHooks;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
await log(client, "info", `Broker-backed OpenCode adapter configured through ${identityPath}.`);
|
|
58
|
+
let adapter;
|
|
59
|
+
let deliveryClient;
|
|
60
|
+
let connecting;
|
|
61
|
+
let inbox;
|
|
62
|
+
let inboxOpening;
|
|
63
|
+
// Concurrent chat turns share one self-registration: a second registration for the same
|
|
64
|
+
// conversation displaces the first's reservations, and the messaging one is single-use.
|
|
65
|
+
let registering;
|
|
66
|
+
const selfRegister = async (sessionName) => {
|
|
67
|
+
const sessionKey = harnessSessionKey(implementationId("opencode"), sessionName);
|
|
68
|
+
const registered = await registerAgent((input, init) => dependencies.fetch(input, init), brokerUrl, {
|
|
69
|
+
workspacePath: context.directory,
|
|
70
|
+
implementationId: implementationId("opencode"),
|
|
71
|
+
sessionName,
|
|
72
|
+
...(sessionKey ? { sessionKey } : {}),
|
|
73
|
+
});
|
|
74
|
+
const added = await client.mcp.add({
|
|
75
|
+
body: {
|
|
76
|
+
name: "pager",
|
|
77
|
+
config: {
|
|
78
|
+
type: "remote",
|
|
79
|
+
url: brokerUrl,
|
|
80
|
+
headers: { [ATTACH_HEADER]: registered.messagingReservation },
|
|
81
|
+
oauth: false,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
query: { directory: context.directory },
|
|
85
|
+
});
|
|
86
|
+
if (added.error)
|
|
87
|
+
throw new Error("Broker messaging tools could not be added");
|
|
88
|
+
reservation = registered.deliveryReservation;
|
|
89
|
+
agentId = registered.agentId;
|
|
90
|
+
};
|
|
91
|
+
const initializeIdentity = async (sessionName) => {
|
|
92
|
+
if (adapter)
|
|
93
|
+
return adapter;
|
|
94
|
+
if (!reservation || !agentId) {
|
|
95
|
+
registering ??= selfRegister(sessionName).catch((error) => {
|
|
96
|
+
registering = undefined;
|
|
97
|
+
throw error;
|
|
98
|
+
});
|
|
99
|
+
await registering;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
await claimConversation(sessionName);
|
|
103
|
+
}
|
|
104
|
+
adapter = new OpenCodeInboundAdapter({
|
|
105
|
+
resolveSession: async (sessionId) => {
|
|
106
|
+
const result = await client.session.get({ path: { id: sessionId } });
|
|
107
|
+
return result.error ? undefined : result.data;
|
|
108
|
+
},
|
|
109
|
+
prompt: async (sessionId, system, text) => {
|
|
110
|
+
const result = await client.session.promptAsync({
|
|
111
|
+
path: { id: sessionId },
|
|
112
|
+
body: { system, parts: [{ type: "text", text }] },
|
|
113
|
+
});
|
|
114
|
+
if (result.error)
|
|
115
|
+
throw new Error(JSON.stringify(result.error));
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
return adapter;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Tell the broker which conversation a launched identity belongs to, once it is connected.
|
|
122
|
+
*
|
|
123
|
+
* OpenCode attaches the messaging channel itself from a config file written before this
|
|
124
|
+
* conversation existed, so the key cannot ride along at attach time the way it does elsewhere. When
|
|
125
|
+
* the key already belongs to an identity, the broker hands back fresh attachments for it and this
|
|
126
|
+
* process moves across, replacing the messaging server the launch configured.
|
|
127
|
+
* @param sessionName - Conversation identifier reported by OpenCode.
|
|
128
|
+
*/
|
|
129
|
+
const claimConversation = async (sessionName) => {
|
|
130
|
+
const sessionKey = harnessSessionKey(implementationId("opencode"), sessionName);
|
|
131
|
+
if (!sessionKey)
|
|
132
|
+
return;
|
|
133
|
+
const claimed = adoptionResult(await call("adopt_session_key", { sessionKey }));
|
|
134
|
+
if (!claimed.adopted)
|
|
135
|
+
return;
|
|
136
|
+
const added = await client.mcp.add({
|
|
137
|
+
body: {
|
|
138
|
+
name: "pager",
|
|
139
|
+
config: {
|
|
140
|
+
type: "remote",
|
|
141
|
+
url: brokerUrl,
|
|
142
|
+
headers: { [ATTACH_HEADER]: claimed.reservation },
|
|
143
|
+
oauth: false,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
query: { directory: context.directory },
|
|
147
|
+
});
|
|
148
|
+
if (added.error)
|
|
149
|
+
throw new Error("Broker messaging tools could not be moved to the resumed identity");
|
|
150
|
+
reservation = claimed.deliveryReservation;
|
|
151
|
+
agentId = claimed.agentId;
|
|
152
|
+
deliveryClient = undefined;
|
|
153
|
+
connecting = undefined;
|
|
154
|
+
};
|
|
155
|
+
const connectDeliveryClient = async () => {
|
|
156
|
+
if (deliveryClient)
|
|
157
|
+
return deliveryClient;
|
|
158
|
+
if (!reservation || !agentId)
|
|
159
|
+
throw new Error("Broker identity has not registered");
|
|
160
|
+
const identity = agentId;
|
|
161
|
+
const attachReservation = reservation;
|
|
162
|
+
connecting ??= BrokerDeliveryClient.attach({
|
|
163
|
+
brokerUrl,
|
|
164
|
+
agentId: identity,
|
|
165
|
+
reservation: attachReservation,
|
|
166
|
+
request: (input, init) => dependencies.fetch(input, init),
|
|
167
|
+
})
|
|
168
|
+
.then((connected) => {
|
|
169
|
+
if (inbox)
|
|
170
|
+
connected.presentDeliveryAddress(inbox.address);
|
|
171
|
+
deliveryClient = connected;
|
|
172
|
+
return connected;
|
|
173
|
+
})
|
|
174
|
+
.finally(() => {
|
|
175
|
+
connecting = undefined;
|
|
176
|
+
});
|
|
177
|
+
return connecting;
|
|
178
|
+
};
|
|
179
|
+
const call = async (tool, args) => {
|
|
180
|
+
const connected = await connectDeliveryClient();
|
|
181
|
+
return connected.call(tool, args);
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Open the inbox once a session can take deliveries, and register it with the broker at once.
|
|
185
|
+
*
|
|
186
|
+
* A failure is reported and not retried: the session keeps its messaging tools but receives no
|
|
187
|
+
* delivery, and its messages wait for a later session.
|
|
188
|
+
*/
|
|
189
|
+
const openInbox = async () => {
|
|
190
|
+
inboxOpening ??= (async () => {
|
|
191
|
+
const deliver = (content) => adapter ? adapter.deliver(content) : Promise.reject(new Error("No OpenCode session is tracked"));
|
|
192
|
+
let opened;
|
|
193
|
+
try {
|
|
194
|
+
opened = await dependencies.openInbox(deliver);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
await log(client, "warn", `Pager inbox could not be opened, so this session receives no messages: ${error instanceof Error ? error.message : String(error)}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// dispose() may have run and closed nothing while this open was still in flight, since it
|
|
201
|
+
// could only close the inbox this assignment is about to publish; close it now instead of
|
|
202
|
+
// leaking the socket and its temporary directory for the rest of the process.
|
|
203
|
+
if (runtimeStopped) {
|
|
204
|
+
opened.close();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
inbox = opened;
|
|
208
|
+
const connected = await connectDeliveryClient();
|
|
209
|
+
connected.presentDeliveryAddress(inbox.address);
|
|
210
|
+
await connected.heartbeat();
|
|
211
|
+
})();
|
|
212
|
+
await inboxOpening;
|
|
213
|
+
};
|
|
214
|
+
const logFailure = (error) => log(client, "warn", error instanceof Error ? error.message : "Broker request failed");
|
|
215
|
+
const stopHeartbeat = dependencies.scheduler.repeat(adapterHeartbeatIntervalMs(dependencies.processEnv), () => {
|
|
216
|
+
if (reservation && agentId)
|
|
217
|
+
void connectDeliveryClient()
|
|
218
|
+
.then((connected) => connected.heartbeat())
|
|
219
|
+
.catch(logFailure);
|
|
220
|
+
});
|
|
221
|
+
let runtimeStopped = false;
|
|
222
|
+
stopRuntime = () => {
|
|
223
|
+
if (runtimeStopped)
|
|
224
|
+
return;
|
|
225
|
+
runtimeStopped = true;
|
|
226
|
+
stopHeartbeat();
|
|
227
|
+
adapter?.stop();
|
|
228
|
+
inbox?.close();
|
|
229
|
+
};
|
|
230
|
+
// A launched identity is held from the moment OpenCode's MCP client attaches its messaging channel,
|
|
231
|
+
// before any chat turn, so the delivery channel this plugin ends it over is attached now too.
|
|
232
|
+
if (identityPath === "launch")
|
|
233
|
+
void connectDeliveryClient().catch(logFailure);
|
|
234
|
+
return {
|
|
235
|
+
...localHooks,
|
|
236
|
+
"chat.message": async ({ sessionID }) => {
|
|
237
|
+
try {
|
|
238
|
+
const activeAdapter = await initializeIdentity(sessionID);
|
|
239
|
+
await activeAdapter.trackSession(sessionID);
|
|
240
|
+
if (activeAdapter.currentSession())
|
|
241
|
+
await openInbox();
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
await logFailure(error);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
event: ({ event }) => {
|
|
248
|
+
if (event.type === "session.deleted")
|
|
249
|
+
adapter?.removeSession(event.properties.info.id);
|
|
250
|
+
return Promise.resolve();
|
|
251
|
+
},
|
|
252
|
+
dispose: async () => {
|
|
253
|
+
stopRuntime();
|
|
254
|
+
// Ends the messaging attachment too, which this plugin does not hold. A failed notice leaves the
|
|
255
|
+
// lease to lapse, as after a crash.
|
|
256
|
+
const connected = await (connecting ?? Promise.resolve(deliveryClient)).catch(() => undefined);
|
|
257
|
+
await connected?.detach().catch(() => undefined);
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const defaultDependencies = {
|
|
263
|
+
readEnvironment: () => readEmbeddedRuntimeEnvironment(process.env, resolveBunExecutable),
|
|
264
|
+
ensureService: (environment) => ensureBrokerService(environment),
|
|
265
|
+
processEnv: process.env,
|
|
266
|
+
fetch: (url, init) => globalThis.fetch(url, init),
|
|
267
|
+
scheduler: {
|
|
268
|
+
repeat: (intervalMs, task) => {
|
|
269
|
+
const handle = setInterval(task, intervalMs);
|
|
270
|
+
return () => {
|
|
271
|
+
clearInterval(handle);
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
openInbox: (accept) => openPagerInbox(accept, tmpdir()),
|
|
276
|
+
};
|
|
277
|
+
/** Broker-backed OpenCode implementation plugin. */
|
|
278
|
+
export const AgentMessagingPlugin = createAgentMessagingPlugin(defaultDependencies);
|
|
279
|
+
function adoptionResult(value) {
|
|
280
|
+
const result = exactRecord(value, "Broker session-key adoption result", [
|
|
281
|
+
"adopted",
|
|
282
|
+
"agentId",
|
|
283
|
+
"reservation",
|
|
284
|
+
"deliveryReservation",
|
|
285
|
+
]);
|
|
286
|
+
if (result.adopted !== true)
|
|
287
|
+
return { adopted: false };
|
|
288
|
+
if (!isAgentId(result.agentId) ||
|
|
289
|
+
typeof result.reservation !== "string" ||
|
|
290
|
+
typeof result.deliveryReservation !== "string") {
|
|
291
|
+
throw new Error("Broker session-key adoption returned an invalid identity");
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
adopted: true,
|
|
295
|
+
agentId: result.agentId,
|
|
296
|
+
reservation: result.reservation,
|
|
297
|
+
deliveryReservation: result.deliveryReservation,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function log(client, level, message) {
|
|
301
|
+
return client.app.log({ body: { service: "pager", level, message } }).then(() => undefined);
|
|
302
|
+
}
|
|
303
|
+
export default AgentMessagingPlugin;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OpenCodePluginContext, OpenCodeToolDefinition } from "./plugin-api.js";
|
|
2
|
+
/**
|
|
3
|
+
* Construct the OpenCode-local tool that requests the host TUI's orderly exit command.
|
|
4
|
+
* @param client - OpenCode client that publishes TUI commands.
|
|
5
|
+
* @param directory - Workspace whose TUI receives the exit command.
|
|
6
|
+
* @param beforeShutdown - Synchronous cleanup that prevents plugin work from delaying exit.
|
|
7
|
+
* @returns - Argument-free model tool that coalesces concurrent shutdown requests.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createOpenCodeSelfCloseTool(client: OpenCodePluginContext["client"], directory: string, beforeShutdown: () => void): OpenCodeToolDefinition;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construct the OpenCode-local tool that requests the host TUI's orderly exit command.
|
|
3
|
+
* @param client - OpenCode client that publishes TUI commands.
|
|
4
|
+
* @param directory - Workspace whose TUI receives the exit command.
|
|
5
|
+
* @param beforeShutdown - Synchronous cleanup that prevents plugin work from delaying exit.
|
|
6
|
+
* @returns - Argument-free model tool that coalesces concurrent shutdown requests.
|
|
7
|
+
*/
|
|
8
|
+
export function createOpenCodeSelfCloseTool(client, directory, beforeShutdown) {
|
|
9
|
+
let closing;
|
|
10
|
+
return {
|
|
11
|
+
description: "Gracefully close this OpenCode session after assigned work is complete and no further response is needed.",
|
|
12
|
+
args: {},
|
|
13
|
+
execute: () => {
|
|
14
|
+
closing ??= requestOpenCodeExit(client, directory, beforeShutdown).catch((error) => {
|
|
15
|
+
closing = undefined;
|
|
16
|
+
throw error;
|
|
17
|
+
});
|
|
18
|
+
return closing;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function requestOpenCodeExit(client, directory, beforeShutdown) {
|
|
23
|
+
beforeShutdown();
|
|
24
|
+
const result = await client.tui.publish({
|
|
25
|
+
body: { type: "tui.command.execute", properties: { command: "app.exit" } },
|
|
26
|
+
query: { directory },
|
|
27
|
+
});
|
|
28
|
+
if (result.error || result.data !== true)
|
|
29
|
+
throw new Error("OpenCode rejected its graceful exit command");
|
|
30
|
+
return "Graceful OpenCode shutdown requested.";
|
|
31
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ForegroundProcessSpawner, TerminalLaunch, TerminalProvider, TerminalSession } from "../../launch/launch.js";
|
|
2
|
+
/** Identifies the kind of terminal operation this provider recorded. */
|
|
3
|
+
export type OrcaOperationType = "terminal_create" | "task_complete" | "terminal_reuse";
|
|
4
|
+
/** Launch terminal plans through the Orca terminal client. */
|
|
5
|
+
export declare class OrcaTerminalProvider implements TerminalProvider {
|
|
6
|
+
#private;
|
|
7
|
+
/** Identifies this terminal provider. */
|
|
8
|
+
readonly name = "orca";
|
|
9
|
+
/** Prefers the externally hosted terminal over inherited local stdio. */
|
|
10
|
+
readonly interactivePriority = 100;
|
|
11
|
+
/**
|
|
12
|
+
* Construct the provider around an explicit Orca executable.
|
|
13
|
+
* @param options - `command` is the Orca executable. A `currentTerminalHandle` makes launches run as a
|
|
14
|
+
* foreground child of this process through `spawnProcess` instead of creating a terminal.
|
|
15
|
+
* `handoffRoot` holds the owner-only environment files and defaults to a fresh temporary directory.
|
|
16
|
+
*/
|
|
17
|
+
constructor(options: {
|
|
18
|
+
command: string;
|
|
19
|
+
currentTerminalHandle?: string;
|
|
20
|
+
handoffRoot?: string;
|
|
21
|
+
spawnProcess: ForegroundProcessSpawner;
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Report every terminal operation this provider has recorded.
|
|
25
|
+
* @returns - Operations in the order they occurred.
|
|
26
|
+
*/
|
|
27
|
+
get operations(): ReadonlyArray<{
|
|
28
|
+
type: OrcaOperationType;
|
|
29
|
+
detail: unknown;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Report whether the configured terminal executable can answer locally.
|
|
33
|
+
* @returns - Whether the provider executable is present and runnable.
|
|
34
|
+
*/
|
|
35
|
+
available(): Promise<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* Launch one implementation-owned plan in an Orca terminal.
|
|
38
|
+
* @param request - Neutral terminal launch request.
|
|
39
|
+
* @returns - Started terminal session and explicit controls.
|
|
40
|
+
*/
|
|
41
|
+
launch(request: TerminalLaunch): Promise<TerminalSession>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Construct the terminal provider from process configuration owned by this implementation.
|
|
45
|
+
* @param environment - Process environment carrying the optional Orca executable override.
|
|
46
|
+
* @returns - Configured Orca terminal provider.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createTerminalProvider(environment: NodeJS.ProcessEnv): TerminalProvider;
|