kcg-code 0.0.0-stage → 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/README.md +228 -2
- package/bin/kcgcode.ts +13 -0
- package/bun-env.d.ts +19 -0
- package/kcg-code.config.example.json +3 -0
- package/package.json +83 -4
- package/public/logo.svg +1 -0
- package/public/manifest.json +18 -0
- package/public/sw.js +49 -0
- package/src/App.tsx +39 -0
- package/src/__tests__/manifest.test.ts +61 -0
- package/src/build-config.ts +9 -0
- package/src/cli/__tests__/dashboard.test.ts +62 -0
- package/src/cli/args.ts +123 -0
- package/src/cli/commands.ts +174 -0
- package/src/cli/dashboard.tsx +343 -0
- package/src/cli/index.ts +37 -0
- package/src/cli/theme.ts +100 -0
- package/src/components/auth/AuthGate.tsx +182 -0
- package/src/components/auth/AvatarEditor.tsx +236 -0
- package/src/components/auth/LockScreen.tsx +714 -0
- package/src/components/auth/ProfileAvatar.tsx +60 -0
- package/src/components/auth/WelcomeScreen.tsx +672 -0
- package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
- package/src/components/auth/avatar-icons.tsx +114 -0
- package/src/components/layout/AppShell.tsx +142 -0
- package/src/components/layout/AppSidebar.tsx +419 -0
- package/src/components/layout/SearchDialog.tsx +189 -0
- package/src/components/layout/SidebarAccount.tsx +111 -0
- package/src/components/layout/SidebarSessionMenu.tsx +163 -0
- package/src/components/layout/ThemeToggle.tsx +67 -0
- package/src/components/projects/FolderBrowser.tsx +142 -0
- package/src/components/projects/HomeChat.tsx +250 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/PanelSection.tsx +102 -0
- package/src/components/projects/ProjectInstructions.tsx +229 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +448 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +123 -0
- package/src/components/sessions/AssistantTurn.tsx +145 -0
- package/src/components/sessions/AttachmentTile.tsx +161 -0
- package/src/components/sessions/CodeBlock.tsx +107 -0
- package/src/components/sessions/ComposerActionSheet.tsx +96 -0
- package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +72 -0
- package/src/components/sessions/ContextPanel.tsx +561 -0
- package/src/components/sessions/DropPlate.tsx +165 -0
- package/src/components/sessions/FileDropOverlay.tsx +232 -0
- package/src/components/sessions/FileMentionDropdown.tsx +47 -0
- package/src/components/sessions/ImageLightbox.tsx +173 -0
- package/src/components/sessions/MarkdownContent.tsx +187 -0
- package/src/components/sessions/Mascot.tsx +47 -0
- package/src/components/sessions/MessageActions.tsx +60 -0
- package/src/components/sessions/MessageRail.tsx +121 -0
- package/src/components/sessions/ModelPicker.tsx +209 -0
- package/src/components/sessions/ModelSearchList.tsx +151 -0
- package/src/components/sessions/NewSessionDialog.tsx +189 -0
- package/src/components/sessions/PendingAttachments.tsx +127 -0
- package/src/components/sessions/PromptCard.tsx +1085 -0
- package/src/components/sessions/PromptPanel.tsx +112 -0
- package/src/components/sessions/SessionComposer.tsx +640 -0
- package/src/components/sessions/SessionHeader.tsx +175 -0
- package/src/components/sessions/SessionList.tsx +396 -0
- package/src/components/sessions/SessionRow.tsx +137 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +236 -0
- package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
- package/src/components/sessions/UserMessage.tsx +177 -0
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +220 -0
- package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
- package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
- package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
- package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
- package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
- package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
- package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
- package/src/components/settings/AboutGroup.tsx +170 -0
- package/src/components/settings/AccountGroup.tsx +122 -0
- package/src/components/settings/DevicesGroup.tsx +151 -0
- package/src/components/settings/LhrRemoteAccess.tsx +262 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1226 -0
- package/src/components/settings/SecurityGroup.tsx +566 -0
- package/src/components/settings/WorkspaceGroup.tsx +370 -0
- package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
- package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
- package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
- package/src/components/settings/prefs.tsx +265 -0
- package/src/components/skills/InstallNotifications.tsx +325 -0
- package/src/components/skills/InstallSkillDialog.tsx +233 -0
- package/src/components/skills/SkillCards.tsx +210 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/alert-dialog.tsx +179 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/bubble.tsx +125 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/dialog.tsx +142 -0
- package/src/components/ui/dropdown-menu.tsx +225 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +234 -0
- package/src/components/ui/input-group.tsx +161 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +85 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/select.tsx +172 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +699 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/config.ts +85 -0
- package/src/db.ts +146 -0
- package/src/frontend.tsx +51 -0
- package/src/hooks/__tests__/useMention.test.ts +79 -0
- package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
- package/src/hooks/__tests__/useRouter.test.ts +17 -0
- package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
- package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
- package/src/hooks/__tests__/useTheme.test.ts +118 -0
- package/src/hooks/use-mobile.ts +16 -0
- package/src/hooks/useAgentPicker.ts +97 -0
- package/src/hooks/useMediaQuery.ts +49 -0
- package/src/hooks/useMention.ts +236 -0
- package/src/hooks/useProjects.ts +88 -0
- package/src/hooks/useResyncOnReconnect.ts +28 -0
- package/src/hooks/useRouter.ts +92 -0
- package/src/hooks/useSessionChat.ts +427 -0
- package/src/hooks/useSessionContext.ts +106 -0
- package/src/hooks/useSessionList.ts +234 -0
- package/src/hooks/useSidebarData.ts +108 -0
- package/src/hooks/useSmoothText.ts +122 -0
- package/src/hooks/useTheme.ts +164 -0
- package/src/hooks/useWebSocket.ts +196 -0
- package/src/index.css +27 -0
- package/src/index.html +14 -0
- package/src/index.ts +49 -0
- package/src/lib/__tests__/attachments.test.ts +58 -0
- package/src/lib/__tests__/avatar-presets.test.ts +25 -0
- package/src/lib/__tests__/collapsible.test.ts +101 -0
- package/src/lib/__tests__/composer.test.ts +118 -0
- package/src/lib/__tests__/highlight.test.ts +42 -0
- package/src/lib/__tests__/home-chat.test.ts +64 -0
- package/src/lib/__tests__/lock-screen.test.ts +64 -0
- package/src/lib/__tests__/model-picker.test.ts +205 -0
- package/src/lib/__tests__/onboarding.test.ts +41 -0
- package/src/lib/__tests__/permission-info.test.ts +61 -0
- package/src/lib/__tests__/project-extensions.test.ts +52 -0
- package/src/lib/__tests__/project-overview.test.ts +173 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-summary.test.ts +115 -0
- package/src/lib/__tests__/session-title.test.ts +58 -0
- package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
- package/src/lib/__tests__/skill-installs.test.ts +134 -0
- package/src/lib/__tests__/skills-catalog.test.ts +96 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/api.ts +306 -0
- package/src/lib/attachments.ts +203 -0
- package/src/lib/auth.ts +92 -0
- package/src/lib/avatar-presets.ts +31 -0
- package/src/lib/collapsible.ts +75 -0
- package/src/lib/composer.ts +89 -0
- package/src/lib/data-events.ts +17 -0
- package/src/lib/headlines.ts +30 -0
- package/src/lib/highlight.ts +186 -0
- package/src/lib/layout.ts +20 -0
- package/src/lib/lock-screen.ts +54 -0
- package/src/lib/model-picker.ts +133 -0
- package/src/lib/onboarding.ts +38 -0
- package/src/lib/passkeys.ts +113 -0
- package/src/lib/pending-prompt.ts +53 -0
- package/src/lib/permission-info.ts +101 -0
- package/src/lib/project-extensions.ts +74 -0
- package/src/lib/project-overview.ts +117 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +115 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-status.ts +45 -0
- package/src/lib/session-summary.ts +68 -0
- package/src/lib/session-title.ts +63 -0
- package/src/lib/sidebar-groups.ts +78 -0
- package/src/lib/skill-installs.ts +360 -0
- package/src/lib/skills-catalog.ts +99 -0
- package/src/lib/time.ts +22 -0
- package/src/lib/tone.ts +80 -0
- package/src/lib/turns.ts +320 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +31 -0
- package/src/pages/projects/ProjectDetailPage.tsx +130 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +299 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +9 -0
- package/src/pages/skills/SkillsPage.tsx +701 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1031 -0
- package/src/server/__tests__/auth.test.ts +370 -0
- package/src/server/__tests__/config.test.ts +101 -0
- package/src/server/__tests__/db.test.ts +485 -0
- package/src/server/__tests__/host.test.ts +14 -0
- package/src/server/__tests__/tunnel-config.test.ts +38 -0
- package/src/server/app.ts +415 -0
- package/src/server/db/auth.ts +311 -0
- package/src/server/db/messages.ts +50 -0
- package/src/server/db/projects.ts +106 -0
- package/src/server/db/prompts.ts +78 -0
- package/src/server/db/rows.ts +150 -0
- package/src/server/db/schema.ts +221 -0
- package/src/server/db/sessions.ts +239 -0
- package/src/server/db/tunnel.ts +124 -0
- package/src/server/host.ts +12 -0
- package/src/server/result.ts +15 -0
- package/src/server/routes/auth-guard.ts +156 -0
- package/src/server/routes/auth.routes.ts +287 -0
- package/src/server/routes/helpers.ts +165 -0
- package/src/server/routes/meta.routes.ts +24 -0
- package/src/server/routes/passkeys.routes.ts +161 -0
- package/src/server/routes/projects.routes.ts +157 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +117 -0
- package/src/server/routes/tunnel.routes.ts +132 -0
- package/src/server/routes/types.ts +35 -0
- package/src/server/routes/uploads.routes.ts +54 -0
- package/src/server/routes/workspace.routes.ts +127 -0
- package/src/server/services/__tests__/app-info.test.ts +18 -0
- package/src/server/services/__tests__/attachments.test.ts +159 -0
- package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
- package/src/server/services/__tests__/auth.test.ts +341 -0
- package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
- package/src/server/services/__tests__/git-info.test.ts +74 -0
- package/src/server/services/__tests__/opencode.test.ts +320 -0
- package/src/server/services/__tests__/project-manager.test.ts +190 -0
- package/src/server/services/__tests__/sandbox.test.ts +141 -0
- package/src/server/services/__tests__/session-manager.test.ts +3469 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +226 -0
- package/src/server/services/__tests__/skills-registry.test.ts +359 -0
- package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
- package/src/server/services/__tests__/tunnel.test.ts +367 -0
- package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
- package/src/server/services/__tests__/workspace.test.ts +249 -0
- package/src/server/services/app-info.ts +29 -0
- package/src/server/services/attachments.ts +193 -0
- package/src/server/services/auth-passkeys.ts +459 -0
- package/src/server/services/auth.ts +633 -0
- package/src/server/services/frpc-installer.ts +183 -0
- package/src/server/services/git-info.ts +87 -0
- package/src/server/services/opencode-client.ts +714 -0
- package/src/server/services/opencode-server.ts +319 -0
- package/src/server/services/opencode-sse.ts +82 -0
- package/src/server/services/project-manager.ts +147 -0
- package/src/server/services/sandbox.ts +83 -0
- package/src/server/services/session-events.ts +256 -0
- package/src/server/services/session-manager.ts +1374 -0
- package/src/server/services/skill-install-jobs.ts +262 -0
- package/src/server/services/skills-registry.ts +468 -0
- package/src/server/services/tunnel-api.ts +207 -0
- package/src/server/services/tunnel-config.ts +45 -0
- package/src/server/services/tunnel-lhr.ts +387 -0
- package/src/server/services/tunnel.ts +633 -0
- package/src/server/services/turn-stream.ts +339 -0
- package/src/server/services/websocket-gateway.ts +374 -0
- package/src/server/services/workspace.ts +223 -0
- package/src/server/services/ws-transport.ts +121 -0
- package/src/types/context.ts +69 -0
- package/src/types/index.ts +13 -0
- package/src/types/message.ts +58 -0
- package/src/types/project.ts +16 -0
- package/src/types/prompt.ts +118 -0
- package/src/types/session.ts +52 -0
- package/src/types/turns.ts +47 -0
- package/src/ws-protocol.ts +93 -0
- package/styles/globals.css +311 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode_Client — client HTTP + SSE ke server headless `opencode serve`.
|
|
3
|
+
*
|
|
4
|
+
* Menggantikan `pty-process.ts` (PTY/TUI): alih-alih menulis byte ke terminal,
|
|
5
|
+
* percakapan dilakukan lewat API terstruktur:
|
|
6
|
+
* - `POST /session` buat session (`ses_...`)
|
|
7
|
+
* - `POST /session/{id}/message` kirim prompt, balasan terstruktur
|
|
8
|
+
* - `POST /session/{id}/prompt_async` kirim prompt, balas 204 (hasil via SSE)
|
|
9
|
+
* - `POST /permission/{id}/reply` jawab izin tool (`once|always|reject`)
|
|
10
|
+
* - `POST /question/{id}/reply|reject` jawab pertanyaan
|
|
11
|
+
* - `POST /session/{id}/abort` hentikan turn berjalan
|
|
12
|
+
* - `GET /event` SSE — event bertipe (permission.asked,
|
|
13
|
+
* question.asked, message.*, session.*)
|
|
14
|
+
*
|
|
15
|
+
* `subscribeEvents` menormalkan frame SSE `{ id, type, properties }` menjadi
|
|
16
|
+
* `{ type, id, ...properties }` agar konsumen mudah membaca `requestID`,
|
|
17
|
+
* `sessionID`, `permission`, `questions`, dst.
|
|
18
|
+
*/
|
|
19
|
+
import type { McpServerState, MessagePart, SessionModel } from "../../types";
|
|
20
|
+
import type { Result, SimpleResult } from "../result";
|
|
21
|
+
import { subscribeOpenCodeEvents } from "./opencode-sse";
|
|
22
|
+
|
|
23
|
+
export interface OpenCodeSessionInfo {
|
|
24
|
+
id: string;
|
|
25
|
+
directory?: string;
|
|
26
|
+
/** Judul session; opencode meng-generate ulang setelah prompt pertama. */
|
|
27
|
+
title?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Satu model yang tersedia pada sebuah provider (hasil GET /config/providers). */
|
|
31
|
+
export interface ModelOption {
|
|
32
|
+
providerID: string;
|
|
33
|
+
providerName: string;
|
|
34
|
+
modelID: string;
|
|
35
|
+
/** Nama tampilan (fallback: modelID). */
|
|
36
|
+
name: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Satu agent (mode) opencode — hasil `GET /agent` server headless. */
|
|
40
|
+
export interface AgentOption {
|
|
41
|
+
name: string;
|
|
42
|
+
mode: "primary" | "subagent" | "all";
|
|
43
|
+
description: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Satu MCP server Project — hasil `GET /mcp` server headless. Bentuknya
|
|
48
|
+
* didefinisikan SEKALI di `types/context.ts` (dipakai juga UI) agar server &
|
|
49
|
+
* klien tidak pernah berbeda.
|
|
50
|
+
*/
|
|
51
|
+
export type McpServerInfo = McpServerState;
|
|
52
|
+
|
|
53
|
+
/** Status koneksi MCP server — mengikuti skema `MCPStatus` opencode. */
|
|
54
|
+
export type McpStatus = McpServerState["status"];
|
|
55
|
+
|
|
56
|
+
/** Asal sebuah skill: bawaan opencode, folder Project, atau global user. */
|
|
57
|
+
export type SkillSource = "builtin" | "project" | "global";
|
|
58
|
+
|
|
59
|
+
/** Satu skill yang terdaftar — hasil `GET /skill` (tanpa isi `content`). */
|
|
60
|
+
export interface SkillInfo {
|
|
61
|
+
name: string;
|
|
62
|
+
description: string | null;
|
|
63
|
+
/** Path SKILL.md; null untuk skill bawaan. */
|
|
64
|
+
location: string | null;
|
|
65
|
+
source: SkillSource;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const MCP_STATUSES: readonly McpStatus[] = [
|
|
69
|
+
"connected",
|
|
70
|
+
"disabled",
|
|
71
|
+
"failed",
|
|
72
|
+
"needs_auth",
|
|
73
|
+
"needs_client_registration",
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Flatten respons `GET /mcp` (`{ [name]: { status, error? } }`) menjadi
|
|
78
|
+
* daftar terurut nama. Status tak dikenal dipetakan ke `unknown`.
|
|
79
|
+
*/
|
|
80
|
+
export function flattenMcp(payload: unknown): McpServerInfo[] {
|
|
81
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return [];
|
|
82
|
+
const out: McpServerInfo[] = [];
|
|
83
|
+
for (const [name, raw] of Object.entries(payload as Record<string, unknown>)) {
|
|
84
|
+
if (name === "") continue;
|
|
85
|
+
const v = (typeof raw === "object" && raw !== null ? raw : {}) as {
|
|
86
|
+
status?: unknown;
|
|
87
|
+
error?: unknown;
|
|
88
|
+
};
|
|
89
|
+
const status = MCP_STATUSES.includes(v.status as McpStatus)
|
|
90
|
+
? (v.status as McpStatus)
|
|
91
|
+
: "unknown";
|
|
92
|
+
out.push({
|
|
93
|
+
name,
|
|
94
|
+
status,
|
|
95
|
+
error: typeof v.error === "string" && v.error !== "" ? v.error : null,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Flatten respons `GET /skill` menjadi daftar ringkas. `content` (isi
|
|
103
|
+
* SKILL.md, bisa panjang) sengaja dibuang. `projectPath` dipakai untuk
|
|
104
|
+
* mengklasifikasi asal skill: di dalam direktori Project -> `project`,
|
|
105
|
+
* `<built-in>` -> `builtin`, selain itu -> `global`.
|
|
106
|
+
*/
|
|
107
|
+
export function flattenSkills(payload: unknown, projectPath?: string): SkillInfo[] {
|
|
108
|
+
if (!Array.isArray(payload)) return [];
|
|
109
|
+
const root = projectPath ? projectPath.replace(/\/+$/, "") : null;
|
|
110
|
+
const out: SkillInfo[] = [];
|
|
111
|
+
for (const raw of payload) {
|
|
112
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
113
|
+
const s = raw as { name?: unknown; description?: unknown; location?: unknown };
|
|
114
|
+
if (typeof s.name !== "string" || s.name === "") continue;
|
|
115
|
+
const loc = typeof s.location === "string" && s.location !== "" ? s.location : null;
|
|
116
|
+
let source: SkillSource = "global";
|
|
117
|
+
if (loc === null || loc === "<built-in>") source = "builtin";
|
|
118
|
+
else if (root && (loc === root || loc.startsWith(`${root}/`))) source = "project";
|
|
119
|
+
out.push({
|
|
120
|
+
name: s.name,
|
|
121
|
+
description: typeof s.description === "string" && s.description !== "" ? s.description : null,
|
|
122
|
+
location: source === "builtin" ? null : loc,
|
|
123
|
+
source,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Flatten respons `GET /agent` menjadi opsi siap pakai UI: hanya agent
|
|
131
|
+
* primary/all (subagent murni tidak bisa dipakai sebagai mode percakapan).
|
|
132
|
+
* Diekspor agar dapat diuji tanpa server.
|
|
133
|
+
*/
|
|
134
|
+
export function flattenAgents(payload: unknown): AgentOption[] {
|
|
135
|
+
if (!Array.isArray(payload)) return [];
|
|
136
|
+
const out: AgentOption[] = [];
|
|
137
|
+
for (const raw of payload) {
|
|
138
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
139
|
+
const a = raw as { name?: unknown; mode?: unknown; description?: unknown };
|
|
140
|
+
if (typeof a.name !== "string" || a.name === "") continue;
|
|
141
|
+
if (a.mode === "subagent") continue; // bukan mode percakapan
|
|
142
|
+
out.push({
|
|
143
|
+
name: a.name,
|
|
144
|
+
mode: a.mode === "all" ? "all" : "primary",
|
|
145
|
+
description: typeof a.description === "string" && a.description !== "" ? a.description : null,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Flatten respons `GET /config/providers` menjadi satu entri per model.
|
|
153
|
+
* Bentuk sumber: `{ providers: [{ id, name, models: { [modelID]: {...} } }] }`.
|
|
154
|
+
* Model berstatus `deprecated` disaring; provider tanpa model diabaikan.
|
|
155
|
+
* Diekspor agar dapat diuji tanpa server.
|
|
156
|
+
*/
|
|
157
|
+
export function flattenProviders(payload: unknown): ModelOption[] {
|
|
158
|
+
const providers = (payload as { providers?: unknown } | null)?.providers;
|
|
159
|
+
if (!Array.isArray(providers)) return [];
|
|
160
|
+
const out: ModelOption[] = [];
|
|
161
|
+
for (const p of providers) {
|
|
162
|
+
if (typeof p !== "object" || p === null) continue;
|
|
163
|
+
const prov = p as { id?: unknown; name?: unknown; models?: unknown };
|
|
164
|
+
if (typeof prov.id !== "string" || prov.id === "") continue;
|
|
165
|
+
const providerName = typeof prov.name === "string" && prov.name !== "" ? prov.name : prov.id;
|
|
166
|
+
const models =
|
|
167
|
+
typeof prov.models === "object" && prov.models !== null
|
|
168
|
+
? (prov.models as Record<string, unknown>)
|
|
169
|
+
: {};
|
|
170
|
+
for (const [modelID, raw] of Object.entries(models)) {
|
|
171
|
+
const m = (typeof raw === "object" && raw !== null ? raw : {}) as {
|
|
172
|
+
name?: unknown;
|
|
173
|
+
status?: unknown;
|
|
174
|
+
};
|
|
175
|
+
if (m.status === "deprecated") continue;
|
|
176
|
+
out.push({
|
|
177
|
+
providerID: prov.id,
|
|
178
|
+
providerName,
|
|
179
|
+
modelID,
|
|
180
|
+
name: typeof m.name === "string" && m.name !== "" ? m.name : modelID,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface OpenCodeMessageResult {
|
|
188
|
+
info: { id: string; role: string; [k: string]: unknown };
|
|
189
|
+
parts: MessagePart[];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export type OpenCodePermissionReply = "once" | "always" | "reject";
|
|
193
|
+
|
|
194
|
+
/** Referensi file untuk part `file` prompt: `@file` teks maupun gambar. */
|
|
195
|
+
export interface OpenCodeFileRef {
|
|
196
|
+
/** Nama tampilan part (path relatif project / nama file asli upload). */
|
|
197
|
+
filename: string;
|
|
198
|
+
/** MIME part — `text/plain` untuk @file, `image/*` untuk gambar. */
|
|
199
|
+
mime: string;
|
|
200
|
+
/** URL absolut yang dibaca opencode (`file:///abs/path`). */
|
|
201
|
+
url: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Satu item task list (`todowrite` opencode) — `GET /session/{id}/todo`.
|
|
206
|
+
* `status`/`priority` dibiarkan string: skema opencode mendeskripsikannya
|
|
207
|
+
* sebagai string bebas (pending/in_progress/completed/cancelled), jadi nilai
|
|
208
|
+
* baru dari versi opencode berikutnya tidak membuat parsing gagal.
|
|
209
|
+
*/
|
|
210
|
+
export interface OpenCodeTodo {
|
|
211
|
+
content: string;
|
|
212
|
+
status: string;
|
|
213
|
+
priority: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Satu file yang berubah di worktree — `GET /file/status`. */
|
|
217
|
+
export interface OpenCodeFileChange {
|
|
218
|
+
path: string;
|
|
219
|
+
added: number;
|
|
220
|
+
removed: number;
|
|
221
|
+
status: "added" | "deleted" | "modified";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Satu event SSE hasil normalisasi (properties digabung ke level atas). */
|
|
225
|
+
export interface OpenCodeEvent {
|
|
226
|
+
type: string;
|
|
227
|
+
id?: string;
|
|
228
|
+
[k: string]: unknown;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface OpenCodeClient {
|
|
232
|
+
/**
|
|
233
|
+
* Buat Session di server headless. Tanpa `title` (disarankan) opencode
|
|
234
|
+
* meng-generate judul dari prompt pertama; dengan `title`, judul tetap.
|
|
235
|
+
*/
|
|
236
|
+
createSession(opts?: { title?: string }): Promise<Result<OpenCodeSessionInfo>>;
|
|
237
|
+
/**
|
|
238
|
+
* Cek keberadaan Session di server headless (`GET /session/{id}`):
|
|
239
|
+
* `{ ok: true }` bila Session masih dikenal server, `{ ok: false }`
|
|
240
|
+
* bila tidak ada / server gagal merespons.
|
|
241
|
+
*/
|
|
242
|
+
getSession(sessionId: string): Promise<SimpleResult>;
|
|
243
|
+
/** Daftar model yang tersedia pada server (`GET /config/providers`),
|
|
244
|
+
* sudah di-flatten menjadi satu entri per model.
|
|
245
|
+
*/
|
|
246
|
+
listModels(): Promise<Result<ModelOption[]>>;
|
|
247
|
+
/**
|
|
248
|
+
* Daftar agent (mode) pada server (`GET /agent`) — hanya primary/all;
|
|
249
|
+
* subagent murni tidak bisa dipakai sebagai mode percakapan.
|
|
250
|
+
*/
|
|
251
|
+
listAgents(): Promise<Result<AgentOption[]>>;
|
|
252
|
+
/** Status MCP server yang terkonfigurasi (`GET /mcp`). */
|
|
253
|
+
listMcp(): Promise<Result<McpServerInfo[]>>;
|
|
254
|
+
/**
|
|
255
|
+
* Skill yang terdaftar (`GET /skill`). `projectPath` untuk menandai skill
|
|
256
|
+
* yang berasal dari folder Project.
|
|
257
|
+
*/
|
|
258
|
+
listSkills(projectPath?: string): Promise<Result<SkillInfo[]>>;
|
|
259
|
+
/**
|
|
260
|
+
* Buang cache instance Project di server (`POST /instance/dispose`) agar
|
|
261
|
+
* skill/config dibaca ulang dari disk — `GET /skill` di-cache per
|
|
262
|
+
* instance. Instance dibuat ulang otomatis pada request berikutnya.
|
|
263
|
+
*/
|
|
264
|
+
disposeInstance(): Promise<SimpleResult>;
|
|
265
|
+
/**
|
|
266
|
+
* Hapus Session di server headless (`DELETE /session/{id}`) beserta
|
|
267
|
+
* seluruh riwayat pesannya di sisi opencode.
|
|
268
|
+
*/
|
|
269
|
+
deleteSession(sessionId: string): Promise<SimpleResult>;
|
|
270
|
+
sendMessage(
|
|
271
|
+
sessionId: string,
|
|
272
|
+
text: string,
|
|
273
|
+
signal?: AbortSignal,
|
|
274
|
+
): Promise<Result<OpenCodeMessageResult>>;
|
|
275
|
+
/**
|
|
276
|
+
* Kirim prompt tanpa menunggu balasan (`POST /session/{id}/prompt_async`,
|
|
277
|
+
* 204). `model` disertakan agar Session memakai model pilihan user, bukan
|
|
278
|
+
* model default opencode. `files` berupa referensi file (teks `@file`
|
|
279
|
+
* maupun gambar) — opencode mem-parse `url` part file dengan `URL()` dan
|
|
280
|
+
* membaca isinya sendiri, sehingga `url` harus path absolut
|
|
281
|
+
* (`file:///abs/path`).
|
|
282
|
+
*/
|
|
283
|
+
promptAsync(
|
|
284
|
+
sessionId: string,
|
|
285
|
+
text: string,
|
|
286
|
+
model?: SessionModel | null,
|
|
287
|
+
files?: OpenCodeFileRef[],
|
|
288
|
+
agent?: string | null,
|
|
289
|
+
/** Instruksi tambahan (custom instruction Project) -> body `system`. */
|
|
290
|
+
system?: string | null,
|
|
291
|
+
): Promise<Result<null>>;
|
|
292
|
+
/** Cari file project untuk autocomplete `@file` (path relatif). */
|
|
293
|
+
findFiles(query: string): Promise<Result<string[]>>;
|
|
294
|
+
/** Task list Session (`todowrite`) — kosong bila agent belum memakainya. */
|
|
295
|
+
listTodos(sessionId: string): Promise<Result<OpenCodeTodo[]>>;
|
|
296
|
+
/** File yang berubah di worktree project (git status versi opencode). */
|
|
297
|
+
fileStatus(): Promise<Result<OpenCodeFileChange[]>>;
|
|
298
|
+
replyPermission(requestId: string, reply: OpenCodePermissionReply): Promise<Result<unknown>>;
|
|
299
|
+
/**
|
|
300
|
+
* `answers` = SATU array label per pertanyaan (urut sesuai `questions`);
|
|
301
|
+
* request pertanyaan tunggal berarti array berisi satu elemen.
|
|
302
|
+
*/
|
|
303
|
+
replyQuestion(requestId: string, answers: string[][]): Promise<Result<unknown>>;
|
|
304
|
+
rejectQuestion(requestId: string): Promise<Result<unknown>>;
|
|
305
|
+
abortSession(sessionId: string): Promise<Result<unknown>>;
|
|
306
|
+
/** Subscribe event SSE; mengembalikan fungsi untuk berhenti subscribe. */
|
|
307
|
+
subscribeEvents(cb: (ev: OpenCodeEvent) => void): () => void;
|
|
308
|
+
health(): Promise<boolean>;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function errResult(msg: string): { ok: false; error: string } {
|
|
312
|
+
return { ok: false, error: msg };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Error reply permission/question saat opencode membalas 404
|
|
317
|
+
* (`QuestionNotFoundError` / `PermissionNotFoundError`): request sudah
|
|
318
|
+
* tidak ditunggu lagi — dijawab dari klien lain, dibatalkan karena turn
|
|
319
|
+
* di-abort, atau server opencode di-restart. Pemanggil memperlakukannya
|
|
320
|
+
* sebagai prompt kedaluwarsa (bukan kegagalan yang bisa dicoba ulang).
|
|
321
|
+
*/
|
|
322
|
+
export const OC_REQUEST_GONE = "OC_REQUEST_GONE";
|
|
323
|
+
|
|
324
|
+
async function requestJson(
|
|
325
|
+
baseUrl: string,
|
|
326
|
+
method: string,
|
|
327
|
+
path: string,
|
|
328
|
+
body?: unknown,
|
|
329
|
+
signal?: AbortSignal,
|
|
330
|
+
): Promise<{ status: number; json: unknown }> {
|
|
331
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
332
|
+
method,
|
|
333
|
+
headers: body !== undefined ? { "content-type": "application/json" } : undefined,
|
|
334
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
335
|
+
signal,
|
|
336
|
+
});
|
|
337
|
+
let json: unknown = null;
|
|
338
|
+
try {
|
|
339
|
+
json = await res.json();
|
|
340
|
+
} catch {
|
|
341
|
+
/* body kosong / bukan JSON */
|
|
342
|
+
}
|
|
343
|
+
return { status: res.status, json };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function createOpenCodeClient(baseUrl: string): OpenCodeClient {
|
|
347
|
+
async function health(): Promise<boolean> {
|
|
348
|
+
try {
|
|
349
|
+
const res = await fetch(`${baseUrl}/global/health`, { signal: AbortSignal.timeout(3000) });
|
|
350
|
+
return res.ok;
|
|
351
|
+
} catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function getSession(sessionId: string): Promise<SimpleResult> {
|
|
357
|
+
try {
|
|
358
|
+
const { status } = await requestJson(
|
|
359
|
+
baseUrl,
|
|
360
|
+
"GET",
|
|
361
|
+
`/session/${sessionId}`,
|
|
362
|
+
undefined,
|
|
363
|
+
AbortSignal.timeout(5000),
|
|
364
|
+
);
|
|
365
|
+
return status === 200 ? { ok: true } : errResult(`OC_SESSION_NOT_FOUND(${status})`);
|
|
366
|
+
} catch (e) {
|
|
367
|
+
return errResult(`OC_GET_SESSION_FAILED: ${(e as Error).message}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function createSession(
|
|
372
|
+
opts: { title?: string } = {},
|
|
373
|
+
): Promise<Result<OpenCodeSessionInfo>> {
|
|
374
|
+
try {
|
|
375
|
+
// Tanpa `title`: opencode memberi placeholder `New session - <ISO>` dan
|
|
376
|
+
// HANYA untuk judul berpola itu ia meng-generate judul otomatis
|
|
377
|
+
// (agent `title`) setelah prompt pertama. Judul eksplisit mematikannya.
|
|
378
|
+
const { status, json } = await requestJson(
|
|
379
|
+
baseUrl,
|
|
380
|
+
"POST",
|
|
381
|
+
"/session",
|
|
382
|
+
opts.title === undefined ? {} : { title: opts.title },
|
|
383
|
+
);
|
|
384
|
+
if (status !== 200) return errResult(`OC_CREATE_SESSION_FAILED(${status})`);
|
|
385
|
+
const info = json as OpenCodeSessionInfo;
|
|
386
|
+
if (typeof info?.id !== "string") return errResult("OC_CREATE_SESSION_FAILED");
|
|
387
|
+
return { ok: true, data: info };
|
|
388
|
+
} catch (e) {
|
|
389
|
+
return errResult(`OC_CREATE_SESSION_FAILED: ${(e as Error).message}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function sendMessage(
|
|
394
|
+
sessionId: string,
|
|
395
|
+
text: string,
|
|
396
|
+
signal?: AbortSignal,
|
|
397
|
+
): Promise<Result<OpenCodeMessageResult>> {
|
|
398
|
+
try {
|
|
399
|
+
const { status, json } = await requestJson(
|
|
400
|
+
baseUrl,
|
|
401
|
+
"POST",
|
|
402
|
+
`/session/${sessionId}/message`,
|
|
403
|
+
{ parts: [{ type: "text", text }] },
|
|
404
|
+
signal,
|
|
405
|
+
);
|
|
406
|
+
if (status !== 200) return errResult(`OC_SEND_MESSAGE_FAILED(${status})`);
|
|
407
|
+
const body = json as OpenCodeMessageResult | null;
|
|
408
|
+
if (!body || typeof body.info?.id !== "string" || !Array.isArray(body.parts)) {
|
|
409
|
+
return errResult("OC_SEND_MESSAGE_FAILED: balasan tidak valid");
|
|
410
|
+
}
|
|
411
|
+
return { ok: true, data: body };
|
|
412
|
+
} catch (e) {
|
|
413
|
+
const err = e as Error & { name?: string };
|
|
414
|
+
const name = err.name ?? "";
|
|
415
|
+
const msg = err.message ?? "";
|
|
416
|
+
if (name === "TimeoutError" || name === "AbortError" || /timed out|aborted/i.test(msg)) {
|
|
417
|
+
return errResult("OC_SEND_MESSAGE_TIMEOUT");
|
|
418
|
+
}
|
|
419
|
+
return errResult(`OC_SEND_MESSAGE_FAILED: ${msg}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function promptAsync(
|
|
424
|
+
sessionId: string,
|
|
425
|
+
text: string,
|
|
426
|
+
model?: SessionModel | null,
|
|
427
|
+
files: OpenCodeFileRef[] = [],
|
|
428
|
+
agent?: string | null,
|
|
429
|
+
system?: string | null,
|
|
430
|
+
): Promise<Result<null>> {
|
|
431
|
+
try {
|
|
432
|
+
// Parts prompt: teks bebas + satu part `file` per referensi (teks/gambar).
|
|
433
|
+
const parts: Record<string, unknown>[] = [{ type: "text", text }];
|
|
434
|
+
for (const f of files) {
|
|
435
|
+
parts.push({ type: "file", mime: f.mime, filename: f.filename, url: f.url });
|
|
436
|
+
}
|
|
437
|
+
const body: Record<string, unknown> = { parts };
|
|
438
|
+
// Skema prompt_async menerima `model: { providerID, modelID }` opsional;
|
|
439
|
+
// tanpa field ini opencode memakai model default-nya.
|
|
440
|
+
if (model) body.model = { providerID: model.providerID, modelID: model.modelID };
|
|
441
|
+
// `agent` opsional (mis. build/plan atau agent kustom user); tanpa
|
|
442
|
+
// field ini opencode memakai agent default-nya.
|
|
443
|
+
if (agent) body.agent = agent;
|
|
444
|
+
// `system` opsional: custom instruction Project, ditambahkan opencode
|
|
445
|
+
// ke system prompt untuk turn ini (skema sama dengan POST /message).
|
|
446
|
+
if (system) body.system = system;
|
|
447
|
+
const { status } = await requestJson(
|
|
448
|
+
baseUrl,
|
|
449
|
+
"POST",
|
|
450
|
+
`/session/${sessionId}/prompt_async`,
|
|
451
|
+
body,
|
|
452
|
+
// Hanya menunggu penerimaan prompt (204), bukan seluruh turn.
|
|
453
|
+
AbortSignal.timeout(30_000),
|
|
454
|
+
);
|
|
455
|
+
// 204 = prompt diterima; opencode juga membalas 200 pada versi tertentu.
|
|
456
|
+
return status === 204 || status === 200
|
|
457
|
+
? { ok: true, data: null }
|
|
458
|
+
: errResult(`OC_PROMPT_ASYNC_FAILED(${status})`);
|
|
459
|
+
} catch (e) {
|
|
460
|
+
return errResult(`OC_PROMPT_ASYNC_FAILED: ${(e as Error).message}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Cari file di project (`GET /find/file?query=`) — dipakai autocomplete
|
|
466
|
+
* referensi `@file` di composer. Mengembalikan path relatif project.
|
|
467
|
+
*/
|
|
468
|
+
async function findFiles(query: string): Promise<Result<string[]>> {
|
|
469
|
+
try {
|
|
470
|
+
const { status, json } = await requestJson(
|
|
471
|
+
baseUrl,
|
|
472
|
+
"GET",
|
|
473
|
+
`/find/file?query=${encodeURIComponent(query)}`,
|
|
474
|
+
undefined,
|
|
475
|
+
AbortSignal.timeout(5000),
|
|
476
|
+
);
|
|
477
|
+
if (status !== 200) return errResult(`OC_FIND_FILES_FAILED(${status})`);
|
|
478
|
+
const raw = Array.isArray(json) ? json : [];
|
|
479
|
+
const out: string[] = [];
|
|
480
|
+
for (const item of raw) {
|
|
481
|
+
if (typeof item === "string" && item.length > 0) out.push(item);
|
|
482
|
+
}
|
|
483
|
+
return { ok: true, data: out };
|
|
484
|
+
} catch (e) {
|
|
485
|
+
return errResult(`OC_FIND_FILES_FAILED: ${(e as Error).message}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function listTodos(sessionId: string): Promise<Result<OpenCodeTodo[]>> {
|
|
490
|
+
try {
|
|
491
|
+
const { status, json } = await requestJson(
|
|
492
|
+
baseUrl,
|
|
493
|
+
"GET",
|
|
494
|
+
`/session/${sessionId}/todo`,
|
|
495
|
+
undefined,
|
|
496
|
+
AbortSignal.timeout(5000),
|
|
497
|
+
);
|
|
498
|
+
if (status !== 200) return errResult(`OC_TODO_FAILED(${status})`);
|
|
499
|
+
const out: OpenCodeTodo[] = [];
|
|
500
|
+
for (const item of Array.isArray(json) ? json : []) {
|
|
501
|
+
if (typeof item !== "object" || item === null) continue;
|
|
502
|
+
const t = item as { content?: unknown; status?: unknown; priority?: unknown };
|
|
503
|
+
if (typeof t.content !== "string" || t.content === "") continue;
|
|
504
|
+
out.push({
|
|
505
|
+
content: t.content,
|
|
506
|
+
status: typeof t.status === "string" ? t.status : "pending",
|
|
507
|
+
priority: typeof t.priority === "string" ? t.priority : "medium",
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return { ok: true, data: out };
|
|
511
|
+
} catch (e) {
|
|
512
|
+
return errResult(`OC_TODO_FAILED: ${(e as Error).message}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function fileStatus(): Promise<Result<OpenCodeFileChange[]>> {
|
|
517
|
+
try {
|
|
518
|
+
const { status, json } = await requestJson(
|
|
519
|
+
baseUrl,
|
|
520
|
+
"GET",
|
|
521
|
+
"/file/status",
|
|
522
|
+
undefined,
|
|
523
|
+
AbortSignal.timeout(5000),
|
|
524
|
+
);
|
|
525
|
+
if (status !== 200) return errResult(`OC_FILE_STATUS_FAILED(${status})`);
|
|
526
|
+
const out: OpenCodeFileChange[] = [];
|
|
527
|
+
for (const item of Array.isArray(json) ? json : []) {
|
|
528
|
+
if (typeof item !== "object" || item === null) continue;
|
|
529
|
+
const f = item as {
|
|
530
|
+
path?: unknown;
|
|
531
|
+
added?: unknown;
|
|
532
|
+
removed?: unknown;
|
|
533
|
+
status?: unknown;
|
|
534
|
+
};
|
|
535
|
+
if (typeof f.path !== "string" || f.path === "") continue;
|
|
536
|
+
const kind =
|
|
537
|
+
f.status === "added" || f.status === "deleted" || f.status === "modified"
|
|
538
|
+
? f.status
|
|
539
|
+
: "modified";
|
|
540
|
+
out.push({
|
|
541
|
+
path: f.path,
|
|
542
|
+
added: typeof f.added === "number" ? f.added : 0,
|
|
543
|
+
removed: typeof f.removed === "number" ? f.removed : 0,
|
|
544
|
+
status: kind,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
return { ok: true, data: out };
|
|
548
|
+
} catch (e) {
|
|
549
|
+
return errResult(`OC_FILE_STATUS_FAILED: ${(e as Error).message}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function listModels(): Promise<Result<ModelOption[]>> {
|
|
554
|
+
try {
|
|
555
|
+
const { status, json } = await requestJson(baseUrl, "GET", "/config/providers");
|
|
556
|
+
if (status !== 200) return errResult(`OC_LIST_MODELS_FAILED(${status})`);
|
|
557
|
+
return { ok: true, data: flattenProviders(json) };
|
|
558
|
+
} catch (e) {
|
|
559
|
+
return errResult(`OC_LIST_MODELS_FAILED: ${(e as Error).message}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function listAgents(): Promise<Result<AgentOption[]>> {
|
|
564
|
+
try {
|
|
565
|
+
const { status, json } = await requestJson(baseUrl, "GET", "/agent");
|
|
566
|
+
if (status !== 200) return errResult(`OC_LIST_AGENTS_FAILED(${status})`);
|
|
567
|
+
return { ok: true, data: flattenAgents(json) };
|
|
568
|
+
} catch (e) {
|
|
569
|
+
return errResult(`OC_LIST_AGENTS_FAILED: ${(e as Error).message}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function listMcp(): Promise<Result<McpServerInfo[]>> {
|
|
574
|
+
try {
|
|
575
|
+
const { status, json } = await requestJson(
|
|
576
|
+
baseUrl,
|
|
577
|
+
"GET",
|
|
578
|
+
"/mcp",
|
|
579
|
+
undefined,
|
|
580
|
+
AbortSignal.timeout(10_000),
|
|
581
|
+
);
|
|
582
|
+
if (status !== 200) return errResult(`OC_LIST_MCP_FAILED(${status})`);
|
|
583
|
+
return { ok: true, data: flattenMcp(json) };
|
|
584
|
+
} catch (e) {
|
|
585
|
+
return errResult(`OC_LIST_MCP_FAILED: ${(e as Error).message}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function listSkills(projectPath?: string): Promise<Result<SkillInfo[]>> {
|
|
590
|
+
try {
|
|
591
|
+
const { status, json } = await requestJson(
|
|
592
|
+
baseUrl,
|
|
593
|
+
"GET",
|
|
594
|
+
"/skill",
|
|
595
|
+
undefined,
|
|
596
|
+
AbortSignal.timeout(10_000),
|
|
597
|
+
);
|
|
598
|
+
if (status !== 200) return errResult(`OC_LIST_SKILLS_FAILED(${status})`);
|
|
599
|
+
return { ok: true, data: flattenSkills(json, projectPath) };
|
|
600
|
+
} catch (e) {
|
|
601
|
+
return errResult(`OC_LIST_SKILLS_FAILED: ${(e as Error).message}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function disposeInstance(): Promise<SimpleResult> {
|
|
606
|
+
try {
|
|
607
|
+
const { status } = await requestJson(
|
|
608
|
+
baseUrl,
|
|
609
|
+
"POST",
|
|
610
|
+
"/instance/dispose",
|
|
611
|
+
undefined,
|
|
612
|
+
AbortSignal.timeout(10_000),
|
|
613
|
+
);
|
|
614
|
+
return status === 200 ? { ok: true } : { ok: false, error: `OC_DISPOSE_FAILED(${status})` };
|
|
615
|
+
} catch (e) {
|
|
616
|
+
return { ok: false, error: `OC_DISPOSE_FAILED: ${(e as Error).message}` };
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function deleteSession(sessionId: string): Promise<SimpleResult> {
|
|
621
|
+
try {
|
|
622
|
+
const { status } = await requestJson(
|
|
623
|
+
baseUrl,
|
|
624
|
+
"DELETE",
|
|
625
|
+
`/session/${sessionId}`,
|
|
626
|
+
undefined,
|
|
627
|
+
AbortSignal.timeout(10_000),
|
|
628
|
+
);
|
|
629
|
+
// 200 = terhapus. 404 dianggap sukses: target sudah tidak ada (idemoten
|
|
630
|
+
// terhadap storage opencode yang mungkin sudah dibersihkan manual).
|
|
631
|
+
return status === 200 || status === 404
|
|
632
|
+
? { ok: true }
|
|
633
|
+
: errResult(`OC_DELETE_SESSION_FAILED(${status})`);
|
|
634
|
+
} catch (e) {
|
|
635
|
+
return errResult(`OC_DELETE_SESSION_FAILED: ${(e as Error).message}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function replyPermission(
|
|
640
|
+
requestId: string,
|
|
641
|
+
reply: OpenCodePermissionReply,
|
|
642
|
+
): Promise<Result<unknown>> {
|
|
643
|
+
try {
|
|
644
|
+
const { status } = await requestJson(baseUrl, "POST", `/permission/${requestId}/reply`, {
|
|
645
|
+
reply,
|
|
646
|
+
});
|
|
647
|
+
if (status === 200) return { ok: true, data: null };
|
|
648
|
+
return errResult(status === 404 ? OC_REQUEST_GONE : `OC_PERMISSION_REPLY_FAILED(${status})`);
|
|
649
|
+
} catch (e) {
|
|
650
|
+
return errResult(`OC_PERMISSION_REPLY_FAILED: ${(e as Error).message}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function replyQuestion(requestId: string, answers: string[][]): Promise<Result<unknown>> {
|
|
655
|
+
try {
|
|
656
|
+
// Skema Question.Reply: `{ answers: Answer[] }` — SATU entri per
|
|
657
|
+
// pertanyaan, masing-masing berupa array label terpilih. Pemanggil
|
|
658
|
+
// sudah mengirim satu array per pertanyaan, jadi body diteruskan
|
|
659
|
+
// apa adanya.
|
|
660
|
+
const { status } = await requestJson(baseUrl, "POST", `/question/${requestId}/reply`, {
|
|
661
|
+
answers,
|
|
662
|
+
});
|
|
663
|
+
if (status === 200) return { ok: true, data: null };
|
|
664
|
+
return errResult(status === 404 ? OC_REQUEST_GONE : `OC_QUESTION_REPLY_FAILED(${status})`);
|
|
665
|
+
} catch (e) {
|
|
666
|
+
return errResult(`OC_QUESTION_REPLY_FAILED: ${(e as Error).message}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function rejectQuestion(requestId: string): Promise<Result<unknown>> {
|
|
671
|
+
try {
|
|
672
|
+
const { status } = await requestJson(baseUrl, "POST", `/question/${requestId}/reject`, {});
|
|
673
|
+
if (status === 200) return { ok: true, data: null };
|
|
674
|
+
return errResult(status === 404 ? OC_REQUEST_GONE : `OC_QUESTION_REJECT_FAILED(${status})`);
|
|
675
|
+
} catch (e) {
|
|
676
|
+
return errResult(`OC_QUESTION_REJECT_FAILED: ${(e as Error).message}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function abortSession(sessionId: string): Promise<Result<unknown>> {
|
|
681
|
+
try {
|
|
682
|
+
const { status } = await requestJson(baseUrl, "POST", `/session/${sessionId}/abort`, {});
|
|
683
|
+
return status === 200 ? { ok: true, data: null } : errResult(`OC_ABORT_FAILED(${status})`);
|
|
684
|
+
} catch (e) {
|
|
685
|
+
return errResult(`OC_ABORT_FAILED: ${(e as Error).message}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function subscribeEvents(cb: (ev: OpenCodeEvent) => void): () => void {
|
|
690
|
+
return subscribeOpenCodeEvents(baseUrl, cb);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
createSession,
|
|
695
|
+
getSession,
|
|
696
|
+
listModels,
|
|
697
|
+
listAgents,
|
|
698
|
+
deleteSession,
|
|
699
|
+
sendMessage,
|
|
700
|
+
promptAsync,
|
|
701
|
+
listMcp,
|
|
702
|
+
listSkills,
|
|
703
|
+
disposeInstance,
|
|
704
|
+
findFiles,
|
|
705
|
+
listTodos,
|
|
706
|
+
fileStatus,
|
|
707
|
+
replyPermission,
|
|
708
|
+
replyQuestion,
|
|
709
|
+
rejectQuestion,
|
|
710
|
+
abortSession,
|
|
711
|
+
subscribeEvents,
|
|
712
|
+
health,
|
|
713
|
+
};
|
|
714
|
+
}
|