@slashfi/agents-sdk 0.77.0 → 0.77.1

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.
Files changed (48) hide show
  1. package/dist/adk-tools.d.ts +2 -2
  2. package/dist/adk-tools.d.ts.map +1 -1
  3. package/dist/adk-tools.js +9 -18
  4. package/dist/adk-tools.js.map +1 -1
  5. package/dist/adk.js +7 -9
  6. package/dist/adk.js.map +1 -1
  7. package/dist/agent-definitions/config.d.ts.map +1 -1
  8. package/dist/agent-definitions/config.js +12 -14
  9. package/dist/agent-definitions/config.js.map +1 -1
  10. package/dist/cjs/adk-tools.js +9 -18
  11. package/dist/cjs/adk-tools.js.map +1 -1
  12. package/dist/cjs/agent-definitions/config.js +12 -14
  13. package/dist/cjs/agent-definitions/config.js.map +1 -1
  14. package/dist/cjs/config-store.js +34 -17
  15. package/dist/cjs/config-store.js.map +1 -1
  16. package/dist/cjs/define-config.js +5 -7
  17. package/dist/cjs/define-config.js.map +1 -1
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/materialize.js +1 -1
  20. package/dist/cjs/materialize.js.map +1 -1
  21. package/dist/cjs/registry-consumer.js +1 -1
  22. package/dist/config-store.d.ts +3 -3
  23. package/dist/config-store.d.ts.map +1 -1
  24. package/dist/config-store.js +34 -17
  25. package/dist/config-store.js.map +1 -1
  26. package/dist/define-config.d.ts +11 -18
  27. package/dist/define-config.d.ts.map +1 -1
  28. package/dist/define-config.js +5 -7
  29. package/dist/define-config.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/materialize.js +1 -1
  34. package/dist/materialize.js.map +1 -1
  35. package/dist/registry-consumer.d.ts +1 -1
  36. package/dist/registry-consumer.js +1 -1
  37. package/dist/validate.d.ts +8 -8
  38. package/package.json +1 -1
  39. package/src/adk-tools.ts +11 -18
  40. package/src/adk.ts +7 -8
  41. package/src/agent-definitions/config.ts +15 -16
  42. package/src/config-store.ts +43 -19
  43. package/src/consumer.test.ts +7 -7
  44. package/src/define-config.ts +11 -20
  45. package/src/index.ts +1 -0
  46. package/src/materialize.ts +1 -1
  47. package/src/ref-naming.test.ts +115 -90
  48. package/src/registry-consumer.ts +1 -1
@@ -16,8 +16,8 @@
16
16
  * ],
17
17
  * refs: [
18
18
  * 'notion',
19
- * { ref: 'postgres', as: 'prod-db', config: { url: 'https://twin.slash.com/secrets/crdb-url' } },
20
- * { ref: 'postgres', as: 'staging', config: { url: 'https://twin.slash.com/secrets/staging-url' } },
19
+ * { ref: 'postgres', name: 'prod-db', config: { url: 'https://twin.slash.com/secrets/crdb-url' } },
20
+ * { ref: 'postgres', name: 'staging', config: { url: 'https://twin.slash.com/secrets/staging-url' } },
21
21
  * ],
22
22
  * });
