@slashfi/agents-sdk 0.79.0 → 0.81.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/src/adk.ts CHANGED
@@ -22,8 +22,9 @@
22
22
  * ```
23
23
  */
24
24
 
25
- import { join } from "node:path";
25
+ import { readFileSync } from "node:fs";
26
26
  import { homedir } from "node:os";
27
+ import { join } from "node:path";
27
28
  import { createAdk } from "./config-store.js";
28
29
  import { createLocalFsStore, getLocalEncryptionKey } from "./local-fs.js";
29
30
  import type { Adk } from "./config-store.js";
@@ -31,6 +32,12 @@ import { AdkError, getError, getRecentErrors } from "./adk-error.js";
31
32
  import { runInit, parseTarget } from "./init.js";
32
33
  import { materializeRef, syncAllRefs } from "./materialize.js";
33
34
  import { adkCheck } from "./adk-check.js";
35
+ import {
36
+ refsRootExists,
37
+ renderResults,
38
+ searchRefs,
39
+ writeSearchIndex,
40
+ } from "./search.js";
34
41
 
35
42
  const args = process.argv.slice(2);
36
43
  const command = args[0];
@@ -39,6 +46,24 @@ const command = args[0];
39
46
  // Helpers
40
47
  // ============================================
41
48
 
49
+ /**
50
+ * Read the SDK's published version from the sibling package.json.
51
+ * Resolved at runtime so a single source-of-truth lives in the manifest.
52
+ * Safe for both `bun src/adk.ts` (dev) and the npm-installed bin (which
53
+ * still runs through bun via the shebang).
54
+ */
55
+ function getCliVersion(): string {
56
+ try {
57
+ const pkgPath = join(import.meta.dir, "..", "package.json");
58
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
59
+ version?: string;
60
+ };
61
+ return pkg.version ?? "unknown";
62
+ } catch {
63
+ return "unknown";
64
+ }
65
+ }
66
+
42
67
  function getArg(flag: string): string | undefined {
43
68
  const idx = args.indexOf(flag);
44
69
  if (idx === -1 || idx + 1 >= args.length) return undefined;
@@ -95,10 +120,12 @@ adk — Agent Development Kit
95
120
  Usage:
96
121
  adk init [--target <agent>:<path>] Setup + install skills for coding agents
97
122
  adk sync [--ref <name>] Materialize tool docs for all refs in config
123
+ adk search <query> [options] BM25 search over materialized refs/tools
98
124
  adk registry <op> [options] Manage registry connections
99
125
  adk ref <op> [options] Manage agent refs
100
126
  adk config-path Print config directory path
101
127
  adk error [id] View recent errors or a specific error
128
+ adk version | --version | -v Print the installed adk SDK version
102
129
 
103
130
  Registry operations:
104
131
  adk registry add <url> --name <name> [--auth-type bearer|api-key|none] [--proxy [--proxy-agent @config]]
@@ -135,6 +162,15 @@ Environment:
135
162
  ADK_TOKEN Bearer token for authenticated registries
136
163
  ADK_ENCRYPTION_KEY Override encryption key (default: auto from ~/.adk/.encryption-key)
137
164
 
165
+ Search options:
166
+ adk search <query> [--json] [--limit N] [--ref <name>] [--tools-only] [--refs-only]
167
+ BM25 over ~/.adk/refs/* — index includes
168
+ ref names, descriptions, tool names,
169
+ tool docs, parameter names, and skill
170
+ resources. Reads ~/.adk/.search-index.json
171
+ when present (rebuilt by \`adk sync\`),
172
+ otherwise walks refs/* on the fly.
173
+
138
174
  Examples:
139
175
  adk init --target claude --target cursor --target codex
140
176
  adk registry add https://registry.slash.com --name public
@@ -142,6 +178,8 @@ Examples:
142
178
  adk ref add notion --registry public
143
179
  adk ref inspect notion --full
144
180
  adk ref call notion notion-search '{"query":"hello"}'
181
+ adk search "schedule reminder" --json
182
+ adk search "email unread inbox" --tools-only --limit 5
145
183
  `);
