@slashfi/agents-sdk 0.67.2 → 0.69.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.
@@ -1,16 +1,25 @@
1
1
  /**
2
2
  * Ref Materialization — Download agent docs to local filesystem.
3
3
  *
4
- * When `adk ref add` succeeds, materialize the agent's tool schemas,
5
- * resources (skills), and generate TypeScript type definitions locally.
4
+ * When `adk ref add` or `adk sync` runs, materialize the agent's tool schemas,
5
+ * resources (skills), and generate readable markdown docs locally.
6
6
  *
7
- * Files are written under the adk config dir at refs/<name>/.
7
+ * Output format:
8
+ * refs/<name>/
9
+ * agent.json — metadata (name, description, tool list)
10
+ * entrypoint.md — agent overview with tool summary
11
+ * tools/<tool>.tool.md — per-tool docs with parameter tables
12
+ * tools/<tool>.tool.json — raw JSON schemas
13
+ * types/<name>.d.ts — TypeScript type stubs
14
+ * skills/ — resources from the agent
15
+ *
16
+ * The .tool.md files are the primary output — designed for LLMs to read.
8
17
  */
9
18
 
10
19
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
11
20
  import { dirname, join } from "node:path";
12
21
  import type { Adk } from "./config-store.js";
13
- // No init imports needed materialize is self-contained
22
+ import type { JsonSchema } from "./types.js";
14
23
 
15
24
  // ============================================
16
25
  // Types
@@ -19,18 +28,25 @@ import type { Adk } from "./config-store.js";
19
28
  interface ToolSchema {
20
29
  name: string;
21
30
  description?: string;
22
- inputSchema?: Record<string, unknown>;
31
+ inputSchema?: JsonSchema;
23
32
  outputSchema?: Record<string, unknown>;
24
33
  }
25
34
 
