@slashfi/agents-sdk 0.78.0 → 0.80.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/dist/adk.js +90 -1
- package/dist/adk.js.map +1 -1
- package/dist/cjs/config-store.js +75 -0
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/materialize.js +37 -10
- package/dist/cjs/materialize.js.map +1 -1
- package/dist/cjs/search.js +406 -0
- package/dist/cjs/search.js.map +1 -0
- package/dist/config-store.d.ts +60 -4
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +74 -0
- package/dist/config-store.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/materialize.d.ts.map +1 -1
- package/dist/materialize.js +37 -10
- package/dist/materialize.js.map +1 -1
- package/dist/search.d.ts +185 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +396 -0
- package/dist/search.js.map +1 -0
- package/package.json +1 -1
- package/src/adk.ts +96 -1
- package/src/config-store.test.ts +154 -0
- package/src/config-store.ts +115 -4
- package/src/index.ts +2 -0
- package/src/materialize.ts +48 -10
- package/src/search.test.ts +406 -0
- package/src/search.ts +541 -0
package/src/adk.ts
CHANGED
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import {
|
|
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:
|
package/src/config-store.test.ts
CHANGED
|
@@ -1155,3 +1155,157 @@ describe("ADK ref registry cache", () => {
|
|
|
1155
1155
|
expect(refs[0].description).toBeUndefined();
|
|
1156
1156
|
});
|
|
1157
1157
|
});
|
|
1158
|
+
|
|
1159
|
+
// ─── isRefAuthComplete + authFields cache ────────────────────────
|
|
1160
|
+
|
|
1161
|
+
describe("isRefAuthComplete + cached authFields", () => {
|
|
1162
|
+
/**
|
|
1163
|
+
* The core idea: `auth-status` knows what fields are required for a
|
|
1164
|
+
* given security scheme (it asks the registry). Cache that answer
|
|
1165
|
+
* shape per-ref so subsequent host-side "is this ref ready to call?"
|
|
1166
|
+
* checks can be evaluated locally with no network round-trip, and
|
|
1167
|
+
* stay accurate as the user fills in or clears credentials in the
|
|
1168
|
+
* entry's config.
|
|
1169
|
+
*
|
|
1170
|
+
* `isRefAuthComplete(entry, cacheEntry)` returns:
|
|
1171
|
+
* - `true` when all required fields are satisfied (present in
|
|
1172
|
+
* `entry.config` OR marked `automated`).
|
|
1173
|
+
* - `false` when at least one required, non-automated field is
|
|
1174
|
+
* missing.
|
|
1175
|
+
* - `null` when the cache has no `authFields` for this ref yet
|
|
1176
|
+
* (caller should fall back or refresh via `auth-status`).
|
|
1177
|
+
*/
|
|
1178
|
+
|
|
1179
|
+
test("cache miss returns null", async () => {
|
|
1180
|
+
const { isRefAuthComplete } = await import("./config-store");
|
|
1181
|
+
const result = isRefAuthComplete(
|
|
1182
|
+
{
|
|
1183
|
+
ref: "@unknown",
|
|
1184
|
+
name: "@unknown",
|
|
1185
|
+
scheme: "https",
|
|
1186
|
+
url: "http://localhost",
|
|
1187
|
+
},
|
|
1188
|
+
undefined,
|
|
1189
|
+
);
|
|
1190
|
+
expect(result).toBeNull();
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
test("proxy mode short-circuits to true regardless of cache", async () => {
|
|
1194
|
+
const { isRefAuthComplete } = await import("./config-store");
|
|
1195
|
+
const result = isRefAuthComplete(
|
|
1196
|
+
{
|
|
1197
|
+
ref: "slash",
|
|
1198
|
+
name: "slash",
|
|
1199
|
+
scheme: "registry",
|
|
1200
|
+
// proxy mode set by ref.add when registry inspection includes it.
|
|
1201
|
+
// biome-ignore lint/suspicious/noExplicitAny: mode isn't on the public type
|
|
1202
|
+
mode: "proxy",
|
|
1203
|
+
} as any,
|
|
1204
|
+
undefined,
|
|
1205
|
+
);
|
|
1206
|
+
expect(result).toBe(true);
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
test("required field present → true", async () => {
|
|
1210
|
+
const { isRefAuthComplete } = await import("./config-store");
|
|
1211
|
+
const result = isRefAuthComplete(
|
|
1212
|
+
{
|
|
1213
|
+
ref: "@oauth",
|
|
1214
|
+
name: "@oauth",
|
|
1215
|
+
scheme: "https",
|
|
1216
|
+
url: "http://localhost",
|
|
1217
|
+
config: {
|
|
1218
|
+
client_id: "abc",
|
|
1219
|
+
client_secret: "xyz",
|
|
1220
|
+
access_token: "tok",
|
|
1221
|
+
},
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
ref: "@oauth",
|
|
1225
|
+
fetchedAt: new Date().toISOString(),
|
|
1226
|
+
authFields: {
|
|
1227
|
+
client_id: { required: true, automated: false },
|
|
1228
|
+
client_secret: { required: true, automated: false },
|
|
1229
|
+
access_token: { required: true, automated: true },
|
|
1230
|
+
},
|
|
1231
|
+
},
|
|
1232
|
+
);
|
|
1233
|
+
expect(result).toBe(true);
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
test("automated field absent still counts as satisfied", async () => {
|
|
1237
|
+
const { isRefAuthComplete } = await import("./config-store");
|
|
1238
|
+
// dynamicRegistration: client_id is automated, so absence is fine.
|
|
1239
|
+
const result = isRefAuthComplete(
|
|
1240
|
+
{
|
|
1241
|
+
ref: "@oauth",
|
|
1242
|
+
name: "@oauth",
|
|
1243
|
+
scheme: "https",
|
|
1244
|
+
url: "http://localhost",
|
|
1245
|
+
config: {
|
|
1246
|
+
// client_id missing
|
|
1247
|
+
access_token: "tok",
|
|
1248
|
+
},
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
ref: "@oauth",
|
|
1252
|
+
fetchedAt: new Date().toISOString(),
|
|
1253
|
+
authFields: {
|
|
1254
|
+
client_id: { required: true, automated: true },
|
|
1255
|
+
access_token: { required: true, automated: true },
|
|
1256
|
+
},
|
|
1257
|
+
},
|
|
1258
|
+
);
|
|
1259
|
+
expect(result).toBe(true);
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
test("required, non-automated field missing → false", async () => {
|
|
1263
|
+
const { isRefAuthComplete } = await import("./config-store");
|
|
1264
|
+
const result = isRefAuthComplete(
|
|
1265
|
+
{
|
|
1266
|
+
ref: "@oauth",
|
|
1267
|
+
name: "@oauth",
|
|
1268
|
+
scheme: "https",
|
|
1269
|
+
url: "http://localhost",
|
|
1270
|
+
config: {
|
|
1271
|
+
client_id: "abc",
|
|
1272
|
+
client_secret: "xyz",
|
|
1273
|
+
// access_token missing — user hasn't completed OAuth yet.
|
|
1274
|
+
},
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
ref: "@oauth",
|
|
1278
|
+
fetchedAt: new Date().toISOString(),
|
|
1279
|
+
authFields: {
|
|
1280
|
+
client_id: { required: true, automated: false },
|
|
1281
|
+
client_secret: { required: true, automated: false },
|
|
1282
|
+
access_token: { required: true, automated: false },
|
|
1283
|
+
},
|
|
1284
|
+
},
|
|
1285
|
+
);
|
|
1286
|
+
expect(result).toBe(false);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
test("non-required field absence is fine", async () => {
|
|
1290
|
+
const { isRefAuthComplete } = await import("./config-store");
|
|
1291
|
+
const result = isRefAuthComplete(
|
|
1292
|
+
{
|
|
1293
|
+
ref: "@oauth",
|
|
1294
|
+
name: "@oauth",
|
|
1295
|
+
scheme: "https",
|
|
1296
|
+
url: "http://localhost",
|
|
1297
|
+
config: { access_token: "tok" },
|
|
1298
|
+
},
|
|
1299
|
+
{
|
|
1300
|
+
ref: "@oauth",
|
|
1301
|
+
fetchedAt: new Date().toISOString(),
|
|
1302
|
+
authFields: {
|
|
1303
|
+
client_id: { required: false, automated: false },
|
|
1304
|
+
access_token: { required: true, automated: false },
|
|
1305
|
+
},
|
|
1306
|
+
},
|
|
1307
|
+
);
|
|
1308
|
+
expect(result).toBe(true);
|
|
1309
|
+
});
|
|
1310
|
+
});
|
|
1311
|
+
|
package/src/config-store.ts
CHANGED
|
@@ -68,16 +68,49 @@ export interface RegistryCacheToolSummary {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
71
|
+
* Slim auth-field metadata cached so hosts can locally answer "is this
|
|
72
|
+
* ref ready to call?" without a registry round-trip. Mirrors the
|
|
73
|
+
* authoritative shape `auth-status` produces — same source of truth,
|
|
74
|
+
* just persisted.
|
|
75
|
+
*
|
|
76
|
+
* For each field name in the security scheme:
|
|
77
|
+
* - `required` — must end up satisfied for `ref.call` to work.
|
|
78
|
+
* - `automated` — adk fills this in itself (e.g. dynamic OAuth
|
|
79
|
+
* client registration). Doesn't need to be `present`
|
|
80
|
+
* in the user's config to count as satisfied.
|
|
81
|
+
*/
|
|
82
|
+
export interface RegistryCacheAuthField {
|
|
83
|
+
required: boolean;
|
|
84
|
+
automated: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Per-ref cache entry. Updated as a side-effect of `ref.add()`,
|
|
89
|
+
* `ref.inspect()`, and `ref.authStatus()` whenever the registry
|
|
90
|
+
* response carries description / tool / security-scheme info.
|
|
91
|
+
* Identity-relative (lives next to the consumer-config that issued
|
|
92
|
+
* the registry call), so permission-filtered views stay consistent.
|
|
75
93
|
*/
|
|
76
94
|
export interface RegistryCacheEntry {
|
|
77
95
|
/** Canonical agent path (e.g. `notion`). Stored for sanity/debug. */
|
|
78
96
|
ref: string;
|
|
79
97
|
description?: string;
|
|
80
98
|
tools?: RegistryCacheToolSummary[];
|
|
99
|
+
/**
|
|
100
|
+
* Auth field requirements derived from the registry's security
|
|
101
|
+
* scheme (extracted by `auth-status`). When present, hosts can
|
|
102
|
+
* compute "is this ref callable?" by intersecting these with the
|
|
103
|
+
* entry's `config` — no network round-trip needed. Absent when the
|
|
104
|
+
* scheme couldn't be fetched (e.g. registry was offline at add
|
|
105
|
+
* time); fall back to whatever heuristic the caller chooses.
|
|
106
|
+
*
|
|
107
|
+
* Note on proxy refs: when the entry is in `proxy` mode the
|
|
108
|
+
* security scheme is exposed by the *proxy*, and the answer to
|
|
109
|
+
* "is this callable?" lives server-side — `authFields` is omitted
|
|
110
|
+
* locally and hosts should treat proxy refs as authoritative
|
|
111
|
+
* regardless of entry-side fields.
|
|
112
|
+
*/
|
|
113
|
+
authFields?: Record<string, RegistryCacheAuthField>;
|
|
81
114
|
/** ISO timestamp of the most recent registry round-trip that wrote this. */
|
|
82
115
|
fetchedAt: string;
|
|
83
116
|
}
|
|
@@ -91,6 +124,46 @@ export interface RegistryCache {
|
|
|
91
124
|
refs: Record<string, RegistryCacheEntry>;
|
|
92
125
|
}
|
|
93
126
|
|
|
127
|
+
/**
|
|
128
|
+
* "Is this ref ready to call?" answered locally using the cached
|
|
129
|
+
* security-scheme requirements. Mirrors the `complete` boolean
|
|
130
|
+
* `auth-status` returns, but doesn't need a network round-trip — the
|
|
131
|
+
* cached `authFields` capture what the registry said is required, and
|
|
132
|
+
* we evaluate satisfaction against the entry's current `config`.
|
|
133
|
+
*
|
|
134
|
+
* Behavior:
|
|
135
|
+
* - `mode: 'proxy'` refs → always true. Auth lives server-side; the
|
|
136
|
+
* proxy is the source of truth, no entry-side fields involved.
|
|
137
|
+
* - Cache miss (no `authFields` for this ref yet) → returns `null`,
|
|
138
|
+
* signaling "I don't know — caller should fall back to its own
|
|
139
|
+
* heuristic or call `auth-status` to populate the cache".
|
|
140
|
+
* - 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.
|
|
145
|
+
*
|
|
146
|
+
* Returning `null` for cache miss is intentional. A boolean would
|
|
147
|
+
* force callers to choose a default that's wrong half the time;
|
|
148
|
+
* `null` lets them branch explicitly.
|
|
149
|
+
*/
|
|
150
|
+
export function isRefAuthComplete(
|
|
151
|
+
entry: RefEntry,
|
|
152
|
+
cacheEntry: RegistryCacheEntry | undefined,
|
|
153
|
+
): boolean | null {
|
|
154
|
+
if (typeof entry === "string") return false;
|
|
155
|
+
if ((entry as { mode?: unknown }).mode === "proxy") return true;
|
|
156
|
+
const authFields = cacheEntry?.authFields;
|
|
157
|
+
if (!authFields) return null;
|
|
158
|
+
const config = entry.config ?? {};
|
|
159
|
+
for (const [field, info] of Object.entries(authFields)) {
|
|
160
|
+
if (!info.required) continue;
|
|
161
|
+
if (info.automated) continue;
|
|
162
|
+
if (!(field in config)) return false;
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
94
167
|
// ============================================
|
|
95
168
|
// Types
|
|
96
169
|
// ============================================
|
|
@@ -650,6 +723,29 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
650
723
|
await writeRegistryCache(cache);
|
|
651
724
|
}
|
|
652
725
|
|
|
726
|
+
/**
|
|
727
|
+
* Merge `authFields` into an existing cache entry without clobbering
|
|
728
|
+
* description/tools, or create a minimal entry if one doesn't exist
|
|
729
|
+
* yet. Called from `authStatus` so the slim {required, automated}
|
|
730
|
+
* shape is always available for `isRefAuthComplete` to answer
|
|
731
|
+
* locally on subsequent calls.
|
|
732
|
+
*/
|
|
733
|
+
async function upsertRegistryCacheAuthFields(
|
|
734
|
+
name: string,
|
|
735
|
+
ref: string,
|
|
736
|
+
authFields: Record<string, RegistryCacheAuthField>,
|
|
737
|
+
): Promise<void> {
|
|
738
|
+
const cache = await readRegistryCache();
|
|
739
|
+
const existing = cache.refs[name];
|
|
740
|
+
cache.refs[name] = {
|
|
741
|
+
...(existing ?? { ref, fetchedAt: new Date().toISOString() }),
|
|
742
|
+
authFields,
|
|
743
|
+
// Refresh fetchedAt so freshness telemetry stays accurate.
|
|
744
|
+
fetchedAt: new Date().toISOString(),
|
|
745
|
+
};
|
|
746
|
+
await writeRegistryCache(cache);
|
|
747
|
+
}
|
|
748
|
+
|
|
653
749
|
async function removeRegistryCacheEntry(name: string): Promise<void> {
|
|
654
750
|
const cache = await readRegistryCache();
|
|
655
751
|
if (!(name in cache.refs)) return;
|
|
@@ -2342,6 +2438,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2342
2438
|
(f) => !f.required || f.present || f.resolvable,
|
|
2343
2439
|
);
|
|
2344
2440
|
|
|
2441
|
+
// Persist the slim {required, automated} per-field shape into the
|
|
2442
|
+
// registry cache so `isRefAuthComplete` can answer subsequent
|
|
2443
|
+
// host-side "is this ref ready?" checks without re-fetching the
|
|
2444
|
+
// security scheme. We deliberately omit `present`/`resolvable`
|
|
2445
|
+
// because those are computed against the current entry.config and
|
|
2446
|
+
// host environment — caching them would go stale immediately.
|
|
2447
|
+
const authFields: Record<string, RegistryCacheAuthField> = {};
|
|
2448
|
+
for (const [field, info] of Object.entries(fields)) {
|
|
2449
|
+
authFields[field] = {
|
|
2450
|
+
required: info.required,
|
|
2451
|
+
automated: info.automated,
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
await upsertRegistryCacheAuthFields(name, entry.ref, authFields);
|
|
2455
|
+
|
|
2345
2456
|
return { name, security, complete, fields };
|
|
2346
2457
|
},
|
|
2347
2458
|
|
package/src/index.ts
CHANGED
|
@@ -437,7 +437,9 @@ export type {
|
|
|
437
437
|
RegistryCache,
|
|
438
438
|
RegistryCacheEntry,
|
|
439
439
|
RegistryCacheToolSummary,
|
|
440
|
+
RegistryCacheAuthField,
|
|
440
441
|
} from "./config-store.js";
|
|
442
|
+
export { isRefAuthComplete } from "./config-store.js";
|
|
441
443
|
export { createLocalFsStore, getLocalEncryptionKey } from "./local-fs.js";
|
|
442
444
|
export { AdkError, getError, getRecentErrors } from "./adk-error.js";
|
|
443
445
|
export {
|
package/src/materialize.ts
CHANGED
|
@@ -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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
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 };
|