kcg-code 0.1.0 → 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 +2 -0
- package/package.json +2 -1
- package/src/cli/__tests__/daemon.test.ts +84 -0
- package/src/cli/__tests__/dashboard.test.ts +170 -8
- package/src/cli/__tests__/keep-awake.test.ts +44 -0
- package/src/cli/__tests__/port.test.ts +86 -0
- package/src/cli/args.ts +69 -6
- package/src/cli/attach.ts +154 -0
- package/src/cli/commands.ts +174 -21
- package/src/cli/daemon.ts +275 -0
- package/src/cli/dashboard.tsx +876 -159
- package/src/cli/index.ts +10 -0
- package/src/cli/keep-awake.ts +148 -0
- package/src/cli/port.ts +92 -0
- package/src/cli/theme.ts +6 -6
- package/src/components/layout/AppShell.tsx +8 -3
- package/src/components/projects/HomeChat.tsx +85 -48
- package/src/components/projects/HomeRecentSessions.tsx +90 -0
- package/src/components/projects/ProjectInstructions.tsx +27 -38
- package/src/components/projects/ProjectSidePanel.tsx +382 -316
- package/src/components/sessions/AgentLogo.tsx +42 -0
- package/src/components/sessions/AgentPicker.tsx +13 -4
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +27 -7
- package/src/components/sessions/ContextPanel.tsx +159 -59
- package/src/components/sessions/ModelPicker.tsx +65 -27
- package/src/components/sessions/ModelSearchList.tsx +137 -57
- package/src/components/sessions/NewSessionDialog.tsx +100 -117
- package/src/components/sessions/SessionComposer.tsx +2 -1
- package/src/components/sessions/SessionHeader.tsx +1 -0
- package/src/components/sessions/SessionList.tsx +173 -66
- package/src/components/sessions/SessionRow.tsx +176 -76
- package/src/components/sessions/SessionView.tsx +17 -13
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +36 -0
- package/src/components/settings/LhrRemoteAccess.tsx +20 -25
- package/src/components/settings/RemoteAccessGroup.tsx +242 -267
- package/src/components/settings/prefs.tsx +69 -1
- package/src/components/skills/InstallNotifications.tsx +4 -3
- package/src/components/skills/InstallSkillDialog.tsx +256 -99
- package/src/components/skills/SkillCards.tsx +52 -8
- package/src/components/ui/adw-dialog.tsx +114 -0
- package/src/components/ui/adw-toggle-group.tsx +75 -0
- package/src/components/ui/search-entry.tsx +70 -0
- package/src/config.ts +32 -3
- package/src/hooks/useAgentPicker.ts +6 -2
- package/src/hooks/useSessionList.ts +95 -2
- package/src/lib/__tests__/project-extensions.test.ts +32 -1
- package/src/lib/__tests__/project-overview.test.ts +15 -0
- package/src/lib/__tests__/routes.test.ts +2 -2
- package/src/lib/__tests__/session-selection.test.ts +41 -0
- package/src/lib/__tests__/skill-installs.test.ts +1 -0
- package/src/lib/__tests__/skills-catalog.test.ts +36 -8
- package/src/lib/agent-types.ts +49 -0
- package/src/lib/project-extensions.ts +66 -4
- package/src/lib/project-overview.ts +14 -8
- package/src/lib/routes.ts +13 -4
- package/src/lib/session-selection.ts +40 -0
- package/src/lib/skill-installs.ts +3 -1
- package/src/lib/skills-catalog.ts +56 -17
- package/src/pages/projects/ProjectDetailPage.tsx +13 -12
- package/src/pages/settings/SettingsPage.tsx +212 -145
- package/src/pages/settings/__tests__/SettingsPage.test.ts +11 -4
- package/src/pages/skills/SkillsPage.tsx +89 -45
- package/src/server/__tests__/app.test.ts +15 -2
- package/src/server/app.ts +13 -1
- package/src/server/routes/projects.routes.ts +14 -4
- package/src/server/routes/sessions.routes.ts +1 -1
- package/src/server/routes/skills.routes.ts +24 -5
- 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__/session-manager.test.ts +40 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +22 -3
- package/src/server/services/__tests__/skills-registry.test.ts +6 -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/session-manager.ts +117 -52
- package/src/server/services/skill-install-jobs.ts +21 -8
- package/src/server/services/skills-registry.ts +18 -6
- package/src/types/message.ts +1 -1
- package/src/components/projects/PanelSection.tsx +0 -102
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* - Header: judul + pemilih Project (pil "Install to"). Project terpilih =
|
|
5
5
|
* `?project=` (dari sidebar) > pilihan terakhir (localStorage) > satu-
|
|
6
6
|
* satunya Project. Pilihan memperbarui URL (`replaceState`).
|
|
7
|
+
* - Skill terpasang dibaca dari KEDUA agent (OpenCode & Claude Code) lalu
|
|
8
|
+
* digabung (`mergeInstalledSkills`); tiap kartu menampilkan logo agent
|
|
9
|
+
* tempat skill itu terpasang. Agent tujuan dipilih di dialog Install.
|
|
7
10
|
* - Dua tab memakai kolom cari yang sama, tapi query disimpan per tab:
|
|
8
11
|
* - Browse: hasil skills.sh (debounce, min. 2 karakter); saat kosong
|
|
9
12
|
* tampil chip saran kata kunci.
|
|
@@ -37,6 +40,7 @@ import {
|
|
|
37
40
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
38
41
|
import { Spinner } from "@/components/ui/spinner";
|
|
39
42
|
import { useRouter } from "@/hooks/useRouter";
|
|
43
|
+
import { AGENT_TYPE_LABELS, AGENT_TYPES, agentQuery } from "@/lib/agent-types";
|
|
40
44
|
import { ApiError, apiFetch } from "@/lib/api";
|
|
41
45
|
import { parseSkillsProject, projectsPath, skillsPath } from "@/lib/routes";
|
|
42
46
|
import {
|
|
@@ -47,9 +51,10 @@ import {
|
|
|
47
51
|
useInstalls,
|
|
48
52
|
} from "@/lib/skill-installs";
|
|
49
53
|
import {
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
type InstalledSkill,
|
|
55
|
+
installedAgentsFor,
|
|
52
56
|
LAST_SKILLS_PROJECT_KEY,
|
|
57
|
+
mergeInstalledSkills,
|
|
53
58
|
resolveSkillsProject,
|
|
54
59
|
SEARCH_SUGGESTIONS,
|
|
55
60
|
visibleInstalledSkills,
|
|
@@ -57,7 +62,9 @@ import {
|
|
|
57
62
|
import { cn } from "@/lib/utils";
|
|
58
63
|
import type { SkillInfo } from "@/server/services/opencode-client";
|
|
59
64
|
import type { RegistrySkill } from "@/server/services/skills-registry";
|
|
60
|
-
import type { Project } from "@/types";
|
|
65
|
+
import type { AgentType, Project } from "@/types";
|
|
66
|
+
|
|
67
|
+
const EMPTY_INSTALLED: InstalledSkill[] = [];
|
|
61
68
|
|
|
62
69
|
/** Jeda ketik sebelum memanggil skills.sh. */
|
|
63
70
|
const SEARCH_DEBOUNCE_MS = 350;
|
|
@@ -219,7 +226,9 @@ function SkillsWorkspace({ project }: SkillsWorkspaceProps) {
|
|
|
219
226
|
const [queries, setQueries] = useState<Record<Tab, string>>({ browse: "", installed: "" });
|
|
220
227
|
const query = queries[tab];
|
|
221
228
|
const setQuery = useCallback((q: string) => setQueries((prev) => ({ ...prev, [tab]: q })), [tab]);
|
|
222
|
-
const [installed, setInstalled] = useState<Load<
|
|
229
|
+
const [installed, setInstalled] = useState<Load<InstalledSkill[]>>({ phase: "idle" });
|
|
230
|
+
/** Sebagian agent gagal dibaca (daftar tetap tampil dari agent lain). */
|
|
231
|
+
const [installedWarning, setInstalledWarning] = useState<string | null>(null);
|
|
223
232
|
const [reloadKey, setReloadKey] = useState(0);
|
|
224
233
|
const [results, setResults] = useState<Load<RegistrySkill[]>>({ phase: "idle" });
|
|
225
234
|
/** Permintaan pencarian sedang berjalan (hasil lama tetap tampil). */
|
|
@@ -244,22 +253,41 @@ function SkillsWorkspace({ project }: SkillsWorkspaceProps) {
|
|
|
244
253
|
return () => window.removeEventListener("keydown", onKey);
|
|
245
254
|
}, []);
|
|
246
255
|
|
|
247
|
-
// Skill terpasang (
|
|
256
|
+
// Skill terpasang dari semua agent (opencode: server headless Project;
|
|
257
|
+
// Claude Code: `.claude/skills` Project + global), dibaca bersamaan lalu
|
|
258
|
+
// digabung. Satu agent gagal tidak menggagalkan yang lain.
|
|
248
259
|
useEffect(() => {
|
|
249
260
|
void reloadKey;
|
|
250
261
|
if (!project) return;
|
|
251
262
|
let cancelled = false;
|
|
252
263
|
setInstalled((prev) => (prev.phase === "ready" ? prev : { phase: "loading" }));
|
|
253
|
-
|
|
254
|
-
.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
264
|
+
void Promise.allSettled(
|
|
265
|
+
AGENT_TYPES.map((a) =>
|
|
266
|
+
apiFetch(`/api/projects/${project.id}/skills${agentQuery(a)}`)
|
|
267
|
+
.then((res) => res.json() as Promise<{ skills: SkillInfo[] }>)
|
|
268
|
+
.then(({ skills }) => skills),
|
|
269
|
+
),
|
|
270
|
+
).then((results) => {
|
|
271
|
+
if (cancelled) return;
|
|
272
|
+
const byAgent: Partial<Record<AgentType, SkillInfo[]>> = {};
|
|
273
|
+
const failed: { agent: AgentType; message: string }[] = [];
|
|
274
|
+
results.forEach((r, i) => {
|
|
275
|
+
const agent = AGENT_TYPES[i] as AgentType;
|
|
276
|
+
if (r.status === "fulfilled") byAgent[agent] = r.value;
|
|
277
|
+
else failed.push({ agent, message: errorMessage(r.reason, "Failed to load skills") });
|
|
262
278
|
});
|
|
279
|
+
if (failed.length === AGENT_TYPES.length) {
|
|
280
|
+
setInstalled({ phase: "error", message: failed[0]?.message ?? "Failed to load skills" });
|
|
281
|
+
setInstalledWarning(null);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
setInstalled({ phase: "ready", data: mergeInstalledSkills(byAgent) });
|
|
285
|
+
setInstalledWarning(
|
|
286
|
+
failed.length > 0
|
|
287
|
+
? `Couldn't read ${failed.map((f) => AGENT_TYPE_LABELS[f.agent]).join(", ")} skills: ${failed[0]?.message}`
|
|
288
|
+
: null,
|
|
289
|
+
);
|
|
290
|
+
});
|
|
263
291
|
return () => {
|
|
264
292
|
cancelled = true;
|
|
265
293
|
};
|
|
@@ -298,11 +326,7 @@ function SkillsWorkspace({ project }: SkillsWorkspaceProps) {
|
|
|
298
326
|
};
|
|
299
327
|
}, [browseQuery, canSearch]);
|
|
300
328
|
|
|
301
|
-
const
|
|
302
|
-
() =>
|
|
303
|
-
installed.phase === "ready" ? installedProjectSkillNames(installed.data) : new Set<string>(),
|
|
304
|
-
[installed],
|
|
305
|
-
);
|
|
329
|
+
const installedList = installed.phase === "ready" ? installed.data : EMPTY_INSTALLED;
|
|
306
330
|
const installedAll = useMemo(
|
|
307
331
|
() => (installed.phase === "ready" ? visibleInstalledSkills(installed.data, "") : []),
|
|
308
332
|
[installed],
|
|
@@ -325,12 +349,12 @@ function SkillsWorkspace({ project }: SkillsWorkspaceProps) {
|
|
|
325
349
|
}, [project]);
|
|
326
350
|
|
|
327
351
|
/** Mulai job instalasi; progres & output tampil di notifikasi global. */
|
|
328
|
-
const install = async (skill: RegistrySkill) => {
|
|
352
|
+
const install = async (skill: RegistrySkill, agents: AgentType[]) => {
|
|
329
353
|
if (!project) return;
|
|
330
354
|
setStarting(true);
|
|
331
355
|
setStartError(null);
|
|
332
356
|
try {
|
|
333
|
-
await startInstall(project.id, skill.source, skill.skillId);
|
|
357
|
+
await startInstall(project.id, skill.source, skill.skillId, agents);
|
|
334
358
|
setPending(null);
|
|
335
359
|
} catch (e) {
|
|
336
360
|
// Dialog tetap terbuka agar user melihat alasannya.
|
|
@@ -440,7 +464,7 @@ function SkillsWorkspace({ project }: SkillsWorkspaceProps) {
|
|
|
440
464
|
query={trimmed}
|
|
441
465
|
canSearch={canSearch}
|
|
442
466
|
results={results}
|
|
443
|
-
|
|
467
|
+
installedAgents={(skill) => installedAgentsFor(skill, installedList)}
|
|
444
468
|
isInstalling={(skill) =>
|
|
445
469
|
findRunningInstall(installs, project.id, skill.source, skill.skillId)?.job.id ??
|
|
446
470
|
null
|
|
@@ -454,6 +478,7 @@ function SkillsWorkspace({ project }: SkillsWorkspaceProps) {
|
|
|
454
478
|
) : (
|
|
455
479
|
<InstalledPanel
|
|
456
480
|
state={installed}
|
|
481
|
+
warning={installedWarning}
|
|
457
482
|
skills={installedFiltered}
|
|
458
483
|
total={installedAll.length}
|
|
459
484
|
query={trimmed}
|
|
@@ -468,6 +493,7 @@ function SkillsWorkspace({ project }: SkillsWorkspaceProps) {
|
|
|
468
493
|
<InstallSkillDialog
|
|
469
494
|
skill={pending}
|
|
470
495
|
projectName={project.name}
|
|
496
|
+
installedAgents={pending ? installedAgentsFor(pending, installedList) : []}
|
|
471
497
|
starting={starting}
|
|
472
498
|
startError={startError}
|
|
473
499
|
onOpenChange={(open) => {
|
|
@@ -476,7 +502,7 @@ function SkillsWorkspace({ project }: SkillsWorkspaceProps) {
|
|
|
476
502
|
setStartError(null);
|
|
477
503
|
}
|
|
478
504
|
}}
|
|
479
|
-
onConfirm={(s) => void install(s)}
|
|
505
|
+
onConfirm={(s, agents) => void install(s, agents)}
|
|
480
506
|
/>
|
|
481
507
|
</>
|
|
482
508
|
)}
|
|
@@ -531,7 +557,7 @@ function BrowsePanel({
|
|
|
531
557
|
query,
|
|
532
558
|
canSearch,
|
|
533
559
|
results,
|
|
534
|
-
|
|
560
|
+
installedAgents,
|
|
535
561
|
isInstalling,
|
|
536
562
|
onSuggest,
|
|
537
563
|
onInstall,
|
|
@@ -539,7 +565,8 @@ function BrowsePanel({
|
|
|
539
565
|
query: string;
|
|
540
566
|
canSearch: boolean;
|
|
541
567
|
results: Load<RegistrySkill[]>;
|
|
542
|
-
|
|
568
|
+
/** Agent yang sudah memuat skill katalog ini dari folder Project. */
|
|
569
|
+
installedAgents: (skill: RegistrySkill) => AgentType[];
|
|
543
570
|
/** Id job instalasi berjalan untuk skill ini (null bila tidak ada). */
|
|
544
571
|
isInstalling: (skill: RegistrySkill) => string | null;
|
|
545
572
|
onSuggest: (q: string) => void;
|
|
@@ -615,7 +642,7 @@ function BrowsePanel({
|
|
|
615
642
|
<RegistrySkillCard
|
|
616
643
|
key={skill.id}
|
|
617
644
|
skill={skill}
|
|
618
|
-
|
|
645
|
+
installedAgents={installedAgents(skill)}
|
|
619
646
|
installing={jobId !== null}
|
|
620
647
|
onInstall={onInstall}
|
|
621
648
|
onShowProgress={() => {
|
|
@@ -631,13 +658,16 @@ function BrowsePanel({
|
|
|
631
658
|
|
|
632
659
|
function InstalledPanel({
|
|
633
660
|
state,
|
|
661
|
+
warning,
|
|
634
662
|
skills,
|
|
635
663
|
total,
|
|
636
664
|
query,
|
|
637
665
|
onBrowse,
|
|
638
666
|
}: {
|
|
639
|
-
state: Load<
|
|
640
|
-
|
|
667
|
+
state: Load<InstalledSkill[]>;
|
|
668
|
+
/** Sebagian agent gagal dibaca. */
|
|
669
|
+
warning: string | null;
|
|
670
|
+
skills: InstalledSkill[];
|
|
641
671
|
total: number;
|
|
642
672
|
query: string;
|
|
643
673
|
onBrowse: () => void;
|
|
@@ -654,25 +684,34 @@ function InstalledPanel({
|
|
|
654
684
|
);
|
|
655
685
|
}
|
|
656
686
|
|
|
687
|
+
const warningNote = warning && (
|
|
688
|
+
<p role="status" className="mb-3 text-xs text-muted-foreground">
|
|
689
|
+
{warning}
|
|
690
|
+
</p>
|
|
691
|
+
);
|
|
692
|
+
|
|
657
693
|
if (total === 0) {
|
|
658
694
|
return (
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
695
|
+
<>
|
|
696
|
+
{warningNote}
|
|
697
|
+
<Empty className="border">
|
|
698
|
+
<EmptyHeader>
|
|
699
|
+
<EmptyMedia variant="icon">
|
|
700
|
+
<SparklesIcon />
|
|
701
|
+
</EmptyMedia>
|
|
702
|
+
<EmptyTitle>No skills installed yet</EmptyTitle>
|
|
703
|
+
<EmptyDescription>
|
|
704
|
+
Browse skills.sh to add your first skill to this project.
|
|
705
|
+
</EmptyDescription>
|
|
706
|
+
</EmptyHeader>
|
|
707
|
+
<EmptyContent>
|
|
708
|
+
<Button type="button" variant="outline" onClick={onBrowse}>
|
|
709
|
+
<SearchIcon data-icon="inline-start" />
|
|
710
|
+
Browse skills
|
|
711
|
+
</Button>
|
|
712
|
+
</EmptyContent>
|
|
713
|
+
</Empty>
|
|
714
|
+
</>
|
|
676
715
|
);
|
|
677
716
|
}
|
|
678
717
|
|
|
@@ -686,6 +725,11 @@ function InstalledPanel({
|
|
|
686
725
|
|
|
687
726
|
return (
|
|
688
727
|
<section aria-label="Installed skills" className="flex flex-col gap-3">
|
|
728
|
+
{warning && (
|
|
729
|
+
<p role="status" className="text-xs text-muted-foreground">
|
|
730
|
+
{warning}
|
|
731
|
+
</p>
|
|
732
|
+
)}
|
|
689
733
|
{query !== "" && (
|
|
690
734
|
<p className="text-xs text-muted-foreground" aria-live="polite">
|
|
691
735
|
{skills.length} of {total} installed skills
|
|
@@ -261,6 +261,7 @@ describe("createKcgServer — alur utama e2e (headless)", () => {
|
|
|
261
261
|
let app: KcgServer;
|
|
262
262
|
let root: string;
|
|
263
263
|
let servers: FakeServers;
|
|
264
|
+
let claudeServers: FakeServers;
|
|
264
265
|
/** Instalasi yang diminta ke Skills_Registry palsu: [cwd, source, skill]. */
|
|
265
266
|
const installCalls: [string, string, string][] = [];
|
|
266
267
|
const skillsRegistry: SkillsRegistry = {
|
|
@@ -297,10 +298,12 @@ describe("createKcgServer — alur utama e2e (headless)", () => {
|
|
|
297
298
|
root = mkdtempSync(path.join(tmpdir(), "kcg-e2e2-"));
|
|
298
299
|
mkdirSync(path.join(root, "proj"), { recursive: true });
|
|
299
300
|
servers = makeFakeServers();
|
|
301
|
+
claudeServers = makeFakeServers();
|
|
300
302
|
app = createKcgServer({
|
|
301
303
|
config: { sandboxRoot: root, configPath: "test" },
|
|
302
304
|
store,
|
|
303
305
|
servers: servers.manager,
|
|
306
|
+
claudeServers: claudeServers.manager,
|
|
304
307
|
uploadsRoot: path.join(root, "uploads"),
|
|
305
308
|
port: 0,
|
|
306
309
|
skillsRegistry,
|
|
@@ -658,7 +661,7 @@ describe("createKcgServer — alur utama e2e (headless)", () => {
|
|
|
658
661
|
expect(sessRes.status).toBe(200);
|
|
659
662
|
});
|
|
660
663
|
|
|
661
|
-
test("agentType claude-code
|
|
664
|
+
test("agentType claude-code memakai backend Claude Code; tipe tak dikenal ditolak", async () => {
|
|
662
665
|
const projRes = await fetch(`${baseUrl()}/api/projects`, {
|
|
663
666
|
method: "POST",
|
|
664
667
|
headers: { "content-type": "application/json" },
|
|
@@ -678,10 +681,20 @@ describe("createKcgServer — alur utama e2e (headless)", () => {
|
|
|
678
681
|
const sessRes = await fetch(`${baseUrl()}/api/sessions`, {
|
|
679
682
|
method: "POST",
|
|
680
683
|
headers: { "content-type": "application/json" },
|
|
681
|
-
body: JSON.stringify({ agentType: "
|
|
684
|
+
body: JSON.stringify({ agentType: "codex", projectId: body2.project.id }),
|
|
682
685
|
});
|
|
683
686
|
expect(sessRes.status).toBe(400);
|
|
684
687
|
expect(((await sessRes.json()) as { error: string }).error).toBe("UNSUPPORTED_AGENT_TYPE");
|
|
688
|
+
|
|
689
|
+
const claudeRes = await fetch(`${baseUrl()}/api/sessions`, {
|
|
690
|
+
method: "POST",
|
|
691
|
+
headers: { "content-type": "application/json" },
|
|
692
|
+
body: JSON.stringify({ agentType: "claude-code", projectId: body2.project.id }),
|
|
693
|
+
});
|
|
694
|
+
expect(claudeRes.status).toBe(201);
|
|
695
|
+
const { session } = (await claudeRes.json()) as { session: Session };
|
|
696
|
+
expect(session.agentType).toBe("claude-code");
|
|
697
|
+
expect(session.ocSessionId).toBe(`ses_${body2.project.id}`);
|
|
685
698
|
});
|
|
686
699
|
|
|
687
700
|
test("body JSON tidak valid -> 400 INVALID_JSON", async () => {
|
package/src/server/app.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { workspaceRoutes } from "./routes/workspace.routes";
|
|
|
42
42
|
import { type AttachmentManager, createAttachmentManager } from "./services/attachments";
|
|
43
43
|
import { type AuthService, createAuthService } from "./services/auth";
|
|
44
44
|
import { createPasskeyService } from "./services/auth-passkeys";
|
|
45
|
+
import { createClaudeCodeServerManager } from "./services/claude-code-client";
|
|
45
46
|
import { createFrpcInstaller } from "./services/frpc-installer";
|
|
46
47
|
import {
|
|
47
48
|
createOpenCodeServerManager,
|
|
@@ -79,6 +80,8 @@ export interface KcgServerOptions {
|
|
|
79
80
|
store?: SessionStore;
|
|
80
81
|
/** Injeksi OpenCode_Server_Manager (untuk pengujian, task 20.2). */
|
|
81
82
|
servers?: OpenCodeServerManager;
|
|
83
|
+
/** Injeksi backend Claude Code (Agent SDK) untuk pengujian. */
|
|
84
|
+
claudeServers?: OpenCodeServerManager;
|
|
82
85
|
/** Injeksi Skills_Registry (skills.sh + CLI `skills`) untuk pengujian. */
|
|
83
86
|
skillsRegistry?: SkillsRegistry;
|
|
84
87
|
/** Injeksi Auth_Service (mis. hash cepat untuk test). */
|
|
@@ -125,6 +128,10 @@ export interface KcgServer {
|
|
|
125
128
|
auth: AuthService;
|
|
126
129
|
/** Tunnel publik (diekspos untuk dashboard terminal & pengujian). */
|
|
127
130
|
tunnel: TunnelManager;
|
|
131
|
+
/** Tunnel localhost.run (diekspos untuk dashboard terminal). */
|
|
132
|
+
lhr: LhrManager;
|
|
133
|
+
/** Provider tunnel terpilih: "kcg" | "lhr". */
|
|
134
|
+
tunnelProvider: () => "kcg" | "lhr";
|
|
128
135
|
/** Pola rute `/api/*` yang terdaftar (untuk test cakupan penjaga). */
|
|
129
136
|
apiRoutePatterns: string[];
|
|
130
137
|
/** Shutdown: simpan status running (budget 5s) -> stop server -> tutup store. */
|
|
@@ -166,6 +173,8 @@ export function createKcgServer(opts: KcgServerOptions = {}): KcgServer {
|
|
|
166
173
|
|
|
167
174
|
const projectManager = createProjectManager(config.sandboxRoot, store);
|
|
168
175
|
const servers = opts.servers ?? createOpenCodeServerManager();
|
|
176
|
+
// Proses `claude` baru di-spawn saat Session Claude Code menerima prompt.
|
|
177
|
+
const claudeServers = opts.claudeServers ?? createClaudeCodeServerManager();
|
|
169
178
|
const attachments: AttachmentManager = createAttachmentManager(uploadsRoot);
|
|
170
179
|
|
|
171
180
|
// Skills_Registry & job instalasi dibuat lebih dulu: session manager
|
|
@@ -174,7 +183,7 @@ export function createKcgServer(opts: KcgServerOptions = {}): KcgServer {
|
|
|
174
183
|
const skillsRegistry = opts.skillsRegistry ?? createSkillsRegistry();
|
|
175
184
|
const skillInstalls = createSkillInstallJobs({
|
|
176
185
|
registry: skillsRegistry,
|
|
177
|
-
refreshSkills: (projectId) => sessionManager.refreshSkills(projectId),
|
|
186
|
+
refreshSkills: (projectId, agentType) => sessionManager.refreshSkills(projectId, agentType),
|
|
178
187
|
validate: (source, skill) => isValidSource(source) && isValidSkillId(skill),
|
|
179
188
|
});
|
|
180
189
|
|
|
@@ -182,6 +191,7 @@ export function createKcgServer(opts: KcgServerOptions = {}): KcgServer {
|
|
|
182
191
|
const sessionManager = createSessionManager({
|
|
183
192
|
store,
|
|
184
193
|
servers,
|
|
194
|
+
claudeServers,
|
|
185
195
|
attachments,
|
|
186
196
|
onMessage: (message) => gateway.notifyMessage(message.sessionId, message),
|
|
187
197
|
onMessagePart: (sessionId, messageId, part) =>
|
|
@@ -409,6 +419,8 @@ export function createKcgServer(opts: KcgServerOptions = {}): KcgServer {
|
|
|
409
419
|
skillInstalls,
|
|
410
420
|
auth,
|
|
411
421
|
tunnel,
|
|
422
|
+
lhr,
|
|
423
|
+
tunnelProvider: () => store.getTunnelSettings(Date.now()).provider,
|
|
412
424
|
apiRoutePatterns: Object.keys(apiRoutes),
|
|
413
425
|
close,
|
|
414
426
|
};
|
|
@@ -9,9 +9,19 @@
|
|
|
9
9
|
* (`app.ts`) bersama tabel rute lain — pola kcgcode (`*.routes.ts`).
|
|
10
10
|
*/
|
|
11
11
|
import type { BunRequest } from "bun";
|
|
12
|
+
import type { AgentType } from "../../types";
|
|
12
13
|
import { errorStatus, json, readJson, serverError } from "./helpers";
|
|
13
14
|
import type { ApiRouteContext } from "./types";
|
|
14
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
|
+
|
|
15
25
|
export function projectsRoutes(ctx: ApiRouteContext) {
|
|
16
26
|
const { projectManager, sessionManager, notifyDataChanged } = ctx;
|
|
17
27
|
return {
|
|
@@ -107,7 +117,7 @@ export function projectsRoutes(ctx: ApiRouteContext) {
|
|
|
107
117
|
"/api/projects/:id/models": {
|
|
108
118
|
GET: async (req: BunRequest<"/api/projects/:id/models">) => {
|
|
109
119
|
try {
|
|
110
|
-
const res = await sessionManager.listModels(req.params.id);
|
|
120
|
+
const res = await sessionManager.listModels(req.params.id, agentTypeParam(req));
|
|
111
121
|
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
112
122
|
return json({ models: res.data });
|
|
113
123
|
} catch (e) {
|
|
@@ -120,7 +130,7 @@ export function projectsRoutes(ctx: ApiRouteContext) {
|
|
|
120
130
|
"/api/projects/:id/mcp": {
|
|
121
131
|
GET: async (req: BunRequest<"/api/projects/:id/mcp">) => {
|
|
122
132
|
try {
|
|
123
|
-
const res = await sessionManager.listMcp(req.params.id);
|
|
133
|
+
const res = await sessionManager.listMcp(req.params.id, agentTypeParam(req));
|
|
124
134
|
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
125
135
|
return json({ mcp: res.data });
|
|
126
136
|
} catch (e) {
|
|
@@ -132,7 +142,7 @@ export function projectsRoutes(ctx: ApiRouteContext) {
|
|
|
132
142
|
"/api/projects/:id/skills": {
|
|
133
143
|
GET: async (req: BunRequest<"/api/projects/:id/skills">) => {
|
|
134
144
|
try {
|
|
135
|
-
const res = await sessionManager.listSkills(req.params.id);
|
|
145
|
+
const res = await sessionManager.listSkills(req.params.id, agentTypeParam(req));
|
|
136
146
|
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
137
147
|
return json({ skills: res.data });
|
|
138
148
|
} catch (e) {
|
|
@@ -145,7 +155,7 @@ export function projectsRoutes(ctx: ApiRouteContext) {
|
|
|
145
155
|
"/api/projects/:id/agents": {
|
|
146
156
|
GET: async (req: BunRequest<"/api/projects/:id/agents">) => {
|
|
147
157
|
try {
|
|
148
|
-
const res = await sessionManager.listAgents(req.params.id);
|
|
158
|
+
const res = await sessionManager.listAgents(req.params.id, agentTypeParam(req));
|
|
149
159
|
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
150
160
|
return json({ agents: res.data });
|
|
151
161
|
} catch (e) {
|
|
@@ -127,7 +127,7 @@ export function sessionsRoutes(ctx: ApiRouteContext) {
|
|
|
127
127
|
const cur = sessionManager.getSession(req.params.id);
|
|
128
128
|
if (!cur.ok)
|
|
129
129
|
return json({ error: "SESSION_NOT_FOUND" }, errorStatus("SESSION_NOT_FOUND"));
|
|
130
|
-
const res = await sessionManager.findFiles(cur.data.projectId, query);
|
|
130
|
+
const res = await sessionManager.findFiles(cur.data.projectId, query, cur.data.agentType);
|
|
131
131
|
if (!res.ok) return json({ error: res.error }, errorStatus(res.error));
|
|
132
132
|
return json({ files: res.data });
|
|
133
133
|
} catch (e) {
|
|
@@ -3,20 +3,32 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - `GET /api/skills/search?q=` — cari skill di skills.sh (proxy + cache).
|
|
5
5
|
* - `GET /api/skills/audit?source=&skill=` — hasil audit keamanan skill.
|
|
6
|
-
* - `POST /api/projects/:id/skills/install` `{ source, skill }` —
|
|
7
|
-
* instalasi (202 + job)
|
|
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.
|
|
8
9
|
* - `GET /api/skills/installs` — job berjalan & yang baru selesai.
|
|
9
10
|
* - `GET /api/skills/installs/:jobId?from=<n>` — status job + baris log
|
|
10
11
|
* mulai indeks `n` (polling bertahap untuk output live).
|
|
11
12
|
* - `POST /api/skills/installs/:jobId/cancel` — batalkan job berjalan.
|
|
12
13
|
*
|
|
13
|
-
* Daftar skill terpasang tetap lewat `GET /api/projects/:id/skills
|
|
14
|
-
*
|
|
14
|
+
* Daftar skill terpasang tetap lewat `GET /api/projects/:id/skills?agent=`
|
|
15
|
+
* di `projects.routes.ts`.
|
|
15
16
|
*/
|
|
16
17
|
import type { BunRequest } from "bun";
|
|
18
|
+
import type { AgentType } from "../../types";
|
|
17
19
|
import { errorStatus, json, readJson, serverError } from "./helpers";
|
|
18
20
|
import type { ApiRouteContext } from "./types";
|
|
19
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
|
+
|
|
20
32
|
export function skillsRoutes(ctx: ApiRouteContext) {
|
|
21
33
|
const { projectManager, skillsRegistry, skillInstalls } = ctx;
|
|
22
34
|
return {
|
|
@@ -54,10 +66,16 @@ export function skillsRoutes(ctx: ApiRouteContext) {
|
|
|
54
66
|
try {
|
|
55
67
|
const body = await readJson(req);
|
|
56
68
|
if (!body.ok) return json({ error: "INVALID_JSON" }, 400);
|
|
57
|
-
const { source, skill } = (body.data ?? {}) as {
|
|
69
|
+
const { source, skill, agents } = (body.data ?? {}) as {
|
|
70
|
+
source?: unknown;
|
|
71
|
+
skill?: unknown;
|
|
72
|
+
agents?: unknown;
|
|
73
|
+
};
|
|
58
74
|
if (typeof source !== "string" || typeof skill !== "string") {
|
|
59
75
|
return json({ error: "INVALID_SKILL_ID" }, 400);
|
|
60
76
|
}
|
|
77
|
+
const agentTypes = parseInstallAgents(agents);
|
|
78
|
+
if (agentTypes === null) return json({ error: "UNSUPPORTED_AGENT_TYPE" }, 400);
|
|
61
79
|
const project = projectManager.getProject(req.params.id);
|
|
62
80
|
if (!project.ok) return json({ error: project.error }, errorStatus(project.error));
|
|
63
81
|
|
|
@@ -67,6 +85,7 @@ export function skillsRoutes(ctx: ApiRouteContext) {
|
|
|
67
85
|
projectPath: project.data.path,
|
|
68
86
|
source,
|
|
69
87
|
skillId: skill,
|
|
88
|
+
agentTypes,
|
|
70
89
|
});
|
|
71
90
|
if (!job.ok) return json({ error: job.error }, errorStatus(job.error));
|
|
72
91
|
return json({ job: job.data }, 202);
|