@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,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot provider implementation for Hem.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the @github/copilot-sdk CopilotClient behind the {@link Provider}
|
|
5
|
+
* interface. Provides full feature parity with OpenCodeProvider:
|
|
6
|
+
* - MCP server registration (hem-broadcast) per session
|
|
7
|
+
* - Per-agent permission profiles (mapped from agent name to Copilot SDK permissions)
|
|
8
|
+
* - Broadcast tool interception via `onPreToolUse` hook → normalized SSE events
|
|
9
|
+
* - Terminable SSE event stream via central emitter pattern
|
|
10
|
+
* - Verbose logging throughout
|
|
11
|
+
*
|
|
12
|
+
* Aligns with Dispatch's ProviderInstance pattern:
|
|
13
|
+
* - `createSession()` — creates a Copilot session with full MCP + permission config
|
|
14
|
+
* - `prompt(sessionId, text, { agent })` — sets agent context, sends prompt, waits for idle
|
|
15
|
+
* - `cleanup()` — destroys all sessions, terminates SSE generators, stops the client
|
|
16
|
+
*
|
|
17
|
+
* Authentication options (checked in order by the SDK):
|
|
18
|
+
* 1. COPILOT_GITHUB_TOKEN / GH_TOKEN / GITHUB_TOKEN env vars
|
|
19
|
+
* 2. Logged-in Copilot CLI user (default — no token needed)
|
|
20
|
+
*/
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
// ── Read-only bash command allowlist ────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Base commands allowed in read-only bash mode.
|
|
26
|
+
* Mirrors the READ_ONLY_BASH config in OpenCodeProvider.
|
|
27
|
+
*/
|
|
28
|
+
const READ_ONLY_CMDS = new Set([
|
|
29
|
+
"cat", "head", "tail", "grep", "find", "ls",
|
|
30
|
+
"wc", "file", "tree", "du",
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* Check whether a shell command is read-only (safe to run without write access).
|
|
34
|
+
* Matches READ_ONLY_BASH from OpenCodeProvider:
|
|
35
|
+
* cat/head/tail/grep/find/ls/wc/file/tree/du → allow
|
|
36
|
+
* git status / git diff → allow (other git subcommands → deny)
|
|
37
|
+
* rm → allow only for ORG agents
|
|
38
|
+
* * → deny
|
|
39
|
+
*/
|
|
40
|
+
function isReadOnlyCommand(cmd) {
|
|
41
|
+
const parts = cmd.trim().split(/\s+/);
|
|
42
|
+
const base = parts[0] ?? "";
|
|
43
|
+
if (READ_ONLY_CMDS.has(base))
|
|
44
|
+
return true;
|
|
45
|
+
if (base === "git") {
|
|
46
|
+
const sub = parts[1] ?? "";
|
|
47
|
+
return sub === "status" || sub === "diff";
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
/** Environment variable names checked for GitHub token, in priority order. */
|
|
52
|
+
const TOKEN_ENV_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
|
|
53
|
+
/** Resolve the broadcast MCP server path relative to this compiled module. */
|
|
54
|
+
function resolveBroadcastMcpPath() {
|
|
55
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
56
|
+
return join(thisDir, "..", "broadcast-mcp.js");
|
|
57
|
+
}
|
|
58
|
+
// ── Permission helpers ──────────────────────────────────────────────────
|
|
59
|
+
/**
|
|
60
|
+
* Determine whether an agent name is allowed to perform write (edit) operations.
|
|
61
|
+
* Mirrors the `edit` field in OpenCodeProvider's per-agent permission profiles.
|
|
62
|
+
*/
|
|
63
|
+
function agentAllowsEdit(agentName) {
|
|
64
|
+
// Agents that deny edit: hem-explore, hem-group, and any unknown agent (default-deny)
|
|
65
|
+
return agentName === "hem-doc" ||
|
|
66
|
+
agentName === "hem-arch" ||
|
|
67
|
+
agentName === "hem-index" ||
|
|
68
|
+
agentName === "hem-org" ||
|
|
69
|
+
agentName === "hem-xref";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Determine whether a shell command is allowed for the given agent.
|
|
73
|
+
* Mirrors the `bash` field in OpenCodeProvider's per-agent permission profiles.
|
|
74
|
+
*/
|
|
75
|
+
function agentAllowsShell(agentName, command) {
|
|
76
|
+
// hem-group uses READ_ONLY_BASH too (bash is explicitly read-only, not "deny")
|
|
77
|
+
if (isReadOnlyCommand(command))
|
|
78
|
+
return true;
|
|
79
|
+
// hem-org additionally allows rm (mirrors ORG_AGENT_BASH)
|
|
80
|
+
if (agentName === "hem-org" && command.trim().split(/\s+/)[0] === "rm")
|
|
81
|
+
return true;
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Determine whether an agent is allowed to fetch URLs (webfetch).
|
|
86
|
+
* Mirrors the `webfetch` field in OpenCodeProvider's per-agent permission profiles.
|
|
87
|
+
*/
|
|
88
|
+
function agentAllowsWebfetch(agentName) {
|
|
89
|
+
// hem-group denies webfetch; all others allow it
|
|
90
|
+
return agentName !== "hem-group";
|
|
91
|
+
}
|
|
92
|
+
// ── CopilotProvider ─────────────────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* Provider implementation backed by the GitHub Copilot SDK.
|
|
95
|
+
*
|
|
96
|
+
* Provides full feature parity with OpenCodeProvider:
|
|
97
|
+
* - MCP server: hem-broadcast registered on every session
|
|
98
|
+
* - Per-agent permissions: mapped from agent name via onPermissionRequest
|
|
99
|
+
* - Broadcast relay: onPreToolUse hook emits normalized message.part.updated events
|
|
100
|
+
* - SSE events: central emitter with terminable generators
|
|
101
|
+
* - Verbose logging throughout
|
|
102
|
+
*
|
|
103
|
+
* When no token is found in environment variables the SDK uses the
|
|
104
|
+
* Copilot CLI's own auth state (set up via `gh auth login`).
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* const provider = await CopilotProvider.create(config);
|
|
109
|
+
* const sessionId = await provider.createSession();
|
|
110
|
+
* const result = await provider.prompt(sessionId, "Explain this code", { agent: "hem-doc" });
|
|
111
|
+
* await provider.cleanup();
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export class CopilotProvider {
|
|
115
|
+
_config;
|
|
116
|
+
_client = null;
|
|
117
|
+
_stopFn = null;
|
|
118
|
+
_factory;
|
|
119
|
+
_sessions = new Map();
|
|
120
|
+
_sessionMeta = new Map();
|
|
121
|
+
_modelValue;
|
|
122
|
+
_modelDetected = false;
|
|
123
|
+
// ── Central SSE event emitter ──────────────────────────────────────
|
|
124
|
+
_eventHandlers = new Set();
|
|
125
|
+
_sseCleanupHandlers = new Set();
|
|
126
|
+
/** Low-level session operations (implementing Provider interface). */
|
|
127
|
+
session;
|
|
128
|
+
/** SSE event subscription (implementing Provider interface). */
|
|
129
|
+
event;
|
|
130
|
+
/**
|
|
131
|
+
* Creates a new Copilot provider.
|
|
132
|
+
*
|
|
133
|
+
* @param config - Provider configuration (model, destination, permissions).
|
|
134
|
+
* @param factory - Optional client factory override for testing.
|
|
135
|
+
*/
|
|
136
|
+
constructor(config, factory) {
|
|
137
|
+
this._config = config;
|
|
138
|
+
this._factory = factory ?? (async (token) => {
|
|
139
|
+
const { CopilotClient } = await import("@github/copilot-sdk");
|
|
140
|
+
const copilot = new CopilotClient(token ? { githubToken: token } : {});
|
|
141
|
+
await copilot.start();
|
|
142
|
+
return {
|
|
143
|
+
client: copilot,
|
|
144
|
+
stop: async () => { await copilot.stop(); },
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
const self = this;
|
|
148
|
+
this.session = {
|
|
149
|
+
async promptAsync(options) {
|
|
150
|
+
// Set agent context before sending so the permission handler is primed
|
|
151
|
+
const meta = self._sessionMeta.get(options.path.id);
|
|
152
|
+
if (meta && options.body.agent && !meta.agentContext.name) {
|
|
153
|
+
meta.agentContext.name = options.body.agent;
|
|
154
|
+
}
|
|
155
|
+
const copilotSession = self._sessions.get(options.path.id);
|
|
156
|
+
if (!copilotSession)
|
|
157
|
+
return { error: "Session not found" };
|
|
158
|
+
const text = options.body.parts.map((p) => p.text).join("\n");
|
|
159
|
+
try {
|
|
160
|
+
await copilotSession.send({ prompt: text });
|
|
161
|
+
return { data: undefined };
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return { error };
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
async abort(options) {
|
|
168
|
+
const copilotSession = self._sessions.get(options.path.id);
|
|
169
|
+
if (!copilotSession)
|
|
170
|
+
return { data: false };
|
|
171
|
+
try {
|
|
172
|
+
await copilotSession.abort();
|
|
173
|
+
return { data: true };
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
return { error };
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
async delete(options) {
|
|
180
|
+
const copilotSession = self._sessions.get(options.path.id);
|
|
181
|
+
if (!copilotSession)
|
|
182
|
+
return { data: false };
|
|
183
|
+
try {
|
|
184
|
+
await copilotSession.destroy();
|
|
185
|
+
if (self._client) {
|
|
186
|
+
await self._client.deleteSession(options.path.id);
|
|
187
|
+
}
|
|
188
|
+
self._sessions.delete(options.path.id);
|
|
189
|
+
self._sessionMeta.delete(options.path.id);
|
|
190
|
+
return { data: true };
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
return { error };
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
this.event = {
|
|
198
|
+
async subscribe() {
|
|
199
|
+
if (!self._client) {
|
|
200
|
+
throw new Error("CopilotProvider not initialized. Call initialize() first.");
|
|
201
|
+
}
|
|
202
|
+
const eventQueue = [];
|
|
203
|
+
let waiting = null;
|
|
204
|
+
let stopped = false;
|
|
205
|
+
const handler = (event) => {
|
|
206
|
+
eventQueue.push(event);
|
|
207
|
+
waiting?.();
|
|
208
|
+
waiting = null;
|
|
209
|
+
};
|
|
210
|
+
const cleanupFn = () => {
|
|
211
|
+
stopped = true;
|
|
212
|
+
self._eventHandlers.delete(handler);
|
|
213
|
+
waiting?.();
|
|
214
|
+
waiting = null;
|
|
215
|
+
};
|
|
216
|
+
self._eventHandlers.add(handler);
|
|
217
|
+
self._sseCleanupHandlers.add(cleanupFn);
|
|
218
|
+
const stream = (async function* () {
|
|
219
|
+
try {
|
|
220
|
+
while (!stopped) {
|
|
221
|
+
while (eventQueue.length > 0) {
|
|
222
|
+
yield eventQueue.shift();
|
|
223
|
+
}
|
|
224
|
+
if (stopped)
|
|
225
|
+
break;
|
|
226
|
+
await new Promise((resolve) => {
|
|
227
|
+
waiting = resolve;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
self._sseCleanupHandlers.delete(cleanupFn);
|
|
233
|
+
self._eventHandlers.delete(handler);
|
|
234
|
+
}
|
|
235
|
+
})();
|
|
236
|
+
return { stream };
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
// ── Identity ───────────────────────────────────────────────────────
|
|
241
|
+
get name() { return "copilot"; }
|
|
242
|
+
get model() {
|
|
243
|
+
return this._modelValue ? `github-copilot/${this._modelValue}` : undefined;
|
|
244
|
+
}
|
|
245
|
+
get config() { return this._config; }
|
|
246
|
+
// ── Static factory ─────────────────────────────────────────────────
|
|
247
|
+
/**
|
|
248
|
+
* Creates and initializes a CopilotProvider.
|
|
249
|
+
* Mirrors Dispatch's `boot()` pattern — callers receive a ready-to-use
|
|
250
|
+
* provider without calling `initialize()` separately.
|
|
251
|
+
*/
|
|
252
|
+
static async create(config, factory) {
|
|
253
|
+
const provider = new CopilotProvider(config, factory);
|
|
254
|
+
await provider.initialize();
|
|
255
|
+
return provider;
|
|
256
|
+
}
|
|
257
|
+
// ── Private helpers ─────────────────────────────────────────────────
|
|
258
|
+
/** Fan out an SSE event to all active subscribers. */
|
|
259
|
+
_emit(event) {
|
|
260
|
+
for (const handler of this._eventHandlers) {
|
|
261
|
+
handler(event);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Build a Copilot SDK permission handler that enforces the OpenCode-equivalent
|
|
266
|
+
* permission profile for the given agent context.
|
|
267
|
+
*
|
|
268
|
+
* Defaults to approve-all when `agentContext.name` is not yet set (i.e., before
|
|
269
|
+
* the first `prompt()` call). In practice, all tool calls arrive after the
|
|
270
|
+
* prompt, so the agent name is always set before any permission request.
|
|
271
|
+
*/
|
|
272
|
+
_buildPermissionHandler(agentContext) {
|
|
273
|
+
return (request) => {
|
|
274
|
+
const agent = agentContext.name;
|
|
275
|
+
// No agent context yet — default to approve-all.
|
|
276
|
+
if (!agent) {
|
|
277
|
+
return { kind: "approved" };
|
|
278
|
+
}
|
|
279
|
+
switch (request.kind) {
|
|
280
|
+
case "write":
|
|
281
|
+
return agentAllowsEdit(agent)
|
|
282
|
+
? { kind: "approved" }
|
|
283
|
+
: { kind: "denied-by-rules" };
|
|
284
|
+
case "shell": {
|
|
285
|
+
const cmd = request.command ?? "";
|
|
286
|
+
return agentAllowsShell(agent, cmd)
|
|
287
|
+
? { kind: "approved" }
|
|
288
|
+
: { kind: "denied-by-rules" };
|
|
289
|
+
}
|
|
290
|
+
case "url":
|
|
291
|
+
return agentAllowsWebfetch(agent)
|
|
292
|
+
? { kind: "approved" }
|
|
293
|
+
: { kind: "denied-by-rules" };
|
|
294
|
+
case "mcp":
|
|
295
|
+
case "read":
|
|
296
|
+
case "custom-tool":
|
|
297
|
+
// Always allow MCP (broadcast), read-only access, and custom tools.
|
|
298
|
+
return { kind: "approved" };
|
|
299
|
+
default:
|
|
300
|
+
return { kind: "denied-by-rules" };
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Build the full session config for a new Copilot session.
|
|
306
|
+
*
|
|
307
|
+
* Includes:
|
|
308
|
+
* - MCP server: hem-broadcast (mirrors OpenCodeProvider's MCP config)
|
|
309
|
+
* - onPermissionRequest: agent-aware permission handler
|
|
310
|
+
* - hooks.onPreToolUse: intercepts broadcast tool calls → emits normalized SSE
|
|
311
|
+
* - workingDirectory: scoped to destination path
|
|
312
|
+
*/
|
|
313
|
+
_buildSessionConfig(agentContext) {
|
|
314
|
+
const broadcastMcpPath = resolveBroadcastMcpPath();
|
|
315
|
+
const modelId = this._config.model.modelID !== "default"
|
|
316
|
+
? this._config.model.modelID
|
|
317
|
+
: undefined;
|
|
318
|
+
return {
|
|
319
|
+
...(modelId ? { model: modelId } : {}),
|
|
320
|
+
workingDirectory: this._config.destinationPath,
|
|
321
|
+
mcpServers: {
|
|
322
|
+
"hem-broadcast": {
|
|
323
|
+
type: "local",
|
|
324
|
+
command: "node",
|
|
325
|
+
args: [broadcastMcpPath],
|
|
326
|
+
tools: ["*"],
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
onPermissionRequest: this._buildPermissionHandler(agentContext),
|
|
330
|
+
hooks: {
|
|
331
|
+
onPreToolUse: async ({ toolName, toolArgs }, { sessionId }) => {
|
|
332
|
+
// Intercept broadcast tool calls and emit a normalized SSE event.
|
|
333
|
+
// Mirrors OpenCode's message.part.updated SSE event for broadcast detection
|
|
334
|
+
// in OrganizationAgent and CrossRefAgent.
|
|
335
|
+
// NOTE: Copilot CLI passes plain MCP tool names (e.g. "broadcast"), not the
|
|
336
|
+
// "{server}_{tool}" format OpenCode uses in SSE events.
|
|
337
|
+
if (toolName === "broadcast") {
|
|
338
|
+
const message = toolArgs?.message;
|
|
339
|
+
if (message) {
|
|
340
|
+
this._emit({
|
|
341
|
+
type: "message.part.updated",
|
|
342
|
+
properties: {
|
|
343
|
+
part: {
|
|
344
|
+
type: "tool",
|
|
345
|
+
sessionID: sessionId,
|
|
346
|
+
tool: "hem-broadcast_broadcast",
|
|
347
|
+
state: { status: "running", input: { message } },
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
358
|
+
/**
|
|
359
|
+
* Starts the Copilot client.
|
|
360
|
+
*
|
|
361
|
+
* Checks `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, and `GITHUB_TOKEN` environment
|
|
362
|
+
* variables and passes the token to the SDK when found. When no token is
|
|
363
|
+
* present the SDK uses the Copilot CLI's own auth state.
|
|
364
|
+
*
|
|
365
|
+
* Registers the client-level `session.created` listener once (used by agents
|
|
366
|
+
* that monitor child session creation via the SSE event stream).
|
|
367
|
+
*
|
|
368
|
+
* Idempotent — subsequent calls are no-ops if already initialized.
|
|
369
|
+
*
|
|
370
|
+
* @throws {Error} If the Copilot client fails to start.
|
|
371
|
+
*/
|
|
372
|
+
async initialize() {
|
|
373
|
+
if (this._client)
|
|
374
|
+
return;
|
|
375
|
+
const token = CopilotProvider._findToken();
|
|
376
|
+
if (this._config.verbose) {
|
|
377
|
+
this._config.verbose(`[copilot] Starting client${token ? ` (token from ${CopilotProvider._findTokenSource()})` : " (CLI auth)"}`);
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
const { client, stop } = await this._factory(token);
|
|
381
|
+
this._client = client;
|
|
382
|
+
this._stopFn = stop;
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
386
|
+
throw new Error(`Failed to start Copilot client: ${message}\n` +
|
|
387
|
+
` Ensure the Copilot CLI is installed and authenticated.\n` +
|
|
388
|
+
` Run \`gh auth login\` or set the COPILOT_GITHUB_TOKEN environment variable.\n` +
|
|
389
|
+
` Run with --verbose for detailed logs.`);
|
|
390
|
+
}
|
|
391
|
+
// Register client-level session.created listener once.
|
|
392
|
+
// Agents (OrganizationAgent, CrossRefAgent) use this to track child sessions.
|
|
393
|
+
this._client.on("session.created", (...args) => {
|
|
394
|
+
const event = args[0];
|
|
395
|
+
if (event?.sessionId) {
|
|
396
|
+
this._emit({
|
|
397
|
+
type: "session.created",
|
|
398
|
+
properties: {
|
|
399
|
+
session: { id: event.sessionId, parentID: undefined },
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
if (this._config.verbose) {
|
|
405
|
+
this._config.verbose("[copilot] Client started");
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// ── Provider interface methods ─────────────────────────────────────
|
|
409
|
+
/**
|
|
410
|
+
* Create a new Copilot session with MCP server and permission configuration.
|
|
411
|
+
*
|
|
412
|
+
* Each session is created with:
|
|
413
|
+
* - hem-broadcast MCP server (matches OpenCodeProvider's MCP config)
|
|
414
|
+
* - A permission handler that enforces agent-specific rules once the agent
|
|
415
|
+
* name is set via `prompt()` or `session.promptAsync()`
|
|
416
|
+
* - An `onPreToolUse` hook for broadcast interception
|
|
417
|
+
* - SSE event listeners for session.idle and session.error
|
|
418
|
+
*
|
|
419
|
+
* @returns The session ID.
|
|
420
|
+
* @throws {Error} If the provider is not initialized.
|
|
421
|
+
*/
|
|
422
|
+
async createSession() {
|
|
423
|
+
if (!this._client) {
|
|
424
|
+
throw new Error("CopilotProvider not initialized. Call initialize() first.");
|
|
425
|
+
}
|
|
426
|
+
const agentContext = { name: undefined };
|
|
427
|
+
const copilotSession = await this._client.createSession(this._buildSessionConfig(agentContext));
|
|
428
|
+
const sid = copilotSession.sessionId;
|
|
429
|
+
this._sessions.set(sid, copilotSession);
|
|
430
|
+
this._sessionMeta.set(sid, { agentContext });
|
|
431
|
+
// Detect the actual default model from the first session (best-effort).
|
|
432
|
+
if (!this._modelDetected) {
|
|
433
|
+
this._modelDetected = true;
|
|
434
|
+
try {
|
|
435
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
436
|
+
const result = await copilotSession.rpc?.model.getCurrent();
|
|
437
|
+
if (result?.modelId) {
|
|
438
|
+
this._modelValue = result.modelId;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// Best-effort — not fatal if model detection fails.
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Emit session lifecycle events into the central SSE stream so agents
|
|
446
|
+
// that monitor the event stream (via event.subscribe()) can track completion.
|
|
447
|
+
copilotSession.on("session.idle", () => {
|
|
448
|
+
this._emit({
|
|
449
|
+
type: "session.idle",
|
|
450
|
+
properties: { sessionID: sid },
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
copilotSession.on("session.error", (...args) => {
|
|
454
|
+
const err = args[0];
|
|
455
|
+
this._emit({
|
|
456
|
+
type: "session.error",
|
|
457
|
+
properties: {
|
|
458
|
+
sessionID: sid,
|
|
459
|
+
error: err?.data?.message ?? "unknown error",
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
if (this._config.verbose) {
|
|
464
|
+
const modelHint = this._config.model.modelID !== "default"
|
|
465
|
+
? ` (model: ${this._config.model.modelID})`
|
|
466
|
+
: "";
|
|
467
|
+
this._config.verbose(`[copilot] Session created: ${sid}${modelHint}`);
|
|
468
|
+
}
|
|
469
|
+
return sid;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Send a prompt to a Copilot session and wait for the response.
|
|
473
|
+
*
|
|
474
|
+
* Sets the agent context from `options.agent` so the permission handler
|
|
475
|
+
* enforces the correct per-agent policy for all subsequent tool calls.
|
|
476
|
+
*
|
|
477
|
+
* Flow:
|
|
478
|
+
* 1. Set agent context (for permission enforcement)
|
|
479
|
+
* 2. `session.send()` — fires the prompt
|
|
480
|
+
* 3. Wait for `session.idle` or `session.error` event
|
|
481
|
+
* 4. `session.getMessages()` — fetch the completed response
|
|
482
|
+
* 5. Return the last `assistant.message` content, or null
|
|
483
|
+
*
|
|
484
|
+
* @param sessionId - The session ID returned by `createSession()`.
|
|
485
|
+
* @param text - The prompt text.
|
|
486
|
+
* @param options - Optional: `agent` sets the permission profile for this session.
|
|
487
|
+
*/
|
|
488
|
+
async prompt(sessionId, text, options) {
|
|
489
|
+
const copilotSession = this._sessions.get(sessionId);
|
|
490
|
+
if (!copilotSession) {
|
|
491
|
+
throw new Error(`Copilot session ${sessionId} not found`);
|
|
492
|
+
}
|
|
493
|
+
// Set agent context so the permission handler knows which policy to apply.
|
|
494
|
+
const meta = this._sessionMeta.get(sessionId);
|
|
495
|
+
if (meta && options?.agent && !meta.agentContext.name) {
|
|
496
|
+
meta.agentContext.name = options.agent;
|
|
497
|
+
}
|
|
498
|
+
if (this._config.verbose) {
|
|
499
|
+
this._config.verbose(`[copilot] Prompt → ${sessionId.slice(0, 8)}…` +
|
|
500
|
+
(options?.agent ? ` (agent: ${options.agent})` : "") +
|
|
501
|
+
` [${text.length.toLocaleString()} chars]`);
|
|
502
|
+
}
|
|
503
|
+
// 1. Fire the prompt
|
|
504
|
+
await copilotSession.send({ prompt: text });
|
|
505
|
+
// 2. Wait for completion
|
|
506
|
+
await new Promise((resolve, reject) => {
|
|
507
|
+
const unsubIdle = copilotSession.on("session.idle", () => {
|
|
508
|
+
unsubIdle();
|
|
509
|
+
unsubErr();
|
|
510
|
+
resolve();
|
|
511
|
+
});
|
|
512
|
+
const unsubErr = copilotSession.on("session.error", (...args) => {
|
|
513
|
+
unsubIdle();
|
|
514
|
+
unsubErr();
|
|
515
|
+
const event = args[0];
|
|
516
|
+
const msg = event?.data?.message ?? "unknown error";
|
|
517
|
+
reject(new Error(`Copilot session error: ${msg}`));
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
if (this._config.verbose) {
|
|
521
|
+
this._config.verbose(`[copilot] Session idle: ${sessionId.slice(0, 8)}…`);
|
|
522
|
+
}
|
|
523
|
+
// 3. Fetch messages and return the last assistant text
|
|
524
|
+
const events = (await copilotSession.getMessages());
|
|
525
|
+
const last = [...events]
|
|
526
|
+
.reverse()
|
|
527
|
+
.find((e) => e.type === "assistant.message");
|
|
528
|
+
return last?.data?.content ?? null;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Shuts down the Copilot client and releases all resources.
|
|
532
|
+
*
|
|
533
|
+
* 1. Terminates all active SSE generators (unblocks pending iterators).
|
|
534
|
+
* 2. Destroys all active sessions.
|
|
535
|
+
* 3. Stops the Copilot client.
|
|
536
|
+
*
|
|
537
|
+
* After cleanup, the provider instance must not be reused.
|
|
538
|
+
* No-op if the provider was never initialized or already shut down.
|
|
539
|
+
*/
|
|
540
|
+
async cleanup() {
|
|
541
|
+
// 1. Terminate all active SSE generators so subscribers don't hang.
|
|
542
|
+
for (const cleanupFn of this._sseCleanupHandlers) {
|
|
543
|
+
cleanupFn();
|
|
544
|
+
}
|
|
545
|
+
this._sseCleanupHandlers.clear();
|
|
546
|
+
if (this._config.verbose && this._sessions.size > 0) {
|
|
547
|
+
this._config.verbose(`[copilot] Cleaning up ${this._sessions.size} session(s)`);
|
|
548
|
+
}
|
|
549
|
+
// 2. Destroy all active sessions.
|
|
550
|
+
const destroyOps = [...this._sessions.values()].map((s) => s.destroy().catch(() => { }));
|
|
551
|
+
await Promise.all(destroyOps);
|
|
552
|
+
this._sessions.clear();
|
|
553
|
+
this._sessionMeta.clear();
|
|
554
|
+
// 3. Stop the client.
|
|
555
|
+
if (this._stopFn) {
|
|
556
|
+
await this._stopFn();
|
|
557
|
+
this._client = null;
|
|
558
|
+
this._stopFn = null;
|
|
559
|
+
}
|
|
560
|
+
if (this._config.verbose) {
|
|
561
|
+
this._config.verbose("[copilot] Client stopped");
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Alias for `cleanup()` — kept for backward compatibility with callers
|
|
566
|
+
* that use the old `shutdown()` name.
|
|
567
|
+
*/
|
|
568
|
+
async shutdown() {
|
|
569
|
+
return this.cleanup();
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Discovers a GitHub token from well-known environment variables.
|
|
573
|
+
*
|
|
574
|
+
* Returns `undefined` when no token is set — the SDK will then use the
|
|
575
|
+
* Copilot CLI's own auth state instead.
|
|
576
|
+
*
|
|
577
|
+
* @internal
|
|
578
|
+
*/
|
|
579
|
+
static _findToken() {
|
|
580
|
+
for (const envVar of TOKEN_ENV_VARS) {
|
|
581
|
+
const value = process.env[envVar];
|
|
582
|
+
if (value)
|
|
583
|
+
return value;
|
|
584
|
+
}
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Returns the name of the env var that provided the token, for logging.
|
|
589
|
+
* @internal
|
|
590
|
+
*/
|
|
591
|
+
static _findTokenSource() {
|
|
592
|
+
for (const envVar of TOKEN_ENV_VARS) {
|
|
593
|
+
if (process.env[envVar])
|
|
594
|
+
return envVar;
|
|
595
|
+
}
|
|
596
|
+
return "unknown";
|
|
597
|
+
}
|
|
598
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider abstraction barrel export for Hem.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the {@link Provider} interface, {@link ProviderConfig},
|
|
5
|
+
* the provider implementations, and permission constants so consumers
|
|
6
|
+
* can import everything from `providers/` in a single statement.
|
|
7
|
+
*
|
|
8
|
+
* Note: SessionClient, SessionTracker, CopilotSessionAdapter are no longer
|
|
9
|
+
* exported — they are internal implementation details of the providers.
|
|
10
|
+
*/
|
|
11
|
+
export type { Provider, ProviderConfig, SseEvent, } from "./types.js";
|
|
12
|
+
export { OpenCodeProvider, READ_ONLY_BASH, ORG_AGENT_BASH } from "./opencode.js";
|
|
13
|
+
export type { CreateOpencodeFn } from "./opencode.js";
|
|
14
|
+
export { CopilotProvider } from "./copilot.js";
|
|
15
|
+
export type { CreateCopilotClientFn, CopilotClientLike, CopilotSessionLike, } from "./copilot.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider abstraction barrel export for Hem.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the {@link Provider} interface, {@link ProviderConfig},
|
|
5
|
+
* the provider implementations, and permission constants so consumers
|
|
6
|
+
* can import everything from `providers/` in a single statement.
|
|
7
|
+
*
|
|
8
|
+
* Note: SessionClient, SessionTracker, CopilotSessionAdapter are no longer
|
|
9
|
+
* exported — they are internal implementation details of the providers.
|
|
10
|
+
*/
|
|
11
|
+
export { OpenCodeProvider, READ_ONLY_BASH, ORG_AGENT_BASH } from "./opencode.js";
|
|
12
|
+
export { CopilotProvider } from "./copilot.js";
|