kcg-code 0.0.0-stage → 0.2.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 +230 -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 +84 -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__/daemon.test.ts +84 -0
- package/src/cli/__tests__/dashboard.test.ts +224 -0
- package/src/cli/__tests__/keep-awake.test.ts +44 -0
- package/src/cli/__tests__/port.test.ts +86 -0
- package/src/cli/args.ts +186 -0
- package/src/cli/attach.ts +154 -0
- package/src/cli/commands.ts +327 -0
- package/src/cli/daemon.ts +275 -0
- package/src/cli/dashboard.tsx +1060 -0
- package/src/cli/index.ts +47 -0
- package/src/cli/keep-awake.ts +148 -0
- package/src/cli/port.ts +92 -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 +147 -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 +287 -0
- package/src/components/projects/HomeRecentSessions.tsx +90 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/ProjectInstructions.tsx +218 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +514 -0
- package/src/components/sessions/AgentLogo.tsx +42 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +132 -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 +92 -0
- package/src/components/sessions/ContextPanel.tsx +661 -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 +247 -0
- package/src/components/sessions/ModelSearchList.tsx +231 -0
- package/src/components/sessions/NewSessionDialog.tsx +172 -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 +641 -0
- package/src/components/sessions/SessionHeader.tsx +176 -0
- package/src/components/sessions/SessionList.tsx +503 -0
- package/src/components/sessions/SessionRow.tsx +237 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +240 -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 +256 -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 +257 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1201 -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 +333 -0
- package/src/components/skills/InstallNotifications.tsx +326 -0
- package/src/components/skills/InstallSkillDialog.tsx +390 -0
- package/src/components/skills/SkillCards.tsx +254 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/adw-dialog.tsx +114 -0
- package/src/components/ui/adw-toggle-group.tsx +75 -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/search-entry.tsx +70 -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 +114 -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 +101 -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 +327 -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 +83 -0
- package/src/lib/__tests__/project-overview.test.ts +188 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-selection.test.ts +41 -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 +135 -0
- package/src/lib/__tests__/skills-catalog.test.ts +124 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/agent-types.ts +49 -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 +136 -0
- package/src/lib/project-overview.ts +123 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +124 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-selection.ts +40 -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 +362 -0
- package/src/lib/skills-catalog.ts +138 -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 +131 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +366 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
- package/src/pages/skills/SkillsPage.tsx +745 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1044 -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 +427 -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 +167 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +136 -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__/claude-code.test.ts +609 -0
- package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
- package/src/server/services/__tests__/claude-skills.test.ts +63 -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 +3509 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
- package/src/server/services/__tests__/skills-registry.test.ts +365 -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/claude-code-client.ts +1093 -0
- package/src/server/services/claude-code-translate.ts +370 -0
- package/src/server/services/claude-mcp.ts +122 -0
- package/src/server/services/claude-skills.ts +102 -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 +1439 -0
- package/src/server/services/skill-install-jobs.ts +275 -0
- package/src/server/services/skills-registry.ts +480 -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,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabel rute API passkey (WebAuthn).
|
|
3
|
+
*
|
|
4
|
+
* Publik (lock screen, sebelum login):
|
|
5
|
+
* - `GET /api/auth/passkeys/availability` `{ supported, count }` untuk alamat ini
|
|
6
|
+
* - `POST /api/auth/passkeys/login/options` -> `{ challengeId, options }`
|
|
7
|
+
* - `POST /api/auth/passkeys/login` `{ challengeId, response }` -> cookie sesi
|
|
8
|
+
*
|
|
9
|
+
* Wajib login — dijaga `guardRoutes`:
|
|
10
|
+
* - `GET /api/auth/passkeys` daftar passkey
|
|
11
|
+
* - `POST /api/auth/passkeys/register/options` `{ current }` -> `{ challengeId, options }`
|
|
12
|
+
* - `POST /api/auth/passkeys/register` `{ challengeId, response, name? }`
|
|
13
|
+
* - `PATCH /api/auth/passkeys/:id` `{ name }`
|
|
14
|
+
* - `DELETE /api/auth/passkeys/:id`
|
|
15
|
+
*/
|
|
16
|
+
import type { BunRequest, Server } from "bun";
|
|
17
|
+
import type { AuthService } from "../services/auth";
|
|
18
|
+
import type { PasskeyService } from "../services/auth-passkeys";
|
|
19
|
+
import { clientIp, sessionCookie } from "./auth-guard";
|
|
20
|
+
import { errorStatus, json, readJson, serverError } from "./helpers";
|
|
21
|
+
|
|
22
|
+
function rateLimited(code: string): number | null {
|
|
23
|
+
if (!code.startsWith("AUTH_RATE_LIMITED:")) return null;
|
|
24
|
+
const n = Number(code.slice("AUTH_RATE_LIMITED:".length));
|
|
25
|
+
return Number.isFinite(n) ? n : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function errorResponse(code: string): Response {
|
|
29
|
+
const wait = rateLimited(code);
|
|
30
|
+
if (wait !== null) {
|
|
31
|
+
return new Response(JSON.stringify({ error: "AUTH_RATE_LIMITED", retryAfterSec: wait }), {
|
|
32
|
+
status: 429,
|
|
33
|
+
headers: { "content-type": "application/json", "retry-after": String(wait) },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return json({ error: code }, errorStatus(code));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function body(req: Request): Promise<Record<string, unknown> | null> {
|
|
40
|
+
const res = await readJson(req);
|
|
41
|
+
if (!res.ok || res.data === null || typeof res.data !== "object") return null;
|
|
42
|
+
return res.data as Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function passkeyRoutes(_auth: AuthService, passkeys: PasskeyService) {
|
|
46
|
+
return {
|
|
47
|
+
"/api/auth/passkeys/availability": {
|
|
48
|
+
GET: (req: Request) => {
|
|
49
|
+
try {
|
|
50
|
+
return json(passkeys.availability(req));
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return serverError(e);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
"/api/auth/passkeys/login/options": {
|
|
58
|
+
POST: async (req: Request, server: Server<unknown>) => {
|
|
59
|
+
try {
|
|
60
|
+
const res = await passkeys.authenticationOptions(req, clientIp(req, server));
|
|
61
|
+
if (!res.ok) return errorResponse(res.error);
|
|
62
|
+
return json(res.data);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return serverError(e);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
"/api/auth/passkeys/login": {
|
|
70
|
+
POST: async (req: Request, server: Server<unknown>) => {
|
|
71
|
+
try {
|
|
72
|
+
const b = await body(req);
|
|
73
|
+
if (!b) return json({ error: "INVALID_JSON" }, 400);
|
|
74
|
+
const res = await passkeys.login(
|
|
75
|
+
req,
|
|
76
|
+
{
|
|
77
|
+
challengeId: typeof b.challengeId === "string" ? b.challengeId : "",
|
|
78
|
+
response: b.response as never,
|
|
79
|
+
},
|
|
80
|
+
{ ip: clientIp(req, server), userAgent: req.headers.get("user-agent") },
|
|
81
|
+
);
|
|
82
|
+
if (!res.ok) return errorResponse(res.error);
|
|
83
|
+
const out = json({ ok: true });
|
|
84
|
+
out.headers.append("set-cookie", sessionCookie(req, res.data.token));
|
|
85
|
+
return out;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return serverError(e);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
"/api/auth/passkeys": {
|
|
93
|
+
GET: (req: Request) => {
|
|
94
|
+
try {
|
|
95
|
+
return json({ passkeys: passkeys.list(req) });
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return serverError(e);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
"/api/auth/passkeys/register/options": {
|
|
103
|
+
POST: async (req: Request, server: Server<unknown>) => {
|
|
104
|
+
try {
|
|
105
|
+
const b = await body(req);
|
|
106
|
+
if (!b) return json({ error: "INVALID_JSON" }, 400);
|
|
107
|
+
const res = await passkeys.registrationOptions(
|
|
108
|
+
req,
|
|
109
|
+
typeof b.current === "string" ? b.current : "",
|
|
110
|
+
clientIp(req, server),
|
|
111
|
+
);
|
|
112
|
+
if (!res.ok) return errorResponse(res.error);
|
|
113
|
+
return json(res.data);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return serverError(e);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
"/api/auth/passkeys/register": {
|
|
121
|
+
POST: async (req: Request) => {
|
|
122
|
+
try {
|
|
123
|
+
const b = await body(req);
|
|
124
|
+
if (!b) return json({ error: "INVALID_JSON" }, 400);
|
|
125
|
+
const res = await passkeys.register(req, {
|
|
126
|
+
challengeId: typeof b.challengeId === "string" ? b.challengeId : "",
|
|
127
|
+
response: b.response as never,
|
|
128
|
+
name: typeof b.name === "string" ? b.name : undefined,
|
|
129
|
+
});
|
|
130
|
+
if (!res.ok) return errorResponse(res.error);
|
|
131
|
+
return json({ passkey: res.data });
|
|
132
|
+
} catch (e) {
|
|
133
|
+
return serverError(e);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
"/api/auth/passkeys/:id": {
|
|
139
|
+
PATCH: async (req: BunRequest<"/api/auth/passkeys/:id">) => {
|
|
140
|
+
try {
|
|
141
|
+
const b = await body(req);
|
|
142
|
+
if (!b) return json({ error: "INVALID_JSON" }, 400);
|
|
143
|
+
const res = passkeys.rename(req.params.id, typeof b.name === "string" ? b.name : "");
|
|
144
|
+
if (!res.ok) return errorResponse(res.error);
|
|
145
|
+
return json({ passkey: res.data });
|
|
146
|
+
} catch (e) {
|
|
147
|
+
return serverError(e);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
DELETE: (req: BunRequest<"/api/auth/passkeys/:id">) => {
|
|
151
|
+
try {
|
|
152
|
+
const res = passkeys.remove(req.params.id);
|
|
153
|
+
if (!res.ok) return errorResponse(res.error);
|
|
154
|
+
return json({ ok: true });
|
|
155
|
+
} catch (e) {
|
|
156
|
+
return serverError(e);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabel rute API Project & Folder_Browser (Requirement 10).
|
|
3
|
+
*
|
|
4
|
+
* `/api/projects` (GET daftar / POST buat), `/api/projects/:id` (PATCH ubah
|
|
5
|
+
* custom instruction, DELETE hapus
|
|
6
|
+
* pendaftaran Project beserta seluruh Session-nya), `/api/fs` (GET list
|
|
7
|
+
* direktori untuk Folder_Browser), dan `/api/projects/:id/models` (GET model
|
|
8
|
+
* yang tersedia pada server headless Project). Dipasang composition root
|
|
9
|
+
* (`app.ts`) bersama tabel rute lain — pola kcgcode (`*.routes.ts`).
|
|
10
|
+
*/
|
|
11
|
+
import type { BunRequest } from "bun";
|
|
12
|
+
import type { AgentType } from "../../types";
|
|
13
|
+
import { errorStatus, json, readJson, serverError } from "./helpers";
|
|
14
|
+
import type { ApiRouteContext } from "./types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Backend yang ditanyakan lewat `?agent=` (default opencode) — daftar model &
|
|
18
|
+
* agent berbeda antara opencode dan Claude Code.
|
|
19
|
+
*/
|
|
20
|
+
function agentTypeParam(req: Request): AgentType {
|
|
21
|
+
const agent = new URL(req.url).searchParams.get("agent");
|
|
22
|
+
return agent === "claude-code" ? "claude-code" : "opencode";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function projectsRoutes(ctx: ApiRouteContext) {
|
|
26
|
+
const { projectManager, sessionManager, notifyDataChanged } = ctx;
|
|
27
|
+
return {
|
|
28
|
+
// ---- Project & Folder_Browser (Requirement 10) ----
|
|
29
|
+
"/api/projects": {
|
|
30
|
+
GET: () => {
|
|
31
|
+
try {
|
|
32
|
+
return json({ projects: projectManager.listProjects() });
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return serverError(e);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
POST: async (req: Request) => {
|
|
38
|
+
try {
|
|
39
|
+
const body = await readJson(req);
|
|
40
|
+
if (!body.ok) return json({ error: "INVALID_JSON" }, 400);
|
|
41
|
+
const { name, path } = body.data as { name?: unknown; path?: unknown };
|
|
42
|
+
const res = projectManager.createProject(
|
|
43
|
+
typeof name === "string" ? name : "",
|
|
44
|
+
typeof path === "string" ? path : "",
|
|
45
|
+
);
|
|
46
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
47
|
+
notifyDataChanged();
|
|
48
|
+
return json({ project: res.data }, 201);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return serverError(e);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
"/api/projects/:id": {
|
|
56
|
+
/**
|
|
57
|
+
* Perbarui Project. Saat ini hanya `instructions` (custom instruction
|
|
58
|
+
* yang dikirim sebagai `system` di setiap prompt). String kosong =
|
|
59
|
+
* hapus instruksi.
|
|
60
|
+
*/
|
|
61
|
+
PATCH: async (req: BunRequest<"/api/projects/:id">) => {
|
|
62
|
+
try {
|
|
63
|
+
const body = await readJson(req);
|
|
64
|
+
if (!body.ok) return json({ error: "INVALID_JSON" }, 400);
|
|
65
|
+
const { instructions } = (body.data ?? {}) as { instructions?: unknown };
|
|
66
|
+
if (instructions !== null && typeof instructions !== "string") {
|
|
67
|
+
return json({ error: "INVALID_INSTRUCTIONS" }, 400);
|
|
68
|
+
}
|
|
69
|
+
const res = projectManager.updateInstructions(req.params.id, instructions ?? "");
|
|
70
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
71
|
+
return json({ project: res.data });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return serverError(e);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
/**
|
|
77
|
+
* Hapus pendaftaran Project. Seluruh Session miliknya dihapus lebih dulu
|
|
78
|
+
* (`releaseProject`) sehingga sesi remote opencode dan lampiran gambar
|
|
79
|
+
* ikut bersih, lalu server headless Project dihentikan. Direktori kerja
|
|
80
|
+
* di filesystem TIDAK disentuh — Project hanyalah referensi ke folder.
|
|
81
|
+
*
|
|
82
|
+
* Kegagalan pembersihan Session -> error diteruskan, baris Project tetap
|
|
83
|
+
* ada supaya operasi bisa dicoba ulang tanpa meninggalkan sesi yatim.
|
|
84
|
+
*/
|
|
85
|
+
DELETE: async (req: BunRequest<"/api/projects/:id">) => {
|
|
86
|
+
try {
|
|
87
|
+
const released = await sessionManager.releaseProject(req.params.id);
|
|
88
|
+
if (!released.ok) {
|
|
89
|
+
return json({ error: released.error ?? "ERROR" }, errorStatus(released.error ?? ""));
|
|
90
|
+
}
|
|
91
|
+
const res = projectManager.deleteProject(req.params.id);
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
return json({ error: res.error ?? "ERROR" }, errorStatus(res.error ?? ""));
|
|
94
|
+
}
|
|
95
|
+
notifyDataChanged();
|
|
96
|
+
return json({ ok: true });
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return serverError(e);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
"/api/fs": {
|
|
104
|
+
GET: (req: Request) => {
|
|
105
|
+
try {
|
|
106
|
+
const url = new URL(req.url);
|
|
107
|
+
const res = projectManager.listDirectory(url.searchParams.get("path") ?? "");
|
|
108
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
109
|
+
return json({ entries: res.data.entries });
|
|
110
|
+
} catch (e) {
|
|
111
|
+
return serverError(e);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// ---- Daftar model yang tersedia pada server headless Project ----
|
|
117
|
+
"/api/projects/:id/models": {
|
|
118
|
+
GET: async (req: BunRequest<"/api/projects/:id/models">) => {
|
|
119
|
+
try {
|
|
120
|
+
const res = await sessionManager.listModels(req.params.id, agentTypeParam(req));
|
|
121
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
122
|
+
return json({ models: res.data });
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return serverError(e);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// ---- MCP server & skill Project (panel samping halaman Project) ----
|
|
130
|
+
"/api/projects/:id/mcp": {
|
|
131
|
+
GET: async (req: BunRequest<"/api/projects/:id/mcp">) => {
|
|
132
|
+
try {
|
|
133
|
+
const res = await sessionManager.listMcp(req.params.id, agentTypeParam(req));
|
|
134
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
135
|
+
return json({ mcp: res.data });
|
|
136
|
+
} catch (e) {
|
|
137
|
+
return serverError(e);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
"/api/projects/:id/skills": {
|
|
143
|
+
GET: async (req: BunRequest<"/api/projects/:id/skills">) => {
|
|
144
|
+
try {
|
|
145
|
+
const res = await sessionManager.listSkills(req.params.id, agentTypeParam(req));
|
|
146
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
147
|
+
return json({ skills: res.data });
|
|
148
|
+
} catch (e) {
|
|
149
|
+
return serverError(e);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// ---- Agent (mode) opencode: build/plan + agent kustom user ----
|
|
155
|
+
"/api/projects/:id/agents": {
|
|
156
|
+
GET: async (req: BunRequest<"/api/projects/:id/agents">) => {
|
|
157
|
+
try {
|
|
158
|
+
const res = await sessionManager.listAgents(req.params.id, agentTypeParam(req));
|
|
159
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
160
|
+
return json({ agents: res.data });
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return serverError(e);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabel rute API Session (Requirement 1, 4): daftar/buat, hapus/resume/ganti
|
|
3
|
+
* model, stop, cari file (`@file`), dan upload lampiran gambar. Dipasang
|
|
4
|
+
* composition root (`app.ts`) — pola kcgcode (`*.routes.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import type { BunRequest } from "bun";
|
|
7
|
+
import type { AgentType } from "../../types";
|
|
8
|
+
import { errorStatus, json, parseModelBody, readJson, serverError } from "./helpers";
|
|
9
|
+
import type { ApiRouteContext } from "./types";
|
|
10
|
+
|
|
11
|
+
export function sessionsRoutes(ctx: ApiRouteContext) {
|
|
12
|
+
const { sessionManager, attachments, notifyDataChanged } = ctx;
|
|
13
|
+
return {
|
|
14
|
+
// ---- Session (Requirement 1, 4) ----
|
|
15
|
+
"/api/sessions": {
|
|
16
|
+
GET: () => {
|
|
17
|
+
try {
|
|
18
|
+
return json({ sessions: sessionManager.listSessions() });
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return serverError(e);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
POST: async (req: Request) => {
|
|
24
|
+
try {
|
|
25
|
+
const body = await readJson(req);
|
|
26
|
+
if (!body.ok) return json({ error: "INVALID_JSON" }, 400);
|
|
27
|
+
const { agentType, projectId, model, agent } = body.data as {
|
|
28
|
+
agentType?: unknown;
|
|
29
|
+
projectId?: unknown;
|
|
30
|
+
model?: unknown;
|
|
31
|
+
agent?: unknown;
|
|
32
|
+
};
|
|
33
|
+
const res = await sessionManager.createSession({
|
|
34
|
+
agentType: (typeof agentType === "string" ? agentType : "") as AgentType,
|
|
35
|
+
projectId: typeof projectId === "string" ? projectId : "",
|
|
36
|
+
model: parseModelBody(model),
|
|
37
|
+
agent: typeof agent === "string" && agent.trim() !== "" ? agent.trim() : null,
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
40
|
+
notifyDataChanged();
|
|
41
|
+
return json({ session: res.session }, 201);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return serverError(e);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
"/api/sessions/:id": {
|
|
49
|
+
/**
|
|
50
|
+
* Hapus Session permanen — juga menghapus session (dan riwayat
|
|
51
|
+
* pesannya) di server headless opencode: server di-spawn ulang bila
|
|
52
|
+
* mati agar data remote tidak tertinggal. Server tak bisa hidup /
|
|
53
|
+
* menolak hapus -> 5xx, data lokal utuh.
|
|
54
|
+
*/
|
|
55
|
+
DELETE: async (req: BunRequest<"/api/sessions/:id">) => {
|
|
56
|
+
try {
|
|
57
|
+
const res = await sessionManager.deleteSession(req.params.id);
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
return json({ error: res.error ?? "ERROR" }, errorStatus(res.error ?? ""));
|
|
60
|
+
}
|
|
61
|
+
notifyDataChanged();
|
|
62
|
+
return json({ ok: true });
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return serverError(e);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
/**
|
|
68
|
+
* Resume Session yang stopped/crashed (tombol "Start" di UI).
|
|
69
|
+
* Memakai ocSessionId lama bila masih dikenal server headless;
|
|
70
|
+
* bila tidak, sesi remote baru dibuat & disimpan ke Session.
|
|
71
|
+
*/
|
|
72
|
+
POST: async (req: BunRequest<"/api/sessions/:id">) => {
|
|
73
|
+
try {
|
|
74
|
+
const res = await sessionManager.resumeSession(req.params.id);
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
return json({ error: res.error ?? "ERROR" }, errorStatus(res.error ?? ""));
|
|
77
|
+
}
|
|
78
|
+
const cur = sessionManager.getSession(req.params.id);
|
|
79
|
+
return json({ session: cur.ok ? cur.data : undefined, ok: true });
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return serverError(e);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* Ganti model dan/atau agent (mode) pilihan Session — body
|
|
86
|
+
* `{ model: {providerID, modelID} | null, agent: string | null }`.
|
|
87
|
+
* `null` mengembalikan ke default opencode. Berlaku pada prompt
|
|
88
|
+
* berikutnya tanpa perlu restart Session.
|
|
89
|
+
*/
|
|
90
|
+
PUT: async (req: BunRequest<"/api/sessions/:id">) => {
|
|
91
|
+
try {
|
|
92
|
+
const body = await readJson(req);
|
|
93
|
+
if (!body.ok) return json({ error: "INVALID_JSON" }, 400);
|
|
94
|
+
const { model: rawModel, agent: rawAgent } = body.data as {
|
|
95
|
+
model?: unknown;
|
|
96
|
+
agent?: unknown;
|
|
97
|
+
};
|
|
98
|
+
if (rawModel !== undefined) {
|
|
99
|
+
const model = rawModel === null ? null : parseModelBody(rawModel);
|
|
100
|
+
const res = sessionManager.setSessionModel(req.params.id, model);
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
return json({ error: res.error ?? "ERROR" }, errorStatus(res.error ?? ""));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (rawAgent !== undefined) {
|
|
106
|
+
const agent =
|
|
107
|
+
typeof rawAgent === "string" && rawAgent.trim() !== "" ? rawAgent.trim() : null;
|
|
108
|
+
const res = sessionManager.setSessionAgent(req.params.id, agent);
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
return json({ error: res.error ?? "ERROR" }, errorStatus(res.error ?? ""));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const cur = sessionManager.getSession(req.params.id);
|
|
114
|
+
return json({ session: cur.ok ? cur.data : undefined, ok: true });
|
|
115
|
+
} catch (e) {
|
|
116
|
+
return serverError(e);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// ---- Cari file Project (autocomplete referensi @file di composer) ----
|
|
122
|
+
"/api/sessions/:id/files": {
|
|
123
|
+
GET: async (req: BunRequest<"/api/sessions/:id/files">) => {
|
|
124
|
+
try {
|
|
125
|
+
const url = new URL(req.url);
|
|
126
|
+
const query = (url.searchParams.get("q") ?? "").slice(0, 200);
|
|
127
|
+
const cur = sessionManager.getSession(req.params.id);
|
|
128
|
+
if (!cur.ok)
|
|
129
|
+
return json({ error: "SESSION_NOT_FOUND" }, errorStatus("SESSION_NOT_FOUND"));
|
|
130
|
+
const res = await sessionManager.findFiles(cur.data.projectId, query, cur.data.agentType);
|
|
131
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
132
|
+
return json({ files: res.data });
|
|
133
|
+
} catch (e) {
|
|
134
|
+
return serverError(e);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// ---- Konteks kerja Session (panel kanan: Environment / Task list) ----
|
|
140
|
+
"/api/sessions/:id/context": {
|
|
141
|
+
GET: async (req: BunRequest<"/api/sessions/:id/context">) => {
|
|
142
|
+
try {
|
|
143
|
+
const res = await sessionManager.sessionContext(req.params.id);
|
|
144
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
145
|
+
return json({ context: res.data });
|
|
146
|
+
} catch (e) {
|
|
147
|
+
return serverError(e);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// ---- Lampiran gambar upload dari perangkat ----
|
|
153
|
+
"/api/sessions/:id/uploads": {
|
|
154
|
+
/**
|
|
155
|
+
* Simpan gambar upload ke Attachment_Store Session. Body multipart
|
|
156
|
+
* `file` (nama+mime+bytes). Ukuran & format divalidasi server.
|
|
157
|
+
*/
|
|
158
|
+
POST: async (req: BunRequest<"/api/sessions/:id/uploads">) => {
|
|
159
|
+
try {
|
|
160
|
+
const cur = sessionManager.getSession(req.params.id);
|
|
161
|
+
if (!cur.ok) return json({ error: "SESSION_NOT_FOUND" }, 404);
|
|
162
|
+
let form: FormData;
|
|
163
|
+
try {
|
|
164
|
+
form = await req.formData();
|
|
165
|
+
} catch {
|
|
166
|
+
return json({ error: "INVALID_UPLOAD" }, 400);
|
|
167
|
+
}
|
|
168
|
+
const file = form.get("file");
|
|
169
|
+
if (!(file instanceof File)) {
|
|
170
|
+
return json({ error: "INVALID_UPLOAD" }, 400);
|
|
171
|
+
}
|
|
172
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
173
|
+
const res = attachments.save(req.params.id, file.name, file.type, bytes);
|
|
174
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
175
|
+
return json({ upload: res.data }, 201);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
return serverError(e);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// ---- Stop Session (abort turn + status stopped, data tetap ada) ----
|
|
183
|
+
"/api/sessions/:id/stop": {
|
|
184
|
+
POST: (req: BunRequest<"/api/sessions/:id/stop">) => {
|
|
185
|
+
try {
|
|
186
|
+
const res = sessionManager.stopSession(req.params.id);
|
|
187
|
+
if (!res.ok) {
|
|
188
|
+
return json({ error: res.error ?? "ERROR" }, errorStatus(res.error ?? ""));
|
|
189
|
+
}
|
|
190
|
+
return json({ ok: true });
|
|
191
|
+
} catch (e) {
|
|
192
|
+
return serverError(e);
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabel rute API katalog & instalasi skill (skills.sh).
|
|
3
|
+
*
|
|
4
|
+
* - `GET /api/skills/search?q=` — cari skill di skills.sh (proxy + cache).
|
|
5
|
+
* - `GET /api/skills/audit?source=&skill=` — hasil audit keamanan skill.
|
|
6
|
+
* - `POST /api/projects/:id/skills/install` `{ source, skill, agents? }` —
|
|
7
|
+
* mulai job instalasi (202 + job) untuk satu atau beberapa agent
|
|
8
|
+
* (`opencode` default, `claude-code`). CLI `skills` berjalan di latar.
|
|
9
|
+
* - `GET /api/skills/installs` — job berjalan & yang baru selesai.
|
|
10
|
+
* - `GET /api/skills/installs/:jobId?from=<n>` — status job + baris log
|
|
11
|
+
* mulai indeks `n` (polling bertahap untuk output live).
|
|
12
|
+
* - `POST /api/skills/installs/:jobId/cancel` — batalkan job berjalan.
|
|
13
|
+
*
|
|
14
|
+
* Daftar skill terpasang tetap lewat `GET /api/projects/:id/skills?agent=`
|
|
15
|
+
* di `projects.routes.ts`.
|
|
16
|
+
*/
|
|
17
|
+
import type { BunRequest } from "bun";
|
|
18
|
+
import type { AgentType } from "../../types";
|
|
19
|
+
import { errorStatus, json, readJson, serverError } from "./helpers";
|
|
20
|
+
import type { ApiRouteContext } from "./types";
|
|
21
|
+
|
|
22
|
+
const INSTALL_AGENTS: readonly AgentType[] = ["opencode", "claude-code"];
|
|
23
|
+
|
|
24
|
+
/** `agents` body -> daftar unik terurut; `null` bila tidak valid / kosong. */
|
|
25
|
+
function parseInstallAgents(raw: unknown): AgentType[] | null {
|
|
26
|
+
if (raw === undefined) return ["opencode"];
|
|
27
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
28
|
+
if (!raw.every((a) => INSTALL_AGENTS.includes(a as AgentType))) return null;
|
|
29
|
+
return INSTALL_AGENTS.filter((a) => raw.includes(a));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function skillsRoutes(ctx: ApiRouteContext) {
|
|
33
|
+
const { projectManager, skillsRegistry, skillInstalls } = ctx;
|
|
34
|
+
return {
|
|
35
|
+
"/api/skills/search": {
|
|
36
|
+
GET: async (req: Request) => {
|
|
37
|
+
try {
|
|
38
|
+
const q = new URL(req.url).searchParams.get("q") ?? "";
|
|
39
|
+
const res = await skillsRegistry.search(q);
|
|
40
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
41
|
+
return json({ skills: res.data });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return serverError(e);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
"/api/skills/audit": {
|
|
49
|
+
GET: async (req: Request) => {
|
|
50
|
+
try {
|
|
51
|
+
const params = new URL(req.url).searchParams;
|
|
52
|
+
const res = await skillsRegistry.audit(
|
|
53
|
+
params.get("source") ?? "",
|
|
54
|
+
params.get("skill") ?? "",
|
|
55
|
+
);
|
|
56
|
+
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
57
|
+
return json({ audit: res.data });
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return serverError(e);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
"/api/projects/:id/skills/install": {
|
|
65
|
+
POST: async (req: BunRequest<"/api/projects/:id/skills/install">) => {
|
|
66
|
+
try {
|
|
67
|
+
const body = await readJson(req);
|
|
68
|
+
if (!body.ok) return json({ error: "INVALID_JSON" }, 400);
|
|
69
|
+
const { source, skill, agents } = (body.data ?? {}) as {
|
|
70
|
+
source?: unknown;
|
|
71
|
+
skill?: unknown;
|
|
72
|
+
agents?: unknown;
|
|
73
|
+
};
|
|
74
|
+
if (typeof source !== "string" || typeof skill !== "string") {
|
|
75
|
+
return json({ error: "INVALID_SKILL_ID" }, 400);
|
|
76
|
+
}
|
|
77
|
+
const agentTypes = parseInstallAgents(agents);
|
|
78
|
+
if (agentTypes === null) return json({ error: "UNSUPPORTED_AGENT_TYPE" }, 400);
|
|
79
|
+
const project = projectManager.getProject(req.params.id);
|
|
80
|
+
if (!project.ok) return json({ error: project.error }, errorStatus(project.error));
|
|
81
|
+
|
|
82
|
+
const job = skillInstalls.start({
|
|
83
|
+
projectId: project.data.id,
|
|
84
|
+
projectName: project.data.name,
|
|
85
|
+
projectPath: project.data.path,
|
|
86
|
+
source,
|
|
87
|
+
skillId: skill,
|
|
88
|
+
agentTypes,
|
|
89
|
+
});
|
|
90
|
+
if (!job.ok) return json({ error: job.error }, errorStatus(job.error));
|
|
91
|
+
return json({ job: job.data }, 202);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return serverError(e);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
"/api/skills/installs": {
|
|
99
|
+
GET: () => {
|
|
100
|
+
try {
|
|
101
|
+
return json({ jobs: skillInstalls.list() });
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return serverError(e);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
"/api/skills/installs/:jobId": {
|
|
109
|
+
GET: (req: BunRequest<"/api/skills/installs/:jobId">) => {
|
|
110
|
+
try {
|
|
111
|
+
const from = Number(new URL(req.url).searchParams.get("from") ?? "0");
|
|
112
|
+
const log = skillInstalls.get(req.params.jobId, Number.isFinite(from) ? from : 0);
|
|
113
|
+
if (!log) return json({ error: "INSTALL_JOB_NOT_FOUND" }, 404);
|
|
114
|
+
return json(log);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
return serverError(e);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
"/api/skills/installs/:jobId/cancel": {
|
|
122
|
+
POST: (req: BunRequest<"/api/skills/installs/:jobId/cancel">) => {
|
|
123
|
+
try {
|
|
124
|
+
const log = skillInstalls.get(req.params.jobId);
|
|
125
|
+
if (!log) return json({ error: "INSTALL_JOB_NOT_FOUND" }, 404);
|
|
126
|
+
if (!skillInstalls.cancel(req.params.jobId)) {
|
|
127
|
+
return json({ error: "INSTALL_JOB_NOT_RUNNING" }, 409);
|
|
128
|
+
}
|
|
129
|
+
return json({ ok: true }, 202);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
return serverError(e);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|