@slashfi/agents-sdk 0.50.5 → 0.60.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.
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Tests for init + materialize modules.
3
+ *
4
+ * Focuses on:
5
+ * - parseTarget parsing (preset-based)
6
+ * - Skill template generation
7
+ * - Type generation from tool schemas
8
+ * - Materialization file structure
9
+ */
10
+
11
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
12
+ import { existsSync, readFileSync, rmSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { parseTarget, runInit, loadPresets, getPreset, renderContent } from "./init.js";
15
+ import type { SkillTarget, Preset } from "./init.js";
16
+ import { materializeRef } from "./materialize.js";
17
+ import type { Adk } from "./config-store.js";
18
+ import { createAdk } from "./config-store.js";
19
+ import type { FsStore } from "./agent-definitions/config.js";
20
+
21
+ // ============================================
22
+ // Helpers
23
+ // ============================================
24
+
25
+ const TEST_DIR = join(import.meta.dir, "../.test-output");
26
+
27
+ function createMemoryStore(): FsStore & { files: Map<string, string> } {
28
+ const files = new Map<string, string>();
29
+ return {
30
+ files,
31
+ async readFile(path: string) {
32
+ return files.get(path) ?? null;
33
+ },
34
+ async writeFile(path: string, content: string) {
35
+ files.set(path, content);
36
+ },
37
+ };
38
+ }
39
+
40
+ function cleanup() {
41
+ if (existsSync(TEST_DIR)) {
42
+ rmSync(TEST_DIR, { recursive: true });
43
+ }
44
+ }
45
+
46
+ // ============================================
47
+ // Presets
48
+ // ============================================
49
+
50
+ describe("presets", () => {
51
+ test("loads all preset files", () => {
52
+ const presets = loadPresets();
53
+ expect(presets.size).toBeGreaterThanOrEqual(4);
54
+ expect(presets.has("claude")).toBe(true);
55
+ expect(presets.has("cursor")).toBe(true);
56
+ expect(presets.has("copilot")).toBe(true);
57
+ expect(presets.has("windsurf")).toBe(true);
58
+ });
59
+
60
+ test("claude preset writes SKILL.md", () => {
61
+ const preset = getPreset("claude");
62
+ expect(preset).toBeDefined();
63
+ expect(preset!.filename).toContain("SKILL.md");
64
+ });
65
+
66
+ test("cursor preset targets .agents/skills", () => {
67
+ const preset = getPreset("cursor");
68
+ expect(preset).toBeDefined();
69
+ expect(preset!.defaultPath).toBe(".agents/skills");
70
+ });
71
+
72
+ test("copilot preset targets .github/skills", () => {
73
+ const preset = getPreset("copilot");
74
+ expect(preset).toBeDefined();
75
+ expect(preset!.defaultPath).toBe(".github/skills");
76
+ });
77
+ });
78
+
79
+ // ============================================
80
+ // parseTarget
81
+ // ============================================
82
+
83
+ describe("parseTarget", () => {
84
+ test("parses preset name with default path", () => {
85
+ const result = parseTarget("claude");
86
+ expect(result.preset.name).toBe("claude");
87
+ expect(result.path).toContain(".claude/skills");
88
+ });
89
+
90
+ test("parses preset:path override", () => {
91
+ const result = parseTarget("cursor:/tmp/my-rules");
92
+ expect(result.preset.name).toBe("cursor");
93
+ expect(result.path).toBe("/tmp/my-rules");
94
+ });
95
+
96
+ test("parses all preset names", () => {
97
+ for (const name of ["claude", "cursor", "copilot", "windsurf"]) {
98
+ const result = parseTarget(name);
99
+ expect(result.preset.name).toBe(name);
100
+ }
101
+ });
102
+
103
+ test("parses preset with custom path", () => {
104
+ const result = parseTarget("claude:/tmp/custom");
105
+ expect(result.preset.name).toBe("claude");
106
+ expect(result.path).toBe("/tmp/custom");
107
+ });
108
+
109
+ test("throws on unknown target", () => {
110
+ expect(() => parseTarget("unknown")).toThrow("Unknown preset");
111
+ });
112
+
113
+ test("throws on unknown with path", () => {
114
+ expect(() => parseTarget("vscode:/tmp")).toThrow("Unknown target");
115
+ });
116
+ });
117
+
118
+ // ============================================
119
+ // renderContent
120
+ // ============================================
121
+
122
+ describe("renderContent", () => {
123
+ const body = "Test content";
124
+ const meta = { name: "test", description: "Test desc" };
125
+
126
+ test("includes YAML frontmatter with name and description", () => {
127
+ const result = renderContent(body, meta);
128
+ expect(result).toContain("---");
129
+ expect(result).toContain("name: test");
130
+ expect(result).toContain("description: Test desc");
131
+ expect(result).toContain("Test content");
132
+ });
133
+ });
134
+
135
+ // ============================================
136
+ // runInit
137
+ // ============================================
138
+
139
+ describe("runInit", () => {
140
+ beforeEach(cleanup);
141
+ afterEach(cleanup);
142
+
143
+ test("writes claude skill file to target path", async () => {
144
+ const store = createMemoryStore();
145
+ const adk = createAdk(store);
146
+ const skillDir = join(TEST_DIR, "claude-skills");
147
+ const preset = getPreset("claude")!;
148
+
149
+ await runInit(adk, [{ preset, path: skillDir }]);
150
+
151
+ const skillPath = join(skillDir, preset.filename);
152
+ expect(existsSync(skillPath)).toBe(true);
153
+
154
+ const content = readFileSync(skillPath, "utf-8");
155
+ expect(content).toContain("name: adk");
156
+ expect(content).toContain("description:");
157
+ expect(content).toContain("adk ref call");
158
+ });
159
+
160
+ test("writes cursor skill file to target path", async () => {
161
+ const store = createMemoryStore();
162
+ const adk = createAdk(store);
163
+ const skillDir = join(TEST_DIR, "cursor-skills");
164
+ const preset = getPreset("cursor")!;
165
+
166
+ await runInit(adk, [{ preset, path: skillDir }]);
167
+
168
+ const skillPath = join(skillDir, preset.filename);
169
+ expect(existsSync(skillPath)).toBe(true);
170
+
171
+ const content = readFileSync(skillPath, "utf-8");
172
+ expect(content).toContain("name: adk");
173
+ expect(content).toContain("adk ref call");
174
+ });
175
+
176
+ test("saves targets to config", async () => {
177
+ const store = createMemoryStore();
178
+ const adk = createAdk(store);
179
+ const skillDir = join(TEST_DIR, "save-test");
180
+ const preset = getPreset("claude")!;
181
+
182
+ await runInit(adk, [{ preset, path: skillDir }]);
183
+
184
+ const configRaw = store.files.get("consumer-config.json");
185
+ expect(configRaw).toBeDefined();
186
+ const config = JSON.parse(configRaw!);
187
+ expect(config.targets).toBeDefined();
188
+ });
189
+
190
+ test("adds default registry on first run", async () => {
191
+ const store = createMemoryStore();
192
+ const adk = createAdk(store);
193
+
194
+ await runInit(adk, []);
195
+
196
+ const configRaw = store.files.get("consumer-config.json");
197
+ expect(configRaw).toBeDefined();
198
+ const config = JSON.parse(configRaw!);
199
+ expect(config.registries).toBeDefined();
200
+ expect(config.registries.some((r: any) => r.url === "https://registry.slash.com")).toBe(true);
201
+ });
202
+
203
+ test("is idempotent with default registry", async () => {
204
+ const store = createMemoryStore();
205
+ const adk = createAdk(store);
206
+
207
+ await runInit(adk, []);
208
+ await runInit(adk, []);
209
+
210
+ const config = JSON.parse(store.files.get("consumer-config.json")!);
211
+ const slashRegistries = config.registries.filter((r: any) => r.url === "https://registry.slash.com");
212
+ expect(slashRegistries.length).toBe(1);
213
+ });
214
+ });
215
+
216
+ // ============================================
217
+ // Type Generation (via materializeRef)
218
+ // ============================================
219
+
220
+ describe("type generation", () => {
221
+ beforeEach(cleanup);
222
+ afterEach(cleanup);
223
+
224
+ test("generates valid .d.ts from tool schemas", async () => {
225
+ const configDir = join(TEST_DIR, "types-test");
226
+
227
+ const mockTools = [
228
+ {
229
+ name: "search_pages",
230
+ description: "Search for Notion pages",
231
+ inputSchema: {
232
+ type: "object",
233
+ properties: {
234
+ query: { type: "string" },
235
+ limit: { type: "number" },
236
+ },
237
+ required: ["query"],
238
+ },
239
+ },
240
+ {
241
+ name: "create_page",
242
+ description: "Create a new page in a database",
243
+ inputSchema: {
244
+ type: "object",
245
+ properties: {
246
+ parent_id: { type: "string" },
247
+ title: { type: "string" },
248
+ },
249
+ required: ["parent_id", "title"],
250
+ },
251
+ },
252
+ {
253
+ name: "API-get-block",
254
+ description: "Get a block by ID",
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ block_id: { type: "string" },
259
+ },
260
+ required: ["block_id"],
261
+ },
262
+ },
263
+ ];
264
+
265
+ const adk = {
266
+ ref: {
267
+ inspect: async () => ({
268
+ path: "notion",
269
+ description: "Notion MCP agent",
270
+ tools: mockTools,
271
+ }),
272
+ resources: async () => ({ result: { resources: [] } }),
273
+ },
274
+ readConfig: async () => ({}),
275
+ writeConfig: async () => {},
276
+ } as unknown as Adk;
277
+
278
+ const result = await materializeRef(adk, "notion", configDir);
279
+
280
+ expect(result.toolCount).toBe(3);
281
+ expect(result.typesGenerated).toBe(true);
282
+
283
+ // Check .d.ts file
284
+ const dtsPath = join(configDir, "refs", "notion", "types", "notion.d.ts");
285
+ expect(existsSync(dtsPath)).toBe(true);
286
+
287
+ const dts = readFileSync(dtsPath, "utf-8");
288
+ expect(dts).toContain("export interface NotionTools");
289
+ expect(dts).toContain('"search_pages"');
290
+ expect(dts).toContain('"create_page"');
291
+ expect(dts).toContain('"API-get-block"');
292
+ expect(dts).toContain("Search for Notion pages");
293
+ expect(dts).toContain("Create a new page in a database");
294
+ expect(dts).toContain("export declare const tools");
295
+ });
296
+
297
+ test("generates tool.json files per tool", async () => {
298
+ const configDir = join(TEST_DIR, "tool-json-test");
299
+
300
+ const mockTools = [
301
+ {
302
+ name: "search",
303
+ description: "Search",
304
+ inputSchema: { type: "object", properties: { q: { type: "string" } } },
305
+ },
306
+ ];
307
+
308
+ const adk = {
309
+ ref: {
310
+ inspect: async () => ({ tools: mockTools }),
311
+ resources: async () => ({ result: { resources: [] } }),
312
+ },
313
+ } as unknown as Adk;
314
+
315
+ const result = await materializeRef(adk, "test-agent", configDir);
316
+
317
+ expect(result.toolCount).toBe(1);
318
+
319
+ const toolPath = join(configDir, "refs", "test-agent", "tools", "search.tool.json");
320
+ expect(existsSync(toolPath)).toBe(true);
321
+
322
+ const tool = JSON.parse(readFileSync(toolPath, "utf-8"));
323
+ expect(tool.name).toBe("search");
324
+ expect(tool.description).toBe("Search");
325
+ expect(tool.inputSchema.properties.q.type).toBe("string");
326
+ });
327
+
328
+ test("generates agent.json metadata", async () => {
329
+ const configDir = join(TEST_DIR, "agent-json-test");
330
+
331
+ const adk = {
332
+ ref: {
333
+ inspect: async () => ({
334
+ description: "Test agent",
335
+ tools: [{ name: "tool1", description: "A tool" }],
336
+ }),
337
+ resources: async () => ({ result: { resources: [] } }),
338
+ },
339
+ } as unknown as Adk;
340
+
341
+ await materializeRef(adk, "myagent", configDir);
342
+
343
+ const metaPath = join(configDir, "refs", "myagent", "agent.json");
344
+ expect(existsSync(metaPath)).toBe(true);
345
+
346
+ const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
347
+ expect(meta.name).toBe("myagent");
348
+ expect(meta.description).toBe("Test agent");
349
+ expect(meta.toolCount).toBe(1);
350
+ expect(meta.tools).toEqual(["tool1"]);
351
+ expect(meta.materializedAt).toBeDefined();
352
+ });
353
+
354
+ test("handles special characters in tool names", async () => {
355
+ const configDir = join(TEST_DIR, "special-chars-test");
356
+
357
+ const adk = {
358
+ ref: {
359
+ inspect: async () => ({
360
+ tools: [
361
+ { name: "API-post-search/v2", description: "Search v2" },
362
+ { name: "get:users.list", description: "List users" },
363
+ ],
364
+ }),
365
+ resources: async () => ({ result: { resources: [] } }),
366
+ },
367
+ } as unknown as Adk;
368
+
369
+ const result = await materializeRef(adk, "test", configDir);
370
+ expect(result.toolCount).toBe(2);
371
+
372
+ // Check filenames are sanitized
373
+ const toolsDir = join(configDir, "refs", "test", "tools");
374
+ expect(existsSync(join(toolsDir, "API-post-search_v2.tool.json"))).toBe(true);
375
+ expect(existsSync(join(toolsDir, "get_users_list.tool.json"))).toBe(true);
376
+
377
+ // Check .d.ts handles them
378
+ const dts = readFileSync(join(configDir, "refs", "test", "types", "test.d.ts"), "utf-8");
379
+ expect(dts).toContain('"API-post-search/v2"');
380
+ expect(dts).toContain('"get:users.list"');
381
+ });
382
+
383
+ test("handles inspect failure gracefully", async () => {
384
+ const configDir = join(TEST_DIR, "fail-test");
385
+
386
+ const adk = {
387
+ ref: {
388
+ inspect: async () => { throw new Error("Not authenticated"); },
389
+ resources: async () => { throw new Error("Not authenticated"); },
390
+ },
391
+ } as unknown as Adk;
392
+
393
+ const result = await materializeRef(adk, "failing", configDir);
394
+ expect(result.toolCount).toBe(0);
395
+ expect(result.skillCount).toBe(0);
396
+ expect(result.typesGenerated).toBe(false);
397
+ });
398
+
399
+ test("pascalCase conversion works for type name", async () => {
400
+ const configDir = join(TEST_DIR, "pascal-test");
401
+
402
+ const adk = {
403
+ ref: {
404
+ inspect: async () => ({
405
+ tools: [{ name: "test", description: "Test" }],
406
+ }),
407
+ resources: async () => ({ result: { resources: [] } }),
408
+ },
409
+ } as unknown as Adk;
410
+
411
+ await materializeRef(adk, "my-cool-agent", configDir);
412
+
413
+ const dts = readFileSync(
414
+ join(configDir, "refs", "my-cool-agent", "types", "my-cool-agent.d.ts"),
415
+ "utf-8",
416
+ );
417
+ expect(dts).toContain("export interface MyCoolAgentTools");
418
+ });
419
+ });
package/src/init.ts ADDED
@@ -0,0 +1,211 @@
1
+ /**
2
+ * ADK Init — Setup + skill injection for coding agents.
3
+ *
4
+ * Uses a preset-based system for scalability:
5
+ * - Presets are JSON files in src/presets/ (one per coding agent)
6
+ * - All use the agentskills.io SKILL.md standard
7
+ * - Adding a new coding agent = adding one JSON file
8
+ *
9
+ * Non-interactive by design. The coding agent is the UX layer.
10
+ */
11
+
12
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
13
+ import { dirname, join, resolve } from "node:path";
14
+ import { homedir } from "node:os";
15
+ import type { Adk } from "./config-store.js";
16
+
17
+ // ============================================
18
+ // Types
19
+ // ============================================
20
+
21
+ export interface Preset {
22
+ name: string;
23
+ defaultPath: string;
24
+ filename: string;
25
+ }
26
+
27
+ export interface SkillTarget {
28
+ preset: Preset;
29
+ path: string;
30
+ }
31
+
32
+ // ============================================
33
+ // Preset Loading
34
+ // ============================================
35
+
36
+ // Presets are inlined to avoid import.meta/CJS compatibility issues.
37
+ // To add a new coding agent, add an entry here and a JSON file in src/presets/.
38
+ const BUILTIN_PRESETS: Preset[] = [
39
+ { name: "claude", defaultPath: "~/.claude/skills", filename: "adk/SKILL.md" },
40
+ { name: "cursor", defaultPath: ".agents/skills", filename: "adk/SKILL.md" },
41
+ { name: "codex", defaultPath: ".agents/skills", filename: "adk/SKILL.md" },
42
+ { name: "copilot", defaultPath: ".github/skills", filename: "adk/SKILL.md" },
43
+ { name: "windsurf", defaultPath: ".windsurf/skills", filename: "adk/SKILL.md" },
44
+ { name: "hermes", defaultPath: "~/.hermes/skills", filename: "adk/SKILL.md" },
45
+ ];
46
+
47
+ let _presets: Map<string, Preset> | null = null;
48
+
49
+ export function loadPresets(): Map<string, Preset> {
50
+ if (_presets) return _presets;
51
+ _presets = new Map();
52
+ for (const preset of BUILTIN_PRESETS) {
53
+ _presets.set(preset.name, preset);
54
+ }
55
+ return _presets;
56
+ }
57
+
58
+ export function getPreset(name: string): Preset | undefined {
59
+ return loadPresets().get(name);
60
+ }
61
+
62
+ export function listPresets(): Preset[] {
63
+ return Array.from(loadPresets().values());
64
+ }
65
+
66
+ // ============================================
67
+ // Skill Content Templates
68
+ // ============================================
69
+
70
+ const ADK_SKILL_CONTENT = `adk is the Agent Development Kit CLI.
71
+
72
+ Config location: run \`adk config-path\` to find the config directory.
73
+ Installed refs: run \`adk ref list\` to see configured agents.
74
+
75
+ Each installed ref has local docs at the config path under refs/<name>/:
76
+ - tools/*.tool.json — full tool schemas (inputSchema, description)
77
+ - skills/ — usage guides and patterns from the agent
78
+ - types/*.d.ts — TypeScript type definitions
79
+
80
+ ## Commands
81
+
82
+ To call a tool: \`adk ref call <name> <tool> '{"param": "value"}'\`
83
+ To browse available agents: \`adk registry browse slash\`
84
+ To install an agent: \`adk ref add <name>\`
85
+ To authenticate: \`adk ref auth <name>\`
86
+ To check auth status: \`adk ref auth-status <name>\`
87
+ To inspect an agent: \`adk ref inspect <name>\`
88
+ To view tool schemas: \`adk ref inspect <name> --full\`
89
+ To list resources: \`adk ref resources <name>\`
90
+ To read a resource: \`adk ref read <name> <uri>\`
91
+ `;
92
+
93
+ export function renderContent(
94
+ content: string,
95
+ meta: { name: string; description: string },
96
+ ): string {
97
+ return `---
98
+ name: ${meta.name}
99
+ description: ${meta.description}
100
+ ---
101
+ ${content}`;
102
+ }
103
+
104
+ // ============================================
105
+ // Target Parsing
106
+ // ============================================
107
+
108
+ function expandHome(p: string): string {
109
+ if (p.startsWith("~/")) return join(homedir(), p.slice(2));
110
+ return p;
111
+ }
112
+
113
+ export function parseTarget(value: string): SkillTarget {
114
+ const colonIdx = value.indexOf(":");
115
+
116
+ if (colonIdx === -1) {
117
+ // Just a preset name
118
+ const preset = getPreset(value);
119
+ if (!preset) {
120
+ const presetNames = Array.from(loadPresets().keys()).join(", ");
121
+ throw new Error(`Unknown preset: ${value}. Available: ${presetNames}`);
122
+ }
123
+ return { preset, path: expandHome(preset.defaultPath) };
124
+ }
125
+
126
+ const key = value.slice(0, colonIdx);
127
+ const path = value.slice(colonIdx + 1);
128
+
129
+ // Check if key is a preset name (with custom path)
130
+ const preset = getPreset(key);
131
+ if (preset) {
132
+ return { preset, path: expandHome(path || preset.defaultPath) };
133
+ }
134
+
135
+ const presetNames = Array.from(loadPresets().keys()).join(", ");
136
+ throw new Error(`Unknown target: ${key}. Available presets: ${presetNames}`);
137
+ }
138
+
139
+ // ============================================
140
+ // Skill Installation
141
+ // ============================================
142
+
143
+ function ensureWrite(path: string, content: string): void {
144
+ const dir = dirname(path);
145
+ if (!existsSync(dir)) {
146
+ mkdirSync(dir, { recursive: true });
147
+ }
148
+ writeFileSync(path, content, "utf-8");
149
+ }
150
+
151
+ export function installSkill(target: SkillTarget): string {
152
+ const outputPath = join(resolve(target.path), target.preset.filename);
153
+ const content = renderContent(ADK_SKILL_CONTENT, {
154
+ name: "adk",
155
+ description: "When connecting to APIs, calling remote tools, managing integrations, or the user asks about adk",
156
+ });
157
+ ensureWrite(outputPath, content);
158
+ return outputPath;
159
+ }
160
+
161
+
162
+ // ============================================
163
+ // Init Command
164
+ // ============================================
165
+
166
+ const DEFAULT_REGISTRY_URL = "https://registry.slash.com";
167
+ const DEFAULT_REGISTRY_NAME = "public";
168
+
169
+ export async function runInit(adk: Adk, targets: SkillTarget[]): Promise<void> {
170
+ // 1. Ensure default registry
171
+ const registries = await adk.registry.list();
172
+ const hasDefault = registries.some(
173
+ (r) => r.url === DEFAULT_REGISTRY_URL || r.name === DEFAULT_REGISTRY_NAME,
174
+ );
175
+ if (!hasDefault) {
176
+ await adk.registry.add({ url: DEFAULT_REGISTRY_URL, name: DEFAULT_REGISTRY_NAME });
177
+ console.log(`\x1b[32m\u2713\x1b[0m Set default registry: ${DEFAULT_REGISTRY_URL}`);
178
+ } else {
179
+ console.log(`\x1b[32m\u2713\x1b[0m Default registry already configured: ${DEFAULT_REGISTRY_URL}`);
180
+ }
181
+
182
+ // 2. Install skills to targets
183
+ if (targets.length === 0) {
184
+ console.log(`\nTo install skills, re-run with targets:\n`);
185
+ const presets = listPresets();
186
+ for (const preset of presets) {
187
+ console.log(` adk init --target ${preset.name}`);
188
+ }
189
+ console.log(`\nCustom path: adk init --target <preset>:<path>`);
190
+ console.log(`\nAsk the user which coding agents they use,`);
191
+ console.log(`then run: adk init --target <preset> [--target <preset> ...]`);
192
+ return;
193
+ }
194
+
195
+ for (const target of targets) {
196
+ const outputPath = installSkill(target);
197
+ console.log(`\x1b[32m\u2713\x1b[0m Installed adk skill \u2192 ${outputPath}`);
198
+ }
199
+
200
+ // 3. Save targets to config
201
+ const config = await adk.readConfig();
202
+ const targetStrings = targets.map((t) => {
203
+ return t.path === expandHome(t.preset.defaultPath) ? t.preset.name : `${t.preset.name}:${t.path}`;
204
+ });
205
+ (config as any).targets = targetStrings;
206
+ await adk.writeConfig(config);
207
+ console.log(`\x1b[32m\u2713\x1b[0m Saved targets to config`);
208
+
209
+ console.log(`\nDone! Your coding agents now know how to use adk.`);
210
+ console.log(`Run \`adk init\` again anytime to refresh skills or add targets.`);
211
+ }