@ilya-lesikov/pi-pi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/3p/pi-ask-user/index.ts +1835 -0
- package/3p/pi-ask-user/package.json +50 -0
- package/3p/pi-ask-user/single-select-layout.ts +203 -0
- package/3p/pi-lsp/extensions/lsp/client.ts +542 -0
- package/3p/pi-lsp/extensions/lsp/config.ts +135 -0
- package/3p/pi-lsp/extensions/lsp/effects/command.ts +42 -0
- package/3p/pi-lsp/extensions/lsp/effects/filesystem.ts +50 -0
- package/3p/pi-lsp/extensions/lsp/effects/runtime.ts +21 -0
- package/3p/pi-lsp/extensions/lsp/errors.ts +113 -0
- package/3p/pi-lsp/extensions/lsp/formatting.ts +315 -0
- package/3p/pi-lsp/extensions/lsp/index.ts +231 -0
- package/3p/pi-lsp/extensions/lsp/protocol.ts +209 -0
- package/3p/pi-lsp/extensions/lsp/retry.ts +39 -0
- package/3p/pi-lsp/extensions/lsp/tools/programs.ts +293 -0
- package/3p/pi-lsp/extensions/lsp/tools.ts +89 -0
- package/3p/pi-lsp/extensions/lsp/types.ts +243 -0
- package/3p/pi-lsp/package.json +54 -0
- package/3p/pi-plannotator/AGENTS.md +583 -0
- package/3p/pi-plannotator/apps/pi-extension/README.md +228 -0
- package/3p/pi-plannotator/apps/pi-extension/assistant-message.ts +128 -0
- package/3p/pi-plannotator/apps/pi-extension/config.test.ts +166 -0
- package/3p/pi-plannotator/apps/pi-extension/config.ts +318 -0
- package/3p/pi-plannotator/apps/pi-extension/current-pi-session.ts +147 -0
- package/3p/pi-plannotator/apps/pi-extension/index.ts +1279 -0
- package/3p/pi-plannotator/apps/pi-extension/package.json +56 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.test.ts +13 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +616 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +342 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator.json +12 -0
- package/3p/pi-plannotator/apps/pi-extension/server/agent-jobs.ts +515 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ai-runtime.ts +169 -0
- package/3p/pi-plannotator/apps/pi-extension/server/annotations.ts +85 -0
- package/3p/pi-plannotator/apps/pi-extension/server/external-annotations.ts +189 -0
- package/3p/pi-plannotator/apps/pi-extension/server/handlers.ts +210 -0
- package/3p/pi-plannotator/apps/pi-extension/server/helpers.ts +78 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ide.ts +46 -0
- package/3p/pi-plannotator/apps/pi-extension/server/integrations.ts +195 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.test.ts +158 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.ts +268 -0
- package/3p/pi-plannotator/apps/pi-extension/server/pr.ts +126 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +64 -0
- package/3p/pi-plannotator/apps/pi-extension/server/reference.ts +362 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverAnnotate.ts +199 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverPlan.ts +505 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +1229 -0
- package/3p/pi-plannotator/apps/pi-extension/server/vcs.ts +121 -0
- package/3p/pi-plannotator/apps/pi-extension/server.test.ts +966 -0
- package/3p/pi-plannotator/apps/pi-extension/server.ts +44 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.test.ts +88 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.ts +39 -0
- package/3p/pi-plannotator/apps/pi-extension/tsconfig.json +16 -0
- package/3p/pi-plannotator/apps/pi-extension/vendor.sh +48 -0
- package/3p/pi-plannotator/package.json +53 -0
- package/3p/pi-plannotator/packages/ai/ai.test.ts +1407 -0
- package/3p/pi-plannotator/packages/ai/base-session.ts +94 -0
- package/3p/pi-plannotator/packages/ai/context.ts +250 -0
- package/3p/pi-plannotator/packages/ai/endpoints.ts +326 -0
- package/3p/pi-plannotator/packages/ai/index.ts +105 -0
- package/3p/pi-plannotator/packages/ai/package.json +21 -0
- package/3p/pi-plannotator/packages/ai/provider.ts +103 -0
- package/3p/pi-plannotator/packages/ai/providers/claude-agent-sdk.ts +447 -0
- package/3p/pi-plannotator/packages/ai/providers/codex-sdk.ts +430 -0
- package/3p/pi-plannotator/packages/ai/providers/command-path.ts +115 -0
- package/3p/pi-plannotator/packages/ai/providers/opencode-sdk.ts +546 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-events.ts +110 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk-node.ts +425 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk.ts +469 -0
- package/3p/pi-plannotator/packages/ai/session-manager.ts +195 -0
- package/3p/pi-plannotator/packages/ai/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/ai/types.ts +379 -0
- package/3p/pi-plannotator/packages/server/agent-jobs.ts +538 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.test.ts +135 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.ts +243 -0
- package/3p/pi-plannotator/packages/server/ai-runtime.ts +108 -0
- package/3p/pi-plannotator/packages/server/annotate.ts +401 -0
- package/3p/pi-plannotator/packages/server/browser.test.ts +87 -0
- package/3p/pi-plannotator/packages/server/browser.ts +245 -0
- package/3p/pi-plannotator/packages/server/claude-review.ts +352 -0
- package/3p/pi-plannotator/packages/server/code-nav.ts +73 -0
- package/3p/pi-plannotator/packages/server/codex-review-schema.json +41 -0
- package/3p/pi-plannotator/packages/server/codex-review.ts +330 -0
- package/3p/pi-plannotator/packages/server/config.ts +8 -0
- package/3p/pi-plannotator/packages/server/draft.ts +1 -0
- package/3p/pi-plannotator/packages/server/editor-annotations.ts +76 -0
- package/3p/pi-plannotator/packages/server/external-annotations.test.ts +18 -0
- package/3p/pi-plannotator/packages/server/external-annotations.ts +207 -0
- package/3p/pi-plannotator/packages/server/git.ts +141 -0
- package/3p/pi-plannotator/packages/server/goal-setup.test.ts +55 -0
- package/3p/pi-plannotator/packages/server/goal-setup.ts +248 -0
- package/3p/pi-plannotator/packages/server/ide.ts +43 -0
- package/3p/pi-plannotator/packages/server/image.test.ts +63 -0
- package/3p/pi-plannotator/packages/server/image.ts +66 -0
- package/3p/pi-plannotator/packages/server/index.ts +648 -0
- package/3p/pi-plannotator/packages/server/integrations.test.ts +172 -0
- package/3p/pi-plannotator/packages/server/integrations.ts +214 -0
- package/3p/pi-plannotator/packages/server/jj.test.ts +69 -0
- package/3p/pi-plannotator/packages/server/jj.ts +86 -0
- package/3p/pi-plannotator/packages/server/p4.ts +417 -0
- package/3p/pi-plannotator/packages/server/package.json +40 -0
- package/3p/pi-plannotator/packages/server/path-utils.ts +18 -0
- package/3p/pi-plannotator/packages/server/pr.ts +146 -0
- package/3p/pi-plannotator/packages/server/project.test.ts +115 -0
- package/3p/pi-plannotator/packages/server/project.ts +45 -0
- package/3p/pi-plannotator/packages/server/reference-handlers.ts +404 -0
- package/3p/pi-plannotator/packages/server/remote.test.ts +150 -0
- package/3p/pi-plannotator/packages/server/remote.ts +74 -0
- package/3p/pi-plannotator/packages/server/repo.ts +80 -0
- package/3p/pi-plannotator/packages/server/resolve-file.test.ts +310 -0
- package/3p/pi-plannotator/packages/server/review-workspace.test.ts +1032 -0
- package/3p/pi-plannotator/packages/server/review-workspace.ts +48 -0
- package/3p/pi-plannotator/packages/server/review.ts +1318 -0
- package/3p/pi-plannotator/packages/server/sessions.ts +111 -0
- package/3p/pi-plannotator/packages/server/share-url.ts +53 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.test.ts +43 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.ts +177 -0
- package/3p/pi-plannotator/packages/server/storage.test.ts +176 -0
- package/3p/pi-plannotator/packages/server/storage.ts +17 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.test.ts +146 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.ts +604 -0
- package/3p/pi-plannotator/packages/server/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/server/vcs.test.ts +48 -0
- package/3p/pi-plannotator/packages/server/vcs.ts +80 -0
- package/3p/pi-plannotator/packages/shared/agent-jobs.ts +132 -0
- package/3p/pi-plannotator/packages/shared/agents.ts +53 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.test.ts +386 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.ts +107 -0
- package/3p/pi-plannotator/packages/shared/at-reference.test.ts +99 -0
- package/3p/pi-plannotator/packages/shared/at-reference.ts +52 -0
- package/3p/pi-plannotator/packages/shared/checklist.ts +52 -0
- package/3p/pi-plannotator/packages/shared/code-file.test.ts +112 -0
- package/3p/pi-plannotator/packages/shared/code-file.ts +41 -0
- package/3p/pi-plannotator/packages/shared/code-nav.test.ts +515 -0
- package/3p/pi-plannotator/packages/shared/code-nav.ts +436 -0
- package/3p/pi-plannotator/packages/shared/compress.ts +51 -0
- package/3p/pi-plannotator/packages/shared/config.ts +262 -0
- package/3p/pi-plannotator/packages/shared/crypto.test.ts +172 -0
- package/3p/pi-plannotator/packages/shared/crypto.ts +97 -0
- package/3p/pi-plannotator/packages/shared/data-dir.ts +42 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.test.ts +30 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.ts +137 -0
- package/3p/pi-plannotator/packages/shared/draft.ts +64 -0
- package/3p/pi-plannotator/packages/shared/external-annotation.ts +397 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.test.ts +59 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.ts +66 -0
- package/3p/pi-plannotator/packages/shared/favicon.ts +5 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.test.ts +65 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.ts +29 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.test.ts +231 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.ts +336 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.test.ts +62 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.ts +32 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.test.ts +135 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.ts +115 -0
- package/3p/pi-plannotator/packages/shared/integrations-common.ts +243 -0
- package/3p/pi-plannotator/packages/shared/jj-core.test.ts +236 -0
- package/3p/pi-plannotator/packages/shared/jj-core.ts +433 -0
- package/3p/pi-plannotator/packages/shared/package.json +55 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.test.ts +88 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.ts +80 -0
- package/3p/pi-plannotator/packages/shared/pr-github.ts +661 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.test.ts +202 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.ts +620 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.test.ts +266 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.ts +123 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.test.ts +104 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.ts +194 -0
- package/3p/pi-plannotator/packages/shared/pr-types.ts +326 -0
- package/3p/pi-plannotator/packages/shared/project.ts +71 -0
- package/3p/pi-plannotator/packages/shared/prompts-integration.test.ts +421 -0
- package/3p/pi-plannotator/packages/shared/prompts.test.ts +504 -0
- package/3p/pi-plannotator/packages/shared/prompts.ts +247 -0
- package/3p/pi-plannotator/packages/shared/reference-common.ts +87 -0
- package/3p/pi-plannotator/packages/shared/repo.ts +71 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.test.ts +113 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.ts +509 -0
- package/3p/pi-plannotator/packages/shared/review-args.test.ts +64 -0
- package/3p/pi-plannotator/packages/shared/review-args.ts +85 -0
- package/3p/pi-plannotator/packages/shared/review-core.test.ts +286 -0
- package/3p/pi-plannotator/packages/shared/review-core.ts +895 -0
- package/3p/pi-plannotator/packages/shared/review-workspace-node.ts +230 -0
- package/3p/pi-plannotator/packages/shared/review-workspace.ts +436 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff-types.ts +76 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.test.ts +322 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.ts +520 -0
- package/3p/pi-plannotator/packages/shared/storage.ts +378 -0
- package/3p/pi-plannotator/packages/shared/tour.ts +61 -0
- package/3p/pi-plannotator/packages/shared/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/shared/types.ts +29 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.test.ts +177 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.ts +351 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.test.ts +332 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.ts +482 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.test.ts +162 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.ts +103 -0
- package/3p/pi-plannotator/packages/shared/worktree.ts +119 -0
- package/3p/pi-subagents/package.json +50 -0
- package/3p/pi-subagents/src/agent-manager.ts +413 -0
- package/3p/pi-subagents/src/agent-runner.ts +502 -0
- package/3p/pi-subagents/src/agent-types.ts +248 -0
- package/3p/pi-subagents/src/context.ts +58 -0
- package/3p/pi-subagents/src/cross-extension-rpc.ts +135 -0
- package/3p/pi-subagents/src/custom-agents.ts +137 -0
- package/3p/pi-subagents/src/default-agents.ts +144 -0
- package/3p/pi-subagents/src/env.ts +33 -0
- package/3p/pi-subagents/src/group-join.ts +141 -0
- package/3p/pi-subagents/src/index.ts +1811 -0
- package/3p/pi-subagents/src/invocation-config.ts +40 -0
- package/3p/pi-subagents/src/memory.ts +165 -0
- package/3p/pi-subagents/src/model-resolver.ts +81 -0
- package/3p/pi-subagents/src/output-file.ts +77 -0
- package/3p/pi-subagents/src/prompts.ts +85 -0
- package/3p/pi-subagents/src/skill-loader.ts +79 -0
- package/3p/pi-subagents/src/types.ts +111 -0
- package/3p/pi-subagents/src/ui/agent-widget.ts +496 -0
- package/3p/pi-subagents/src/ui/conversation-viewer.ts +250 -0
- package/3p/pi-subagents/src/worktree.ts +162 -0
- package/3p/pi-tasks/package.json +52 -0
- package/3p/pi-tasks/src/auto-clear.ts +91 -0
- package/3p/pi-tasks/src/index.ts +1150 -0
- package/3p/pi-tasks/src/process-tracker.ts +140 -0
- package/3p/pi-tasks/src/reminder-cadence.ts +90 -0
- package/3p/pi-tasks/src/task-store.ts +324 -0
- package/3p/pi-tasks/src/tasks-config.ts +27 -0
- package/3p/pi-tasks/src/types.ts +40 -0
- package/3p/pi-tasks/src/ui/settings-menu.ts +152 -0
- package/3p/pi-tasks/src/ui/task-widget.ts +296 -0
- package/AGENTS.md +28 -0
- package/LICENSE +201 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +77 -0
- package/extensions/orchestrator/agents/code-reviewer.ts +89 -0
- package/extensions/orchestrator/agents/explore.ts +33 -0
- package/extensions/orchestrator/agents/librarian.ts +43 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +75 -0
- package/extensions/orchestrator/agents/planner.ts +61 -0
- package/extensions/orchestrator/agents/registry.ts +156 -0
- package/extensions/orchestrator/agents/task.ts +50 -0
- package/extensions/orchestrator/agents/tool-routing.ts +84 -0
- package/extensions/orchestrator/ast-search.ts +85 -0
- package/extensions/orchestrator/cbm.ts +330 -0
- package/extensions/orchestrator/command-handlers.test.ts +163 -0
- package/extensions/orchestrator/command-handlers.ts +116 -0
- package/extensions/orchestrator/commands.test.ts +81 -0
- package/extensions/orchestrator/commands.ts +75 -0
- package/extensions/orchestrator/config.test.ts +221 -0
- package/extensions/orchestrator/config.ts +230 -0
- package/extensions/orchestrator/context.test.ts +293 -0
- package/extensions/orchestrator/context.ts +206 -0
- package/extensions/orchestrator/event-handlers.test.ts +190 -0
- package/extensions/orchestrator/event-handlers.ts +1416 -0
- package/extensions/orchestrator/exa.ts +104 -0
- package/extensions/orchestrator/flant-infra.ts +486 -0
- package/extensions/orchestrator/index.ts +80 -0
- package/extensions/orchestrator/integration.test.ts +1214 -0
- package/extensions/orchestrator/orchestrator.test.ts +252 -0
- package/extensions/orchestrator/orchestrator.ts +538 -0
- package/extensions/orchestrator/phases/brainstorm.test.ts +15 -0
- package/extensions/orchestrator/phases/brainstorm.ts +273 -0
- package/extensions/orchestrator/phases/implementation.ts +40 -0
- package/extensions/orchestrator/phases/machine.test.ts +293 -0
- package/extensions/orchestrator/phases/machine.ts +209 -0
- package/extensions/orchestrator/phases/planning.ts +255 -0
- package/extensions/orchestrator/phases/review.ts +193 -0
- package/extensions/orchestrator/plannotator.ts +56 -0
- package/extensions/orchestrator/pp-menu.ts +866 -0
- package/extensions/orchestrator/state.test.ts +343 -0
- package/extensions/orchestrator/state.ts +237 -0
- package/extensions/orchestrator/validate-artifacts.test.ts +88 -0
- package/extensions/orchestrator/validate-artifacts.ts +272 -0
- package/extensions/orchestrator/vendor.d.ts +26 -0
- package/package.json +73 -0
- package/scripts/postinstall.sh +18 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Jobs — Pi (node:http) server handler.
|
|
3
|
+
*
|
|
4
|
+
* Manages background agent processes (spawn, monitor, kill) and exposes
|
|
5
|
+
* HTTP routes + SSE broadcasting for job status updates.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors packages/server/agent-jobs.ts but uses node:http primitives.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
11
|
+
import { spawn, execFileSync, type ChildProcess } from "node:child_process";
|
|
12
|
+
import {
|
|
13
|
+
type AgentJobInfo,
|
|
14
|
+
type AgentJobEvent,
|
|
15
|
+
type AgentCapability,
|
|
16
|
+
type AgentCapabilities,
|
|
17
|
+
isTerminalStatus,
|
|
18
|
+
jobSource,
|
|
19
|
+
serializeAgentSSEEvent,
|
|
20
|
+
AGENT_HEARTBEAT_COMMENT,
|
|
21
|
+
AGENT_HEARTBEAT_INTERVAL_MS,
|
|
22
|
+
} from "../generated/agent-jobs.js";
|
|
23
|
+
import { formatClaudeLogEvent } from "../generated/claude-review.js";
|
|
24
|
+
import { json, parseBody } from "./helpers.js";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Route prefixes
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
const BASE = "/api/agents";
|
|
31
|
+
const JOBS = `${BASE}/jobs`;
|
|
32
|
+
const JOBS_STREAM = `${JOBS}/stream`;
|
|
33
|
+
const CAPABILITIES = `${BASE}/capabilities`;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// which() helper for Node.js
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function whichCmd(cmd: string): boolean {
|
|
40
|
+
try {
|
|
41
|
+
const bin = process.platform === "win32" ? "where" : "which";
|
|
42
|
+
execFileSync(bin, [cmd], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Factory
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
export interface AgentJobHandlerOptions {
|
|
54
|
+
mode: "plan" | "review" | "annotate";
|
|
55
|
+
getServerUrl: () => string;
|
|
56
|
+
getCwd: () => string;
|
|
57
|
+
/** Server-side command builder for known providers (codex, claude, tour). */
|
|
58
|
+
buildCommand?: (provider: string, config?: Record<string, unknown>) => Promise<{
|
|
59
|
+
command: string[];
|
|
60
|
+
outputPath?: string;
|
|
61
|
+
captureStdout?: boolean;
|
|
62
|
+
stdinPrompt?: string;
|
|
63
|
+
cwd?: string;
|
|
64
|
+
prompt?: string;
|
|
65
|
+
label?: string;
|
|
66
|
+
/** Underlying engine used (e.g., "claude" or "codex"). Stored on AgentJobInfo for UI display. */
|
|
67
|
+
engine?: string;
|
|
68
|
+
/** Model used (e.g., "sonnet", "opus"). Stored on AgentJobInfo for UI display. */
|
|
69
|
+
model?: string;
|
|
70
|
+
/** Claude --effort level. */
|
|
71
|
+
effort?: string;
|
|
72
|
+
/** Codex reasoning effort level. */
|
|
73
|
+
reasoningEffort?: string;
|
|
74
|
+
/** Whether Codex fast mode was enabled. */
|
|
75
|
+
fastMode?: boolean;
|
|
76
|
+
/** PR URL at launch time. */
|
|
77
|
+
prUrl?: string;
|
|
78
|
+
/** PR diff scope at launch time. */
|
|
79
|
+
diffScope?: string;
|
|
80
|
+
/** Diff context snapshot at launch (stored on AgentJobInfo for per-job "Copy All"). */
|
|
81
|
+
diffContext?: AgentJobInfo["diffContext"];
|
|
82
|
+
} | null>;
|
|
83
|
+
/** Called when a job completes successfully — parse results and push annotations. */
|
|
84
|
+
onJobComplete?: (job: AgentJobInfo, meta: { outputPath?: string; stdout?: string; cwd?: string }) => void | Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createAgentJobHandler(options: AgentJobHandlerOptions) {
|
|
88
|
+
const { mode, getServerUrl, getCwd } = options;
|
|
89
|
+
|
|
90
|
+
// --- State ---
|
|
91
|
+
const jobs = new Map<string, { info: AgentJobInfo; proc: ChildProcess | null }>();
|
|
92
|
+
const jobOutputPaths = new Map<string, string>();
|
|
93
|
+
const subscribers = new Set<ServerResponse>();
|
|
94
|
+
let version = 0;
|
|
95
|
+
|
|
96
|
+
// --- Capability detection (run once) ---
|
|
97
|
+
const capabilities: AgentCapability[] = [
|
|
98
|
+
{ id: "claude", name: "Claude Code", available: whichCmd("claude") },
|
|
99
|
+
{ id: "codex", name: "Codex CLI", available: whichCmd("codex") },
|
|
100
|
+
{ id: "tour", name: "Code Tour", available: whichCmd("claude") || whichCmd("codex") },
|
|
101
|
+
];
|
|
102
|
+
const capabilitiesResponse: AgentCapabilities = {
|
|
103
|
+
mode,
|
|
104
|
+
providers: capabilities,
|
|
105
|
+
available: capabilities.some((c) => c.available),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// --- SSE broadcasting ---
|
|
109
|
+
function broadcast(event: AgentJobEvent): void {
|
|
110
|
+
version++;
|
|
111
|
+
const data = serializeAgentSSEEvent(event);
|
|
112
|
+
for (const res of subscribers) {
|
|
113
|
+
try {
|
|
114
|
+
res.write(data);
|
|
115
|
+
} catch {
|
|
116
|
+
subscribers.delete(res);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Process lifecycle ---
|
|
122
|
+
function spawnJob(
|
|
123
|
+
provider: string,
|
|
124
|
+
command: string[],
|
|
125
|
+
label: string,
|
|
126
|
+
outputPath?: string,
|
|
127
|
+
spawnOptions?: { captureStdout?: boolean; stdinPrompt?: string; cwd?: string; prompt?: string; engine?: string; model?: string; effort?: string; reasoningEffort?: string; fastMode?: boolean; prUrl?: string; diffScope?: string; diffContext?: AgentJobInfo["diffContext"] },
|
|
128
|
+
): AgentJobInfo {
|
|
129
|
+
const id = crypto.randomUUID();
|
|
130
|
+
const source = jobSource(id);
|
|
131
|
+
|
|
132
|
+
const info: AgentJobInfo = {
|
|
133
|
+
id,
|
|
134
|
+
source,
|
|
135
|
+
provider,
|
|
136
|
+
label,
|
|
137
|
+
status: "starting",
|
|
138
|
+
startedAt: Date.now(),
|
|
139
|
+
command,
|
|
140
|
+
cwd: getCwd(),
|
|
141
|
+
...(spawnOptions?.engine && { engine: spawnOptions.engine }),
|
|
142
|
+
...(spawnOptions?.model && { model: spawnOptions.model }),
|
|
143
|
+
...(spawnOptions?.effort && { effort: spawnOptions.effort }),
|
|
144
|
+
...(spawnOptions?.reasoningEffort && { reasoningEffort: spawnOptions.reasoningEffort }),
|
|
145
|
+
...(spawnOptions?.fastMode && { fastMode: spawnOptions.fastMode }),
|
|
146
|
+
...(spawnOptions?.prUrl && { prUrl: spawnOptions.prUrl }),
|
|
147
|
+
...(spawnOptions?.diffScope && { diffScope: spawnOptions.diffScope }),
|
|
148
|
+
...(spawnOptions?.diffContext && { diffContext: spawnOptions.diffContext }),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
let proc: ChildProcess | null = null;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const spawnCwd = spawnOptions?.cwd ?? getCwd();
|
|
155
|
+
const captureStdout = spawnOptions?.captureStdout ?? false;
|
|
156
|
+
const hasStdinPrompt = !!spawnOptions?.stdinPrompt;
|
|
157
|
+
|
|
158
|
+
proc = spawn(command[0], command.slice(1), {
|
|
159
|
+
cwd: spawnCwd,
|
|
160
|
+
stdio: [
|
|
161
|
+
hasStdinPrompt ? "pipe" : "ignore",
|
|
162
|
+
captureStdout ? "pipe" : "ignore",
|
|
163
|
+
"pipe",
|
|
164
|
+
],
|
|
165
|
+
env: {
|
|
166
|
+
...process.env,
|
|
167
|
+
PLANNOTATOR_AGENT_SOURCE: source,
|
|
168
|
+
PLANNOTATOR_API_URL: getServerUrl(),
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Write prompt to stdin and close (for providers that read prompt from stdin)
|
|
173
|
+
if (hasStdinPrompt && proc.stdin) {
|
|
174
|
+
proc.stdin.write(spawnOptions!.stdinPrompt!);
|
|
175
|
+
proc.stdin.end();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
info.status = "running";
|
|
179
|
+
info.cwd = spawnCwd;
|
|
180
|
+
if (spawnOptions?.prompt) info.prompt = spawnOptions.prompt;
|
|
181
|
+
jobs.set(id, { info, proc });
|
|
182
|
+
if (outputPath) jobOutputPaths.set(id, outputPath);
|
|
183
|
+
if (spawnOptions?.cwd) jobOutputPaths.set(`${id}:cwd`, spawnOptions.cwd);
|
|
184
|
+
broadcast({ type: "job:started", job: { ...info } });
|
|
185
|
+
|
|
186
|
+
// --- Stdout capture (Claude JSONL streaming) ---
|
|
187
|
+
let stdoutBuf = "";
|
|
188
|
+
if (captureStdout && proc.stdout) {
|
|
189
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
190
|
+
const text = chunk.toString();
|
|
191
|
+
stdoutBuf += text;
|
|
192
|
+
|
|
193
|
+
// Forward JSONL lines as log events
|
|
194
|
+
const lines = text.split('\n');
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
if (!line.trim()) continue;
|
|
197
|
+
// Tour jobs with the Claude engine also stream Claude JSONL.
|
|
198
|
+
if (provider === "claude" || spawnOptions?.engine === "claude") {
|
|
199
|
+
const formatted = formatClaudeLogEvent(line);
|
|
200
|
+
if (formatted !== null) {
|
|
201
|
+
broadcast({ type: "job:log", jobId: id, delta: formatted + '\n' });
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const event = JSON.parse(line);
|
|
207
|
+
if (event.type === 'result') continue;
|
|
208
|
+
} catch { /* not JSON — forward as raw log */ }
|
|
209
|
+
broadcast({ type: "job:log", jobId: id, delta: line + '\n' });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// --- Stderr: buffer tail for errors + live log streaming ---
|
|
215
|
+
let stderrBuf = "";
|
|
216
|
+
let logPending = "";
|
|
217
|
+
let logFlushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
218
|
+
|
|
219
|
+
if (proc.stderr) {
|
|
220
|
+
proc.stderr.on("data", (chunk: Buffer) => {
|
|
221
|
+
const text = chunk.toString();
|
|
222
|
+
stderrBuf = (stderrBuf + text).slice(-500);
|
|
223
|
+
logPending += text;
|
|
224
|
+
|
|
225
|
+
if (!logFlushTimer) {
|
|
226
|
+
logFlushTimer = setTimeout(() => {
|
|
227
|
+
if (logPending) {
|
|
228
|
+
broadcast({ type: "job:log", jobId: id, delta: logPending });
|
|
229
|
+
logPending = "";
|
|
230
|
+
}
|
|
231
|
+
logFlushTimer = null;
|
|
232
|
+
}, 200);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Monitor process close (fires after stdio streams are fully drained,
|
|
238
|
+
// unlike 'exit' which fires before — critical for stdout capture)
|
|
239
|
+
proc.on("close", async (exitCode) => {
|
|
240
|
+
// Flush remaining stderr
|
|
241
|
+
if (logFlushTimer) { clearTimeout(logFlushTimer); logFlushTimer = null; }
|
|
242
|
+
if (logPending) {
|
|
243
|
+
broadcast({ type: "job:log", jobId: id, delta: logPending });
|
|
244
|
+
logPending = "";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const entry = jobs.get(id);
|
|
248
|
+
if (!entry || isTerminalStatus(entry.info.status)) return;
|
|
249
|
+
|
|
250
|
+
entry.info.endedAt = Date.now();
|
|
251
|
+
entry.info.exitCode = exitCode ?? undefined;
|
|
252
|
+
entry.info.status = exitCode === 0 ? "done" : "failed";
|
|
253
|
+
|
|
254
|
+
if (exitCode !== 0 && stderrBuf) {
|
|
255
|
+
entry.info.error = stderrBuf;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Ingest results before broadcasting completion
|
|
259
|
+
const jobOutputPath = jobOutputPaths.get(id);
|
|
260
|
+
const jobCwd = jobOutputPaths.get(`${id}:cwd`);
|
|
261
|
+
if (exitCode === 0 && options.onJobComplete) {
|
|
262
|
+
try {
|
|
263
|
+
await options.onJobComplete(entry.info, {
|
|
264
|
+
outputPath: jobOutputPath,
|
|
265
|
+
stdout: captureStdout ? stdoutBuf : undefined,
|
|
266
|
+
cwd: jobCwd,
|
|
267
|
+
});
|
|
268
|
+
} catch {
|
|
269
|
+
// Result ingestion failure shouldn't prevent job completion broadcast
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
jobOutputPaths.delete(id);
|
|
273
|
+
jobOutputPaths.delete(`${id}:cwd`);
|
|
274
|
+
|
|
275
|
+
broadcast({ type: "job:completed", job: { ...entry.info } });
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Handle spawn errors after process starts
|
|
279
|
+
proc.on("error", (err) => {
|
|
280
|
+
const entry = jobs.get(id);
|
|
281
|
+
if (!entry || isTerminalStatus(entry.info.status)) return;
|
|
282
|
+
|
|
283
|
+
entry.info.status = "failed";
|
|
284
|
+
entry.info.endedAt = Date.now();
|
|
285
|
+
entry.info.error = err.message;
|
|
286
|
+
broadcast({ type: "job:completed", job: { ...entry.info } });
|
|
287
|
+
});
|
|
288
|
+
} catch (err) {
|
|
289
|
+
jobs.set(id, { info, proc: null });
|
|
290
|
+
broadcast({ type: "job:started", job: { ...info } });
|
|
291
|
+
|
|
292
|
+
info.status = "failed";
|
|
293
|
+
info.endedAt = Date.now();
|
|
294
|
+
info.error = err instanceof Error ? err.message : String(err);
|
|
295
|
+
broadcast({ type: "job:completed", job: { ...info } });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { ...info };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function killJob(id: string): boolean {
|
|
302
|
+
const entry = jobs.get(id);
|
|
303
|
+
if (!entry || isTerminalStatus(entry.info.status)) return false;
|
|
304
|
+
|
|
305
|
+
if (entry.proc) {
|
|
306
|
+
try {
|
|
307
|
+
entry.proc.kill();
|
|
308
|
+
} catch {
|
|
309
|
+
// Process may have already exited
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
entry.info.status = "killed";
|
|
314
|
+
entry.info.endedAt = Date.now();
|
|
315
|
+
jobOutputPaths.delete(id);
|
|
316
|
+
jobOutputPaths.delete(`${id}:cwd`);
|
|
317
|
+
broadcast({ type: "job:completed", job: { ...entry.info } });
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function killAll(): number {
|
|
322
|
+
let count = 0;
|
|
323
|
+
for (const [id, entry] of jobs) {
|
|
324
|
+
if (!isTerminalStatus(entry.info.status)) {
|
|
325
|
+
killJob(id);
|
|
326
|
+
count++;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return count;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getAllJobs(): AgentJobInfo[] {
|
|
333
|
+
return Array.from(jobs.values()).map((e) => ({ ...e.info }));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// --- HTTP handler ---
|
|
337
|
+
return {
|
|
338
|
+
killAll,
|
|
339
|
+
|
|
340
|
+
async handle(
|
|
341
|
+
req: IncomingMessage,
|
|
342
|
+
res: ServerResponse,
|
|
343
|
+
url: URL,
|
|
344
|
+
): Promise<boolean> {
|
|
345
|
+
// --- GET /api/agents/capabilities ---
|
|
346
|
+
if (url.pathname === CAPABILITIES && req.method === "GET") {
|
|
347
|
+
json(res, capabilitiesResponse);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// --- SSE stream ---
|
|
352
|
+
if (url.pathname === JOBS_STREAM && req.method === "GET") {
|
|
353
|
+
res.writeHead(200, {
|
|
354
|
+
"Content-Type": "text/event-stream",
|
|
355
|
+
"Cache-Control": "no-cache",
|
|
356
|
+
Connection: "keep-alive",
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
res.setTimeout(0);
|
|
360
|
+
|
|
361
|
+
// Send current state as snapshot
|
|
362
|
+
const snapshot: AgentJobEvent = {
|
|
363
|
+
type: "snapshot",
|
|
364
|
+
jobs: getAllJobs(),
|
|
365
|
+
};
|
|
366
|
+
res.write(serializeAgentSSEEvent(snapshot));
|
|
367
|
+
|
|
368
|
+
subscribers.add(res);
|
|
369
|
+
|
|
370
|
+
// Heartbeat to keep connection alive
|
|
371
|
+
const heartbeatTimer = setInterval(() => {
|
|
372
|
+
try {
|
|
373
|
+
res.write(AGENT_HEARTBEAT_COMMENT);
|
|
374
|
+
} catch {
|
|
375
|
+
clearInterval(heartbeatTimer);
|
|
376
|
+
subscribers.delete(res);
|
|
377
|
+
}
|
|
378
|
+
}, AGENT_HEARTBEAT_INTERVAL_MS);
|
|
379
|
+
|
|
380
|
+
// Clean up on disconnect
|
|
381
|
+
res.on("close", () => {
|
|
382
|
+
clearInterval(heartbeatTimer);
|
|
383
|
+
subscribers.delete(res);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// --- GET /api/agents/jobs (snapshot / polling fallback) ---
|
|
390
|
+
if (url.pathname === JOBS && req.method === "GET") {
|
|
391
|
+
const since = url.searchParams.get("since");
|
|
392
|
+
if (since !== null) {
|
|
393
|
+
const sinceVersion = parseInt(since, 10);
|
|
394
|
+
if (!isNaN(sinceVersion) && sinceVersion === version) {
|
|
395
|
+
res.writeHead(304);
|
|
396
|
+
res.end();
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
json(res, { jobs: getAllJobs(), version });
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// --- POST /api/agents/jobs (launch) ---
|
|
405
|
+
if (url.pathname === JOBS && req.method === "POST") {
|
|
406
|
+
try {
|
|
407
|
+
const body = await parseBody(req);
|
|
408
|
+
const provider = typeof body.provider === "string" ? body.provider : "";
|
|
409
|
+
let rawCommand = Array.isArray(body.command) ? body.command : [];
|
|
410
|
+
let command = rawCommand.filter((c: unknown): c is string => typeof c === "string");
|
|
411
|
+
let label = typeof body.label === "string" ? body.label : `${provider} agent`;
|
|
412
|
+
let outputPath: string | undefined;
|
|
413
|
+
|
|
414
|
+
// Validate provider is a known, available capability
|
|
415
|
+
const cap = capabilities.find((c) => c.id === provider);
|
|
416
|
+
if (!cap || !cap.available) {
|
|
417
|
+
json(res, { error: `Unknown or unavailable provider: ${provider}` }, 400);
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Try server-side command building for known providers
|
|
422
|
+
let captureStdout = false;
|
|
423
|
+
let stdinPrompt: string | undefined;
|
|
424
|
+
let spawnCwd: string | undefined;
|
|
425
|
+
let promptText: string | undefined;
|
|
426
|
+
let jobEngine: string | undefined;
|
|
427
|
+
let jobModel: string | undefined;
|
|
428
|
+
let jobEffort: string | undefined;
|
|
429
|
+
let jobReasoningEffort: string | undefined;
|
|
430
|
+
let jobFastMode: boolean | undefined;
|
|
431
|
+
let jobPrUrl: string | undefined;
|
|
432
|
+
let jobDiffScope: string | undefined;
|
|
433
|
+
let jobDiffContext: AgentJobInfo["diffContext"] | undefined;
|
|
434
|
+
if (options.buildCommand) {
|
|
435
|
+
// Thread config from POST body to buildCommand
|
|
436
|
+
const config: Record<string, unknown> = {};
|
|
437
|
+
if (typeof body.engine === "string") config.engine = body.engine;
|
|
438
|
+
if (typeof body.model === "string") config.model = body.model;
|
|
439
|
+
if (typeof body.reasoningEffort === "string") config.reasoningEffort = body.reasoningEffort;
|
|
440
|
+
if (typeof body.effort === "string") config.effort = body.effort;
|
|
441
|
+
if (body.fastMode === true) config.fastMode = true;
|
|
442
|
+
const built = await options.buildCommand(provider, Object.keys(config).length > 0 ? config : undefined);
|
|
443
|
+
if (built) {
|
|
444
|
+
command = built.command;
|
|
445
|
+
outputPath = built.outputPath;
|
|
446
|
+
captureStdout = built.captureStdout ?? false;
|
|
447
|
+
stdinPrompt = built.stdinPrompt;
|
|
448
|
+
spawnCwd = built.cwd;
|
|
449
|
+
promptText = built.prompt;
|
|
450
|
+
if (built.label) label = built.label;
|
|
451
|
+
jobEngine = built.engine;
|
|
452
|
+
jobModel = built.model;
|
|
453
|
+
jobEffort = built.effort;
|
|
454
|
+
jobReasoningEffort = built.reasoningEffort;
|
|
455
|
+
jobFastMode = built.fastMode;
|
|
456
|
+
jobPrUrl = built.prUrl;
|
|
457
|
+
jobDiffScope = built.diffScope;
|
|
458
|
+
jobDiffContext = built.diffContext;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (command.length === 0) {
|
|
463
|
+
json(res, { error: 'Missing "command" array' }, 400);
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const job = spawnJob(provider, command, label, outputPath, {
|
|
468
|
+
captureStdout,
|
|
469
|
+
stdinPrompt,
|
|
470
|
+
cwd: spawnCwd,
|
|
471
|
+
prompt: promptText,
|
|
472
|
+
engine: jobEngine,
|
|
473
|
+
model: jobModel,
|
|
474
|
+
effort: jobEffort,
|
|
475
|
+
reasoningEffort: jobReasoningEffort,
|
|
476
|
+
fastMode: jobFastMode,
|
|
477
|
+
prUrl: jobPrUrl,
|
|
478
|
+
diffScope: jobDiffScope,
|
|
479
|
+
diffContext: jobDiffContext,
|
|
480
|
+
});
|
|
481
|
+
json(res, { job }, 201);
|
|
482
|
+
} catch {
|
|
483
|
+
json(res, { error: "Invalid JSON" }, 400);
|
|
484
|
+
}
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// --- DELETE /api/agents/jobs/:id (kill one) ---
|
|
489
|
+
if (url.pathname.startsWith(JOBS + "/") && url.pathname !== JOBS_STREAM && req.method === "DELETE") {
|
|
490
|
+
const id = url.pathname.slice(JOBS.length + 1);
|
|
491
|
+
if (!id) {
|
|
492
|
+
json(res, { error: "Missing job ID" }, 400);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
const found = killJob(id);
|
|
496
|
+
if (!found) {
|
|
497
|
+
json(res, { error: "Job not found or already terminal" }, 404);
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
json(res, { ok: true });
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// --- DELETE /api/agents/jobs (kill all) ---
|
|
505
|
+
if (url.pathname === JOBS && req.method === "DELETE") {
|
|
506
|
+
const count = killAll();
|
|
507
|
+
json(res, { ok: true, killed: count });
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Not handled
|
|
512
|
+
return false;
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
|
|
5
|
+
import { resolveCommandFromWhichOutput } from "../generated/ai/providers/command-path.js";
|
|
6
|
+
import { json, toWebRequest } from "./helpers.js";
|
|
7
|
+
|
|
8
|
+
export interface PiAIRuntime {
|
|
9
|
+
endpoints: Record<string, (req: Request) => Promise<Response>>;
|
|
10
|
+
dispose: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface CreatePiAIRuntimeOptions {
|
|
14
|
+
cwd?: string;
|
|
15
|
+
getCwd?: () => string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function whichCmd(cmd: string): string | null {
|
|
19
|
+
try {
|
|
20
|
+
const bin = process.platform === "win32" ? "where" : "which";
|
|
21
|
+
const output = execFileSync(bin, [cmd], {
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
24
|
+
});
|
|
25
|
+
return resolveCommandFromWhichOutput(output);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function createPiAIRuntime(options: CreatePiAIRuntimeOptions = {}): Promise<PiAIRuntime | null> {
|
|
32
|
+
try {
|
|
33
|
+
const ai = await import("../generated/ai/index.js");
|
|
34
|
+
const cwd = options.cwd ?? process.cwd();
|
|
35
|
+
const registry = new ai.ProviderRegistry();
|
|
36
|
+
const sessionManager = new ai.SessionManager();
|
|
37
|
+
const modelDiscovery: Promise<void>[] = [];
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await import("../generated/ai/providers/claude-agent-sdk.js");
|
|
41
|
+
const claudePath = whichCmd("claude");
|
|
42
|
+
const provider = await ai.createProvider({
|
|
43
|
+
type: "claude-agent-sdk",
|
|
44
|
+
cwd,
|
|
45
|
+
...(claudePath && { claudeExecutablePath: claudePath }),
|
|
46
|
+
});
|
|
47
|
+
registry.register(provider);
|
|
48
|
+
} catch {
|
|
49
|
+
// Claude SDK not available.
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await import("../generated/ai/providers/codex-sdk.js");
|
|
54
|
+
await import("@openai/codex-sdk");
|
|
55
|
+
const codexPath = whichCmd("codex");
|
|
56
|
+
const provider = await ai.createProvider({
|
|
57
|
+
type: "codex-sdk",
|
|
58
|
+
cwd,
|
|
59
|
+
...(codexPath && { codexExecutablePath: codexPath }),
|
|
60
|
+
});
|
|
61
|
+
registry.register(provider);
|
|
62
|
+
} catch {
|
|
63
|
+
// Codex SDK not available.
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await import("../generated/ai/providers/pi-sdk-node.js");
|
|
68
|
+
const piPath = whichCmd("pi");
|
|
69
|
+
if (piPath) {
|
|
70
|
+
const provider = await ai.createProvider({
|
|
71
|
+
type: "pi-sdk",
|
|
72
|
+
cwd,
|
|
73
|
+
piExecutablePath: piPath,
|
|
74
|
+
} as any);
|
|
75
|
+
if (provider && "fetchModels" in provider) {
|
|
76
|
+
modelDiscovery.push(
|
|
77
|
+
(provider as { fetchModels: () => Promise<void> })
|
|
78
|
+
.fetchModels()
|
|
79
|
+
.catch(() => {}),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
registry.register(provider);
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Pi not available.
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await import("../generated/ai/providers/opencode-sdk.js");
|
|
90
|
+
const opencodePath = whichCmd("opencode");
|
|
91
|
+
if (opencodePath) {
|
|
92
|
+
const provider = await ai.createProvider({
|
|
93
|
+
type: "opencode-sdk",
|
|
94
|
+
cwd,
|
|
95
|
+
});
|
|
96
|
+
if (provider && "fetchModels" in provider) {
|
|
97
|
+
modelDiscovery.push(
|
|
98
|
+
(provider as { fetchModels: () => Promise<void> })
|
|
99
|
+
.fetchModels()
|
|
100
|
+
.catch(() => {}),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
registry.register(provider);
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// OpenCode not available.
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
endpoints: ai.createAIEndpoints({
|
|
111
|
+
registry,
|
|
112
|
+
sessionManager,
|
|
113
|
+
getCwd: options.getCwd,
|
|
114
|
+
beforeCapabilities: async () => {
|
|
115
|
+
await Promise.allSettled(modelDiscovery);
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
dispose: () => {
|
|
119
|
+
sessionManager.disposeAll();
|
|
120
|
+
registry.disposeAll();
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function handlePiAIRequest(
|
|
129
|
+
req: IncomingMessage,
|
|
130
|
+
res: ServerResponse,
|
|
131
|
+
url: URL,
|
|
132
|
+
runtime: PiAIRuntime | null,
|
|
133
|
+
): Promise<boolean> {
|
|
134
|
+
if (!url.pathname.startsWith("/api/ai/")) return false;
|
|
135
|
+
|
|
136
|
+
if (!runtime) {
|
|
137
|
+
if (url.pathname === "/api/ai/capabilities" && req.method === "GET") {
|
|
138
|
+
json(res, { available: false, providers: [] });
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
json(res, { error: "AI backend not available" }, 503);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const handler = runtime.endpoints[url.pathname];
|
|
146
|
+
if (!handler) {
|
|
147
|
+
json(res, { error: "Not found" }, 404);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const webReq = toWebRequest(req);
|
|
153
|
+
const webRes = await handler(webReq);
|
|
154
|
+
const headers: Record<string, string> = {};
|
|
155
|
+
webRes.headers.forEach((value, key) => {
|
|
156
|
+
headers[key] = value;
|
|
157
|
+
});
|
|
158
|
+
res.writeHead(webRes.status, headers);
|
|
159
|
+
if (webRes.body) {
|
|
160
|
+
Readable.fromWeb(webRes.body as any).pipe(res);
|
|
161
|
+
} else {
|
|
162
|
+
res.end();
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
json(res, { error: err instanceof Error ? err.message : "AI endpoint error" }, 500);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return true;
|
|
169
|
+
}
|