@kweaver-ai/kweaver-sdk 0.6.6 → 0.6.8

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,383 @@
1
+ /**
2
+ * Pure helpers and orchestrators for managing agent member associations
3
+ * (skills, tools, mcps) via get → mutate(config) → update.
4
+ */
5
+ import { getAgent, updateAgent } from "../api/agent-list.js";
6
+ import { getSkill } from "../api/skills.js";
7
+ import { ensureValidToken, formatHttpError } from "../auth/oauth.js";
8
+ import { resolveBusinessDomain } from "../config/store.js";
9
+ /** Deep-clone a JSON-serializable object so mutations don't leak to callers. */
10
+ function clone(value) {
11
+ return JSON.parse(JSON.stringify(value));
12
+ }
13
+ /**
14
+ * Descend into `config` along `path`, creating empty objects and a terminal
15
+ * empty array along the way if any node is missing. Returns the terminal array.
16
+ */
17
+ function ensureArrayAtPath(root, path) {
18
+ let cursor = root;
19
+ for (let i = 0; i < path.length - 1; i += 1) {
20
+ const key = path[i];
21
+ const next = cursor[key];
22
+ if (next === undefined || next === null) {
23
+ cursor[key] = {};
24
+ }
25
+ else if (typeof next !== "object" || Array.isArray(next)) {
26
+ throw new Error(`Config path conflict at ${path.slice(0, i + 1).join(".")}: expected object, got ${Array.isArray(next) ? "array" : typeof next}`);
27
+ }
28
+ cursor = cursor[key];
29
+ }
30
+ const terminalKey = path[path.length - 1];
31
+ const terminal = cursor[terminalKey];
32
+ if (terminal === undefined || terminal === null) {
33
+ cursor[terminalKey] = [];
34
+ }
35
+ else if (!Array.isArray(terminal)) {
36
+ throw new Error(`Config path conflict at ${path.join(".")}: expected array, got ${typeof terminal}`);
37
+ }
38
+ return cursor[terminalKey];
39
+ }
40
+ export function mutateConfigMembers(input) {
41
+ if (input.path.length === 0) {
42
+ throw new Error("mutateConfigMembers: path must have at least one segment");
43
+ }
44
+ const newConfig = clone(input.config);
45
+ const arr = ensureArrayAtPath(newConfig, input.path);
46
+ const existingIds = arr.map((el) => String(el[input.idField] ?? ""));
47
+ const currentSet = new Set(existingIds);
48
+ const added = [];
49
+ const alreadyAttached = [];
50
+ for (const id of input.addIds) {
51
+ if (currentSet.has(id)) {
52
+ alreadyAttached.push(id);
53
+ }
54
+ else {
55
+ arr.push({ [input.idField]: id });
56
+ currentSet.add(id);
57
+ added.push(id);
58
+ }
59
+ }
60
+ const removeSet = new Set(input.removeIds);
61
+ const removed = [];
62
+ const notAttached = [];
63
+ if (removeSet.size > 0) {
64
+ const survivors = [];
65
+ const survivingIdSet = new Set();
66
+ for (const el of arr) {
67
+ const id = String(el[input.idField] ?? "");
68
+ if (removeSet.has(id)) {
69
+ if (!removed.includes(id))
70
+ removed.push(id);
71
+ continue;
72
+ }
73
+ survivors.push(el);
74
+ survivingIdSet.add(id);
75
+ }
76
+ for (const id of input.removeIds) {
77
+ if (!removed.includes(id) && !survivingIdSet.has(id)) {
78
+ notAttached.push(id);
79
+ }
80
+ }
81
+ arr.length = 0;
82
+ arr.push(...survivors);
83
+ }
84
+ const finalIds = arr.map((el) => String(el[input.idField] ?? ""));
85
+ return {
86
+ newConfig,
87
+ finalIds,
88
+ report: {
89
+ finalIds,
90
+ added,
91
+ alreadyAttached,
92
+ removed,
93
+ notAttached,
94
+ },
95
+ };
96
+ }
97
+ function mergeAgentBody(current, newConfig) {
98
+ return {
99
+ name: current.name,
100
+ profile: current.profile,
101
+ avatar_type: current.avatar_type,
102
+ avatar: current.avatar,
103
+ product_key: current.product_key,
104
+ config: newConfig,
105
+ };
106
+ }
107
+ export async function patchAgentMembers(input) {
108
+ const { agentId, spec, addIds, removeIds, strict, deps } = input;
109
+ const warnings = [];
110
+ // 1. validate (add only)
111
+ if (addIds.length > 0) {
112
+ const results = await Promise.all(addIds.map(async (id) => ({ id, info: await deps.fetchById(id) })));
113
+ const missing = results.filter((r) => !r.info.exists).map((r) => r.id);
114
+ if (missing.length > 0) {
115
+ throw new Error(`${spec.memberKind}(s) ${missing.join(", ")} not found (aborting, agent not modified)`);
116
+ }
117
+ const drafts = results.filter((r) => r.info.exists && !r.info.published).map((r) => r.id);
118
+ if (drafts.length > 0) {
119
+ if (strict) {
120
+ throw new Error(`${spec.memberKind}(s) ${drafts.join(", ")} are in draft status (aborted by --strict)`);
121
+ }
122
+ for (const id of drafts) {
123
+ warnings.push(`${spec.memberKind} ${id} is in draft status (use --strict to reject, or publish it first)`);
124
+ }
125
+ }
126
+ }
127
+ // 2. fetch current agent
128
+ const currentRaw = await deps.getAgent(agentId);
129
+ const current = JSON.parse(currentRaw);
130
+ const config = (current.config ?? {});
131
+ // 3. mutate
132
+ const { newConfig, report } = mutateConfigMembers({
133
+ config,
134
+ path: spec.configPath,
135
+ idField: spec.idField,
136
+ addIds,
137
+ removeIds,
138
+ });
139
+ // Short-circuit: no-op (skip the write if neither add nor remove changed anything)
140
+ const nothingChanged = report.added.length === 0 && report.removed.length === 0;
141
+ if (nothingChanged) {
142
+ return { ...report, warnings };
143
+ }
144
+ // 4. write
145
+ await deps.updateAgent(agentId, mergeAgentBody(current, newConfig));
146
+ // 5. report
147
+ return { ...report, warnings };
148
+ }
149
+ export async function listAgentMembers(input) {
150
+ const { agentId, spec, deps } = input;
151
+ const currentRaw = await deps.getAgent(agentId);
152
+ const current = JSON.parse(currentRaw);
153
+ const config = (current.config ?? {});
154
+ // Read (don't create) the path. If any segment is missing, result is empty.
155
+ let cursor = config;
156
+ for (const key of spec.configPath) {
157
+ if (cursor && typeof cursor === "object" && !Array.isArray(cursor) && key in cursor) {
158
+ cursor = cursor[key];
159
+ }
160
+ else {
161
+ return [];
162
+ }
163
+ }
164
+ if (!Array.isArray(cursor))
165
+ return [];
166
+ const ids = cursor.map((el) => String(el[spec.idField] ?? ""));
167
+ const results = await Promise.all(ids.map(async (id) => {
168
+ try {
169
+ const info = await deps.fetchById(id);
170
+ return {
171
+ id,
172
+ name: info.name ?? null,
173
+ status: info.status ?? (info.exists ? (info.published ? "published" : "unpublish") : "unknown"),
174
+ };
175
+ }
176
+ catch {
177
+ return { id, name: null, status: "unknown" };
178
+ }
179
+ }));
180
+ return results;
181
+ }
182
+ // ── Skill command handler ────────────────────────────────────────────────────
183
+ const SKILL_SPEC = {
184
+ memberKind: "skill",
185
+ configPath: ["skills", "skills"],
186
+ idField: "skill_id",
187
+ };
188
+ function parseWriteArgs(args, verb) {
189
+ const agentId = args[0];
190
+ if (!agentId || agentId.startsWith("-")) {
191
+ throw new Error(`Missing <agent-id> for ${verb}`);
192
+ }
193
+ const ids = [];
194
+ let strict = false;
195
+ let businessDomain = "";
196
+ for (let i = 1; i < args.length; i += 1) {
197
+ const arg = args[i];
198
+ if (arg === "--strict") {
199
+ strict = true;
200
+ continue;
201
+ }
202
+ if (arg === "-bd" || arg === "--biz-domain") {
203
+ businessDomain = args[i + 1] ?? "";
204
+ if (!businessDomain || businessDomain.startsWith("-")) {
205
+ throw new Error("Missing value for biz-domain flag");
206
+ }
207
+ i += 1;
208
+ continue;
209
+ }
210
+ if (arg.startsWith("-")) {
211
+ throw new Error(`Unsupported flag: ${arg}`);
212
+ }
213
+ ids.push(arg);
214
+ }
215
+ if (ids.length === 0) {
216
+ throw new Error(`Missing <member-id> for ${verb}`);
217
+ }
218
+ return { agentId, ids, strict, businessDomain };
219
+ }
220
+ function printReport(kind, agentId, report) {
221
+ for (const w of report.warnings)
222
+ process.stderr.write(`! ${w}\n`);
223
+ for (const id of report.added)
224
+ console.log(`✓ ${id} added`);
225
+ for (const id of report.alreadyAttached)
226
+ console.log(`• ${id} already attached (skipped)`);
227
+ for (const id of report.removed)
228
+ console.log(`✓ ${id} removed`);
229
+ for (const id of report.notAttached)
230
+ console.log(`• ${id} not attached (skipped)`);
231
+ console.log(`Agent ${agentId} now has ${report.finalIds.length} ${kind}(s) attached.`);
232
+ }
233
+ async function runSkillAdd(args) {
234
+ try {
235
+ const parsed = parseWriteArgs(args, "add");
236
+ const token = await ensureValidToken();
237
+ const businessDomain = parsed.businessDomain || resolveBusinessDomain();
238
+ const deps = {
239
+ getAgent: (id) => getAgent({ baseUrl: token.baseUrl, accessToken: token.accessToken, agentId: id, businessDomain }),
240
+ updateAgent: (id, body) => updateAgent({ baseUrl: token.baseUrl, accessToken: token.accessToken, agentId: id, body: JSON.stringify(body), businessDomain }),
241
+ fetchById: async (id) => {
242
+ try {
243
+ const info = await getSkill({ baseUrl: token.baseUrl, accessToken: token.accessToken, skillId: id, businessDomain });
244
+ return {
245
+ exists: true,
246
+ published: info.status === "published",
247
+ name: info.name,
248
+ status: info.status,
249
+ };
250
+ }
251
+ catch {
252
+ return { exists: false, published: false };
253
+ }
254
+ },
255
+ };
256
+ const report = await patchAgentMembers({
257
+ agentId: parsed.agentId,
258
+ spec: SKILL_SPEC,
259
+ addIds: parsed.ids,
260
+ removeIds: [],
261
+ strict: parsed.strict,
262
+ deps,
263
+ });
264
+ printReport("skill", parsed.agentId, report);
265
+ return 0;
266
+ }
267
+ catch (error) {
268
+ process.stderr.write(`✗ ${error instanceof Error ? error.message : String(error)}\n`);
269
+ return 1;
270
+ }
271
+ }
272
+ async function runSkillRemove(args) {
273
+ try {
274
+ const parsed = parseWriteArgs(args, "remove");
275
+ const token = await ensureValidToken();
276
+ const businessDomain = parsed.businessDomain || resolveBusinessDomain();
277
+ const deps = {
278
+ getAgent: (id) => getAgent({ baseUrl: token.baseUrl, accessToken: token.accessToken, agentId: id, businessDomain }),
279
+ updateAgent: (id, body) => updateAgent({ baseUrl: token.baseUrl, accessToken: token.accessToken, agentId: id, body: JSON.stringify(body), businessDomain }),
280
+ fetchById: async () => ({ exists: true, published: true }),
281
+ };
282
+ const report = await patchAgentMembers({
283
+ agentId: parsed.agentId,
284
+ spec: SKILL_SPEC,
285
+ addIds: [],
286
+ removeIds: parsed.ids,
287
+ strict: false,
288
+ deps,
289
+ });
290
+ printReport("skill", parsed.agentId, report);
291
+ return 0;
292
+ }
293
+ catch (error) {
294
+ process.stderr.write(`✗ ${error instanceof Error ? error.message : String(error)}\n`);
295
+ return 1;
296
+ }
297
+ }
298
+ async function runSkillList(args) {
299
+ const agentId = args[0];
300
+ if (!agentId || agentId.startsWith("-")) {
301
+ process.stderr.write("Missing <agent-id> for list\n");
302
+ return 1;
303
+ }
304
+ let pretty = true;
305
+ let businessDomain = "";
306
+ for (let i = 1; i < args.length; i += 1) {
307
+ const arg = args[i];
308
+ if (arg === "--pretty") {
309
+ pretty = true;
310
+ continue;
311
+ }
312
+ if (arg === "--compact") {
313
+ pretty = false;
314
+ continue;
315
+ }
316
+ if (arg === "-bd" || arg === "--biz-domain") {
317
+ businessDomain = args[i + 1] ?? "";
318
+ if (!businessDomain || businessDomain.startsWith("-")) {
319
+ process.stderr.write("Missing value for biz-domain flag\n");
320
+ return 1;
321
+ }
322
+ i += 1;
323
+ continue;
324
+ }
325
+ process.stderr.write(`Unsupported flag: ${arg}\n`);
326
+ return 1;
327
+ }
328
+ const token = await ensureValidToken();
329
+ businessDomain = businessDomain || resolveBusinessDomain();
330
+ const deps = {
331
+ getAgent: (id) => getAgent({ baseUrl: token.baseUrl, accessToken: token.accessToken, agentId: id, businessDomain }),
332
+ fetchById: async (id) => {
333
+ try {
334
+ const info = await getSkill({ baseUrl: token.baseUrl, accessToken: token.accessToken, skillId: id, businessDomain });
335
+ return { exists: true, published: info.status === "published", name: info.name, status: info.status };
336
+ }
337
+ catch {
338
+ return { exists: false, published: false };
339
+ }
340
+ },
341
+ };
342
+ try {
343
+ const rows = await listAgentMembers({ agentId, spec: SKILL_SPEC, deps });
344
+ const output = rows.map((r) => ({ skill_id: r.id, name: r.name, status: r.status }));
345
+ console.log(JSON.stringify(output, null, pretty ? 2 : 0));
346
+ return 0;
347
+ }
348
+ catch (error) {
349
+ process.stderr.write(`✗ ${error instanceof Error ? error.message : String(error)}\n`);
350
+ return 1;
351
+ }
352
+ }
353
+ export async function runAgentSkillCommand(args) {
354
+ const [verb, ...rest] = args;
355
+ if (!verb || verb === "--help" || verb === "-h") {
356
+ console.log(`kweaver agent skill
357
+
358
+ Subcommands:
359
+ add <agent-id> <skill-id>... [--strict] [-bd <bd>] Attach skills to an agent
360
+ remove <agent-id> <skill-id>... [-bd <bd>] Detach skills from an agent
361
+ list <agent-id> [--pretty|--compact] [-bd <bd>] List skills attached to an agent
362
+
363
+ Notes:
364
+ --strict On add, reject skills that exist but are not in 'published' status.
365
+ Default behaviour: warn and continue.
366
+ Dedupe is automatic for add; remove silently skips not-attached ids.`);
367
+ return 0;
368
+ }
369
+ try {
370
+ if (verb === "add")
371
+ return await runSkillAdd(rest);
372
+ if (verb === "remove")
373
+ return await runSkillRemove(rest);
374
+ if (verb === "list")
375
+ return await runSkillList(rest);
376
+ process.stderr.write(`Unknown agent skill subcommand: ${verb}\n`);
377
+ return 1;
378
+ }
379
+ catch (error) {
380
+ process.stderr.write(`${formatHttpError(error)}\n`);
381
+ return 1;
382
+ }
383
+ }
@@ -1,5 +1,6 @@
1
1
  import { ensureValidToken, formatHttpError, with401RefreshRetry } from "../auth/oauth.js";