26
- interface MaterializeResult {
35
+ export interface MaterializeResult {
27
36
  toolCount: number;
28
37
  skillCount: number;
29
38
  typesGenerated: boolean;
39
+ docsGenerated: boolean;
40
+ }
41
+
42
+ export interface SyncResult {
43
+ refs: Array<{ name: string; result: MaterializeResult; error?: string }>;
44
+ totalTools: number;
45
+ totalSkills: number;
30
46
  }
31
47
 
32
48
  // ============================================
33
- // Helpers
49
+ // File Helpers
34
50
  // ============================================
35
51
 
36
52
  function ensureWrite(path: string, content: string): void {
@@ -41,27 +57,163 @@ function ensureWrite(path: string, content: string): void {
41
57
  writeFileSync(path, content, "utf-8");
42
58
  }
43
59
 
60
+ function toKebabCase(name: string): string {
61
+ return name
62
+ .replace(/[^a-zA-Z0-9]+/g, "-")
63
+ .replace(/^-|-$/g, "")
64
+ .toLowerCase();
65
+ }
66
+
67
+ function pascalCase(s: string): string {
68
+ return s
69
+ .replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
70
+ .replace(/^./, (c) => c.toUpperCase());
71
+ }
72
+
73
+ // ============================================
74
+ // JSON Schema → TypeScript type string
75
+ // ============================================
76
+
77
+ function jsonSchemaToTsType(schema: JsonSchema): string {
78
+ if (!schema) return "unknown";
79
+
80
+ const s = schema as any;
81
+
82
+ // const literal
83
+ if (s.const !== undefined) return JSON.stringify(s.const);
84
+
85
+ // enum
86
+ if (schema.enum) return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
87
+
88
+ // oneOf / anyOf → union
89
+ if (s.oneOf) return (s.oneOf as JsonSchema[]).map(jsonSchemaToTsType).join(" | ");
90
+ if (s.anyOf) return (s.anyOf as JsonSchema[]).map(jsonSchemaToTsType).join(" | ");
91
+
92
+ // allOf → intersection
93
+ if (s.allOf) return (s.allOf as JsonSchema[]).map(jsonSchemaToTsType).join(" & ");
94
+
95
+ // $ref
96
+ if (s.$ref) {
97
+ const match = (s.$ref as string).match(/\/([^/]+)$/);
98
+ return match ? match[1] : "unknown";
99
+ }
100
+
101
+ switch (schema.type) {
102
+ case "string":
103
+ return s.format ? `string /* ${s.format} */` : "string";
104
+ case "integer":
105
+ return "number /* integer */";
106
+ case "number":
107
+ return "number";
108
+ case "boolean":
109
+ return "boolean";
110
+ case "null":
111
+ return "null";
112
+ case "array":
113
+ return schema.items ? `${jsonSchemaToTsType(schema.items as JsonSchema)}[]` : "unknown[]";
114
+ case "object": {
115
+ if (schema.properties) {
116
+ const required = new Set((schema.required as string[]) ?? []);
117
+ const props = Object.entries(schema.properties)
118
+ .map(([k, v]) => `${k}${required.has(k) ? "" : "?"}: ${jsonSchemaToTsType(v as JsonSchema)}`)
119
+ .join("; ");
120
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
121
+ const addlType = jsonSchemaToTsType(schema.additionalProperties as JsonSchema);
122
+ return props ? `{ ${props}; [key: string]: ${addlType} }` : `Record<string, ${addlType}>`;
123
+ }
124
+ return `{ ${props} }`;
125
+ }
126
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
127
+ return `Record<string, ${jsonSchemaToTsType(schema.additionalProperties as JsonSchema)}>`;
128
+ }
129
+ return "Record<string, unknown>";
130
+ }
131
+ default:
132
+ if (Array.isArray(schema.type)) {
133
+ return (schema.type as string[]).map((t) => (t === "null" ? "null" : t === "integer" ? "number" : t)).join(" | ");
134
+ }
135
+ return "unknown";
136
+ }
137
+ }
138
+
139
+ // ============================================
140
+ // Markdown Generation
141
+ // ============================================
142
+
143
+ /** Generate a .tool.md file — readable tool docs with parameter tables. */
144
+ function generateToolMd(tool: ToolSchema): string {
145
+ const schema = tool.inputSchema ?? ({ type: "object", properties: {} } as JsonSchema);
146
+ const lines: string[] = [];
147
+
148
+ lines.push(`# ${tool.name}`);
149
+ lines.push("");
150
+ if (tool.description) {
151
+ lines.push(tool.description);
152
+ lines.push("");
153
+ }
154
+
155
+ const props = (schema.properties ?? {}) as Record<string, JsonSchema>;
156
+ const required = new Set((schema.required as string[]) ?? []);
157
+ const paramNames = Object.keys(props);
158
+
159
+ if (paramNames.length > 0) {
160
+ lines.push("## Parameters");
161
+ lines.push("");
162
+ lines.push("| Name | Type | Required | Description |");
163
+ lines.push("|------|------|----------|-------------|");
164
+ for (const name of paramNames) {
165
+ const prop = props[name];
166
+ const type = jsonSchemaToTsType(prop);
167
+ const req = required.has(name) ? "✓" : "";
168
+ const desc = (prop.description ?? "").replace(/\|/g, "\\|");
169
+ lines.push(`| ${name} | \`${type}\` | ${req} | ${desc} |`);
170
+ }
171
+ lines.push("");
172
+ }
173
+
174
+ return lines.join("\n");
175
+ }
176
+
177
+ /** Generate entrypoint.md — agent overview with tool listing. */
178
+ function generateEntrypoint(refName: string, description: string | undefined, tools: ToolSchema[]): string {
179
+ const lines: string[] = [];
180
+ lines.push(`# ${refName}`);
181
+ lines.push("");
182
+ if (description) {
183
+ lines.push(description);
184
+ lines.push("");
185
+ }
186
+ lines.push(`## Available Tools (${tools.length})`);
187
+ lines.push("");
188
+ for (const tool of tools) {
189
+ lines.push(`- **${tool.name}**: ${tool.description ?? "No description"}`);
190
+ }
191
+ lines.push("");
192
+ lines.push(`> Auto-generated by \`adk sync\` at ${new Date().toISOString()}`);
193
+ lines.push("");
194
+ return lines.join("\n");
195
+ }
196
+
44
197
  /** Generate a .d.ts file from tool schemas. */
