Skip to content

Commit 7b45efe

Browse files
authored
Add skill CRUD and slash-command support (#101)
- Wire skill RPCs through server and web native API - Surface installed skills and /skill subcommands in composer menus - Add shared skill contracts and utilities
1 parent 3cfcf50 commit 7b45efe

14 files changed

Lines changed: 1203 additions & 17 deletions

File tree

apps/server/src/serverLayers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { EnvironmentVariablesLive } from "./persistence/Services/EnvironmentVari
2828

2929
import { TerminalManagerLive } from "./terminal/Layers/Manager";
3030
import { KeybindingsLive } from "./keybindings";
31+
import { SkillServiceLive } from "./skills/SkillService";
3132
import { GitManagerLive } from "./git/Layers/GitManager";
3233
import { GitCoreLive } from "./git/Layers/GitCore";
3334
import { GitHubCliLive } from "./git/Layers/GitHubCli";
@@ -159,5 +160,6 @@ export function makeServerRuntimeServicesLayer() {
159160
prReviewLayer,
160161
terminalLayer,
161162
KeybindingsLive,
163+
SkillServiceLive,
162164
).pipe(Layer.provideMerge(NodeServices.layer));
163165
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* SkillService - Effect service for skill CRUD and search operations.
3+
*
4+
* Wraps the shared skill utilities from `@okcode/shared/skill` as an Effect
5+
* service using the project's `ServiceMap.Service` pattern.
6+
*
7+
* @module SkillService
8+
*/
9+
import type {
10+
SkillCreateResult,
11+
SkillListResult,
12+
SkillReadResult,
13+
SkillSearchResult,
14+
} from "@okcode/contracts";
15+
import { Effect, Layer, Schema, ServiceMap } from "effect";
16+
import {
17+
listSkills,
18+
readSkill,
19+
searchSkills,
20+
createSkill,
21+
deleteSkill,
22+
} from "@okcode/shared/skill";
23+
24+
/**
25+
* SkillServiceError - Tagged error for skill service failures.
26+
*/
27+
export class SkillServiceError extends Schema.TaggedErrorClass<SkillServiceError>()(
28+
"SkillServiceError",
29+
{
30+
operation: Schema.String,
31+
detail: Schema.String,
32+
cause: Schema.optional(Schema.Defect),
33+
},
34+
) {
35+
override get message(): string {
36+
return `Skill service error (${this.operation}): ${this.detail}`;
37+
}
38+
}
39+
40+
/**
41+
* SkillServiceShape - Service API for skill CRUD and search operations.
42+
*/
43+
export interface SkillServiceShape {
44+
/**
45+
* List all installed skills.
46+
*/
47+
readonly list: (input: {
48+
readonly cwd?: string | undefined;
49+
}) => Effect.Effect<SkillListResult, SkillServiceError>;
50+
51+
/**
52+
* Read a skill by name.
53+
*/
54+
readonly read: (input: {
55+
readonly name: string;
56+
readonly cwd?: string | undefined;
57+
}) => Effect.Effect<SkillReadResult, SkillServiceError>;
58+
59+
/**
60+
* Create a new skill with scaffold template.
61+
*/
62+
readonly create: (input: {
63+
readonly name: string;
64+
readonly description: string;
65+
readonly scope: "global" | "project";
66+
readonly cwd?: string | undefined;
67+
}) => Effect.Effect<SkillCreateResult, SkillServiceError>;
68+
69+
/**
70+
* Delete a skill.
71+
*/
72+
readonly delete: (input: {
73+
readonly name: string;
74+
readonly scope: "global" | "project";
75+
readonly cwd?: string | undefined;
76+
}) => Effect.Effect<void, SkillServiceError>;
77+
78+
/**
79+
* Search skills by query.
80+
*/
81+
readonly search: (input: {
82+
readonly query: string;
83+
readonly cwd?: string | undefined;
84+
}) => Effect.Effect<SkillSearchResult, SkillServiceError>;
85+
}
86+
87+
/**
88+
* SkillService - Service tag for skill CRUD and search operations.
89+
*/
90+
export class SkillService extends ServiceMap.Service<SkillService, SkillServiceShape>()(
91+
"okcode/skills/SkillService",
92+
) {}
93+
94+
export const SkillServiceLive = Layer.succeed(SkillService, {
95+
list: (input) =>
96+
Effect.try({
97+
try: () => {
98+
const entries = listSkills(input.cwd);
99+
return {
100+
skills: entries.map((e) => ({
101+
name: e.name,
102+
scope: e.scope,
103+
description: e.description,
104+
tags: e.tags,
105+
path: e.path,
106+
})),
107+
};
108+
},
109+
catch: (cause) =>
110+
new SkillServiceError({
111+
operation: "list",
112+
detail: cause instanceof Error ? cause.message : String(cause),
113+
cause: cause instanceof Error ? cause : undefined,
114+
}),
115+
}),
116+
117+
read: (input) =>
118+
Effect.try({
119+
try: () => {
120+
const result = readSkill(input.name, input.cwd);
121+
if (!result) {
122+
throw new Error(`Skill "${input.name}" not found`);
123+
}
124+
return {
125+
name: result.name,
126+
scope: result.scope,
127+
description: result.description,
128+
content: result.content.raw,
129+
path: result.path,
130+
tags: result.tags,
131+
};
132+
},
133+
catch: (cause) =>
134+
new SkillServiceError({
135+
operation: "read",
136+
detail: cause instanceof Error ? cause.message : String(cause),
137+
cause: cause instanceof Error ? cause : undefined,
138+
}),
139+
}),
140+
141+
create: (input) =>
142+
Effect.try({
143+
try: () => createSkill(input.name, input.description, input.scope, input.cwd),
144+
catch: (cause) =>
145+
new SkillServiceError({
146+
operation: "create",
147+
detail: cause instanceof Error ? cause.message : String(cause),
148+
cause: cause instanceof Error ? cause : undefined,
149+
}),
150+
}),
151+
152+
delete: (input) =>
153+
Effect.try({
154+
try: () => deleteSkill(input.name, input.scope, input.cwd),
155+
catch: (cause) =>
156+
new SkillServiceError({
157+
operation: "delete",
158+
detail: cause instanceof Error ? cause.message : String(cause),
159+
cause: cause instanceof Error ? cause : undefined,
160+
}),
161+
}),
162+
163+
search: (input) =>
164+
Effect.try({
165+
try: () => {
166+
const entries = searchSkills(input.query, input.cwd);
167+
return {
168+
skills: entries.map((e) => ({
169+
name: e.name,
170+
scope: e.scope,
171+
description: e.description,
172+
tags: e.tags,
173+
path: e.path,
174+
})),
175+
};
176+
},
177+
catch: (cause) =>
178+
new SkillServiceError({
179+
operation: "search",
180+
detail: cause instanceof Error ? cause.message : String(cause),
181+
cause: cause instanceof Error ? cause : undefined,
182+
}),
183+
}),
184+
} satisfies SkillServiceShape);

apps/server/src/wsServer.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import { decodeJsonResult, formatSchemaError } from "@okcode/shared/schemaJson";
8383
import { PrReview } from "./prReview/Services/PrReview.ts";
8484
import { GitActionExecutionError } from "./git/Errors.ts";
8585
import { EnvironmentVariables } from "./persistence/Services/EnvironmentVariables.ts";
86+
import { SkillService } from "./skills/SkillService.ts";
8687
import { resolveRuntimeEnvironment } from "./runtimeEnvironment.ts";
8788

8889
/**
@@ -263,6 +264,7 @@ export type ServerRuntimeServices =
263264
| PrReview
264265
| TerminalManager
265266
| Keybindings
267+
| SkillService
266268
| Open
267269
| AnalyticsService
268270
| EnvironmentVariables;
@@ -653,6 +655,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
653655
const prReview = yield* PrReview;
654656
const { openInEditor } = yield* Open;
655657
const environmentVariables = yield* EnvironmentVariables;
658+
const skillService = yield* SkillService;
656659

657660
const subscriptionsScope = yield* Scope.make("sequential");
658661
yield* Effect.addFinalizer(() => Scope.close(subscriptionsScope, Exit.void));
@@ -1222,6 +1225,31 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
12221225
return { path: pickedPath };
12231226
}
12241227

1228+
case WS_METHODS.skillList: {
1229+
const body = stripRequestTag(request.body);
1230+
return yield* skillService.list(body);
1231+
}
1232+
1233+
case WS_METHODS.skillRead: {
1234+
const body = stripRequestTag(request.body);
1235+
return yield* skillService.read(body);
1236+
}
1237+
1238+
case WS_METHODS.skillCreate: {
1239+
const body = stripRequestTag(request.body);
1240+
return yield* skillService.create(body);
1241+
}
1242+
1243+
case WS_METHODS.skillDelete: {
1244+
const body = stripRequestTag(request.body);
1245+
return yield* skillService.delete(body);
1246+
}
1247+
1248+
case WS_METHODS.skillSearch: {
1249+
const body = stripRequestTag(request.body);
1250+
return yield* skillService.search(body);
1251+
}
1252+
12251253
default: {
12261254
const _exhaustiveCheck: never = request.body;
12271255
return yield* new RouteRequestError({

0 commit comments

Comments
 (0)