@pruddiman/hem 0.0.1-beta-5671db0
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/dist/agents/arbiter-agent.d.ts +72 -0
- package/dist/agents/arbiter-agent.js +149 -0
- package/dist/agents/architecture-agent.d.ts +148 -0
- package/dist/agents/architecture-agent.js +459 -0
- package/dist/agents/base-agent.d.ts +44 -0
- package/dist/agents/base-agent.js +57 -0
- package/dist/agents/crossref-agent.d.ts +140 -0
- package/dist/agents/crossref-agent.js +560 -0
- package/dist/agents/crossref-arbiter-agent.d.ts +72 -0
- package/dist/agents/crossref-arbiter-agent.js +147 -0
- package/dist/agents/documentation-agent.d.ts +55 -0
- package/dist/agents/documentation-agent.js +159 -0
- package/dist/agents/exploration-agent.d.ts +58 -0
- package/dist/agents/exploration-agent.js +102 -0
- package/dist/agents/grouping-agent.d.ts +167 -0
- package/dist/agents/grouping-agent.js +557 -0
- package/dist/agents/index-agent.d.ts +86 -0
- package/dist/agents/index-agent.js +360 -0
- package/dist/agents/organization-agent.d.ts +144 -0
- package/dist/agents/organization-agent.js +607 -0
- package/dist/auth.d.ts +372 -0
- package/dist/auth.js +1072 -0
- package/dist/broadcast-mcp.d.ts +21 -0
- package/dist/broadcast-mcp.js +59 -0
- package/dist/changelog.d.ts +85 -0
- package/dist/changelog.js +223 -0
- package/dist/decision-queue.d.ts +173 -0
- package/dist/decision-queue.js +265 -0
- package/dist/diff-scope.d.ts +24 -0
- package/dist/diff-scope.js +28 -0
- package/dist/discovery.d.ts +54 -0
- package/dist/discovery.js +405 -0
- package/dist/grouping.d.ts +37 -0
- package/dist/grouping.js +343 -0
- package/dist/helpers/format.d.ts +5 -0
- package/dist/helpers/format.js +13 -0
- package/dist/helpers/index.d.ts +11 -0
- package/dist/helpers/index.js +11 -0
- package/dist/helpers/parsing.d.ts +52 -0
- package/dist/helpers/parsing.js +128 -0
- package/dist/helpers/paths.d.ts +41 -0
- package/dist/helpers/paths.js +67 -0
- package/dist/helpers/strings.d.ts +45 -0
- package/dist/helpers/strings.js +97 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.js +1087 -0
- package/dist/merge-utils.d.ts +22 -0
- package/dist/merge-utils.js +34 -0
- package/dist/orchestrator.d.ts +194 -0
- package/dist/orchestrator.js +1169 -0
- package/dist/output.d.ts +106 -0
- package/dist/output.js +243 -0
- package/dist/progress.d.ts +228 -0
- package/dist/progress.js +644 -0
- package/dist/providers/copilot.d.ts +247 -0
- package/dist/providers/copilot.js +598 -0
- package/dist/providers/index.d.ts +15 -0
- package/dist/providers/index.js +12 -0
- package/dist/providers/opencode.d.ts +156 -0
- package/dist/providers/opencode.js +416 -0
- package/dist/providers/types.d.ts +156 -0
- package/dist/providers/types.js +16 -0
- package/dist/resources.d.ts +76 -0
- package/dist/resources.js +151 -0
- package/dist/search-index.d.ts +71 -0
- package/dist/search-index.js +187 -0
- package/dist/search-mcp.d.ts +25 -0
- package/dist/search-mcp.js +100 -0
- package/dist/server-utils.d.ts +56 -0
- package/dist/server-utils.js +135 -0
- package/dist/session.d.ts +227 -0
- package/dist/session.js +370 -0
- package/dist/types.d.ts +272 -0
- package/dist/types.js +5 -0
- package/dist/worktree.d.ts +82 -0
- package/dist/worktree.js +187 -0
- package/package.json +45 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode SDK provider implementation for Hem.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the OpenCode server lifecycle, permission configuration,
|
|
5
|
+
* MCP tool registration, and model string formatting behind the
|
|
6
|
+
* {@link Provider} interface.
|
|
7
|
+
*
|
|
8
|
+
* Aligns with Dispatch's ProviderInstance pattern:
|
|
9
|
+
* - `createSession()` — delegates to `client.session.create()`
|
|
10
|
+
* - `prompt(sessionId, text)` — uses `promptAsync` + SSE wait (avoids
|
|
11
|
+
* HTTP timeout on slow LLM responses)
|
|
12
|
+
* - `cleanup()` — stops the server
|
|
13
|
+
*
|
|
14
|
+
* Also exposes low-level `session` and `event` properties that delegate
|
|
15
|
+
* directly to the underlying OpenCode client, for agents that need
|
|
16
|
+
* fire-and-forget prompting or SSE monitoring (OrganizationAgent etc.).
|
|
17
|
+
*
|
|
18
|
+
* Reference: FR-005, FR-006, FR-007.
|
|
19
|
+
*/
|
|
20
|
+
import { createOpencode } from "@opencode-ai/sdk";
|
|
21
|
+
import type { Provider, ProviderConfig } from "./types.js";
|
|
22
|
+
/**
|
|
23
|
+
* Read-only bash permission set for agents that need filesystem inspection
|
|
24
|
+
* but must never modify files or run arbitrary commands.
|
|
25
|
+
*
|
|
26
|
+
* Reference: T005.
|
|
27
|
+
*/
|
|
28
|
+
export declare const READ_ONLY_BASH: {
|
|
29
|
+
readonly "cat *": "allow";
|
|
30
|
+
readonly "head *": "allow";
|
|
31
|
+
readonly "tail *": "allow";
|
|
32
|
+
readonly "grep *": "allow";
|
|
33
|
+
readonly "find *": "allow";
|
|
34
|
+
readonly "ls *": "allow";
|
|
35
|
+
readonly "wc *": "allow";
|
|
36
|
+
readonly "file *": "allow";
|
|
37
|
+
readonly "tree *": "allow";
|
|
38
|
+
readonly "du *": "allow";
|
|
39
|
+
readonly "git status *": "allow";
|
|
40
|
+
readonly "git diff *": "allow";
|
|
41
|
+
readonly "*": "deny";
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Extended bash permission set for organization workers. Inherits all
|
|
45
|
+
* read-only commands from {@link READ_ONLY_BASH} and additionally allows
|
|
46
|
+
* `rm` so workers can delete files when executing arbiter DELETE decisions
|
|
47
|
+
* instead of writing empty content as a deletion proxy.
|
|
48
|
+
*/
|
|
49
|
+
export declare const ORG_AGENT_BASH: {
|
|
50
|
+
readonly "rm *": "allow";
|
|
51
|
+
readonly "cat *": "allow";
|
|
52
|
+
readonly "head *": "allow";
|
|
53
|
+
readonly "tail *": "allow";
|
|
54
|
+
readonly "grep *": "allow";
|
|
55
|
+
readonly "find *": "allow";
|
|
56
|
+
readonly "ls *": "allow";
|
|
57
|
+
readonly "wc *": "allow";
|
|
58
|
+
readonly "file *": "allow";
|
|
59
|
+
readonly "tree *": "allow";
|
|
60
|
+
readonly "du *": "allow";
|
|
61
|
+
readonly "git status *": "allow";
|
|
62
|
+
readonly "git diff *": "allow";
|
|
63
|
+
readonly "*": "deny";
|
|
64
|
+
};
|
|
65
|
+
/** Overridable factory for `createOpencode`, used for testing. */
|
|
66
|
+
export type CreateOpencodeFn = typeof createOpencode;
|
|
67
|
+
/**
|
|
68
|
+
* Provider implementation backed by the OpenCode SDK.
|
|
69
|
+
*
|
|
70
|
+
* Manages the full OpenCode server lifecycle: port discovery, server
|
|
71
|
+
* startup (with retry), permission scoping, MCP tool registration,
|
|
72
|
+
* and clean shutdown.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const provider = await OpenCodeProvider.create(config);
|
|
77
|
+
* const sessionId = await provider.createSession();
|
|
78
|
+
* const result = await provider.prompt(sessionId, "Explain this code");
|
|
79
|
+
* await provider.cleanup();
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare class OpenCodeProvider implements Provider {
|
|
83
|
+
private _config;
|
|
84
|
+
private _client;
|
|
85
|
+
private _shutdownFn;
|
|
86
|
+
private _factory;
|
|
87
|
+
private _findPort;
|
|
88
|
+
private _modelOverride;
|
|
89
|
+
/** Low-level session operations (implementing Provider interface). */
|
|
90
|
+
readonly session: Provider["session"];
|
|
91
|
+
/** SSE event subscription (implementing Provider interface). */
|
|
92
|
+
readonly event: Provider["event"];
|
|
93
|
+
/**
|
|
94
|
+
* Creates a new OpenCode provider.
|
|
95
|
+
*
|
|
96
|
+
* @param config - Provider configuration (model, destination, permissions).
|
|
97
|
+
* @param factory - Optional `createOpencode` override for testing.
|
|
98
|
+
* @param findPort - Optional port finder override for testing.
|
|
99
|
+
*/
|
|
100
|
+
constructor(config: ProviderConfig, factory?: CreateOpencodeFn, findPort?: () => Promise<number>);
|
|
101
|
+
get name(): string;
|
|
102
|
+
get model(): string | undefined;
|
|
103
|
+
get config(): ProviderConfig;
|
|
104
|
+
/**
|
|
105
|
+
* Creates and initializes an OpenCodeProvider.
|
|
106
|
+
* Mirrors Dispatch's `boot()` pattern — callers receive a ready-to-use
|
|
107
|
+
* provider without calling `initialize()` separately.
|
|
108
|
+
*/
|
|
109
|
+
static create(config: ProviderConfig, factory?: CreateOpencodeFn, findPort?: () => Promise<number>): Promise<OpenCodeProvider>;
|
|
110
|
+
/**
|
|
111
|
+
* Starts the OpenCode server and configures permissions and MCP tools.
|
|
112
|
+
*
|
|
113
|
+
* Must be called exactly once before using the provider. Subsequent
|
|
114
|
+
* calls are no-ops if already initialized.
|
|
115
|
+
*
|
|
116
|
+
* @throws {Error} If `destinationPath` is empty or blank.
|
|
117
|
+
* @throws {Error} If the OpenCode server fails to start.
|
|
118
|
+
*/
|
|
119
|
+
initialize(): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Create a new OpenCode session.
|
|
122
|
+
*
|
|
123
|
+
* @returns The session ID.
|
|
124
|
+
* @throws {Error} If the provider is not initialized.
|
|
125
|
+
*/
|
|
126
|
+
createSession(): Promise<string>;
|
|
127
|
+
/**
|
|
128
|
+
* Send a prompt to an OpenCode session and wait for the response.
|
|
129
|
+
*
|
|
130
|
+
* Uses the async prompt flow to avoid HTTP timeout issues on slow LLM
|
|
131
|
+
* responses (mirrors Dispatch's OpenCode provider):
|
|
132
|
+
* 1. `promptAsync()` — fire-and-forget POST that returns 204 immediately
|
|
133
|
+
* 2. `event.subscribe()` — SSE stream for session lifecycle events
|
|
134
|
+
* 3. Wait for `session.idle` (success) or `session.error` (failure)
|
|
135
|
+
* 4. `session.messages()` — fetch the completed response
|
|
136
|
+
*
|
|
137
|
+
* @param sessionId - The session ID returned by `createSession()`.
|
|
138
|
+
* @param text - The prompt text.
|
|
139
|
+
* @param options - Optional: `agent` selects the permission profile.
|
|
140
|
+
*/
|
|
141
|
+
prompt(sessionId: string, text: string, options?: {
|
|
142
|
+
agent?: string;
|
|
143
|
+
}): Promise<string | null>;
|
|
144
|
+
/**
|
|
145
|
+
* Shuts down the OpenCode server and releases all resources.
|
|
146
|
+
*
|
|
147
|
+
* After cleanup, the provider instance must not be reused.
|
|
148
|
+
* No-op if the provider was never initialized or already shut down.
|
|
149
|
+
*/
|
|
150
|
+
cleanup(): Promise<void>;
|
|
151
|
+
/**
|
|
152
|
+
* Alias for `cleanup()` — kept for backward compatibility with callers
|
|
153
|
+
* that use the old `shutdown()` name.
|
|
154
|
+
*/
|
|
155
|
+
shutdown(): Promise<void>;
|
|
156
|
+
}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode SDK provider implementation for Hem.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the OpenCode server lifecycle, permission configuration,
|
|
5
|
+
* MCP tool registration, and model string formatting behind the
|
|
6
|
+
* {@link Provider} interface.
|
|
7
|
+
*
|
|
8
|
+
* Aligns with Dispatch's ProviderInstance pattern:
|
|
9
|
+
* - `createSession()` — delegates to `client.session.create()`
|
|
10
|
+
* - `prompt(sessionId, text)` — uses `promptAsync` + SSE wait (avoids
|
|
11
|
+
* HTTP timeout on slow LLM responses)
|
|
12
|
+
* - `cleanup()` — stops the server
|
|
13
|
+
*
|
|
14
|
+
* Also exposes low-level `session` and `event` properties that delegate
|
|
15
|
+
* directly to the underlying OpenCode client, for agents that need
|
|
16
|
+
* fire-and-forget prompting or SSE monitoring (OrganizationAgent etc.).
|
|
17
|
+
*
|
|
18
|
+
* Reference: FR-005, FR-006, FR-007.
|
|
19
|
+
*/
|
|
20
|
+
import { resolve, join, dirname } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { createOpencode } from "@opencode-ai/sdk";
|
|
23
|
+
import { findFreePort, trackServer, untrackServer, startWithRetry } from "../server-utils.js";
|
|
24
|
+
// ── Permission Constants ────────────────────────────────────────────────
|
|
25
|
+
/**
|
|
26
|
+
* Read-only bash permission set for agents that need filesystem inspection
|
|
27
|
+
* but must never modify files or run arbitrary commands.
|
|
28
|
+
*
|
|
29
|
+
* Reference: T005.
|
|
30
|
+
*/
|
|
31
|
+
export const READ_ONLY_BASH = {
|
|
32
|
+
"cat *": "allow",
|
|
33
|
+
"head *": "allow",
|
|
34
|
+
"tail *": "allow",
|
|
35
|
+
"grep *": "allow",
|
|
36
|
+
"find *": "allow",
|
|
37
|
+
"ls *": "allow",
|
|
38
|
+
"wc *": "allow",
|
|
39
|
+
"file *": "allow",
|
|
40
|
+
"tree *": "allow",
|
|
41
|
+
"du *": "allow",
|
|
42
|
+
"git status *": "allow",
|
|
43
|
+
"git diff *": "allow",
|
|
44
|
+
"*": "deny",
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Extended bash permission set for organization workers. Inherits all
|
|
48
|
+
* read-only commands from {@link READ_ONLY_BASH} and additionally allows
|
|
49
|
+
* `rm` so workers can delete files when executing arbiter DELETE decisions
|
|
50
|
+
* instead of writing empty content as a deletion proxy.
|
|
51
|
+
*/
|
|
52
|
+
export const ORG_AGENT_BASH = {
|
|
53
|
+
...READ_ONLY_BASH,
|
|
54
|
+
"rm *": "allow",
|
|
55
|
+
};
|
|
56
|
+
// ── OpenCode Provider ───────────────────────────────────────────────────
|
|
57
|
+
/**
|
|
58
|
+
* Provider implementation backed by the OpenCode SDK.
|
|
59
|
+
*
|
|
60
|
+
* Manages the full OpenCode server lifecycle: port discovery, server
|
|
61
|
+
* startup (with retry), permission scoping, MCP tool registration,
|
|
62
|
+
* and clean shutdown.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* const provider = await OpenCodeProvider.create(config);
|
|
67
|
+
* const sessionId = await provider.createSession();
|
|
68
|
+
* const result = await provider.prompt(sessionId, "Explain this code");
|
|
69
|
+
* await provider.cleanup();
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export class OpenCodeProvider {
|
|
73
|
+
_config;
|
|
74
|
+
_client = null;
|
|
75
|
+
_shutdownFn = null;
|
|
76
|
+
_factory;
|
|
77
|
+
_findPort;
|
|
78
|
+
_modelOverride;
|
|
79
|
+
/** Low-level session operations (implementing Provider interface). */
|
|
80
|
+
session;
|
|
81
|
+
/** SSE event subscription (implementing Provider interface). */
|
|
82
|
+
event;
|
|
83
|
+
/**
|
|
84
|
+
* Creates a new OpenCode provider.
|
|
85
|
+
*
|
|
86
|
+
* @param config - Provider configuration (model, destination, permissions).
|
|
87
|
+
* @param factory - Optional `createOpencode` override for testing.
|
|
88
|
+
* @param findPort - Optional port finder override for testing.
|
|
89
|
+
*/
|
|
90
|
+
constructor(config, factory = createOpencode, findPort = findFreePort) {
|
|
91
|
+
this._config = config;
|
|
92
|
+
this._factory = factory;
|
|
93
|
+
this._findPort = findPort;
|
|
94
|
+
// Parse model override from config for use in prompt() calls.
|
|
95
|
+
// The SDK concatenates providerID + "/" + modelID, so for encoded modelIDs like
|
|
96
|
+
// "github-copilot/claude-opus-4.6" (opencode sub-provider), split them so the
|
|
97
|
+
// SDK receives { providerID: "github-copilot", modelID: "claude-opus-4.6" }.
|
|
98
|
+
if (config.model.modelID !== "default") {
|
|
99
|
+
const { providerID, modelID } = config.model;
|
|
100
|
+
if (modelID.includes("/")) {
|
|
101
|
+
const slashIdx = modelID.indexOf("/");
|
|
102
|
+
this._modelOverride = {
|
|
103
|
+
providerID: modelID.slice(0, slashIdx),
|
|
104
|
+
modelID: modelID.slice(slashIdx + 1),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
this._modelOverride = { providerID, modelID };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Low-level session ops — delegate to underlying client
|
|
112
|
+
const self = this;
|
|
113
|
+
this.session = {
|
|
114
|
+
async promptAsync(options) {
|
|
115
|
+
if (!self._client)
|
|
116
|
+
throw new Error("OpenCodeProvider not initialized");
|
|
117
|
+
return self._client.session.promptAsync(options);
|
|
118
|
+
},
|
|
119
|
+
async abort(options) {
|
|
120
|
+
if (!self._client)
|
|
121
|
+
throw new Error("OpenCodeProvider not initialized");
|
|
122
|
+
return self._client.session.abort(options);
|
|
123
|
+
},
|
|
124
|
+
async delete(options) {
|
|
125
|
+
if (!self._client)
|
|
126
|
+
throw new Error("OpenCodeProvider not initialized");
|
|
127
|
+
return self._client.session.delete(options);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
this.event = {
|
|
131
|
+
async subscribe(options) {
|
|
132
|
+
if (!self._client)
|
|
133
|
+
throw new Error("OpenCodeProvider not initialized");
|
|
134
|
+
// The underlying SDK returns SdkEvent; cast to SseEvent (same shape).
|
|
135
|
+
const result = await self._client.event.subscribe(options);
|
|
136
|
+
return result;
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// ── Identity ───────────────────────────────────────────────────────
|
|
141
|
+
get name() { return "opencode"; }
|
|
142
|
+
get model() {
|
|
143
|
+
if (this._config.model.modelID === "default")
|
|
144
|
+
return undefined;
|
|
145
|
+
const { providerID, modelID } = this._config.model;
|
|
146
|
+
// modelID may already include a sub-provider prefix (e.g. "github-copilot/opus-4.6")
|
|
147
|
+
// when the user picked an opencode sub-provider model via `hem config`. Use it as-is.
|
|
148
|
+
return modelID.includes("/") ? modelID : `${providerID}/${modelID}`;
|
|
149
|
+
}
|
|
150
|
+
get config() { return this._config; }
|
|
151
|
+
// ── Static factory ─────────────────────────────────────────────────
|
|
152
|
+
/**
|
|
153
|
+
* Creates and initializes an OpenCodeProvider.
|
|
154
|
+
* Mirrors Dispatch's `boot()` pattern — callers receive a ready-to-use
|
|
155
|
+
* provider without calling `initialize()` separately.
|
|
156
|
+
*/
|
|
157
|
+
static async create(config, factory, findPort) {
|
|
158
|
+
const provider = new OpenCodeProvider(config, factory, findPort);
|
|
159
|
+
await provider.initialize();
|
|
160
|
+
return provider;
|
|
161
|
+
}
|
|
162
|
+
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
163
|
+
/**
|
|
164
|
+
* Starts the OpenCode server and configures permissions and MCP tools.
|
|
165
|
+
*
|
|
166
|
+
* Must be called exactly once before using the provider. Subsequent
|
|
167
|
+
* calls are no-ops if already initialized.
|
|
168
|
+
*
|
|
169
|
+
* @throws {Error} If `destinationPath` is empty or blank.
|
|
170
|
+
* @throws {Error} If the OpenCode server fails to start.
|
|
171
|
+
*/
|
|
172
|
+
async initialize() {
|
|
173
|
+
if (this._client)
|
|
174
|
+
return;
|
|
175
|
+
const absoluteDestination = resolve(this._config.destinationPath);
|
|
176
|
+
if (absoluteDestination.trim() === "") {
|
|
177
|
+
throw new Error("destinationPath must be a non-empty string. " +
|
|
178
|
+
"Provide a valid directory path for generated documentation.");
|
|
179
|
+
}
|
|
180
|
+
const { providerID, modelID } = this._config.model;
|
|
181
|
+
const modelString = modelID === "default"
|
|
182
|
+
? undefined
|
|
183
|
+
// modelID may already include a sub-provider prefix (e.g. "github-copilot/opus-4.6")
|
|
184
|
+
// when the user picked an opencode sub-provider model via `hem config`. Use it as-is.
|
|
185
|
+
: modelID.includes("/") ? modelID : `${providerID}/${modelID}`;
|
|
186
|
+
// Writing agents get scoped access to the destination directory.
|
|
187
|
+
const writingAgentPermission = {
|
|
188
|
+
edit: "allow",
|
|
189
|
+
bash: READ_ONLY_BASH,
|
|
190
|
+
webfetch: "allow",
|
|
191
|
+
external_directory: "allow",
|
|
192
|
+
};
|
|
193
|
+
// Organization workers additionally need `rm` to execute arbiter DELETE
|
|
194
|
+
// decisions (instead of writing empty content as a deletion proxy).
|
|
195
|
+
const orgAgentPermission = {
|
|
196
|
+
edit: "allow",
|
|
197
|
+
bash: ORG_AGENT_BASH,
|
|
198
|
+
webfetch: "allow",
|
|
199
|
+
external_directory: "allow",
|
|
200
|
+
};
|
|
201
|
+
// Resolve the path to the compiled broadcast MCP server script.
|
|
202
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
203
|
+
const broadcastMcpPath = join(thisDir, "..", "broadcast-mcp.js");
|
|
204
|
+
const searchMcpPath = join(thisDir, "..", "search-mcp.js");
|
|
205
|
+
const searchDbPath = this._config.searchDbPath;
|
|
206
|
+
try {
|
|
207
|
+
const { client, server } = await startWithRetry(async () => {
|
|
208
|
+
const port = await this._findPort();
|
|
209
|
+
return this._factory({
|
|
210
|
+
port,
|
|
211
|
+
config: {
|
|
212
|
+
...(modelString != null && { model: modelString }),
|
|
213
|
+
mcp: {
|
|
214
|
+
"hem-broadcast": {
|
|
215
|
+
type: "local",
|
|
216
|
+
command: ["node", broadcastMcpPath],
|
|
217
|
+
enabled: true,
|
|
218
|
+
},
|
|
219
|
+
...(searchDbPath && {
|
|
220
|
+
"hem-search": {
|
|
221
|
+
type: "local",
|
|
222
|
+
command: ["node", searchMcpPath, searchDbPath],
|
|
223
|
+
enabled: true,
|
|
224
|
+
},
|
|
225
|
+
}),
|
|
226
|
+
},
|
|
227
|
+
permission: {
|
|
228
|
+
edit: "deny",
|
|
229
|
+
bash: "deny",
|
|
230
|
+
webfetch: "deny",
|
|
231
|
+
},
|
|
232
|
+
agent: {
|
|
233
|
+
"hem-explore": {
|
|
234
|
+
permission: {
|
|
235
|
+
edit: "deny",
|
|
236
|
+
bash: READ_ONLY_BASH,
|
|
237
|
+
webfetch: "allow",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
"hem-doc": {
|
|
241
|
+
permission: writingAgentPermission,
|
|
242
|
+
},
|
|
243
|
+
"hem-arch": {
|
|
244
|
+
permission: writingAgentPermission,
|
|
245
|
+
},
|
|
246
|
+
"hem-index": {
|
|
247
|
+
permission: writingAgentPermission,
|
|
248
|
+
},
|
|
249
|
+
"hem-org": {
|
|
250
|
+
permission: orgAgentPermission,
|
|
251
|
+
},
|
|
252
|
+
"hem-xref": {
|
|
253
|
+
permission: writingAgentPermission,
|
|
254
|
+
},
|
|
255
|
+
"hem-group": {
|
|
256
|
+
permission: {
|
|
257
|
+
edit: "deny",
|
|
258
|
+
bash: READ_ONLY_BASH,
|
|
259
|
+
webfetch: "deny",
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
trackServer(() => server.close());
|
|
267
|
+
this._client = client;
|
|
268
|
+
this._shutdownFn = async () => {
|
|
269
|
+
server.close();
|
|
270
|
+
untrackServer();
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
275
|
+
const isPortConflict = /EADDRINUSE|port.*in use|already.*listening/i.test(message);
|
|
276
|
+
const hint = isPortConflict
|
|
277
|
+
? ` A process is already using this port. Kill stale OpenCode processes or restart your terminal.\n`
|
|
278
|
+
: ` This may be a Node.js version issue (requires 20+) or a network problem.\n`;
|
|
279
|
+
throw new Error(`Failed to start OpenCode server: ${message}\n` +
|
|
280
|
+
hint +
|
|
281
|
+
` Run with --verbose for detailed logs.`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// ── Provider interface methods ─────────────────────────────────────
|
|
285
|
+
/**
|
|
286
|
+
* Create a new OpenCode session.
|
|
287
|
+
*
|
|
288
|
+
* @returns The session ID.
|
|
289
|
+
* @throws {Error} If the provider is not initialized.
|
|
290
|
+
*/
|
|
291
|
+
async createSession() {
|
|
292
|
+
if (!this._client) {
|
|
293
|
+
throw new Error("OpenCodeProvider not initialized. Call initialize() first.");
|
|
294
|
+
}
|
|
295
|
+
const { data: session, error } = await this._client.session.create();
|
|
296
|
+
if (error || !session) {
|
|
297
|
+
throw new Error(`Failed to create OpenCode session: ${String(error ?? "unknown error")}`);
|
|
298
|
+
}
|
|
299
|
+
return session.id;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Send a prompt to an OpenCode session and wait for the response.
|
|
303
|
+
*
|
|
304
|
+
* Uses the async prompt flow to avoid HTTP timeout issues on slow LLM
|
|
305
|
+
* responses (mirrors Dispatch's OpenCode provider):
|
|
306
|
+
* 1. `promptAsync()` — fire-and-forget POST that returns 204 immediately
|
|
307
|
+
* 2. `event.subscribe()` — SSE stream for session lifecycle events
|
|
308
|
+
* 3. Wait for `session.idle` (success) or `session.error` (failure)
|
|
309
|
+
* 4. `session.messages()` — fetch the completed response
|
|
310
|
+
*
|
|
311
|
+
* @param sessionId - The session ID returned by `createSession()`.
|
|
312
|
+
* @param text - The prompt text.
|
|
313
|
+
* @param options - Optional: `agent` selects the permission profile.
|
|
314
|
+
*/
|
|
315
|
+
async prompt(sessionId, text, options) {
|
|
316
|
+
if (!this._client) {
|
|
317
|
+
throw new Error("OpenCodeProvider not initialized. Call initialize() first.");
|
|
318
|
+
}
|
|
319
|
+
// 1. Fire-and-forget prompt
|
|
320
|
+
const { error: promptError } = await this._client.session.promptAsync({
|
|
321
|
+
path: { id: sessionId },
|
|
322
|
+
body: {
|
|
323
|
+
parts: [{ type: "text", text }],
|
|
324
|
+
agent: options?.agent,
|
|
325
|
+
...(this._modelOverride ? { model: this._modelOverride } : {}),
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
if (promptError) {
|
|
329
|
+
throw new Error(`OpenCode promptAsync failed: ${JSON.stringify(promptError)}`);
|
|
330
|
+
}
|
|
331
|
+
// 2. Subscribe to SSE events
|
|
332
|
+
const controller = new AbortController();
|
|
333
|
+
const { stream } = await this._client.event.subscribe({
|
|
334
|
+
signal: controller.signal,
|
|
335
|
+
});
|
|
336
|
+
// 3. Wait for session.idle or session.error
|
|
337
|
+
try {
|
|
338
|
+
for await (const event of stream) {
|
|
339
|
+
if (!isSessionEvent(event, sessionId))
|
|
340
|
+
continue;
|
|
341
|
+
if (event.type === "session.error") {
|
|
342
|
+
const err = event.properties?.error;
|
|
343
|
+
throw new Error(`OpenCode session error: ${err ? JSON.stringify(err) : "unknown error"}`);
|
|
344
|
+
}
|
|
345
|
+
if (event.type === "session.idle") {
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
controller.abort();
|
|
352
|
+
}
|
|
353
|
+
// 4. Fetch completed messages
|
|
354
|
+
const { data: messages } = await this._client.session.messages({
|
|
355
|
+
path: { id: sessionId },
|
|
356
|
+
});
|
|
357
|
+
if (!messages || messages.length === 0)
|
|
358
|
+
return null;
|
|
359
|
+
const lastAssistant = [...messages]
|
|
360
|
+
.reverse()
|
|
361
|
+
.find((m) => m.info.role === "assistant");
|
|
362
|
+
if (!lastAssistant)
|
|
363
|
+
return null;
|
|
364
|
+
// Check for assistant-level error
|
|
365
|
+
if (lastAssistant.info.error) {
|
|
366
|
+
throw new Error(`OpenCode assistant error: ${JSON.stringify(lastAssistant.info.error)}`);
|
|
367
|
+
}
|
|
368
|
+
// Extract text parts
|
|
369
|
+
const textParts = lastAssistant.parts.filter((p) => p.type === "text" && "text" in p);
|
|
370
|
+
return textParts.map((p) => p.text).join("\n") || null;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Shuts down the OpenCode server and releases all resources.
|
|
374
|
+
*
|
|
375
|
+
* After cleanup, the provider instance must not be reused.
|
|
376
|
+
* No-op if the provider was never initialized or already shut down.
|
|
377
|
+
*/
|
|
378
|
+
async cleanup() {
|
|
379
|
+
if (this._shutdownFn) {
|
|
380
|
+
await this._shutdownFn();
|
|
381
|
+
this._client = null;
|
|
382
|
+
this._shutdownFn = null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Alias for `cleanup()` — kept for backward compatibility with callers
|
|
387
|
+
* that use the old `shutdown()` name.
|
|
388
|
+
*/
|
|
389
|
+
async shutdown() {
|
|
390
|
+
return this.cleanup();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// ── SSE event filter ────────────────────────────────────────────────────
|
|
394
|
+
/**
|
|
395
|
+
* Check whether an SSE event belongs to the given session.
|
|
396
|
+
*
|
|
397
|
+
* Different event types store the session ID in different places:
|
|
398
|
+
* - `session.*` events → `properties.sessionID`
|
|
399
|
+
* - `message.*` events → `properties.info.sessionID` or `properties.part.sessionID`
|
|
400
|
+
*/
|
|
401
|
+
function isSessionEvent(event, sessionId) {
|
|
402
|
+
const props = event.properties;
|
|
403
|
+
if (props.sessionID === sessionId)
|
|
404
|
+
return true;
|
|
405
|
+
if (props.info &&
|
|
406
|
+
typeof props.info === "object" &&
|
|
407
|
+
props.info.sessionID === sessionId) {
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
if (props.part &&
|
|
411
|
+
typeof props.part === "object" &&
|
|
412
|
+
props.part.sessionID === sessionId) {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|