45
198
  function generateTypes(refName: string, tools: ToolSchema[]): string {
46
199
  const lines: string[] = [
47
- `// Auto-generated by adk ref add`,
200
+ `// Auto-generated by adk sync`,
48
201
  `// Agent: ${refName}`,
49
202
  `// Tools: ${tools.length}`,
50
203
  ``,
51
204
  `export interface ${pascalCase(refName)}Tools {`,
52
205
  ];
53
-
54
206
  for (const tool of tools) {
207
+ const paramsType = tool.inputSchema
208
+ ? jsonSchemaToTsType(tool.inputSchema)
209
+ : "Record<string, unknown>";
55
210
  lines.push(` /** ${tool.description ?? tool.name} */`);
56
211
  lines.push(` ${JSON.stringify(tool.name)}: {`);
57
212
  lines.push(` name: ${JSON.stringify(tool.name)};`);
58
- if (tool.description) {
59
- lines.push(` description: ${JSON.stringify(tool.description)};`);
60
- }
61
- lines.push(` params: Record<string, unknown>;`);
213
+ if (tool.description) lines.push(` description: ${JSON.stringify(tool.description)};`);
214
+ lines.push(` params: ${paramsType};`);
62
215
  lines.push(` };`);
63
216
  }
64
-
65
217
  lines.push(`}`);
66
218
  lines.push(``);
67
219
  lines.push(`export declare const tools: (keyof ${pascalCase(refName)}Tools)[];`);
@@ -69,14 +221,8 @@ function generateTypes(refName: string, tools: ToolSchema[]): string {
69
221
  return lines.join("\n");
70
222
  }
71
223
 
72
- function pascalCase(s: string): string {
73
- return s
74
- .replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
75
- .replace(/^./, (c) => c.toUpperCase());
76
- }
77
-
78
224
  // ============================================
79
- // Materialize
225
+ // Materialize a single ref
80
226
  // ============================================
81
227
 
82
228
  export async function materializeRef(
@@ -92,8 +238,9 @@ export async function materializeRef(
92
238
  let toolCount = 0;
93
239
  let skillCount = 0;
94
240
  let typesGenerated = false;
241
+ let docsGenerated = false;
95
242
 
96
- // 1. Fetch and write tool schemas
243
+ // 1. Fetch tool schemas and generate docs
97
244
  try {
98
245
  const info = await adk.ref.inspect(refName, { full: true });
99
246
  if (info?.tools && info.tools.length > 0) {
@@ -104,15 +251,18 @@ export async function materializeRef(
104
251
  outputSchema: t.outputSchema,
105
252
  }));
106
253
 
254
+ // Write .tool.md files (primary output — readable by LLMs)
107
255
  for (const tool of tools) {
108
- const safeName = tool.name.replace(/[^a-zA-Z0-9_-]/g, "_");
109
- ensureWrite(
110
- join(toolsDir, `${safeName}.tool.json`),
111
- JSON.stringify(tool, null, 2),
112
- );
256
+ const safeName = toKebabCase(tool.name);
257
+ ensureWrite(join(toolsDir, `${safeName}.tool.md`), generateToolMd(tool));
258
+ ensureWrite(join(toolsDir, `${safeName}.tool.json`), JSON.stringify(tool, null, 2));
113
259
  }
114
260
  toolCount = tools.length;
115
261
 
262
+ // Write entrypoint.md
263
+ ensureWrite(join(refDir, "entrypoint.md"), generateEntrypoint(refName, info.description, tools));
264
+ docsGenerated = true;
265
+
116
266
  // Write agent.json metadata
117
267
  ensureWrite(
118
268
  join(refDir, "agent.json"),
@@ -126,10 +276,7 @@ export async function materializeRef(
126
276
  );
127
277
 
128
278
  // Generate .d.ts
129
- ensureWrite(
130
- join(typesDir, `${refName}.d.ts`),
131
- generateTypes(refName, tools),
132
- );
279
+ ensureWrite(join(typesDir, `${refName}.d.ts`), generateTypes(refName, tools));
133
280
  typesGenerated = true;
134
281
  }
135
282
  } catch {
@@ -153,6 +300,69 @@ export async function materializeRef(
153
300
  // resources fetch failed — might not be supported
154
301
  }
155
302
 
156
- return { toolCount, skillCount, typesGenerated };
303
+ return { toolCount, skillCount, typesGenerated, docsGenerated };
157
304
  }
158
305
 
306
+ // ============================================
307
+ // Sync all refs from consumer config
308
+ // ============================================
309
+
310
+ const DEFAULT_CONCURRENCY = 3;
311
+
312
+ export async function syncAllRefs(
313
+ adk: Adk,
314
+ configDir: string,
315
+ opts?: { filter?: string; concurrency?: number; onProgress?: (name: string, status: string) => void },
316
+ ): Promise<SyncResult> {
317
+ const config = await adk.readConfig();
318
+ const refs = config.refs ?? [];
319
+
320
+ // Resolve ref names and filter
321
+ const names: string[] = [];
322
+ for (const refEntry of refs) {
323
+ const name = typeof refEntry === "string"
324
+ ? refEntry
325
+ : (refEntry as any).as ?? (refEntry as any).ref ?? (refEntry as any).name;
326
+ if (!name) continue;
327
+ if (opts?.filter && name !== opts.filter) continue;
328
+ names.push(name);
329
+ }
330
+
331
+ const results: SyncResult["refs"] = [];
332
+ let totalTools = 0;
333
+ let totalSkills = 0;
334
+ const concurrency = opts?.concurrency ?? DEFAULT_CONCURRENCY;
335
+
336
+ // Process in batches of `concurrency`
337
+ for (let i = 0; i < names.length; i += concurrency) {
338
+ const batch = names.slice(i, i + concurrency);
339
+ const batchResults = await Promise.allSettled(
340
+ batch.map(async (name) => {
341
+ opts?.onProgress?.(name, "syncing");
342
+ try {
343
+ const result = await materializeRef(adk, name, configDir);
344
+ opts?.onProgress?.(name, `done (${result.toolCount} tools)`);
345
+ return { name, result };
346
+ } catch (err: any) {
347
+ const error = err?.message ?? String(err);
348
+ opts?.onProgress?.(name, `error: ${error}`);
349
+ return {
350
+ name,
351
+ result: { toolCount: 0, skillCount: 0, typesGenerated: false, docsGenerated: false } as MaterializeResult,
352
+ error,
353
+ };
354
+ }
355
+ }),
356
+ );
357
+
358
+ for (const settled of batchResults) {
359
+ if (settled.status === "fulfilled") {
360
+ results.push(settled.value);
361
+ totalTools += settled.value.result.toolCount;
362
+ totalSkills += settled.value.result.skillCount;
363
+ }
364
+ }
365
+ }
366
+
367
+ return { refs: results, totalTools, totalSkills };
368
+ }
package/src/types.ts CHANGED
@@ -204,17 +204,35 @@ export interface OAuth2SecurityScheme {
204
204
 
205
205
  /**
206
206
  * API key authentication.
207
- * Used by agents that wrap APIs using a single key
207
+ * Used by agents that wrap APIs using one or more keys
208
208
  * (e.g. OpenAI, Anthropic, Stripe, Datadog).
209
+ *
210
+ * @example
211
+ * // Single key (backwards-compatible)
212
+ * security: { type: 'apiKey', in: 'header', name: 'Authorization', prefix: 'Bearer' }
213
+ *
214
+ * // Multiple keys (e.g. Datadog)
215
+ * security: {
216
+ * type: 'apiKey',
217
+ * headers: {
218
+ * 'DD-API-KEY': { description: 'Your Datadog API key' },
219
+ * 'DD-APPLICATION-KEY': { description: 'Your Datadog application key' },
220
+ * }
221
+ * }
209
222
  */
210
223
  export interface ApiKeySecurityScheme {
211
224
  type: "apiKey";
212
- /** Where the key is sent */
213
- in: "header" | "query";
214
- /** Header or query parameter name (e.g. "X-API-Key", "Authorization") */
215
- name: string;
216
- /** Optional prefix (e.g. "Bearer" for Authorization header) */
225
+ /** Where the key is sent (single-key mode) */
226
+ in?: "header" | "query";
227
+ /** Header or query parameter name (single-key mode, e.g. "X-API-Key") */
228
+ name?: string;
229
+ /** Optional prefix (single-key mode, e.g. "Bearer") */
217
230
  prefix?: string;
231
+ /**
232
+ * Named headers the user must provide values for (multi-key mode).
233
+ * When present, this is the source of truth — `in`/`name`/`prefix` are ignored.
234
+ */
235
+ headers?: Record<string, { description?: string }>;
218
236
  }
219
237
 
220
238
  /**