146
184
  }
147
185
 
@@ -575,6 +613,58 @@ switch (command) {
575
613
  for (const f of failed) console.log(` ${f.name}: ${f.error}`);
576
614
  }
577
615
  console.log(`\nDocs written to: ${configDir}/refs/`);
616
+ // Persist the BM25 search index so `adk search` can skip the
617
+ // recursive ref walk on every query. Best-effort — failure here
618
+ // shouldn't fail `adk sync` since the search path falls back to a
619
+ // fresh walk.
620
+ try {
621
+ const { path, documentCount } = writeSearchIndex(configDir);
622
+ console.log(`Search index: ${path} (${documentCount} docs)`);
623
+ } catch (err) {
624
+ console.log(
625
+ `\x1b[33m!\x1b[0m Failed to write search index: ${err instanceof Error ? err.message : String(err)}`,
626
+ );
627
+ }
628
+ break;
629
+ }
630
+ case "search": {
631
+ const configDir = process.env.ADK_CONFIG_DIR ?? join(homedir(), ".adk");
632
+ const refsRoot = join(configDir, "refs");
633
+ // Positional query — first non-flag argv after the `search` command.
634
+ const query = args
635
+ .filter((a) => !a.startsWith("--"))
636
+ .slice(1)
637
+ .join(" ")
638
+ .trim();
639
+ if (!query) {
640
+ console.log("Usage: adk search \"<query>\" [--json] [--limit N] [--ref name] [--tools-only] [--refs-only]");
641
+ process.exit(1);
642
+ }
643
+ if (!refsRootExists(refsRoot)) {
644
+ const msg = `No materialized refs found at ${refsRoot}. Run \`adk sync\` first.`;
645
+ if (hasFlag("--json")) {
646
+ console.log(JSON.stringify({ error: msg, results: [] }));
647
+ } else {
648
+ console.log(msg);
649
+ }
650
+ break;
651
+ }
652
+ const limitArg = getArg("--limit");
653
+ const limit = limitArg ? Number.parseInt(limitArg, 10) : undefined;
654
+ const ref = getArg("--ref");
655
+ const toolsOnly = hasFlag("--tools-only");
656
+ const refsOnly = hasFlag("--refs-only");
657
+ const results = searchRefs(refsRoot, query, {
658
+ ...(limit !== undefined && Number.isFinite(limit) && { limit }),
659
+ ...(ref && { ref }),
660
+ ...(toolsOnly && { toolsOnly: true }),
661
+ ...(refsOnly && { refsOnly: true }),
662
+ });
663
+ if (hasFlag("--json")) {
664
+ console.log(JSON.stringify(results, null, 2));
665
+ } else {
666
+ console.log(renderResults(results));
667
+ }
578
668
  break;
579
669
  }
580
670
  case "config-path": {
@@ -619,6 +709,11 @@ switch (command) {
619
709
  const result = await adkCheck({ file, code, run: isRun, noCheck });
620
710
  process.exit(result.exitCode);
621
711
  }
712
+ case "--version":
713
+ case "-v":
714
+ case "version":
715
+ console.log(getCliVersion());
716
+ break;
622
717
  case "--help":
623
718
  case "-h":
624
719
  case undefined:
@@ -1307,5 +1307,61 @@ describe("isRefAuthComplete + cached authFields", () => {
1307
1307
  );
1308
1308
  expect(result).toBe(true);
1309
1309
  });
