@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,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone stdio-based MCP server exposing the Hem doc search index.
|
|
4
|
+
*
|
|
5
|
+
* Exposes two tools:
|
|
6
|
+
* - `search_docs({ query, limit? })` — FTS5 keyword search over indexed docs
|
|
7
|
+
* - `get_docs_for_source({ source_path })` — find docs covering a source file
|
|
8
|
+
*
|
|
9
|
+
* Receives the SQLite DB path as the first CLI argument:
|
|
10
|
+
* node dist/search-mcp.js /path/to/.hem/search-index.db
|
|
11
|
+
*
|
|
12
|
+
* Opens the DB in read-only mode. Multiple agent processes may connect
|
|
13
|
+
* simultaneously; SQLite WAL mode handles concurrent reads safely.
|
|
14
|
+
*
|
|
15
|
+
* Registered in OpenCode via config.mcp as:
|
|
16
|
+
* { type: "local", command: ["node", "dist/search-mcp.js", dbPath], enabled: true }
|
|
17
|
+
*/
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { z } from "zod/v4";
|
|
22
|
+
import { SearchIndex } from "./search-index.js";
|
|
23
|
+
/**
|
|
24
|
+
* Build the hem-search MCP server bound to the given SearchIndex.
|
|
25
|
+
* Exported for tests; the CLI bootstrap below wires it up to a stdio
|
|
26
|
+
* transport with a real read-only SQLite-backed index.
|
|
27
|
+
*/
|
|
28
|
+
export function createSearchMcpServer(index) {
|
|
29
|
+
const server = new McpServer({
|
|
30
|
+
name: "hem-search",
|
|
31
|
+
version: "1.0.0",
|
|
32
|
+
});
|
|
33
|
+
server.registerTool("search_docs", {
|
|
34
|
+
description: "Search existing documentation files by keyword. Returns the most relevant " +
|
|
35
|
+
"docs with short snippets. Use this before creating or updating a doc to find " +
|
|
36
|
+
"existing coverage. Example: search_docs({ query: 'authentication session token' })",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
query: z.string().describe("Search terms — keywords, phrases, or topic names"),
|
|
39
|
+
limit: z
|
|
40
|
+
.number()
|
|
41
|
+
.int()
|
|
42
|
+
.min(1)
|
|
43
|
+
.max(20)
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Maximum results to return (default 5, max 20)"),
|
|
46
|
+
},
|
|
47
|
+
}, ({ query, limit }) => {
|
|
48
|
+
const results = index.search(query, limit ?? 5);
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: "text",
|
|
53
|
+
text: JSON.stringify({ results }),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
server.registerTool("get_docs_for_source", {
|
|
59
|
+
description: "Find documentation files that cover a given source code file. " +
|
|
60
|
+
"Useful for checking whether a source file is already documented. " +
|
|
61
|
+
"Example: get_docs_for_source({ source_path: 'src/auth/login.ts' })",
|
|
62
|
+
inputSchema: {
|
|
63
|
+
source_path: z
|
|
64
|
+
.string()
|
|
65
|
+
.describe("Relative path to the source file, e.g. 'src/auth/login.ts'"),
|
|
66
|
+
},
|
|
67
|
+
}, ({ source_path }) => {
|
|
68
|
+
const doc_paths = index.getDocsForSource(source_path);
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: JSON.stringify({ doc_paths }),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
return server;
|
|
79
|
+
}
|
|
80
|
+
async function main() {
|
|
81
|
+
const dbPath = process.argv[2];
|
|
82
|
+
if (!dbPath) {
|
|
83
|
+
console.error("[hem-search] Usage: node search-mcp.js <dbPath>");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const index = SearchIndex.open(dbPath, /* readonly */ true);
|
|
87
|
+
const server = createSearchMcpServer(index);
|
|
88
|
+
const transport = new StdioServerTransport();
|
|
89
|
+
await server.connect(transport);
|
|
90
|
+
console.error("[hem-search] MCP server running on stdio");
|
|
91
|
+
}
|
|
92
|
+
// Only auto-run when invoked directly (not when imported by tests).
|
|
93
|
+
const isDirectRun = process.argv[1] !== undefined &&
|
|
94
|
+
fileURLToPath(import.meta.url) === process.argv[1];
|
|
95
|
+
if (isDirectRun) {
|
|
96
|
+
main().catch((err) => {
|
|
97
|
+
console.error("[hem-search] Fatal:", err);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server lifecycle utilities for Hem.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - `findFreePort()` — discovers an available TCP port to avoid conflicts
|
|
6
|
+
* with stale OpenCode server processes from previous runs.
|
|
7
|
+
* - `trackServer()` / `untrackServer()` — registers the active server's
|
|
8
|
+
* `close()` function for automatic cleanup on process exit or signal
|
|
9
|
+
* (SIGINT, SIGTERM, SIGHUP, uncaughtException, unhandledRejection),
|
|
10
|
+
* preventing zombie child processes.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Find a free TCP port on 127.0.0.1.
|
|
14
|
+
*
|
|
15
|
+
* Binds a temporary server to port 0 (letting the OS assign an ephemeral
|
|
16
|
+
* port), reads the assigned port, then closes the server. This avoids
|
|
17
|
+
* the SDK's default port 4096 which may be held by a stale process.
|
|
18
|
+
*/
|
|
19
|
+
export declare function findFreePort(): Promise<number>;
|
|
20
|
+
/**
|
|
21
|
+
* Register a server's `close()` function for automatic cleanup on
|
|
22
|
+
* process exit, signal (SIGINT, SIGTERM, SIGHUP), uncaughtException, or
|
|
23
|
+
* unhandledRejection.
|
|
24
|
+
*
|
|
25
|
+
* Call this immediately after `createOpencode()` resolves.
|
|
26
|
+
* Replaces any previously tracked server.
|
|
27
|
+
*/
|
|
28
|
+
export declare function trackServer(closeFn: () => void): void;
|
|
29
|
+
/**
|
|
30
|
+
* Clear the tracked server cleanup function.
|
|
31
|
+
*
|
|
32
|
+
* Call this after `server.close()` in a `finally` block to prevent
|
|
33
|
+
* double-close on normal shutdown.
|
|
34
|
+
*/
|
|
35
|
+
export declare function untrackServer(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Attempt a `findFreePort` + server-creation sequence with automatic retry
|
|
38
|
+
* on port-conflict errors (`EADDRINUSE` and similar).
|
|
39
|
+
*
|
|
40
|
+
* Calls `factory()` up to `maxAttempts` times. If the factory throws an
|
|
41
|
+
* error whose message matches a port-conflict pattern, the error is
|
|
42
|
+
* swallowed and the next attempt proceeds. Non-port-conflict errors
|
|
43
|
+
* propagate immediately without further retries.
|
|
44
|
+
*
|
|
45
|
+
* The factory callback is responsible for calling `findFreePort()` and
|
|
46
|
+
* passing the result to `createOpencode()` (or equivalent). This keeps
|
|
47
|
+
* the retry logic decoupled from any specific server-creation API.
|
|
48
|
+
*
|
|
49
|
+
* @param factory - Async callback that discovers a port and creates
|
|
50
|
+
* the server. Called once per attempt.
|
|
51
|
+
* @param maxAttempts - Maximum number of attempts (default 3).
|
|
52
|
+
* @returns The value returned by a successful `factory()` call.
|
|
53
|
+
* @throws The last port-conflict error if all attempts are exhausted,
|
|
54
|
+
* or any non-port-conflict error immediately.
|
|
55
|
+
*/
|
|
56
|
+
export declare function startWithRetry<T>(factory: () => Promise<T>, maxAttempts?: number): Promise<T>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server lifecycle utilities for Hem.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - `findFreePort()` — discovers an available TCP port to avoid conflicts
|
|
6
|
+
* with stale OpenCode server processes from previous runs.
|
|
7
|
+
* - `trackServer()` / `untrackServer()` — registers the active server's
|
|
8
|
+
* `close()` function for automatic cleanup on process exit or signal
|
|
9
|
+
* (SIGINT, SIGTERM, SIGHUP, uncaughtException, unhandledRejection),
|
|
10
|
+
* preventing zombie child processes.
|
|
11
|
+
*/
|
|
12
|
+
import { createServer } from "node:net";
|
|
13
|
+
// ── Free port discovery ─────────────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* Find a free TCP port on 127.0.0.1.
|
|
16
|
+
*
|
|
17
|
+
* Binds a temporary server to port 0 (letting the OS assign an ephemeral
|
|
18
|
+
* port), reads the assigned port, then closes the server. This avoids
|
|
19
|
+
* the SDK's default port 4096 which may be held by a stale process.
|
|
20
|
+
*/
|
|
21
|
+
export function findFreePort() {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const srv = createServer();
|
|
24
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
25
|
+
const addr = srv.address();
|
|
26
|
+
if (addr === null || typeof addr === "string") {
|
|
27
|
+
srv.close(() => reject(new Error("Failed to determine port from server address")));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const port = addr.port;
|
|
31
|
+
srv.close(() => resolve(port));
|
|
32
|
+
});
|
|
33
|
+
srv.on("error", reject);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// ── Server process cleanup ──────────────────────────────────────────
|
|
37
|
+
//
|
|
38
|
+
// Only one OpenCode server is active at a time (the auth server is closed
|
|
39
|
+
// before the generation server starts), so a single `activeCleanup` slot
|
|
40
|
+
// is sufficient.
|
|
41
|
+
let activeCleanup = null;
|
|
42
|
+
let handlersRegistered = false;
|
|
43
|
+
function onExit() {
|
|
44
|
+
if (activeCleanup) {
|
|
45
|
+
activeCleanup();
|
|
46
|
+
activeCleanup = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function onSignal(signal) {
|
|
50
|
+
onExit();
|
|
51
|
+
// Re-raise the signal so the process exits with the correct code.
|
|
52
|
+
// We must remove our listener first to avoid infinite recursion.
|
|
53
|
+
process.removeAllListeners(signal);
|
|
54
|
+
process.kill(process.pid, signal);
|
|
55
|
+
}
|
|
56
|
+
function ensureHandlers() {
|
|
57
|
+
if (handlersRegistered)
|
|
58
|
+
return;
|
|
59
|
+
handlersRegistered = true;
|
|
60
|
+
process.on("exit", onExit);
|
|
61
|
+
process.on("SIGINT", () => onSignal("SIGINT"));
|
|
62
|
+
process.on("SIGTERM", () => onSignal("SIGTERM"));
|
|
63
|
+
process.on("SIGHUP", () => onSignal("SIGHUP"));
|
|
64
|
+
process.on("uncaughtException", () => {
|
|
65
|
+
onExit();
|
|
66
|
+
process.exit(1);
|
|
67
|
+
});
|
|
68
|
+
process.on("unhandledRejection", () => {
|
|
69
|
+
onExit();
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Register a server's `close()` function for automatic cleanup on
|
|
75
|
+
* process exit, signal (SIGINT, SIGTERM, SIGHUP), uncaughtException, or
|
|
76
|
+
* unhandledRejection.
|
|
77
|
+
*
|
|
78
|
+
* Call this immediately after `createOpencode()` resolves.
|
|
79
|
+
* Replaces any previously tracked server.
|
|
80
|
+
*/
|
|
81
|
+
export function trackServer(closeFn) {
|
|
82
|
+
ensureHandlers();
|
|
83
|
+
activeCleanup = closeFn;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Clear the tracked server cleanup function.
|
|
87
|
+
*
|
|
88
|
+
* Call this after `server.close()` in a `finally` block to prevent
|
|
89
|
+
* double-close on normal shutdown.
|
|
90
|
+
*/
|
|
91
|
+
export function untrackServer() {
|
|
92
|
+
activeCleanup = null;
|
|
93
|
+
}
|
|
94
|
+
// ── Retry utility for port acquisition ──────────────────────────────
|
|
95
|
+
/** Default number of retry attempts for port acquisition. */
|
|
96
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
97
|
+
/** Regex to detect port-conflict errors worth retrying. */
|
|
98
|
+
const PORT_CONFLICT_RE = /EADDRINUSE|port.*in use|already.*listening/i;
|
|
99
|
+
/**
|
|
100
|
+
* Attempt a `findFreePort` + server-creation sequence with automatic retry
|
|
101
|
+
* on port-conflict errors (`EADDRINUSE` and similar).
|
|
102
|
+
*
|
|
103
|
+
* Calls `factory()` up to `maxAttempts` times. If the factory throws an
|
|
104
|
+
* error whose message matches a port-conflict pattern, the error is
|
|
105
|
+
* swallowed and the next attempt proceeds. Non-port-conflict errors
|
|
106
|
+
* propagate immediately without further retries.
|
|
107
|
+
*
|
|
108
|
+
* The factory callback is responsible for calling `findFreePort()` and
|
|
109
|
+
* passing the result to `createOpencode()` (or equivalent). This keeps
|
|
110
|
+
* the retry logic decoupled from any specific server-creation API.
|
|
111
|
+
*
|
|
112
|
+
* @param factory - Async callback that discovers a port and creates
|
|
113
|
+
* the server. Called once per attempt.
|
|
114
|
+
* @param maxAttempts - Maximum number of attempts (default 3).
|
|
115
|
+
* @returns The value returned by a successful `factory()` call.
|
|
116
|
+
* @throws The last port-conflict error if all attempts are exhausted,
|
|
117
|
+
* or any non-port-conflict error immediately.
|
|
118
|
+
*/
|
|
119
|
+
export async function startWithRetry(factory, maxAttempts = DEFAULT_MAX_ATTEMPTS) {
|
|
120
|
+
let lastError;
|
|
121
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
122
|
+
try {
|
|
123
|
+
return await factory();
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
if (!PORT_CONFLICT_RE.test(message)) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
lastError = error;
|
|
131
|
+
// Port conflict — retry with a fresh port on next iteration
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw lastError;
|
|
135
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode session utilities for Hem.
|
|
3
|
+
*
|
|
4
|
+
* Provides the `SessionClient` interface, the centralized `SessionTracker`
|
|
5
|
+
* for monitoring session completion, the shared `promptAndWait()` helper
|
|
6
|
+
* for sending prompts, and JSON extraction helpers used by agent classes.
|
|
7
|
+
*
|
|
8
|
+
* `promptAndWait()` uses the async prompt flow:
|
|
9
|
+
* 1. `session.promptAsync()` — fire the prompt without blocking
|
|
10
|
+
* 2. Register the session with the centralized `SessionTracker`
|
|
11
|
+
* 3. Wait for the tracker to resolve (session disappears from the
|
|
12
|
+
* server's status map, meaning it has completed)
|
|
13
|
+
* 4. Read `session.messages()` to extract the response
|
|
14
|
+
*
|
|
15
|
+
* LLM-specific prompt building and session orchestration have been
|
|
16
|
+
* extracted into dedicated agent classes under `src/agents/`.
|
|
17
|
+
*/
|
|
18
|
+
/** How often the tracker polls session status (ms). */
|
|
19
|
+
export declare const POLL_INTERVAL_MS = 250;
|
|
20
|
+
/** Hard ceiling timeout — if a session hasn't completed after this long,
|
|
21
|
+
* give up and recover from tool history (ms). */
|
|
22
|
+
export declare const MAX_PROMPT_TIMEOUT_MS: number;
|
|
23
|
+
/** Tighter timeout for exploration sessions — they typically finish in 2-3 min.
|
|
24
|
+
* Surfaces hangs 25 minutes earlier than the default ceiling. */
|
|
25
|
+
export declare const EXPLORATION_TIMEOUT_MS: number;
|
|
26
|
+
/** Timeout for documentation generation sessions — more complex than
|
|
27
|
+
* exploration but still bounded; surfaces hangs 20 minutes earlier. */
|
|
28
|
+
export declare const DOCUMENTATION_TIMEOUT_MS: number;
|
|
29
|
+
/**
|
|
30
|
+
* Status of an individual session as reported by the OpenCode server.
|
|
31
|
+
*/
|
|
32
|
+
export interface SessionStatus {
|
|
33
|
+
type: "idle" | "busy" | "retry";
|
|
34
|
+
/** Present when type is "retry". */
|
|
35
|
+
attempt?: number;
|
|
36
|
+
message?: string;
|
|
37
|
+
next?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A part from a message response. This is the union of all part types
|
|
41
|
+
* the SDK may return. We keep it loose to avoid coupling to every SDK
|
|
42
|
+
* part variant — callers inspect `type` to narrow.
|
|
43
|
+
*/
|
|
44
|
+
export interface MessagePart {
|
|
45
|
+
type: string;
|
|
46
|
+
text?: string;
|
|
47
|
+
/** Present on "patch" parts — lists file paths the edit tool touched. */
|
|
48
|
+
files?: string[];
|
|
49
|
+
/** Present on "tool" parts — name of the tool invoked. */
|
|
50
|
+
tool?: string;
|
|
51
|
+
/** Present on "tool" parts — tool call state with input/output. */
|
|
52
|
+
state?: {
|
|
53
|
+
status?: string;
|
|
54
|
+
input?: Record<string, unknown>;
|
|
55
|
+
output?: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Minimal client interface required by agents.
|
|
60
|
+
*
|
|
61
|
+
* Mirrors the subset of `OpencodeClient` used for session creation,
|
|
62
|
+
* async prompting, status polling, and message retrieval.
|
|
63
|
+
* Easy to mock in tests without importing the SDK.
|
|
64
|
+
*/
|
|
65
|
+
export interface SessionClient {
|
|
66
|
+
session: {
|
|
67
|
+
create(options?: {
|
|
68
|
+
body?: {
|
|
69
|
+
title?: string;
|
|
70
|
+
};
|
|
71
|
+
}): Promise<{
|
|
72
|
+
data?: {
|
|
73
|
+
id: string;
|
|
74
|
+
};
|
|
75
|
+
error?: unknown;
|
|
76
|
+
}>;
|
|
77
|
+
promptAsync(options: {
|
|
78
|
+
path: {
|
|
79
|
+
id: string;
|
|
80
|
+
};
|
|
81
|
+
body: {
|
|
82
|
+
parts: Array<{
|
|
83
|
+
type: "text";
|
|
84
|
+
text: string;
|
|
85
|
+
}>;
|
|
86
|
+
agent?: string;
|
|
87
|
+
};
|
|
88
|
+
}): Promise<{
|
|
89
|
+
data?: void;
|
|
90
|
+
error?: unknown;
|
|
91
|
+
}>;
|
|
92
|
+
status(options?: {
|
|
93
|
+
query?: {
|
|
94
|
+
directory?: string;
|
|
95
|
+
};
|
|
96
|
+
}): Promise<{
|
|
97
|
+
data?: Record<string, SessionStatus>;
|
|
98
|
+
error?: unknown;
|
|
99
|
+
}>;
|
|
100
|
+
messages(options: {
|
|
101
|
+
path: {
|
|
102
|
+
id: string;
|
|
103
|
+
};
|
|
104
|
+
}): Promise<{
|
|
105
|
+
data?: Array<{
|
|
106
|
+
info: {
|
|
107
|
+
role: string;
|
|
108
|
+
};
|
|
109
|
+
parts: Array<MessagePart>;
|
|
110
|
+
}>;
|
|
111
|
+
error?: unknown;
|
|
112
|
+
}>;
|
|
113
|
+
abort(options: {
|
|
114
|
+
path: {
|
|
115
|
+
id: string;
|
|
116
|
+
};
|
|
117
|
+
}): Promise<{
|
|
118
|
+
data?: boolean;
|
|
119
|
+
error?: unknown;
|
|
120
|
+
}>;
|
|
121
|
+
delete(options: {
|
|
122
|
+
path: {
|
|
123
|
+
id: string;
|
|
124
|
+
};
|
|
125
|
+
}): Promise<{
|
|
126
|
+
data?: boolean;
|
|
127
|
+
error?: unknown;
|
|
128
|
+
}>;
|
|
129
|
+
};
|
|
130
|
+
event: {
|
|
131
|
+
subscribe(options?: {
|
|
132
|
+
query?: {
|
|
133
|
+
directory?: string;
|
|
134
|
+
};
|
|
135
|
+
}): Promise<{
|
|
136
|
+
stream: AsyncGenerator<SseEvent, void, unknown>;
|
|
137
|
+
}>;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Minimal SSE event type for broadcast interception.
|
|
142
|
+
* We only need to match `message.part.updated` and `session.created` events.
|
|
143
|
+
*/
|
|
144
|
+
export interface SseEvent {
|
|
145
|
+
type: string;
|
|
146
|
+
properties: Record<string, unknown>;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Centralized session completion tracker.
|
|
150
|
+
*
|
|
151
|
+
* Runs a single poll loop that calls `GET /session/status` once per
|
|
152
|
+
* interval and resolves individual session promises as they disappear
|
|
153
|
+
* from the server's status map (meaning they have completed).
|
|
154
|
+
*
|
|
155
|
+
* The OpenCode server only reports actively-processing sessions in the
|
|
156
|
+
* status map — completed sessions are removed entirely. The tracker
|
|
157
|
+
* exploits this: "present in map" = busy, "absent from map" = done.
|
|
158
|
+
*
|
|
159
|
+
* **Subagent awareness**: subscribes to SSE `session.created` events to
|
|
160
|
+
* detect child sessions spawned by any tracked parent. A parent session
|
|
161
|
+
* is not resolved as "done" until the parent itself AND all its
|
|
162
|
+
* descendants have disappeared from the status map.
|
|
163
|
+
*
|
|
164
|
+
* The poll loop is lazy: it starts on the first `track()` call and
|
|
165
|
+
* pauses when the tracked set becomes empty.
|
|
166
|
+
*/
|
|
167
|
+
export interface SessionTracker {
|
|
168
|
+
/**
|
|
169
|
+
* Register a session ID to track.
|
|
170
|
+
*
|
|
171
|
+
* @param sessionId - The session to monitor.
|
|
172
|
+
* @param label - Human-readable label for logging (e.g., group ID).
|
|
173
|
+
* Defaults to truncated session ID.
|
|
174
|
+
* @returns A promise that resolves to `"done"` when the session completes,
|
|
175
|
+
* or `"timeout"` if the per-session deadline is exceeded.
|
|
176
|
+
*/
|
|
177
|
+
track(sessionId: string, label?: string): Promise<"done" | "timeout">;
|
|
178
|
+
/** Stop the poll loop, SSE subscription, and clean up. */
|
|
179
|
+
stop(): void;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Creates a centralized session tracker with subagent awareness.
|
|
183
|
+
*
|
|
184
|
+
* The tracker subscribes to SSE `session.created` events to detect child
|
|
185
|
+
* sessions spawned by tracked parents. A parent is only resolved as "done"
|
|
186
|
+
* when both the parent and all its descendants have completed.
|
|
187
|
+
*
|
|
188
|
+
* If the SSE subscription fails, the tracker gracefully degrades to the
|
|
189
|
+
* original poll-only behavior (no child tracking).
|
|
190
|
+
*
|
|
191
|
+
* @param client - The OpenCode SDK client (for `session.status()` and `event.subscribe()`).
|
|
192
|
+
* @param options - Configuration: verbose logger, poll interval, per-session timeout.
|
|
193
|
+
* @returns A `SessionTracker` instance.
|
|
194
|
+
*/
|
|
195
|
+
export declare function createSessionTracker(client: SessionClient, options?: {
|
|
196
|
+
verbose?: (msg: string) => void;
|
|
197
|
+
pollIntervalMs?: number;
|
|
198
|
+
maxTimeoutMs?: number;
|
|
199
|
+
}): SessionTracker;
|
|
200
|
+
/**
|
|
201
|
+
* Sends a prompt to an OpenCode session and returns the complete response.
|
|
202
|
+
*
|
|
203
|
+
* Uses the async prompt flow:
|
|
204
|
+
* 1. `session.promptAsync()` fires the prompt without blocking.
|
|
205
|
+
* 2. Registers the session with the centralized `SessionTracker` and
|
|
206
|
+
* waits for it to complete (disappear from the status map).
|
|
207
|
+
* 3. Reads `session.messages()` to extract the assistant's response.
|
|
208
|
+
* 4. If the hard ceiling is hit, recovers file paths from tool history.
|
|
209
|
+
*
|
|
210
|
+
* @param client - The OpenCode SDK client.
|
|
211
|
+
* @param sessionId - The session to prompt.
|
|
212
|
+
* @param parts - The message parts to send.
|
|
213
|
+
* @param options - Settings: tracker (required), verbose callback, agent selection.
|
|
214
|
+
* @returns The response parts from the assistant message.
|
|
215
|
+
* @throws {AuthExpiredError} If auth expires during the API call.
|
|
216
|
+
* @throws {Error} If the prompt fails to be accepted.
|
|
217
|
+
*/
|
|
218
|
+
export declare function promptAndWait(client: SessionClient, sessionId: string, parts: Array<{
|
|
219
|
+
type: "text";
|
|
220
|
+
text: string;
|
|
221
|
+
}>, options: {
|
|
222
|
+
verbose?: (msg: string) => void;
|
|
223
|
+
agent?: string;
|
|
224
|
+
tracker: SessionTracker;
|
|
225
|
+
/** Human-readable label for tracker logging. */
|
|
226
|
+
label?: string;
|
|
227
|
+
}): Promise<Array<MessagePart>>;
|