@indemn/cli 1.7.0 → 1.7.2
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CONTRIBUTING.md +2 -2
- package/dist/mcp/registry.d.ts.map +1 -1
- package/dist/mcp/registry.js +6 -4
- package/dist/mcp/registry.js.map +1 -1
- package/dist/mcp/tool-meta.d.ts.map +1 -1
- package/dist/mcp/tool-meta.js +35 -9
- package/dist/mcp/tool-meta.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +232 -49
- package/dist/mcp/tools.js.map +1 -1
- package/dist/report/styles.d.ts +1 -0
- package/dist/report/styles.d.ts.map +1 -1
- package/dist/report/styles.js +5 -1
- package/dist/report/styles.js.map +1 -1
- package/dist/sdk/auth.js +2 -2
- package/dist/sdk/auth.js.map +1 -1
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +11 -1
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/config-change.d.ts +4 -0
- package/dist/sdk/config-change.d.ts.map +1 -1
- package/dist/sdk/config-change.js +27 -5
- package/dist/sdk/config-change.js.map +1 -1
- package/dist/sdk/kb.d.ts +12 -0
- package/dist/sdk/kb.d.ts.map +1 -1
- package/dist/sdk/kb.js +45 -5
- package/dist/sdk/kb.js.map +1 -1
- package/dist/sdk/voice.js +5 -5
- package/dist/sdk/voice.js.map +1 -1
- package/package.json +4 -6
- package/src/mcp/registry.ts +9 -4
- package/src/mcp/tool-meta.ts +35 -9
- package/src/mcp/tools.ts +235 -49
- package/src/report/styles.ts +5 -1
- package/src/sdk/auth.ts +2 -2
- package/src/sdk/client.ts +5 -1
- package/src/sdk/config-change.ts +27 -5
- package/src/sdk/kb.ts +49 -8
- package/src/sdk/voice.ts +5 -5
- package/test/mcp/__snapshots__/tools-snapshot.test.ts.snap +1 -0
- package/test/mcp/darwin-fixes.test.ts +231 -0
- package/test/mcp/tools-snapshot.test.ts +1 -1
package/src/mcp/tools.ts
CHANGED
|
@@ -54,9 +54,12 @@ function withAuthorization<T extends AuthFields & { configuration?: Record<strin
|
|
|
54
54
|
return { ...args, configuration: { ...(args.configuration ?? {}), authorization } };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
/** The CI gate's threshold when the caller gives none (evaluations repo scripts/eval_gate.py uses the same). */
|
|
58
|
+
const DEFAULT_MIN_PASS_RATE = 0.8;
|
|
59
|
+
|
|
60
|
+
// Helper: format a successful MCP tool result (the registry adds the context and formats it)
|
|
58
61
|
function success(data: unknown) {
|
|
59
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(data
|
|
62
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(data) }] };
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
// Helper: format an error MCP tool result
|
|
@@ -130,6 +133,20 @@ const parseJsonText = (v: unknown) => {
|
|
|
130
133
|
try { return JSON.parse(v); } catch { return v; }
|
|
131
134
|
};
|
|
132
135
|
const jsonObject = () => z.preprocess(parseJsonText, z.record(z.any()));
|
|
136
|
+
// A list a model may also write as JSON text.
|
|
137
|
+
const jsonArray = <T extends z.ZodTypeAny>(item: T) => z.preprocess(parseJsonText, z.array(item));
|
|
138
|
+
|
|
139
|
+
// What the knowledge base import accepts: typed, so a wrong shape is refused here instead of being
|
|
140
|
+
// "imported" as nothing (copilot answers success for any body).
|
|
141
|
+
const kbImportSchema = z.preprocess(parseJsonText, z.object({
|
|
142
|
+
type: z.enum(['file']).describe('Kind of import; files from the organization\'s file store'),
|
|
143
|
+
sources: z.array(z.object({
|
|
144
|
+
name: z.string().optional().describe('File name shown in the knowledge base'),
|
|
145
|
+
file_type: z.string().optional().describe('Extension, e.g. pdf, txt, docx (taken from the name when missing)'),
|
|
146
|
+
url: z.string().describe('The file\'s link in the file store'),
|
|
147
|
+
}).passthrough()).min(1),
|
|
148
|
+
is_qna_import: z.boolean().optional().describe('The file holds question/answer pairs'),
|
|
149
|
+
}).passthrough());
|
|
133
150
|
|
|
134
151
|
// A skill for bulk_create_skills with flat fields (the configuration is built as create_function builds it);
|
|
135
152
|
// a proposal from generate_skills_from_doc keeps its own configurationSchema.
|
|
@@ -170,12 +187,23 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
170
187
|
},
|
|
171
188
|
(err) => { if (what && notFound.test(err instanceof Error ? err.message : String(err))) missing.push(`${what} ${id}`); })
|
|
172
189
|
: undefined;
|
|
190
|
+
const agentId = typeof args.agent_id === 'string' ? args.agent_id : undefined;
|
|
191
|
+
const label = (name: unknown) => ({ name: typeof name === 'string' && name ? name : undefined });
|
|
173
192
|
await Promise.all([
|
|
174
193
|
add(args.agent_id ?? args.bot_id, 'agent', (id) => agents.getAgent(id)),
|
|
175
|
-
|
|
194
|
+
agentId ? add(args.function_id, 'skill', (id) => skillOf(agentId, id)) : undefined,
|
|
176
195
|
add(args.kb_id, 'knowledge base', (id) => kb.getKB(id)),
|
|
196
|
+
...((Array.isArray(args.kb_ids) ? args.kb_ids : []) as unknown[]).map((id) => add(id, 'knowledge base', (x) => kb.getKB(x))),
|
|
177
197
|
add(args.test_set_id, 'test set', (id) => testSets.getTestSet(id)),
|
|
178
198
|
add(args.rubric_id, 'rubric', (id) => rubrics.getRubric(id)),
|
|
199
|
+
// The rest name things the cards used to show as raw ids.
|
|
200
|
+
typeof args.kb_id === 'string' ? add(args.source_id, null, (id) => kb.findDataSource(String(args.kb_id), id).then((d) => label(d?.question ?? d?.name ?? d?.source_url))) : undefined,
|
|
201
|
+
add(args.ticket_id, null, (id) => tickets.get(id).then((t) => { const x = t as { ticket_ref?: string; title?: string }; return label([x.ticket_ref, x.title].filter(Boolean).join(' ')); })),
|
|
202
|
+
add(args.rule_id, null, (id) => tickets.listRules({ includeTrashed: true }).then((rules) => label((rules as Array<{ _id?: string; id?: string; name?: string }>).find((r) => (r._id ?? r.id) === id)?.name))),
|
|
203
|
+
add(args.distribution_id, null, (id) => distribution.list().then((d) => { const p = (d.paths ?? []).find((x) => x.id === id); return label(p ? `${p.path_name ?? ''} (${p.host_name ?? '*'}${p.path_expression ?? ''})`.trim() : undefined); })),
|
|
204
|
+
add(args.template_id, null, (id) => distribution.listTemplates().then((ts) => label((ts as Array<{ _id?: string; name?: string }>).find((t) => t._id === id)?.name))),
|
|
205
|
+
add(args.owner_id, null, (id) => orgs.listMembers().then((ms) => { const m = ms.find((x) => x.user_id === id); return label(m?.name ?? m?.email); })),
|
|
206
|
+
agentId && typeof args.function_id === 'string' ? add(args.parameter_id, null, (id) => skillOf(agentId, String(args.function_id)).then((s) => functions.listParameters(agentId, s._id)).then((ps) => label(ps.find((p) => p._id === id)?.name))) : undefined,
|
|
179
207
|
add(orgId, null, (id) => client.copilotGet<{ organization?: { name?: string } }>(`/organization/${id}`).then((r) => r.organization)),
|
|
180
208
|
]);
|
|
181
209
|
return { names, missing };
|
|
@@ -231,6 +259,18 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
231
259
|
const config = new ConfigSDK(client);
|
|
232
260
|
const functions = new FunctionsSDK(client);
|
|
233
261
|
// A skill of an agent by its id.
|
|
262
|
+
/** An export result with a signed download_url: the services hand out unsigned file-store links (403 in a browser). */
|
|
263
|
+
const withDownloadLink = async <T extends object>(result: T, stored?: string) => {
|
|
264
|
+
if (!stored || typeof stored !== 'string') return result;
|
|
265
|
+
const signed = /X-Amz-Signature=/.test(stored) ? stored : await kb.signedLink(stored);
|
|
266
|
+
return { ...result, download_url: signed ?? stored, ...(signed ? {} : { note: 'The link could not be signed; it may need signing in to the dashboard.' }) };
|
|
267
|
+
};
|
|
268
|
+
/** A report file written by this process, put in the organization's file store so the person can download it. */
|
|
269
|
+
const deliver = async (result: { file_path: string; file_name: string; file_type: string }, upload?: boolean) => {
|
|
270
|
+
if (!upload) return result;
|
|
271
|
+
const { signedUrl, url } = await kb.storeFile(result.file_path);
|
|
272
|
+
return { ...result, download_url: signedUrl ?? url };
|
|
273
|
+
};
|
|
234
274
|
const skillOf = async (agentId: string, id: string) => {
|
|
235
275
|
const skill = (await functions.listFunctions(agentId)).find((f) => f._id === id);
|
|
236
276
|
if (!skill) throw new Error(`Skill ${id} is not on agent ${agentId}.`);
|
|
@@ -259,11 +299,15 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
259
299
|
reg.tool(
|
|
260
300
|
'list_agents',
|
|
261
301
|
'List the agents (bots) of the organization: id, name, channels. Start here to find an agent id.',
|
|
262
|
-
{
|
|
302
|
+
{
|
|
303
|
+
channel: z.string().optional().describe('Only agents on this channel, e.g. web, voice, email'),
|
|
304
|
+
search: z.string().optional().describe('Only agents whose name contains this text'),
|
|
305
|
+
},
|
|
263
306
|
async (args) => {
|
|
264
307
|
try {
|
|
265
308
|
const pages = await orgPages(client);
|
|
266
309
|
const wanted = args.channel?.trim().toUpperCase();
|
|
310
|
+
const needle = args.search?.trim().toLowerCase();
|
|
267
311
|
// What a person needs, not the stored record (a page of full records ran to 73k characters).
|
|
268
312
|
const rows = (await agents.listAgents()).map((a) => ({
|
|
269
313
|
_id: a._id,
|
|
@@ -275,7 +319,8 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
275
319
|
updated_at: a.updatedAt,
|
|
276
320
|
url: `${pages}/ai-studio/bot/${a._id}/configurations`,
|
|
277
321
|
}));
|
|
278
|
-
|
|
322
|
+
const byChannel = wanted ? rows.filter((a) => a.channels.some((c) => c.toUpperCase() === wanted)) : rows;
|
|
323
|
+
return success(needle ? byChannel.filter((a) => a.name.toLowerCase().includes(needle)) : byChannel);
|
|
279
324
|
} catch (err) { return error(err); }
|
|
280
325
|
},
|
|
281
326
|
);
|
|
@@ -392,7 +437,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
392
437
|
preview: async (a) => {
|
|
393
438
|
const agent = await agents.getAgent(String(a.agent_id));
|
|
394
439
|
const lifecycle = await lifecycleOf(String(a.agent_id));
|
|
395
|
-
return `This will delete agent "${agent.name}"${mayBeLive(lifecycle) ? `, which ${liveText(lifecycle)}` : ''}. It cannot be restored
|
|
440
|
+
return `This will delete agent "${agent.name}"${mayBeLive(lifecycle) ? `, which ${liveText(lifecycle)}` : ''}. It cannot be restored.`;
|
|
396
441
|
},
|
|
397
442
|
},
|
|
398
443
|
);
|
|
@@ -450,7 +495,16 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
450
495
|
},
|
|
451
496
|
async (args) => {
|
|
452
497
|
try {
|
|
453
|
-
|
|
498
|
+
const copy = await agents.cloneAgent(args.agent_id, { name: args.name, channels: args.channels });
|
|
499
|
+
// copilot's clone answers { new_agent_id, new_agent_name, ... } and leaves the description empty:
|
|
500
|
+
// carry the original's over.
|
|
501
|
+
const copyId = copy._id ?? (copy as unknown as { new_agent_id?: string }).new_agent_id;
|
|
502
|
+
const source = await agents.getAgent(args.agent_id);
|
|
503
|
+
if (source.description && !copy.description && copyId) {
|
|
504
|
+
const described = await agents.updateAgent(copyId, { description: source.description }).then(() => true).catch(() => false);
|
|
505
|
+
return success({ ...copy, ...(described ? { description: source.description } : {}) });
|
|
506
|
+
}
|
|
507
|
+
return success(copy);
|
|
454
508
|
} catch (err) { return error(err); }
|
|
455
509
|
},
|
|
456
510
|
);
|
|
@@ -468,7 +522,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
468
522
|
'import_agent',
|
|
469
523
|
'Create a new agent in this org from a bundle made by export_agent (possibly from another org or platform). Lists skills that failed and secrets to fill in.',
|
|
470
524
|
{
|
|
471
|
-
bundle:
|
|
525
|
+
bundle: jsonObject().describe('The bundle returned by export_agent (an object, or JSON text)'),
|
|
472
526
|
name: z.string().optional().describe('Name for the new agent (default: the bundle name)'),
|
|
473
527
|
link_knowledge_bases: z.boolean().optional().describe('Link the same knowledge base ids if they exist in this org'),
|
|
474
528
|
allow_duplicate: z.boolean().optional(),
|
|
@@ -515,16 +569,18 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
515
569
|
system_prompt: a.system_prompt as string | undefined,
|
|
516
570
|
greeting: a.first_message as string | undefined,
|
|
517
571
|
model: a.model ? modelArg(String(a.model)) : undefined,
|
|
572
|
+
parameters: a.temperature !== undefined ? { temperature: a.temperature } : undefined,
|
|
518
573
|
});
|
|
519
574
|
|
|
520
575
|
reg.tool(
|
|
521
576
|
'set_config',
|
|
522
|
-
'Change an agent\'s prompt, greeting and/or
|
|
577
|
+
'Change an agent\'s prompt, greeting, model and/or temperature. The confirmation shows the prompt diff and whether the agent is published; published agents need confirm_published too. The result says what was saved (read back from the platform).',
|
|
523
578
|
{
|
|
524
579
|
agent_id: z.string().describe('The agent ID'),
|
|
525
580
|
system_prompt: z.string().optional().describe('New system prompt (whole text)'),
|
|
526
581
|
first_message: z.string().optional().describe('New greeting shown to users'),
|
|
527
582
|
model: z.string().optional().describe('provider/model, e.g. openai/gpt-4.1 (see list_llm_models)'),
|
|
583
|
+
temperature: z.number().min(0).max(2).optional().describe('Model temperature (0 = most predictable); only for models that expose it'),
|
|
528
584
|
confirm_published: z.boolean().optional().describe('Required with confirm when the agent is published and the user explicitly agreed'),
|
|
529
585
|
},
|
|
530
586
|
async (args, meta) => {
|
|
@@ -592,10 +648,16 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
592
648
|
...skillAuthFields,
|
|
593
649
|
configuration: jsonObject().optional().describe('Other configurationSchema fields (an object, or JSON text); the fields above win'),
|
|
594
650
|
transfer_to: z.string().optional().describe('transfer_call: phone number (E.164, e.g. +15551234567) or SIP address (sip:...) calls are transferred to'),
|
|
651
|
+
parameters: jsonArray(z.object({
|
|
652
|
+
name: z.string(), type: z.string().describe('string, number or boolean'),
|
|
653
|
+
description: z.string().optional(), required: z.boolean().optional(),
|
|
654
|
+
allowed_values: z.array(z.string()).optional().describe('Fixed choices the customer picks from'),
|
|
655
|
+
})).optional().describe('What the skill collects from the customer before it runs ({{name}} placeholders in url/payload)'),
|
|
595
656
|
},
|
|
596
657
|
async (args) => {
|
|
597
658
|
try {
|
|
598
659
|
const { agent_id, type, name, description } = args;
|
|
660
|
+
const parameters = (args.parameters ?? []) as Array<{ name: string; type: string; description?: string; required?: boolean; allowed_values?: string[] }>;
|
|
599
661
|
const configurationSchema = skillConfiguration(type, withAuthorization(args));
|
|
600
662
|
// copilot sends a REST answer straight to the customer unless craftResponse is on; with nothing to say
|
|
601
663
|
// how to present it, let the agent phrase it.
|
|
@@ -613,9 +675,16 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
613
675
|
if (same) throw new Error(`The agent already has a skill named "${name}" (function_id ${same._id}). Change it with update_function instead of creating another.`);
|
|
614
676
|
// copilot turns configurationSchema into the executable form only on update: create, then configure.
|
|
615
677
|
const created = await functions.createFunction(agent_id, { type, name, description });
|
|
616
|
-
|
|
678
|
+
// Parameters are their own records: a skill created with parameters used to lose them silently.
|
|
679
|
+
const addParameters = async () => {
|
|
680
|
+
const saved = [];
|
|
681
|
+
for (const { allowed_values, ...p } of parameters) saved.push(await functions.addParameter(agent_id, created._id, { ...p, allowedValues: allowed_values }));
|
|
682
|
+
return saved;
|
|
683
|
+
};
|
|
684
|
+
if (!configurationSchema) return success(parameters.length ? { ...created, parameters: await addParameters() } : created);
|
|
617
685
|
try {
|
|
618
|
-
|
|
686
|
+
const configured = await functions.updateFunction(agent_id, created._id, { type, description: description ?? '', configurationSchema });
|
|
687
|
+
return success(parameters.length ? { ...configured, parameters: await addParameters() } : configured);
|
|
619
688
|
} catch (err) {
|
|
620
689
|
// Leave no half-made skill behind: remove the empty one, then say why nothing was saved.
|
|
621
690
|
const why = err instanceof Error ? err.message : String(err);
|
|
@@ -707,12 +776,12 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
707
776
|
|
|
708
777
|
reg.tool(
|
|
709
778
|
'export_functions',
|
|
710
|
-
'Export every skill of an agent as a JSON file (to copy them or keep a backup before import_functions).',
|
|
779
|
+
'Export every skill of an agent as a JSON file (to copy them or keep a backup before import_functions); download_url is the link to give the person.',
|
|
711
780
|
{ agent_id: z.string().describe('The agent ID') },
|
|
712
781
|
async (args) => {
|
|
713
782
|
try {
|
|
714
|
-
const result = await functions.exportFunctions(args.agent_id);
|
|
715
|
-
return success(result);
|
|
783
|
+
const result = await functions.exportFunctions(args.agent_id) as { fileUrl?: string; signedUrl?: string; url?: string };
|
|
784
|
+
return success(await withDownloadLink(result, result.signedUrl ?? result.fileUrl ?? result.url));
|
|
716
785
|
} catch (err) { return error(err); }
|
|
717
786
|
},
|
|
718
787
|
);
|
|
@@ -807,6 +876,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
807
876
|
return success(result);
|
|
808
877
|
} catch (err) { return error(err); }
|
|
809
878
|
},
|
|
879
|
+
liveChange('a skill parameter', ['function_id']),
|
|
810
880
|
);
|
|
811
881
|
|
|
812
882
|
reg.tool(
|
|
@@ -829,6 +899,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
829
899
|
return success(result);
|
|
830
900
|
} catch (err) { return error(err); }
|
|
831
901
|
},
|
|
902
|
+
liveChange('a skill parameter', ['function_id', 'parameter_id']),
|
|
832
903
|
);
|
|
833
904
|
|
|
834
905
|
reg.tool(
|
|
@@ -950,11 +1021,12 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
950
1021
|
|
|
951
1022
|
reg.tool(
|
|
952
1023
|
'list_kb_data',
|
|
953
|
-
'The question/answer pairs typed into a knowledge base (website and file sources are listed by get_knowledge_base).',
|
|
1024
|
+
'The question/answer pairs typed into a knowledge base (website and file sources are listed by get_knowledge_base). Each entry: id, question, answer, whether it is trained. full: true adds the stored chunks.',
|
|
954
1025
|
{
|
|
955
1026
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
956
1027
|
page: z.number().optional().describe('Page number'),
|
|
957
1028
|
limit: z.number().optional().describe('Items per page'),
|
|
1029
|
+
full: z.boolean().optional().describe('Include the chunks and statistics of every entry'),
|
|
958
1030
|
},
|
|
959
1031
|
async (args) => {
|
|
960
1032
|
try {
|
|
@@ -962,7 +1034,15 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
962
1034
|
if (args.page !== undefined) query.page = String(args.page);
|
|
963
1035
|
if (args.limit !== undefined) query.limit = String(args.limit);
|
|
964
1036
|
const result = await kb.listDataSources(args.kb_id, Object.keys(query).length ? query : undefined);
|
|
965
|
-
return success(result);
|
|
1037
|
+
if (args.full) return success(result);
|
|
1038
|
+
// The stored record repeats every chunk twice (chunks, chunks_text): a page of it is tens of kilobytes.
|
|
1039
|
+
const entries = (result.dataSources ?? []).map((d) => ({
|
|
1040
|
+
_id: d._id, type: d.type, question: d.question ?? d.name, answer: d.answer,
|
|
1041
|
+
...(d.source_url ? { source_url: d.source_url } : {}),
|
|
1042
|
+
trained: (d as { trained?: boolean }).trained, ...(d.status ? { status: d.status } : {}),
|
|
1043
|
+
...((d as { error_message?: string }).error_message ? { error: (d as { error_message?: string }).error_message } : {}),
|
|
1044
|
+
}));
|
|
1045
|
+
return success({ ...result, dataSources: entries });
|
|
966
1046
|
} catch (err) { return error(err); }
|
|
967
1047
|
},
|
|
968
1048
|
);
|
|
@@ -1144,30 +1224,37 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1144
1224
|
|
|
1145
1225
|
reg.tool(
|
|
1146
1226
|
'export_kb',
|
|
1147
|
-
'Export the question/answer pairs of a knowledge base as a CSV or XLSX
|
|
1227
|
+
'Export the question/answer pairs of a knowledge base as a CSV or XLSX file; download_url is the link to give the person (valid for a while).',
|
|
1148
1228
|
{
|
|
1149
1229
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
1150
|
-
format: z.string().optional().describe('Export format (
|
|
1230
|
+
format: z.string().optional().describe('Export format: csv (default) or xlsx'),
|
|
1151
1231
|
},
|
|
1152
1232
|
async (args) => {
|
|
1153
1233
|
try {
|
|
1154
|
-
const result = await kb.exportKB(args.kb_id, args.format || '
|
|
1155
|
-
return success(result);
|
|
1234
|
+
const result = await kb.exportKB(args.kb_id, args.format || 'csv') as { fileUrl?: string; signedUrl?: string };
|
|
1235
|
+
return success(await withDownloadLink(result, result.signedUrl ?? result.fileUrl));
|
|
1156
1236
|
} catch (err) { return error(err); }
|
|
1157
1237
|
},
|
|
1158
1238
|
);
|
|
1159
1239
|
|
|
1160
1240
|
reg.tool(
|
|
1161
1241
|
'import_kb',
|
|
1162
|
-
'Import
|
|
1242
|
+
'Import files (already in the organization\'s file store) into a knowledge base; processed in the background (see kb_scrape_status). For a website use scrape_url.',
|
|
1163
1243
|
{
|
|
1164
1244
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
1165
|
-
data:
|
|
1245
|
+
data: kbImportSchema.describe('{ type: "file", sources: [{ name, file_type, url }] } (an object, or JSON text)'),
|
|
1166
1246
|
},
|
|
1167
1247
|
async (args) => {
|
|
1168
1248
|
try {
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1249
|
+
const data = args.data as { type: string; sources: Array<{ name?: string; url: string; file_type?: string }> };
|
|
1250
|
+
// The import service tells the file's kind from file_type: fill it from the name or the link.
|
|
1251
|
+
const sources = data.sources.map((s) => ({
|
|
1252
|
+
...s,
|
|
1253
|
+
name: s.name ?? s.url.split('?')[0].split('/').pop() ?? 'file',
|
|
1254
|
+
file_type: (s.file_type ?? (s.name ?? s.url.split('?')[0]).split('.').pop() ?? '').toLowerCase(),
|
|
1255
|
+
}));
|
|
1256
|
+
const result = await kb.importKB(args.kb_id, { ...data, sources });
|
|
1257
|
+
return success({ ...(result as object), imported: sources.map((s) => s.name) });
|
|
1171
1258
|
} catch (err) { return error(err); }
|
|
1172
1259
|
},
|
|
1173
1260
|
);
|
|
@@ -1220,7 +1307,8 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1220
1307
|
return success({
|
|
1221
1308
|
conversation_id: conversation.request_id, test: conversation.isTestMode === true,
|
|
1222
1309
|
url: conversationUrl(client, projectId, conversation.request_id),
|
|
1223
|
-
|
|
1310
|
+
// System lines carry the dashboard's template ("{{userLabel}} has ended the call"): say it in words.
|
|
1311
|
+
messages: messages.map((m) => ({ role: m.role, text: (m.text ?? '').replace(/\{\{\s*userLabel\s*\}\}/g, 'The customer'), at: m.createdAt })),
|
|
1224
1312
|
});
|
|
1225
1313
|
} catch (err) { return error(err); }
|
|
1226
1314
|
},
|
|
@@ -1322,10 +1410,18 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1322
1410
|
|
|
1323
1411
|
reg.tool(
|
|
1324
1412
|
'list_agent_numbers',
|
|
1325
|
-
'Phone numbers attached to an agent, or (without agent_id) every number the org owns.',
|
|
1413
|
+
'Phone numbers attached to an agent, or (without agent_id) every number the org owns: the ones attached to an agent (with the agent) and the free ones.',
|
|
1326
1414
|
{ agent_id: z.string().optional() },
|
|
1327
1415
|
async (args) => {
|
|
1328
|
-
try {
|
|
1416
|
+
try {
|
|
1417
|
+
if (args.agent_id) return success(await voice.agentNumbers(args.agent_id));
|
|
1418
|
+
// copilot's org list holds only unattached ("reserve") numbers; the attached ones sit on the agents.
|
|
1419
|
+
const [free, all] = await Promise.all([voice.orgNumbers(), agents.listAgents()]);
|
|
1420
|
+
const attached = all
|
|
1421
|
+
.filter((a) => (a as { connected_phone?: string }).connected_phone)
|
|
1422
|
+
.map((a) => ({ phone_number: (a as { connected_phone?: string }).connected_phone, attached: true, agent_id: a._id, agent_name: a.name }));
|
|
1423
|
+
return success([...attached, ...free.map((n) => ({ phone_number: n.phoneNumber, attached: false, name: n.friendlyName, type: n.type }))]);
|
|
1424
|
+
} catch (err) { return error(err); }
|
|
1329
1425
|
},
|
|
1330
1426
|
);
|
|
1331
1427
|
|
|
@@ -1374,7 +1470,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1374
1470
|
async (args, meta) => {
|
|
1375
1471
|
try {
|
|
1376
1472
|
// A purchase needs the person, not the model: confirm: true from a host without approval forms is not enough.
|
|
1377
|
-
if (!meta.confirmedByPerson) throw new Error('Buying a number needs a person to approve it in an approval form, and this host cannot show one. Nothing was bought. Buy it in the dashboard
|
|
1473
|
+
if (!meta.confirmedByPerson) throw new Error('Buying a number needs a person to approve it in an approval form, and this host cannot show one. Nothing was bought. Buy it in the dashboard instead.');
|
|
1378
1474
|
return success(await voice.buyNumber(args.agent_id, args.phone_number, { country: args.country, tollFree: args.toll_free }));
|
|
1379
1475
|
} catch (err) { return error(err); }
|
|
1380
1476
|
},
|
|
@@ -1512,19 +1608,64 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1512
1608
|
try { return success(await orgSettings.setTimezone(args.timezone)); } catch (err) { return error(err); }
|
|
1513
1609
|
});
|
|
1514
1610
|
|
|
1611
|
+
const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
1612
|
+
const clock = (hhmm: string) => {
|
|
1613
|
+
const [h, m] = hhmm.split(':').map(Number);
|
|
1614
|
+
const hour = ((h + 11) % 12) + 1;
|
|
1615
|
+
return `${hour}${m ? `:${String(m).padStart(2, '0')}` : ''}${h < 12 ? 'am' : 'pm'}`;
|
|
1616
|
+
};
|
|
1617
|
+
/** What agents say about the hours, built from the hours themselves so it never contradicts them. */
|
|
1618
|
+
const hoursMessage = (days: Array<{ day: number; slots: Array<{ start: string; end: string }> }>) => {
|
|
1619
|
+
const open = days.filter((d) => d.slots.length).sort((a, b) => a.day - b.day);
|
|
1620
|
+
if (!open.length) return 'We are currently closed.';
|
|
1621
|
+
const parts = open.map((d) => `${DAY_NAMES[d.day]} ${d.slots.map((s) => `${clock(s.start)} to ${clock(s.end)}`).join(' and ')}`);
|
|
1622
|
+
return `We're open ${parts.join('; ')}.`;
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1515
1625
|
reg.tool(
|
|
1516
1626
|
'set_business_hours',
|
|
1517
|
-
'Replace the org business hours. days: 0 Sunday to 6
|
|
1518
|
-
{
|
|
1627
|
+
'Replace the org business hours: every day not listed is closed. days: 0 Sunday to 6. enabled turns enforcement on or off (unchanged when omitted). The out-of-hours message is rebuilt from the hours unless given.',
|
|
1628
|
+
{
|
|
1629
|
+
enabled: z.boolean().optional().describe('Enforce the hours (agents say the out-of-hours message outside them); unchanged when omitted'),
|
|
1630
|
+
hours: jsonArray(z.object({ day: z.number().int().min(0).max(6), slots: z.array(z.object({ start: z.string().describe('HH:MM'), end: z.string().describe('HH:MM') })) })).optional(),
|
|
1631
|
+
out_of_hours_message: z.string().optional(),
|
|
1632
|
+
},
|
|
1519
1633
|
async (args) => {
|
|
1520
1634
|
try {
|
|
1521
|
-
|
|
1635
|
+
const current = (await orgSettings.get()) as { businessHoursConfig?: { businessHoursEnabled?: boolean } };
|
|
1636
|
+
const enabled = args.enabled ?? Boolean(current.businessHoursConfig?.businessHoursEnabled);
|
|
1637
|
+
const given = (args.hours ?? []) as Array<{ day: number; slots: Array<{ start: string; end: string }> }>;
|
|
1638
|
+
// Replace means replace: a day the person did not mention is closed.
|
|
1639
|
+
const days = args.hours ? DAY_NAMES.map((_, day) => ({ day, slots: given.filter((d) => d.day === day).flatMap((d) => d.slots) })) : undefined;
|
|
1640
|
+
return success(await orgSettings.setBusinessHours({
|
|
1641
|
+
businessHoursEnabled: enabled,
|
|
1642
|
+
...(days ? { businessHours: days, businessHoursUtterance: args.out_of_hours_message ?? hoursMessage(days) } : {}),
|
|
1643
|
+
...(!days && args.out_of_hours_message ? { businessHoursUtterance: args.out_of_hours_message } : {}),
|
|
1644
|
+
}));
|
|
1522
1645
|
} catch (err) { return error(err); }
|
|
1523
1646
|
},
|
|
1524
|
-
|
|
1647
|
+
{ preview: async (a) => {
|
|
1648
|
+
const hours = (a.hours ?? []) as Array<{ day: number; slots: Array<{ start: string; end: string }> }>;
|
|
1649
|
+
const what = a.hours ? hoursMessage(DAY_NAMES.map((_, day) => ({ day, slots: hours.filter((d) => d.day === day).flatMap((d) => d.slots) }))) : 'the hours unchanged';
|
|
1650
|
+
return `This will replace the org business hours: ${what}${a.enabled === undefined ? '' : a.enabled ? ' Enforced: agents say the out-of-hours message outside them.' : ' Not enforced.'}`;
|
|
1651
|
+
} },
|
|
1652
|
+
);
|
|
1653
|
+
|
|
1654
|
+
reg.tool('add_holiday', 'Close the org on a date.', {
|
|
1655
|
+
date: z.string().describe('YYYY-MM-DD'), name: z.string(),
|
|
1656
|
+
message: z.string().optional().describe('What agents say that day (default: closed on account of the holiday)'),
|
|
1657
|
+
type: z.string().optional().describe('public or custom (default custom)'),
|
|
1658
|
+
}, async (args) => {
|
|
1659
|
+
try {
|
|
1660
|
+
return success(await orgSettings.addHolidays([{
|
|
1661
|
+
name: args.name, date: args.date, type: args.type ?? 'custom',
|
|
1662
|
+
holidayFallbackUtterance: args.message ?? `Our offices are currently closed on account of ${args.name}.`,
|
|
1663
|
+
}]));
|
|
1664
|
+
} catch (err) { return error(err); }
|
|
1665
|
+
});
|
|
1525
1666
|
|
|
1526
|
-
reg.tool('
|
|
1527
|
-
try { return success(await orgSettings.
|
|
1667
|
+
reg.tool('remove_holiday', 'Open the org again on a date that was a holiday.', { date: z.string().describe('YYYY-MM-DD, as get_org_settings lists it') }, async (args) => {
|
|
1668
|
+
try { return success(await orgSettings.removeHolidays([args.date])); } catch (err) { return error(err); }
|
|
1528
1669
|
});
|
|
1529
1670
|
|
|
1530
1671
|
reg.tool(
|
|
@@ -1562,7 +1703,16 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1562
1703
|
'Who changed what in the org, newest first, filtered here (the server has no filters).',
|
|
1563
1704
|
{ since: z.string().optional(), action: z.string().optional(), resource_type: z.string().optional(), user_id: z.string().optional(), limit: z.number().int().min(1).max(500).optional() },
|
|
1564
1705
|
async (args) => {
|
|
1565
|
-
try {
|
|
1706
|
+
try {
|
|
1707
|
+
const entries = await new AuditSDK(client).list({ since: args.since, action: args.action, resourceType: args.resource_type, user: args.user_id, limit: args.limit });
|
|
1708
|
+
// copilot's log carries the person's id only: name them from the member list (one read).
|
|
1709
|
+
const members = entries.some((e) => e.id_user) ? await orgs.listMembers().catch(() => []) : [];
|
|
1710
|
+
const byId = new Map(members.map((m) => [m.user_id, m]));
|
|
1711
|
+
return success(entries.map((e) => {
|
|
1712
|
+
const m = byId.get(String(e.id_user ?? ''));
|
|
1713
|
+
return m ? { ...e, user_name: m.name, user_email: m.email } : e;
|
|
1714
|
+
}));
|
|
1715
|
+
} catch (err) { return error(err); }
|
|
1566
1716
|
},
|
|
1567
1717
|
);
|
|
1568
1718
|
|
|
@@ -1604,11 +1754,13 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1604
1754
|
|
|
1605
1755
|
reg.tool(
|
|
1606
1756
|
'update_ticket',
|
|
1607
|
-
'Change a ticket: fields, assignee (user id, or unassign: true), or close it with resolve: true
|
|
1757
|
+
'Change a ticket: fields, a note (added to its notes, or the closing note with resolve), assignee (user id, or unassign: true), or close it with resolve: true.',
|
|
1608
1758
|
{
|
|
1609
|
-
ticket_id: z.string(), title: z.string().optional(), description: z.string().optional(),
|
|
1759
|
+
ticket_id: z.string(), title: z.string().optional(), description: z.string().optional(),
|
|
1760
|
+
notes: z.string().optional().describe('Replace the whole notes text'),
|
|
1610
1761
|
status: z.enum(['not_started', 'in_progress']).optional(), priority: ticketPriority.optional(),
|
|
1611
|
-
assignee_user_id: z.string().optional(), unassign: z.boolean().optional(), resolve: z.boolean().optional(),
|
|
1762
|
+
assignee_user_id: z.string().optional(), unassign: z.boolean().optional(), resolve: z.boolean().optional(),
|
|
1763
|
+
note: z.string().optional().describe('A note to add: appended to the ticket\'s notes, or the closing note when resolving'),
|
|
1612
1764
|
link_ticket_id: z.string().optional().describe('Link to another ticket'),
|
|
1613
1765
|
},
|
|
1614
1766
|
async (args) => {
|
|
@@ -1616,6 +1768,11 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1616
1768
|
const out: Record<string, unknown> = {};
|
|
1617
1769
|
const changes: Record<string, unknown> = {};
|
|
1618
1770
|
for (const k of ['title', 'description', 'notes', 'status', 'priority'] as const) if (args[k] !== undefined) changes[k] = args[k];
|
|
1771
|
+
// A note outside resolve used to be dropped silently: it goes onto the ticket's notes.
|
|
1772
|
+
if (args.note && !args.resolve) {
|
|
1773
|
+
const existing = String(((await tickets.get(args.ticket_id)) as { notes?: unknown }).notes ?? '').trim();
|
|
1774
|
+
changes.notes = args.notes !== undefined ? `${args.notes}\n${args.note}`.trim() : existing ? `${existing}\n${args.note}` : args.note;
|
|
1775
|
+
}
|
|
1619
1776
|
if (Object.keys(changes).length) out.updated = await tickets.update(args.ticket_id, changes);
|
|
1620
1777
|
if (args.assignee_user_id || args.unassign) out.assigned = await tickets.assign(args.ticket_id, args.unassign ? null : { type: 'user', target: args.assignee_user_id! });
|
|
1621
1778
|
if (args.link_ticket_id) out.linked = await tickets.link(args.ticket_id, args.link_ticket_id);
|
|
@@ -1718,7 +1875,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1718
1875
|
reg.tool(
|
|
1719
1876
|
'platform_save',
|
|
1720
1877
|
'Create (no id) or change (id) a V2 agent, template, connector binding or scenario. Components and connectors are read-only shared catalogs.',
|
|
1721
|
-
{ resource: z.enum(['agents', 'templates', 'connector-bindings', 'scenarios']), id: z.string().optional(), body:
|
|
1878
|
+
{ resource: z.enum(['agents', 'templates', 'connector-bindings', 'scenarios']), id: z.string().optional(), body: jsonObject().describe('The item to save (an object, or JSON text)') },
|
|
1722
1879
|
async (args) => {
|
|
1723
1880
|
try { return success(args.id ? await platform.update(args.resource, args.id, args.body) : await platform.create(args.resource, args.body)); } catch (err) { return error(err); }
|
|
1724
1881
|
},
|
|
@@ -1883,7 +2040,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1883
2040
|
'Judge a finished run like the CI gate: pass rate = passed/total, optional p95 limit, failed runs fail by default.',
|
|
1884
2041
|
{
|
|
1885
2042
|
run_id: z.string(),
|
|
1886
|
-
min_pass_rate: z.number().min(0).max(1).optional(),
|
|
2043
|
+
min_pass_rate: z.number().min(0).max(1).optional().describe('Pass rate the run must reach (default 0.8)'),
|
|
1887
2044
|
max_p95_ms: z.number().optional(),
|
|
1888
2045
|
error_policy: z.enum(['block', 'warn']).optional(),
|
|
1889
2046
|
fail_on_error: z.boolean().optional(),
|
|
@@ -1892,7 +2049,9 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1892
2049
|
try {
|
|
1893
2050
|
const run = await evals.getRunInOrg(args.run_id);
|
|
1894
2051
|
if (run.status !== 'completed' && run.status !== 'failed') return success({ finished: false, status: run.status });
|
|
1895
|
-
|
|
2052
|
+
// Without a threshold any finished run "passed" (a 1-of-3 run was reported as a pass): judge against 80%.
|
|
2053
|
+
const minPassRate = args.min_pass_rate ?? DEFAULT_MIN_PASS_RATE;
|
|
2054
|
+
return success({ finished: true, min_pass_rate: minPassRate, ...gateVerdict(run, { minPassRate, maxP95Ms: args.max_p95_ms, errorPolicy: args.error_policy, failOnError: args.fail_on_error }) });
|
|
1896
2055
|
} catch (err) { return error(err); }
|
|
1897
2056
|
},
|
|
1898
2057
|
);
|
|
@@ -2006,20 +2165,42 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
2006
2165
|
},
|
|
2007
2166
|
);
|
|
2008
2167
|
|
|
2168
|
+
// get_conversation says "agent"; the evaluations service says "bot". Both are accepted.
|
|
2169
|
+
const transcriptTurn = z.object({ sender: z.enum(['user', 'bot', 'agent', 'system']), text: z.string() });
|
|
2170
|
+
const transcriptForService = (turns: Array<{ sender: string; text: string }>) => turns
|
|
2171
|
+
.filter((t) => t.sender !== 'system')
|
|
2172
|
+
.map((t) => ({ sender: (t.sender === 'user' ? 'user' : 'bot') as 'user' | 'bot', text: t.text }));
|
|
2173
|
+
const transcriptPreconditions = async (a: Record<string, unknown>) => {
|
|
2174
|
+
const agentId = String(a.agent_id);
|
|
2175
|
+
await evals.assertAgentInOrg(agentId);
|
|
2176
|
+
const configs = await evals.listEvaluatorConfigs(agentId) as { items?: unknown[] } | unknown[];
|
|
2177
|
+
const count = Array.isArray(configs) ? configs.length : (configs.items ?? []).length;
|
|
2178
|
+
if (!count) throw new Error('This agent has no evaluator yet, so a transcript cannot be graded. Create a rubric and run an evaluation first (run_evaluation stores the evaluator).');
|
|
2179
|
+
if (JSON.stringify(a.baseline_transcript) === JSON.stringify(a.replay_transcript)) {
|
|
2180
|
+
throw new Error('The baseline and the replay are the same conversation; grading compares a replay with its baseline. Give the replayed conversation as replay_transcript.');
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
|
|
2009
2184
|
reg.tool(
|
|
2010
2185
|
'evaluate_transcript',
|
|
2011
|
-
'Grade a replayed conversation against a baseline with the agent\'s stored evaluator (paid model calls).',
|
|
2186
|
+
'Grade a replayed conversation against a baseline with the agent\'s stored evaluator (paid model calls). The agent needs an evaluator (a finished run), and the replay must differ from the baseline.',
|
|
2012
2187
|
{
|
|
2013
2188
|
agent_id: z.string(),
|
|
2014
|
-
baseline_transcript:
|
|
2015
|
-
replay_transcript:
|
|
2189
|
+
baseline_transcript: jsonArray(transcriptTurn),
|
|
2190
|
+
replay_transcript: jsonArray(transcriptTurn),
|
|
2016
2191
|
},
|
|
2017
2192
|
async (args) => {
|
|
2018
2193
|
try {
|
|
2019
|
-
await
|
|
2020
|
-
return success(await evals.evaluateTranscript(
|
|
2194
|
+
await transcriptPreconditions(args);
|
|
2195
|
+
return success(await evals.evaluateTranscript({
|
|
2196
|
+
agent_id: args.agent_id,
|
|
2197
|
+
baseline_transcript: transcriptForService(args.baseline_transcript),
|
|
2198
|
+
replay_transcript: transcriptForService(args.replay_transcript),
|
|
2199
|
+
}));
|
|
2021
2200
|
} catch (err) { return error(err); }
|
|
2022
2201
|
},
|
|
2202
|
+
// Checked before the approval card: a card for something that cannot run wastes the person's answer.
|
|
2203
|
+
{ preview: async (a) => { await transcriptPreconditions(a); return `This will grade a replayed transcript for agent ${a.agent_id} with paid model calls.`; } },
|
|
2023
2204
|
);
|
|
2024
2205
|
|
|
2025
2206
|
reg.tool(
|
|
@@ -2236,17 +2417,20 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
2236
2417
|
// Export tools
|
|
2237
2418
|
// ---------------------------------------------------------------------------
|
|
2238
2419
|
|
|
2420
|
+
const uploadArg = z.boolean().optional().describe('Put the file in the organization\'s file store and return download_url (use it when the person cannot reach this machine\'s files)');
|
|
2421
|
+
|
|
2239
2422
|
reg.tool(
|
|
2240
2423
|
'export_eval_markdown',
|
|
2241
2424
|
'Export evaluation results as a structured markdown file with conversations, scores, and criteria',
|
|
2242
2425
|
{
|
|
2243
2426
|
run_id: z.string().describe('The evaluation run ID'),
|
|
2244
2427
|
output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
|
|
2428
|
+
upload: uploadArg,
|
|
2245
2429
|
},
|
|
2246
2430
|
async (args) => {
|
|
2247
2431
|
try {
|
|
2248
2432
|
const result = await exportEvalMarkdown(args.run_id, args.output_path, client);
|
|
2249
|
-
return success(result);
|
|
2433
|
+
return success(await deliver(result, args.upload));
|
|
2250
2434
|
} catch (err) { return error(err); }
|
|
2251
2435
|
},
|
|
2252
2436
|
);
|
|
@@ -2257,11 +2441,12 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
2257
2441
|
{
|
|
2258
2442
|
run_id: z.string().describe('The evaluation run ID'),
|
|
2259
2443
|
output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
|
|
2444
|
+
upload: uploadArg,
|
|
2260
2445
|
},
|
|
2261
2446
|
async (args) => {
|
|
2262
2447
|
try {
|
|
2263
2448
|
const result = await exportEvalReport(args.run_id, args.output_path, client);
|
|
2264
|
-
return success(result);
|
|
2449
|
+
return success(await deliver(result, args.upload));
|
|
2265
2450
|
} catch (err) { return error(err); }
|
|
2266
2451
|
},
|
|
2267
2452
|
);
|
|
@@ -2272,11 +2457,12 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
2272
2457
|
{
|
|
2273
2458
|
agent_id: z.string().describe('The agent ID'),
|
|
2274
2459
|
output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
|
|
2460
|
+
upload: uploadArg,
|
|
2275
2461
|
},
|
|
2276
2462
|
async (args) => {
|
|
2277
2463
|
try {
|
|
2278
2464
|
const result = await exportAgentCard(args.agent_id, args.output_path, client);
|
|
2279
|
-
return success(result);
|
|
2465
|
+
return success(await deliver(result, args.upload));
|
|
2280
2466
|
} catch (err) { return error(err); }
|
|
2281
2467
|
},
|
|
2282
2468
|
);
|
package/src/report/styles.ts
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
import { Font, StyleSheet } from '@react-pdf/renderer';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
5
6
|
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
|
|
8
|
+
// Fonts and logo: copied next to the built code by `npm run build`; the published package also carries
|
|
9
|
+
// src/report/assets, so an install whose build step skipped the copy still finds them.
|
|
10
|
+
const candidates = [join(__dirname, 'assets'), join(__dirname, '..', '..', 'src', 'report', 'assets')];
|
|
11
|
+
export const assetsDir = candidates.find((dir) => existsSync(join(dir, 'fonts', 'Barlow-Regular.ttf'))) ?? candidates[0];
|
|
8
12
|
|
|
9
13
|
// Register Barlow font family from bundled files
|
|
10
14
|
Font.register({
|
package/src/sdk/auth.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { initializeApp, getApp } from 'firebase/app';
|
|
2
|
-
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';
|
|
1
|
+
import { initializeApp, getApp } from '@firebase/app';
|
|
2
|
+
import { getAuth, signInWithEmailAndPassword } from '@firebase/auth';
|
|
3
3
|
import { DEFAULT_HOSTS, IndemnClient } from './client.js';
|
|
4
4
|
import { exchangeIdToken, toStoredSession } from './session.js';
|
|
5
5
|
import type { Credential } from './credentials.js';
|
package/src/sdk/client.ts
CHANGED
|
@@ -483,7 +483,11 @@ export class IndemnClient {
|
|
|
483
483
|
throw new SessionExpiredError('Authentication failed. Run `indemn login` to re-authenticate.');
|
|
484
484
|
}
|
|
485
485
|
if (res.status === 403) {
|
|
486
|
-
|
|
486
|
+
// Keep the server's reason: "Tickets are not enabled for this organization." is not a permission problem.
|
|
487
|
+
const text = await res.text().catch(() => '');
|
|
488
|
+
let reason = '';
|
|
489
|
+
try { const b = JSON.parse(text) as { message?: unknown; error?: unknown; msg?: unknown }; reason = String(b.message ?? b.error ?? b.msg ?? ''); } catch { reason = ''; }
|
|
490
|
+
throw new Error(reason ? `Not allowed: ${reason}` : 'Access denied. You may not have permission for this operation.');
|
|
487
491
|
}
|
|
488
492
|
if (res.status === 404) {
|
|
489
493
|
// Keep the server's reason: voice-service answers 404 with specific messages (no phone number, ...).
|