@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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agents/arbiter-agent.d.ts +72 -0
  3. package/dist/agents/arbiter-agent.js +149 -0
  4. package/dist/agents/architecture-agent.d.ts +148 -0
  5. package/dist/agents/architecture-agent.js +459 -0
  6. package/dist/agents/base-agent.d.ts +44 -0
  7. package/dist/agents/base-agent.js +57 -0
  8. package/dist/agents/crossref-agent.d.ts +140 -0
  9. package/dist/agents/crossref-agent.js +560 -0
  10. package/dist/agents/crossref-arbiter-agent.d.ts +72 -0
  11. package/dist/agents/crossref-arbiter-agent.js +147 -0
  12. package/dist/agents/documentation-agent.d.ts +55 -0
  13. package/dist/agents/documentation-agent.js +159 -0
  14. package/dist/agents/exploration-agent.d.ts +58 -0
  15. package/dist/agents/exploration-agent.js +102 -0
  16. package/dist/agents/grouping-agent.d.ts +167 -0
  17. package/dist/agents/grouping-agent.js +557 -0
  18. package/dist/agents/index-agent.d.ts +86 -0
  19. package/dist/agents/index-agent.js +360 -0
  20. package/dist/agents/organization-agent.d.ts +144 -0
  21. package/dist/agents/organization-agent.js +607 -0
  22. package/dist/auth.d.ts +372 -0
  23. package/dist/auth.js +1072 -0
  24. package/dist/broadcast-mcp.d.ts +21 -0
  25. package/dist/broadcast-mcp.js +59 -0
  26. package/dist/changelog.d.ts +85 -0
  27. package/dist/changelog.js +223 -0
  28. package/dist/decision-queue.d.ts +173 -0
  29. package/dist/decision-queue.js +265 -0
  30. package/dist/diff-scope.d.ts +24 -0
  31. package/dist/diff-scope.js +28 -0
  32. package/dist/discovery.d.ts +54 -0
  33. package/dist/discovery.js +405 -0
  34. package/dist/grouping.d.ts +37 -0
  35. package/dist/grouping.js +343 -0
  36. package/dist/helpers/format.d.ts +5 -0
  37. package/dist/helpers/format.js +13 -0
  38. package/dist/helpers/index.d.ts +11 -0
  39. package/dist/helpers/index.js +11 -0
  40. package/dist/helpers/parsing.d.ts +52 -0
  41. package/dist/helpers/parsing.js +128 -0
  42. package/dist/helpers/paths.d.ts +41 -0
  43. package/dist/helpers/paths.js +67 -0
  44. package/dist/helpers/strings.d.ts +45 -0
  45. package/dist/helpers/strings.js +97 -0
  46. package/dist/index.d.ts +135 -0
  47. package/dist/index.js +1087 -0
  48. package/dist/merge-utils.d.ts +22 -0
  49. package/dist/merge-utils.js +34 -0
  50. package/dist/orchestrator.d.ts +194 -0
  51. package/dist/orchestrator.js +1169 -0
  52. package/dist/output.d.ts +106 -0
  53. package/dist/output.js +243 -0
  54. package/dist/progress.d.ts +228 -0
  55. package/dist/progress.js +644 -0
  56. package/dist/providers/copilot.d.ts +247 -0
  57. package/dist/providers/copilot.js +598 -0
  58. package/dist/providers/index.d.ts +15 -0
  59. package/dist/providers/index.js +12 -0
  60. package/dist/providers/opencode.d.ts +156 -0
  61. package/dist/providers/opencode.js +416 -0
  62. package/dist/providers/types.d.ts +156 -0
  63. package/dist/providers/types.js +16 -0
  64. package/dist/resources.d.ts +76 -0
  65. package/dist/resources.js +151 -0
  66. package/dist/search-index.d.ts +71 -0
  67. package/dist/search-index.js +187 -0
  68. package/dist/search-mcp.d.ts +25 -0
  69. package/dist/search-mcp.js +100 -0
  70. package/dist/server-utils.d.ts +56 -0
  71. package/dist/server-utils.js +135 -0
  72. package/dist/session.d.ts +227 -0
  73. package/dist/session.js +370 -0
  74. package/dist/types.d.ts +272 -0
  75. package/dist/types.js +5 -0
  76. package/dist/worktree.d.ts +82 -0
  77. package/dist/worktree.js +187 -0
  78. 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";