23
23
  * ```
@@ -158,14 +158,10 @@ export type RefEntry = {
158
158
 
159
159
  /**
160
160
  * Local identifier for this ref. Used by all operations
161
- * (call/remove/auth/update/…) to look up the entry. If omitted,
162
- * the canonical `ref` string is used as the identifier — the
163
- * common case "one local instance per agent" requires only
164
- * `{ ref: 'notion', ... }`. Set `name` to a different value only
165
- * when you need multiple local instances of the same remote
166
- * agent (e.g. `{ ref: 'notion', name: 'work-notion' }`).
161
+ * (call/remove/auth/update/…) to look up the entry. Add paths
162
+ * default this to `ref` when omitted.
167
163
  */
168
- name?: string;
164
+ name: string;
169
165
 
170
166
  /** Connection scheme */
171
167
  scheme?: 'mcp' | 'https' | 'registry';
@@ -173,12 +169,6 @@ export type RefEntry = {
173
169
  /** Direct URL to the agent (e.g. https://mcp.notion.com/mcp) */
174
170
  url?: string;
175
171
 
176
- /**
177
- * @deprecated Use `name` instead. `as` is preserved for reading
178
- * old consumer-config.json files; new writes emit `name`.
179
- */
180
- as?: string;
181
-
182
172
  /** Per-instance config (headers, secrets, etc. — values support {{secret-uri}} templates) */
183
173
  config?: RefConfig;
184
174
 
@@ -189,6 +179,9 @@ export type RefEntry = {
189
179
  status?: 'active' | 'inactive' | 'error';
190
180
  };
191
181
 
182
+ /** Input accepted by add paths. `name` defaults to `ref` when omitted. */
183
+ export type RefAddInput = Omit<RefEntry, "name"> & { name?: string };
184
+
192
185
  // ============================================
193
186
  // Consumer Config
194
187
  // ============================================
@@ -259,15 +252,13 @@ export interface ResolvedConfig {
259
252
  /**
260
253
  * Normalize a ref entry to its full form.
261
254
  *
262
- * Local identifier resolution order: `entry.name` `entry.as` (legacy)
263
- * `entry.ref` (canonical). This order makes the tool/API surface
264
- * consistent with the `ref.add({ ref, name })` contract while still
265
- * reading old `{ ref, as }` entries from pre-0.74 consumer-config.json.
255
+ * Add paths default `name` to `ref` before writing. `normalizeRef` keeps the
256
+ * same invariant for in-memory/test configs that omit `name`.
266
257
  */
267
258
  export function normalizeRef(entry: RefEntry): ResolvedRef {
268
259
  return {
269
260
  ...entry,
270
- name: entry.name ?? entry.as ?? entry.ref,
261
+ name: entry.name ?? entry.ref,
271
262
  config: entry.config ?? {},
272
263
  };
273
264
  }
package/src/index.ts CHANGED
@@ -307,6 +307,7 @@ export type {
307
307
  RegistryAuth,
308
308
  RegistryEntry,
309
309
  RefConfig,
310
+ RefAddInput,
310
311
  RefEntry,
311
312
  ConsumerConfig,
312
313
  ResolvedRegistry,
@@ -326,7 +326,7 @@ export async function syncAllRefs(
326
326
  for (const refEntry of refs) {
327
327
  const name = typeof refEntry === "string"
328
328
  ? refEntry
329
- : (refEntry as any).as ?? (refEntry as any).ref ?? (refEntry as any).name;
329
+ : (refEntry as any).name ?? (refEntry as any).ref;
330
330
  if (!name) continue;
331
331
  if (opts?.filter && name !== opts.filter) continue;
332
332
  names.push(name);
@@ -1,15 +1,10 @@
1
1
  /**
2
2
  * Tests for the `ref` naming contract introduced in 0.74:
3
3
  *
4
- * - `RefEntry` gains an optional `name?` field for the local identifier.
5
- * The legacy `as?` field still parses for backward compat.
6
- * - `normalizeRef` resolves the identifier as `name ?? as ?? ref`, so
7
- * all lookup paths (get, list, update, remove) accept entries written
8
- * in either shape.
9
- * - `createRefTool` (the adk-tools.ts MCP tool) drops `as` from its
10
- * schema and defaults `ref` to `name` on add, so "Add a ref called X"
11
- * via an LLM that picks either field lands on the same stored
12
- * `{ ref: 'X' }` entry.
4
+ * - `name` is the local identifier for every stored ref entry.
5
+ * - Add paths default `name` to `ref` when omitted.
6
+ * - `as` is not part of the stored or public ref shape.
7
+ * - Adding a duplicate `name` is a loud error, not a replace.
13
8
  */
14
9
 
15
10
  import { describe, expect, test } from "bun:test";
@@ -42,7 +37,7 @@ async function readJson<T = unknown>(fs: FsStore, path: string): Promise<T> {
42
37
  // ─── RefEntry: name field on write ───────────────────────────────────
43
38
 
44
39
  describe("ref.add — identifier field", () => {
45
- test("single-instance case stores only `ref` (no `name`/`as`)", async () => {
40
+ test("single-instance case stores explicit `name` equal to `ref`", async () => {
46
41
  const fs = createMemoryFs();
47
42
  const adk = createAdk(fs);
48
43
 
@@ -58,8 +53,7 @@ describe("ref.add — identifier field", () => {
58
53
  );
59
54
  expect(parsed.refs).toHaveLength(1);
60
55
  expect(parsed.refs[0].ref).toBe("test-ref");
61
- expect(parsed.refs[0].name).toBeUndefined();
62
- expect(parsed.refs[0].as).toBeUndefined();
56
+ expect(parsed.refs[0].name).toBe("test-ref");
63
57
  });
64
58
 
65
59
  test("aliasing case stores both `ref` and `name`", async () => {
@@ -79,14 +73,40 @@ describe("ref.add — identifier field", () => {
79
73
  );
80
74
  expect(parsed.refs[0].ref).toBe("notion");
81
75
  expect(parsed.refs[0].name).toBe("work-notion");
82
- // Legacy `as` field is never emitted on new writes.
83
- expect(parsed.refs[0].as).toBeUndefined();
76
+ });
77
+
78
+ test("adding a duplicate name rejects instead of replacing", async () => {
79
+ const fs = createMemoryFs();
80
+ const adk = createAdk(fs);
81
+
82
+ await adk.ref.add({
83
+ ref: "notion",
84
+ name: "work",
85
+ scheme: "mcp",
86
+ url: "http://localhost:12345",
87
+ });
88
+
89
+ await expect(
90
+ adk.ref.add({
91
+ ref: "linear",
92
+ name: "work",
93
+ scheme: "mcp",
94
+ url: "http://localhost:12346",
95
+ }),
96
+ ).rejects.toThrow(/already exists/);
97
+
98
+ const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
99
+ fs,
100
+ "consumer-config.json",
101
+ );
102
+ expect(parsed.refs).toHaveLength(1);
103
+ expect(parsed.refs[0].ref).toBe("notion");
84
104
  });
85
105
  });
86
106
 
87
107
  // ─── Lookup compatibility: new `name` field works end-to-end ─────────
88
108
 
89
- describe("ref lookup — name/as/ref resolution", () => {
109
+ describe("ref lookup — name/ref resolution", () => {
90
110
  test("entries written with `name` are findable by `name`", async () => {
91
111
  const fs = createMemoryFs();
92
112
  const adk = createAdk(fs);
@@ -119,9 +139,7 @@ describe("ref lookup — name/as/ref resolution", () => {
119
139
  expect(refs[0]?.name).toBe("work-notion");
120
140
  });
121
141
 
122
- test("legacy entries written with `as` remain findable by `as`", async () => {
123
- // Simulate a consumer-config.json produced by a pre-0.74 client that
124
- // still writes the `as` field. The read path must still resolve it.
142
+ test("entries without `name` normalize to `ref`", async () => {
125
143
  const fs = createMemoryFs();
126
144
  await fs.writeFile(
127
145
  "consumer-config.json",
@@ -129,7 +147,6 @@ describe("ref lookup — name/as/ref resolution", () => {
129
147
  refs: [
130
148
  {
131
149
  ref: "notion",
132
- as: "work-notion",
133
150
  scheme: "mcp",
134
151
  url: "http://localhost:12345",
135
152
  },
@@ -138,56 +155,26 @@ describe("ref lookup — name/as/ref resolution", () => {
138
155
  );
139
156
  const adk = createAdk(fs);
140
157
 
141
- const entry = await adk.ref.get("work-notion");
142
- expect(entry).not.toBeNull();
143
- expect(entry?.ref).toBe("notion");
144
- expect(entry?.as).toBe("work-notion");
145
- });
146
-
147
- test("when both `name` and `as` are present, `name` wins", async () => {
148
- const fs = createMemoryFs();
149
- await fs.writeFile(
150
- "consumer-config.json",
151
- JSON.stringify({
152
- refs: [
153
- {
154
- ref: "notion",
155
- name: "new-identifier",
156
- as: "legacy-identifier",
157
- scheme: "mcp",
158
- url: "http://localhost:12345",
159
- },
160
- ],
161
- }),
162
- );
163
- const adk = createAdk(fs);
164
-
165
- const byNew = await adk.ref.get("new-identifier");
166
- expect(byNew).not.toBeNull();
167
- expect(byNew?.ref).toBe("notion");
158
+ const refs = await adk.ref.list();
159
+ expect(refs[0]?.name).toBe("notion");
160
+ expect(await adk.ref.get("notion")).not.toBeNull();
168
161
  });
169
162
  });
170
163
 
171
- // ─── ref.update: renaming clears the legacy `as` field ───────────────
164
+ // ─── ref.update: name is the only rename field ───────────────────────
172
165
 
173
- describe("ref.update — name/as handling", () => {
174
- test("passing `name` in updates sets name and clears legacy `as`", async () => {
166
+ describe("ref.update — name handling", () => {
167
+ test("passing `name` in updates sets name", async () => {
175
168
  const fs = createMemoryFs();
176
- await fs.writeFile(
177
- "consumer-config.json",
178
- JSON.stringify({
179
- refs: [
180
- {
181
- ref: "notion",
182
- as: "old-alias",
183
- scheme: "mcp",
184
- url: "http://localhost:12345",
185
- },
186
- ],
187
- }),
188
- );
189
169
  const adk = createAdk(fs);
190
170
 
171
+ await adk.ref.add({
172
+ ref: "notion",
173
+ name: "old-alias",
174
+ scheme: "mcp",
175
+ url: "http://localhost:12345",
176
+ });
177
+
191
178
  const ok = await adk.ref.update("old-alias", { name: "new-alias" });
192
179
  expect(ok).toBe(true);
193
180
 
@@ -196,35 +183,29 @@ describe("ref.update — name/as handling", () => {
196
183
  "consumer-config.json",
197
184
  );
198
185
  expect(parsed.refs[0].name).toBe("new-alias");
199
- expect(parsed.refs[0].as).toBeUndefined();
200
186
  expect(parsed.refs[0].ref).toBe("notion");
201
187
  });
202
188
 
203
- test("passing only `as` updates the legacy field (pre-0.74 callers)", async () => {
189
+ test("renaming to an existing name rejects", async () => {
204
190
  const fs = createMemoryFs();
205
- await fs.writeFile(
206
- "consumer-config.json",
207
- JSON.stringify({
208
- refs: [
209
- {
210
- ref: "notion",
211
- as: "first",
212
- scheme: "mcp",
213
- url: "http://localhost:12345",
214
- },
215
- ],
216
- }),
217
- );
218
191
  const adk = createAdk(fs);
219
192
 
220
- const ok = await adk.ref.update("first", { as: "second" });
221
- expect(ok).toBe(true);
193
+ await adk.ref.add({
194
+ ref: "notion",
195
+ name: "first",
196
+ scheme: "mcp",
197
+ url: "http://localhost:12345",
198
+ });
199
+ await adk.ref.add({
200
+ ref: "linear",
201
+ name: "second",
202
+ scheme: "mcp",
203
+ url: "http://localhost:12346",
204
+ });
222
205
 
223
- const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
224
- fs,
225
- "consumer-config.json",
206
+ await expect(adk.ref.update("first", { name: "second" })).rejects.toThrow(
207
+ /already exists/,
226
208
  );
227
- expect(parsed.refs[0].as).toBe("second");
228
209
  });
229
210
  });
230
211
 
@@ -232,10 +213,8 @@ describe("ref.update — name/as handling", () => {
232
213
  //
233
214
  // When an LLM is prompted with "Add a ref called X", its tool-call
234
215
  // arguments can land on either `{ ref: 'X', … }` or `{ name: 'X', … }`
235
- // depending on sampling. Pre-0.74, the former stored `{ ref: 'X' }`
236
- // and the latter stored `{ ref: undefined }` (broken lookup). The
237
- // `add` handler now defaults `ref ??= name` so both paths converge on
238
- // the same stored entry.
216
+ // depending on sampling. The `add` handler defaults the missing field so
217
+ // both paths converge on the same stored `{ ref: 'X', name: 'X' }` entry.
239
218
 
240
219
  describe("ref tool — add operation defaults ref to name", () => {
241
220
  function makeRefTool(adk: ReturnType<typeof createAdk>) {
@@ -267,8 +246,7 @@ describe("ref tool — add operation defaults ref to name", () => {
267
246
  );
268
247
  expect(parsed.refs).toHaveLength(1);
269
248
  expect(parsed.refs[0].ref).toBe("test-identity-ref");
270
- // name was not stored because it equals ref (single-instance case)
271
- expect(parsed.refs[0].name).toBeUndefined();
249
+ expect(parsed.refs[0].name).toBe("test-identity-ref");
272
250
 
273
251
  const entry = await adk.ref.get("test-identity-ref");
274
252
  expect(entry).not.toBeNull();
@@ -295,7 +273,7 @@ describe("ref tool — add operation defaults ref to name", () => {
295
273
  "consumer-config.json",
296
274
  );
297
275
  expect(parsed.refs[0].ref).toBe("test-identity-ref");
298
- expect(parsed.refs[0].name).toBeUndefined();
276
+ expect(parsed.refs[0].name).toBe("test-identity-ref");
299
277
  });
300
278
 
301
279
  test("LLM sends `ref` + different `name` → stored as canonical + alias", async () => {
@@ -349,3 +327,50 @@ describe("ref tool — add operation defaults ref to name", () => {
349
327
  expect(raw).toBeNull();
350
328
  });
351
329
  });
330
+
331
+ describe("ref tool — auth state hook", () => {
332
+ test("passes tool input to getAuthStateContext", async () => {
333
+ const authCalls: Array<{
334
+ name: string;
335
+ opts: { stateContext?: Record<string, unknown> };
336
+ }> = [];
337
+ const adk = {
338
+ ref: {
339
+ auth: async (
340
+ name: string,
341
+ opts: { stateContext?: Record<string, unknown> },
342
+ ) => {
343
+ authCalls.push({ name, opts });
344
+ return { complete: true };
345
+ },
346
+ },
347
+ } as unknown as ReturnType<typeof createAdk>;
348
+ const tools = createAdkTools({
349
+ resolveScope: () => adk,
350
+ hooks: {
351
+ getAuthStateContext: async (input) => ({
352
+ name: input.name,
353
+ ref: input.ref,
354
+ }),
355
+ },
356
+ });
357
+ const refTool = tools.find((t) => t.name === "ref");
358
+ if (!refTool) throw new Error("ref tool not found");
359
+
360
+ await refTool.execute(
361
+ {
362
+ operation: "auth",
363
+ ref: "google-gmail",
364
+ name: "work2",
365
+ },
366
+ {} as ToolContext,
367
+ );
368
+
369
+ expect(authCalls).toHaveLength(1);
370
+ expect(authCalls[0]?.name).toBe("work2");
371
+ expect(authCalls[0]?.opts.stateContext).toEqual({
372
+ ref: "google-gmail",
373
+ name: "work2",
374
+ });
375
+ });
376
+ });
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * const config = defineConfig({
14
14
  * registries: ['https://registry.slash.com'],
15
- * refs: ['notion', { ref: 'postgres', as: 'prod-db', config: { url: '...' } }],
15
+ * refs: ['notion', { ref: 'postgres', name: 'prod-db', config: { url: '...' } }],
16
16
  * });
17
17
  *
18
18
  * const consumer = await createRegistryConsumer(config);