1310
+
1311
+ test("resolvableFields satisfies required, non-automated fields absent from config", async () => {
1312
+ // Scenario: registry-hosted OAuth where the platform injects
1313
+ // client_id / client_secret at runtime via resolveCredentials.
1314
+ // The registry sees them as user-provided (required + non-automated)
1315
+ // but the consumer environment satisfies them externally.
1316
+ const { isRefAuthComplete } = await import("./config-store");
1317
+ const result = isRefAuthComplete(
1318
+ {
1319
+ ref: "google-gmail",
1320
+ name: "google-gmail",
1321
+ scheme: "registry",
1322
+ config: {
1323
+ // client_id / client_secret missing — resolved from env vars.
1324
+ access_token: "tok",
1325
+ },
1326
+ },
1327
+ {
1328
+ ref: "google-gmail",
1329
+ fetchedAt: new Date().toISOString(),
1330
+ authFields: {
1331
+ client_id: { required: true, automated: false },
1332
+ client_secret: { required: true, automated: false },
1333
+ access_token: { required: true, automated: true },
1334
+ },
1335
+ },
1336
+ { resolvableFields: ["client_id", "client_secret"] },
1337
+ );
1338
+ expect(result).toBe(true);
1339
+ });
1340
+
1341
+ test("resolvableFields does not bypass missing fields it doesn't list", async () => {
1342
+ const { isRefAuthComplete } = await import("./config-store");
1343
+ const result = isRefAuthComplete(
1344
+ {
1345
+ ref: "@oauth",
1346
+ name: "@oauth",
1347
+ scheme: "https",
1348
+ url: "http://localhost",
1349
+ config: {
1350
+ client_id: "abc",
1351
+ // client_secret missing AND not listed as resolvable.
1352
+ },
1353
+ },
1354
+ {
1355
+ ref: "@oauth",
1356
+ fetchedAt: new Date().toISOString(),
1357
+ authFields: {
1358
+ client_id: { required: true, automated: false },
1359
+ client_secret: { required: true, automated: false },
1360
+ },
1361
+ },
1362
+ { resolvableFields: ["client_id"] },
1363
+ );
1364
+ expect(result).toBe(false);
1365
+ });
1310
1366
  });
1311
1367
 
@@ -124,6 +124,25 @@ export interface RegistryCache {
124
124
  refs: Record<string, RegistryCacheEntry>;
125
125
  }
126
126
 
