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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit test pemindai skill Claude Code (src/server/services/claude-skills.ts)
|
|
3
|
+
* — frontmatter SKILL.md dan pemindaian `.claude/skills` di direktori temp.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { claudeGlobalSkillsDir, listClaudeSkills, parseSkillFrontmatter } from "../claude-skills";
|
|
10
|
+
|
|
11
|
+
function writeSkill(dir: string, body: string): void {
|
|
12
|
+
mkdirSync(dir, { recursive: true });
|
|
13
|
+
writeFileSync(join(dir, "SKILL.md"), body);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("parseSkillFrontmatter", () => {
|
|
17
|
+
test("nilai satu baris, berkutip maupun tidak", () => {
|
|
18
|
+
expect(
|
|
19
|
+
parseSkillFrontmatter('---\nname: pdf\ndescription: "Read PDFs: fast"\n---\n# PDF'),
|
|
20
|
+
).toEqual({ name: "pdf", description: "Read PDFs: fast" });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("blok `>` digabung menjadi satu baris", () => {
|
|
24
|
+
expect(
|
|
25
|
+
parseSkillFrontmatter("---\nname: x\ndescription: >\n first line\n second line\n---\n"),
|
|
26
|
+
).toEqual({ name: "x", description: "first line second line" });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("tanpa frontmatter -> null", () => {
|
|
30
|
+
expect(parseSkillFrontmatter("# Just markdown")).toEqual({ name: null, description: null });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("listClaudeSkills", () => {
|
|
35
|
+
test("Project + global, ikuti symlink, fallback nama folder", async () => {
|
|
36
|
+
const root = mkdtempSync(join(tmpdir(), "kcg-cc-skills-"));
|
|
37
|
+
const project = join(root, "proj");
|
|
38
|
+
const global = join(root, "home-skills");
|
|
39
|
+
// Pola CLI `skills --agent claude-code`: isi di .agents/skills, symlink di .claude/skills.
|
|
40
|
+
writeSkill(join(project, ".agents/skills/linked"), "---\nname: linked\ndescription: L\n---\n");
|
|
41
|
+
mkdirSync(join(project, ".claude/skills"), { recursive: true });
|
|
42
|
+
symlinkSync(join(project, ".agents/skills/linked"), join(project, ".claude/skills/linked"));
|
|
43
|
+
writeSkill(join(project, ".claude/skills/plain"), "no frontmatter");
|
|
44
|
+
writeSkill(join(global, "global-one"), "---\nname: global-one\n---\n");
|
|
45
|
+
|
|
46
|
+
const skills = await listClaudeSkills(project, global);
|
|
47
|
+
expect(skills.map((s) => [s.name, s.source, s.description])).toEqual([
|
|
48
|
+
["global-one", "global", null],
|
|
49
|
+
["linked", "project", "L"],
|
|
50
|
+
["plain", "project", null],
|
|
51
|
+
]);
|
|
52
|
+
expect(skills[1]?.location).toBe(join(project, ".claude/skills/linked/SKILL.md"));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("direktori tidak ada -> daftar kosong", async () => {
|
|
56
|
+
expect(await listClaudeSkills("/nonexistent-kcg", "/nonexistent-kcg-global")).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("claudeGlobalSkillsDir menghormati CLAUDE_CONFIG_DIR", () => {
|
|
60
|
+
expect(claudeGlobalSkillsDir({ CLAUDE_CONFIG_DIR: "/cfg" })).toBe("/cfg/skills");
|
|
61
|
+
expect(claudeGlobalSkillsDir({})).toEndWith("/.claude/skills");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -3467,3 +3467,43 @@ test("deleteSession membersihkan lampiran gambar Session", async () => {
|
|
|
3467
3467
|
h.close();
|
|
3468
3468
|
}
|
|
3469
3469
|
});
|
|
3470
|
+
|
|
3471
|
+
test("Session claude-code memakai backend Claude Code; opencode tidak tersentuh", async () => {
|
|
3472
|
+
const claude = makeFakeServers();
|
|
3473
|
+
const h = freshHarnessWithHooks({}, {}, { claudeServers: claude.manager });
|
|
3474
|
+
try {
|
|
3475
|
+
const sid = await createSession(h, "claude-code");
|
|
3476
|
+
expect(claude.ensureCalls).toHaveLength(1);
|
|
3477
|
+
expect(h.fake.ensureCalls).toHaveLength(0);
|
|
3478
|
+
const cc = claude.clients.get("p1");
|
|
3479
|
+
if (!cc) throw new Error("claude client tidak ada");
|
|
3480
|
+
|
|
3481
|
+
expect((await h.sm.sendFreeTextInput(sid, "halo")).ok).toBe(true);
|
|
3482
|
+
await flush();
|
|
3483
|
+
expect(cc.calls).toContain("promptAsync:halo");
|
|
3484
|
+
|
|
3485
|
+
// Event dari backend Claude Code memetakan ke Session lokal yang sama.
|
|
3486
|
+
cc.emit({
|
|
3487
|
+
type: "permission.asked",
|
|
3488
|
+
sessionID: "ses_remote1",
|
|
3489
|
+
requestID: "per_cc",
|
|
3490
|
+
permission: "bash",
|
|
3491
|
+
patterns: ["ls"],
|
|
3492
|
+
});
|
|
3493
|
+
expect(h.prompts.map((p) => p.id)).toEqual(["per_cc"]);
|
|
3494
|
+
expect((await h.sm.resolvePrompt(sid, "per_cc", "approve")).ok).toBe(true);
|
|
3495
|
+
expect(cc.calls).toContain("replyPermission:per_cc:once");
|
|
3496
|
+
|
|
3497
|
+
// Daftar model per backend + task list dibaca dari session remote.
|
|
3498
|
+
expect((await h.sm.listModels("p1", "claude-code")).ok).toBe(true);
|
|
3499
|
+
expect(cc.calls).toContain("listModels");
|
|
3500
|
+
expect((await h.sm.sessionContext(sid)).ok).toBe(true);
|
|
3501
|
+
expect(cc.calls).toContain("listTodos:ses_remote1");
|
|
3502
|
+
expect(h.fake.ensureCalls).toHaveLength(0);
|
|
3503
|
+
|
|
3504
|
+
expect((await h.sm.releaseProject("p1")).ok).toBe(true);
|
|
3505
|
+
expect(claude.stopped).toContain("p1");
|
|
3506
|
+
} finally {
|
|
3507
|
+
h.close();
|
|
3508
|
+
}
|
|
3509
|
+
});
|
|
@@ -12,6 +12,8 @@ interface Harness {
|
|
|
12
12
|
release: (res: Result<InstallResult>) => void;
|
|
13
13
|
emit: (line: string) => void;
|
|
14
14
|
refreshCalls: string[];
|
|
15
|
+
/** Agent yang diteruskan ke `registry.install` per panggilan. */
|
|
16
|
+
installAgents: (readonly string[] | undefined)[];
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -23,11 +25,13 @@ function harness(refreshed = true): Harness {
|
|
|
23
25
|
let emitFn: (line: string) => void = () => {};
|
|
24
26
|
let releaseFn: (res: Result<InstallResult>) => void = () => {};
|
|
25
27
|
const refreshCalls: string[] = [];
|
|
28
|
+
const installAgents: (readonly string[] | undefined)[] = [];
|
|
26
29
|
const registry: SkillsRegistry = {
|
|
27
30
|
search: async () => ({ ok: true, data: [] }),
|
|
28
31
|
audit: async () => ({ ok: true, data: { audits: null, overall: null } }),
|
|
29
|
-
install: (_path, _source, _skill, onLine, signal) =>
|
|
32
|
+
install: (_path, _source, _skill, onLine, signal, agentTypes) =>
|
|
30
33
|
new Promise((resolve) => {
|
|
34
|
+
installAgents.push(agentTypes);
|
|
31
35
|
emitFn = (l) => onLine?.(l);
|
|
32
36
|
releaseFn = resolve;
|
|
33
37
|
signal?.addEventListener("abort", () =>
|
|
@@ -37,8 +41,8 @@ function harness(refreshed = true): Harness {
|
|
|
37
41
|
};
|
|
38
42
|
const jobs = createSkillInstallJobs({
|
|
39
43
|
registry,
|
|
40
|
-
refreshSkills: async (projectId) => {
|
|
41
|
-
refreshCalls.push(projectId);
|
|
44
|
+
refreshSkills: async (projectId, agentType) => {
|
|
45
|
+
refreshCalls.push(agentType === "opencode" ? projectId : `${projectId}:${agentType}`);
|
|
42
46
|
return { ok: true, data: { refreshed } };
|
|
43
47
|
},
|
|
44
48
|
validate: (source, skill) => source === "a/b" && !skill.startsWith("-"),
|
|
@@ -52,6 +56,7 @@ function harness(refreshed = true): Harness {
|
|
|
52
56
|
release: (r) => releaseFn(r),
|
|
53
57
|
emit: (l) => emitFn(l),
|
|
54
58
|
refreshCalls,
|
|
59
|
+
installAgents,
|
|
55
60
|
};
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -64,6 +69,20 @@ const input = {
|
|
|
64
69
|
};
|
|
65
70
|
|
|
66
71
|
describe("createSkillInstallJobs", () => {
|
|
72
|
+
test("agentTypes diteruskan ke instalasi & refresh tiap agent (default opencode)", async () => {
|
|
73
|
+
const h = harness();
|
|
74
|
+
const a = h.jobs.start({ ...input, agentTypes: ["opencode", "claude-code"] });
|
|
75
|
+
expect(a.ok && a.data.agentTypes).toEqual(["opencode", "claude-code"]);
|
|
76
|
+
h.release({ ok: true, data: { name: "c", path: null } });
|
|
77
|
+
if (a.ok) await h.jobs.settled(a.data.id);
|
|
78
|
+
const b = h.jobs.start(input);
|
|
79
|
+
expect(b.ok && b.data.agentTypes).toEqual(["opencode"]);
|
|
80
|
+
h.release({ ok: true, data: { name: "c", path: null } });
|
|
81
|
+
if (b.ok) await h.jobs.settled(b.data.id);
|
|
82
|
+
expect(h.installAgents).toEqual([["opencode", "claude-code"], ["opencode"]]);
|
|
83
|
+
expect(h.refreshCalls).toEqual(["p1", "p1:claude-code", "p1"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
67
86
|
test("job berjalan, log bertahap via from, sukses + refresh", async () => {
|
|
68
87
|
const h = harness();
|
|
69
88
|
const started = h.jobs.start(input);
|
|
@@ -146,6 +146,12 @@ describe("parsing", () => {
|
|
|
146
146
|
]);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
test("buildInstallArgs: beberapa agent setelah satu --agent", () => {
|
|
150
|
+
const args = buildInstallArgs("a/b", "c", ["opencode", "claude-code"]);
|
|
151
|
+
const i = args.indexOf("--agent");
|
|
152
|
+
expect(args.slice(i, i + 4)).toEqual(["--agent", "opencode", "claude-code", "-y"]);
|
|
153
|
+
});
|
|
154
|
+
|
|
149
155
|
test("parseInstallOutput: JSON setelah teks lain; status harus installed/updated", () => {
|
|
150
156
|
const ok = parseInstallOutput(
|
|
151
157
|
'log line\n[{"name":"c","status":"installed","path":"/p/.agents/skills/c"}]',
|