2
2
  import { runAgentChatCommand } from "./agent-chat.js";
3
+ import { runAgentSkillCommand } from "./agent-members.js";
3
4
  import { listAgents, getAgent, getAgentByKey, createAgent, updateAgent, deleteAgent, publishAgent, unpublishAgent, listPersonalAgents, listPublishedAgentTemplates, getPublishedAgentTemplate, listAgentCategories, } from "../api/agent-list.js";
4
5
  import { listConversations, listMessages, getTracesByConversation } from "../api/conversations.js";
5
6
  import { fetchAgentInfo } from "../api/agent-chat.js";
@@ -516,6 +517,7 @@ Subcommands:
516
517
  unpublish <agent_id> Unpublish an agent
517
518
  chat <agent_id> Start interactive chat with an agent
518
519
  chat <agent_id> -m "message" Send a single message (non-interactive)
520
+ skill <verb> ... Manage skills attached to an agent (add/remove/list)
519
521
  sessions <agent_id> List all conversations for an agent
520
522
  history <agent_id> <conversation_id> Show message history for a conversation
521
523
  trace <agent_id> <conversation_id> Get trace data for a conversation`);
@@ -554,6 +556,8 @@ Subcommands:
554
556
  return runAgentPublishCommand(rest);
555
557
  if (subcommand === "unpublish")
556
558
  return runAgentUnpublishCommand(rest);
559
+ if (subcommand === "skill")
560
+ return runAgentSkillCommand(rest);
557
561
  return -1;
558
562
  };
559
563
  // Show subcommand-specific help inline (no retry needed)