127
+ /**
128
+ * Options for `isRefAuthComplete`.
129
+ */
130
+ export interface RefAuthCompleteOptions {
131
+ /**
132
+ * Field names the consumer can resolve at call time without them
133
+ * being present in `entry.config` — typically OAuth client_id /
134
+ * client_secret resolved from environment variables or platform
135
+ * config by the host's `resolveCredentials` callback.
136
+ *
137
+ * Required-non-automated fields listed here count as satisfied even
138
+ * when missing from `entry.config`. The default behaviour (no opt
139
+ * passed) requires every such field to live in config, which is
140
+ * correct for self-hosted SDK consumers but wrong for platforms
141
+ * that inject OAuth client credentials at runtime.
142
+ */
143
+ resolvableFields?: ReadonlyArray<string>;
144
+ }
145
+
127
146
  /**
128
147
  * "Is this ref ready to call?" answered locally using the cached
129
148
  * security-scheme requirements. Mirrors the `complete` boolean
@@ -138,10 +157,11 @@ export interface RegistryCache {
138
157
  * signaling "I don't know — caller should fall back to its own
139
158
  * heuristic or call `auth-status` to populate the cache".
140
159
  * - Cache hit → for every required, non-automated field, checks
141
- * presence in `entry.config`. Mirrors the `present || resolvable`
142
- * check in `auth-status` but evaluates against current config.
143
- * `automated` fields (e.g. dynamic OAuth client_id) count as
144
- * satisfied even when absent adk supplies them at call time.
160
+ * presence in `entry.config` OR (if `opts.resolvableFields`
161
+ * includes the field name) treats it as satisfied externally.
162
+ * Mirrors the `present || resolvable` check in `auth-status`.
163
+ * `automated` fields (e.g. dynamic OAuth client_id minted by the
164
+ * registry) always count as satisfied.
145
165
  *
146
166
  * Returning `null` for cache miss is intentional. A boolean would
147
167
  * force callers to choose a default that's wrong half the time;
@@ -150,16 +170,23 @@ export interface RegistryCache {
150
170
  export function isRefAuthComplete(
151
171
  entry: RefEntry,
152
172
  cacheEntry: RegistryCacheEntry | undefined,
173
+ opts?: RefAuthCompleteOptions,
153
174
  ): boolean | null {
154
175
  if (typeof entry === "string") return false;
155
176
  if ((entry as { mode?: unknown }).mode === "proxy") return true;
156
177
  const authFields = cacheEntry?.authFields;
157
178
  if (!authFields) return null;
158
179
  const config = entry.config ?? {};
180
+ const resolvable =
181
+ opts?.resolvableFields && opts.resolvableFields.length > 0
182
+ ? new Set(opts.resolvableFields)
183
+ : null;
159
184
  for (const [field, info] of Object.entries(authFields)) {
160
185
  if (!info.required) continue;
161
186
  if (info.automated) continue;
162
- if (!(field in config)) return false;
187
+ if (field in config) continue;
188
+ if (resolvable && resolvable.has(field)) continue;
189
+ return false;
163
190
  }
164
191
  return true;
165
192
  }
@@ -288,20 +288,58 @@ export async function materializeRef(
288
288
  }
289
289
 
290
290
  // 2. Fetch and write resources (skills)
291
+ //
292
+ // `list_resources` returns URIs only — content is omitted to keep the
293
+ // listing payload small. We have to follow up with `read_resources(uris)`
294
+ // to actually fetch the body. Then the per-resource field is `content`,
295
+ // not `text` (per `CallAgentReadResourcesResponse`).
296
+ //
297
+ // Response shape varies depending on the call path: direct calls return
298
+ // `{success, agentPath, resources}` while proxied calls return
299
+ // `{success, result: {success, agentPath, resources}}` (the proxy wraps
300
+ // the inner registry response). Unwrap both shapes the same way.
291
301
  try {
292
- const resourcesResult = await adk.ref.resources(refName);
293
- const response = resourcesResult as any;
294
- if (response?.result?.resources) {
295
- for (const resource of response.result.resources) {
296
- if (resource.uri && resource.text) {
297
- const filename = resource.uri.split("/").pop() ?? "resource.md";
298
- ensureWrite(join(skillsDir, filename), resource.text);
299
- skillCount++;
300
- }
302
+ type ResourceListEntry = {
303
+ uri?: string;
304
+ name?: string;
305
+ mimeType?: string;
306
+ };
307
+ type ResourceReadEntry = ResourceListEntry & {
308
+ content?: string;
309
+ error?: string;
310
+ };
311
+ const unwrapResources = <T>(raw: unknown): T[] => {
312
+ const r = raw as Record<string, unknown> | null | undefined;
313
+ if (!r) return [];
314
+ if (Array.isArray(r.resources)) return r.resources as T[];
315
+ const inner = r.result as Record<string, unknown> | undefined;
316
+ if (inner && Array.isArray(inner.resources))
317
+ return inner.resources as T[];
318
+ return [];
319
+ };
320
+
321
+ const listed = unwrapResources<ResourceListEntry>(
322
+ await adk.ref.resources(refName),
323
+ );
324
+ const uris = listed
325
+ .map((r) => r.uri)
326
+ .filter((u): u is string => typeof u === "string" && u.length > 0);
327
+
328
+ if (uris.length > 0) {
329
+ const fetched = unwrapResources<ResourceReadEntry>(
330
+ await adk.ref.read(refName, uris),
331
+ );
332
+ for (const resource of fetched) {
333
+ if (!resource.uri) continue;
334
+ if (typeof resource.content !== "string") continue;
335
+ const filename = resource.uri.split("/").pop() || "resource.md";
336
+ ensureWrite(join(skillsDir, filename), resource.content);
337
+ skillCount++;
301
338
  }
302
339
  }
303
340
  } catch {
304
- // resources fetch failed — might not be supported
341
+ // resources fetch failed — registry might not support resources, or
342
+ // ref isn't authenticated yet. Best-effort only.
305
343
  }
306
344
 
307
345
  return { toolCount, skillCount, typesGenerated, docsGenerated };