@indemn/cli 1.7.5 → 1.7.7
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/.mcp.json +1 -1
- package/README.md +2 -2
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +2 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/org.d.ts.map +1 -1
- package/dist/cli/org.js +30 -0
- package/dist/cli/org.js.map +1 -1
- package/dist/mcp/registry.d.ts.map +1 -1
- package/dist/mcp/registry.js +3 -1
- package/dist/mcp/registry.js.map +1 -1
- package/dist/mcp/tool-meta.d.ts.map +1 -1
- package/dist/mcp/tool-meta.js +8 -1
- package/dist/mcp/tool-meta.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +14 -7
- package/dist/mcp/tools.js.map +1 -1
- package/dist/sdk/orgs.d.ts +12 -0
- package/dist/sdk/orgs.d.ts.map +1 -1
- package/dist/sdk/orgs.js +15 -0
- package/dist/sdk/orgs.js.map +1 -1
- package/package.json +1 -1
- package/skills/sandbox/SKILL.md +26 -0
- package/src/cli/init.ts +2 -1
- package/src/cli/org.ts +29 -0
- package/src/mcp/registry.ts +3 -1
- package/src/mcp/tool-meta.ts +8 -1
- package/src/mcp/tools.ts +16 -7
- package/src/sdk/orgs.ts +27 -0
- package/test/mcp/__snapshots__/tools-snapshot.test.ts.snap +1 -0
- package/test/mcp/registry.test.ts +14 -0
- package/test/mcp/sandbox.test.ts +45 -0
- package/test/mcp/tools-snapshot.test.ts +11 -1
- package/test/sdk/orgs.test.ts +15 -0
package/dist/sdk/orgs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orgs.d.ts","sourceRoot":"","sources":["../../src/sdk/orgs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AASjE,qBAAa,OAAO;IACN,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,YAAY;IAExC,+FAA+F;IACzF,iBAAiB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAIhD,8EAA8E;IACxE,WAAW,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAkCzC,+DAA+D;IACzD,YAAY,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;CAIzC"}
|
|
1
|
+
{"version":3,"file":"orgs.d.ts","sourceRoot":"","sources":["../../src/sdk/orgs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AASjE,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,qBAAa,OAAO;IACN,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,YAAY;IAExC,+FAA+F;IACzF,iBAAiB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAIhD,8EAA8E;IACxE,WAAW,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAkCzC;;;OAGG;IACG,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAetD,+DAA+D;IACzD,YAAY,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;CAIzC"}
|
package/dist/sdk/orgs.js
CHANGED
|
@@ -45,6 +45,21 @@ export class OrgsSDK {
|
|
|
45
45
|
}
|
|
46
46
|
return [...byUser.values()];
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* A sandbox organization for a prospect. Only the Indemn support account may create one (copilot
|
|
50
|
+
* refuses anyone else), and copilot always puts it on the sandbox plan set for the platform.
|
|
51
|
+
*/
|
|
52
|
+
async createSandbox(name) {
|
|
53
|
+
const res = await this.client.copilotPost('/organization/sandbox', { organizationName: name });
|
|
54
|
+
const base = this.client.getHosts().copilot.replace(/\/api\/?$/, '');
|
|
55
|
+
return {
|
|
56
|
+
organization_id: res.id_organization,
|
|
57
|
+
name,
|
|
58
|
+
project_id: res.id_project,
|
|
59
|
+
plan_id: res.id_plan,
|
|
60
|
+
url: `${base}/#/organization/${res.id_organization}/ai-studio/bots`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
48
63
|
/** Channels an agent can be published on (WEB, VOICE, ...). */
|
|
49
64
|
async listChannels() {
|
|
50
65
|
const channels = await this.client.copilotGet('/organization/:org_id/ai-studio/channels');
|
package/dist/sdk/orgs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orgs.js","sourceRoot":"","sources":["../../src/sdk/orgs.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"orgs.js","sourceRoot":"","sources":["../../src/sdk/orgs.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sandbox
|
|
3
|
+
description: Create a sandbox organization for a prospect on the Indemn platform. Use when the person asks for a sandbox, a sandbox org, a trial org, or a new organization for a prospect or customer.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sandbox organizations
|
|
7
|
+
|
|
8
|
+
A sandbox is a new organization for a prospect, set up so they can try the platform. The platform puts every sandbox on its sandbox plan (a trial, then the plan's monthly price on invoice terms), invoiced to the Indemn support account.
|
|
9
|
+
|
|
10
|
+
## Who can do it
|
|
11
|
+
|
|
12
|
+
Only the Indemn support account. The platform refuses anyone else, including other Indemn staff. If the person is not signed in as support, say so and stop; do not try another way.
|
|
13
|
+
|
|
14
|
+
## What to do
|
|
15
|
+
|
|
16
|
+
1. Get the organization's name from the person. If they did not give one, ask for it. Ask for nothing else: never ask for a plan, price, trial length or payment terms. The platform sets them.
|
|
17
|
+
2. Create it:
|
|
18
|
+
- MCP: `create_sandbox_org` with `name`. The person approves it in the approval form.
|
|
19
|
+
- Terminal: `indemn org create-sandbox "<name>"`. It asks the person to confirm; it has no `--yes`.
|
|
20
|
+
3. Reply with the new organization's name, id and dashboard link. Mention that they can rename it in Organization Settings and invite the prospect's users from there.
|
|
21
|
+
|
|
22
|
+
## If it fails
|
|
23
|
+
|
|
24
|
+
- **Only the Indemn support account can create sandbox organizations**: the person is signed in as someone else.
|
|
25
|
+
- **Sandbox plan is not configured**: the platform has no sandbox plan set. Tell the person to contact the Indemn engineering team; do not create an org another way.
|
|
26
|
+
- **Organization created and subscribed, but setting up its project failed**: the org exists. Give its id and tell the person to finish the set-up from the dashboard; do not create it again.
|
package/src/cli/init.ts
CHANGED
|
@@ -47,6 +47,7 @@ The \`indemn\` CLI and its MCP server manage Indemn AI agents. Run \`indemn whoa
|
|
|
47
47
|
|
|
48
48
|
- **agent-builder**: create, configure and test agents (prompt, skills, knowledge bases, voice, chat).
|
|
49
49
|
- **evaluations**: rubrics, test sets, runs, CI gates, reports.
|
|
50
|
+
- **sandbox**: create a sandbox organization for a prospect (support account only).
|
|
50
51
|
`;
|
|
51
52
|
|
|
52
53
|
export interface InitReport {
|
|
@@ -121,7 +122,7 @@ export function initStatus(cwd: string): Array<[string, boolean, string]> {
|
|
|
121
122
|
let link: string | null = null;
|
|
122
123
|
try { link = findLinkFile(cwd)?.path ?? null; } catch { link = null; }
|
|
123
124
|
return [
|
|
124
|
-
['Skills', existsSync(join(cwd, '.claude', 'skills', 'agent-builder', 'SKILL.md')), '.claude/skills/agent-builder, evaluations'],
|
|
125
|
+
['Skills', existsSync(join(cwd, '.claude', 'skills', 'agent-builder', 'SKILL.md')), '.claude/skills/agent-builder, evaluations, sandbox'],
|
|
125
126
|
['Rules', existsSync(join(cwd, '.claude', 'rules', 'indemn.md')), '.claude/rules/indemn.md'],
|
|
126
127
|
['MCP server', Boolean(mcp.mcpServers?.indemn), '.mcp.json'],
|
|
127
128
|
['Permissions', (settings.permissions?.ask ?? []).some((r: string) => r.startsWith('mcp__indemn__')), '.claude/settings.json'],
|
package/src/cli/org.ts
CHANGED
|
@@ -4,6 +4,7 @@ import ora from 'ora';
|
|
|
4
4
|
import { IndemnClient } from '../sdk/client.js';
|
|
5
5
|
import { OrgsSDK } from '../sdk/orgs.js';
|
|
6
6
|
import { getAskers, setAskers } from '../core/scope.js';
|
|
7
|
+
import { CliError, EXIT, isNonInteractive } from '../core/runtime.js';
|
|
7
8
|
import type { OrgContext } from '../sdk/client.js';
|
|
8
9
|
import { confirmOrExit, writeLink } from './link.js';
|
|
9
10
|
import { formatTable, printError, printJson, printSuccess } from './utils/output.js';
|
|
@@ -97,6 +98,34 @@ export function registerOrgCommands(program: Command): void {
|
|
|
97
98
|
}
|
|
98
99
|
});
|
|
99
100
|
|
|
101
|
+
org
|
|
102
|
+
.command('create-sandbox')
|
|
103
|
+
.description('Create a sandbox organization for a prospect (Indemn support account only; the platform picks the sandbox plan)')
|
|
104
|
+
.argument('<name>', 'Name of the new organization')
|
|
105
|
+
.option('--json', 'Output as JSON')
|
|
106
|
+
.action(async (name: string, options) => {
|
|
107
|
+
try {
|
|
108
|
+
const question = `Create organization "${name}" on the sandbox plan? It starts its trial now and is then invoiced to the support account.`;
|
|
109
|
+
// A new org on a paid plan: a person answers here; there is no --yes.
|
|
110
|
+
if (isNonInteractive()) throw new CliError(`${question} Run this in a terminal so a person can confirm it.`, EXIT.NEEDS_INPUT);
|
|
111
|
+
await confirmOrExit(question, false);
|
|
112
|
+
const client = new IndemnClient({ ignoreLink: true });
|
|
113
|
+
const spinner = ora('Creating sandbox organization...').start();
|
|
114
|
+
const created = await new OrgsSDK(client).createSandbox(name);
|
|
115
|
+
spinner.stop();
|
|
116
|
+
if (options.json) {
|
|
117
|
+
printJson(created);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
printSuccess(`Created ${created.name} (${created.organization_id}) on ${client.getEnvironment()}.`);
|
|
121
|
+
console.log(` ${created.url}`);
|
|
122
|
+
console.log(chalk.dim('Rename it any time in Organization Settings.'));
|
|
123
|
+
} catch (err) {
|
|
124
|
+
printError(err);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
100
129
|
org
|
|
101
130
|
.command('switch')
|
|
102
131
|
.description('Pin this folder to an org (there is no global default org any more)')
|
package/src/mcp/registry.ts
CHANGED
|
@@ -118,7 +118,9 @@ export async function guardDestructive(
|
|
|
118
118
|
message: `${formText} Proceed?`,
|
|
119
119
|
requestedSchema: {
|
|
120
120
|
type: 'object',
|
|
121
|
-
|
|
121
|
+
// Ticked already, so approving takes Enter twice in Claude Code (to Accept, then submit), not Space then
|
|
122
|
+
// Enter. Still one boolean field: Darwin's card reads that as an approval (Approve / Decline).
|
|
123
|
+
properties: { confirm: { type: 'boolean', title: 'Proceed', description: 'Ticked already: press Enter twice to approve (Space unticks).', default: true } },
|
|
122
124
|
required: ['confirm'],
|
|
123
125
|
},
|
|
124
126
|
}, { timeout: PERSON_TIMEOUT_MS });
|
package/src/mcp/tool-meta.ts
CHANGED
|
@@ -39,6 +39,12 @@ export const TOOL_META: Record<string, ToolMeta> = {
|
|
|
39
39
|
// Login and organizations
|
|
40
40
|
whoami: { ...read('Who am I'), orgScoped: false },
|
|
41
41
|
list_organizations: { ...read('List my organizations'), orgScoped: false },
|
|
42
|
+
create_sandbox_org: {
|
|
43
|
+
...write('Create sandbox organization'),
|
|
44
|
+
orgScoped: false,
|
|
45
|
+
guard: true,
|
|
46
|
+
describe: (a) => `This will create a new organization "${a.name}" on the sandbox plan (its trial, then the plan's monthly price, invoiced to the support account).`,
|
|
47
|
+
},
|
|
42
48
|
list_org_members: { ...read('List organization members'), paginate: true },
|
|
43
49
|
list_channels: read('List channels'),
|
|
44
50
|
|
|
@@ -108,7 +114,8 @@ export const TOOL_META: Record<string, ToolMeta> = {
|
|
|
108
114
|
delete_knowledge_base: { ...destroy('Delete knowledge base'), guard: true, describe: (a) => `This will delete knowledge base ${a.kb_id} with all its sources and vectors.` },
|
|
109
115
|
list_kb_data: read('List knowledge base entries'),
|
|
110
116
|
add_kb_data: { ...write('Add knowledge base entry'), guard: true, describe: (a) => `This will add an entry to knowledge base ${a.kb_id}; agents using it answer from it at once.` },
|
|
111
|
-
|
|
117
|
+
// The approval says which crawl it is: a whole-site crawl stops at kb-service's default of 100 pages.
|
|
118
|
+
scrape_url: { ...write('Scrape a website into a knowledge base', { openWorldHint: true }), guard: true, describe: (a) => `${a.crawl_mode === 'selected' ? `This will import only the page ${a.url}` : `This will crawl the whole site from ${a.url} (up to 100 pages)`} into knowledge base ${a.kb_id}; agents using it answer from it once it is imported.` },
|
|
112
119
|
upload_kb_file: { ...write('Upload file to knowledge base'), guard: true, describe: (a) => `This will upload ${a.file_path} into knowledge base ${a.kb_id}; agents using it answer from it once it is imported.` },
|
|
113
120
|
update_kb_data: {
|
|
114
121
|
...write('Update knowledge base entry', { idempotentHint: true }),
|
package/src/mcp/tools.ts
CHANGED
|
@@ -241,6 +241,19 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
241
241
|
},
|
|
242
242
|
);
|
|
243
243
|
|
|
244
|
+
reg.tool(
|
|
245
|
+
'create_sandbox_org',
|
|
246
|
+
'Create a sandbox organization for a prospect when the person asks for "a sandbox" or "a new org" and gives its name. Only the Indemn support account can do this; the platform picks the sandbox plan itself, so never ask for a plan, price or terms. Ask for the name if it is missing. Returns the new org id and its dashboard link; the person can rename it later in Organization Settings. The host asks the person to approve; without an approval form this is refused.',
|
|
247
|
+
{ name: z.string().trim().min(1).describe('Name of the new organization, as the person gave it') },
|
|
248
|
+
async (args, meta) => {
|
|
249
|
+
try {
|
|
250
|
+
// A new org on a paid plan needs the person, not the model: confirm: true is not enough.
|
|
251
|
+
if (!meta.confirmedByPerson) throw new Error('Creating a sandbox organization needs a person to approve it in an approval form, and this host cannot show one. Nothing was created. Run `indemn org create-sandbox` in a terminal instead.');
|
|
252
|
+
return success(await orgs.createSandbox(args.name));
|
|
253
|
+
} catch (err) { return error(err); }
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
|
|
244
257
|
reg.tool(
|
|
245
258
|
'list_org_members',
|
|
246
259
|
'List the owners, admins and agents of an organization, with their roles.',
|
|
@@ -1057,16 +1070,12 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1057
1070
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
1058
1071
|
question: z.string().optional().describe('FAQ question'),
|
|
1059
1072
|
answer: z.string().optional().describe('FAQ answer'),
|
|
1060
|
-
source_url: z.string().optional().describe('Source URL (initiates scraping via import endpoint)'),
|
|
1061
1073
|
content: z.string().optional().describe('Raw content'),
|
|
1062
1074
|
},
|
|
1063
1075
|
async (args) => {
|
|
1064
1076
|
try {
|
|
1077
|
+
// A website goes through scrape_url, where the person chooses the whole site or one page.
|
|
1065
1078
|
const { kb_id, ...input } = args;
|
|
1066
|
-
if (input.source_url) {
|
|
1067
|
-
const result = await kb.scrapeUrl(kb_id, input.source_url as string);
|
|
1068
|
-
return success(result);
|
|
1069
|
-
}
|
|
1070
1079
|
const result = await kb.addDataSource(kb_id, input);
|
|
1071
1080
|
return success(result);
|
|
1072
1081
|
} catch (err) { return error(err); }
|
|
@@ -1075,11 +1084,11 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
1075
1084
|
|
|
1076
1085
|
reg.tool(
|
|
1077
1086
|
'scrape_url',
|
|
1078
|
-
'Scrape a website URL and add it as a data source to a knowledge base. The crawl runs in the background; its pages appear in the knowledge base only when it ends.',
|
|
1087
|
+
'Scrape a website URL and add it as a data source to a knowledge base. The crawl runs in the background; its pages appear in the knowledge base only when it ends. Unless the person already said, ask them first whether to crawl the whole site (up to 100 pages) or import only this page.',
|
|
1079
1088
|
{
|
|
1080
1089
|
kb_id: z.string().describe('The knowledge base ID'),
|
|
1081
1090
|
url: z.string().describe('URL to scrape'),
|
|
1082
|
-
crawl_mode: z.
|
|
1091
|
+
crawl_mode: z.enum(['full', 'selected']).describe('What the person chose: "full" crawls the whole site from url (up to 100 pages); "selected" imports only this page (one call per page).'),
|
|
1083
1092
|
},
|
|
1084
1093
|
async (args) => {
|
|
1085
1094
|
try {
|
package/src/sdk/orgs.ts
CHANGED
|
@@ -8,6 +8,14 @@ interface RawProjectUser {
|
|
|
8
8
|
id_project?: { name?: string } | string | null;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export interface SandboxOrg {
|
|
12
|
+
organization_id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
project_id?: string;
|
|
15
|
+
plan_id?: string;
|
|
16
|
+
url: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
export class OrgsSDK {
|
|
12
20
|
constructor(private client: IndemnClient) {}
|
|
13
21
|
|
|
@@ -51,6 +59,25 @@ export class OrgsSDK {
|
|
|
51
59
|
return [...byUser.values()];
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
/**
|
|
63
|
+
* A sandbox organization for a prospect. Only the Indemn support account may create one (copilot
|
|
64
|
+
* refuses anyone else), and copilot always puts it on the sandbox plan set for the platform.
|
|
65
|
+
*/
|
|
66
|
+
async createSandbox(name: string): Promise<SandboxOrg> {
|
|
67
|
+
const res = await this.client.copilotPost<{ id_organization: string; id_project?: string; id_plan?: string }>(
|
|
68
|
+
'/organization/sandbox',
|
|
69
|
+
{ organizationName: name },
|
|
70
|
+
);
|
|
71
|
+
const base = this.client.getHosts().copilot.replace(/\/api\/?$/, '');
|
|
72
|
+
return {
|
|
73
|
+
organization_id: res.id_organization,
|
|
74
|
+
name,
|
|
75
|
+
project_id: res.id_project,
|
|
76
|
+
plan_id: res.id_plan,
|
|
77
|
+
url: `${base}/#/organization/${res.id_organization}/ai-studio/bots`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
/** Channels an agent can be published on (WEB, VOICE, ...). */
|
|
55
82
|
async listChannels(): Promise<Channel[]> {
|
|
56
83
|
const channels = await this.client.copilotGet<Channel[]>('/organization/:org_id/ai-studio/channels');
|
|
@@ -92,6 +92,20 @@ describe('registry', () => {
|
|
|
92
92
|
await pair.close();
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
it('shows Proceed ticked, so approving is Enter twice, and still takes an untick as a no', async () => {
|
|
96
|
+
const { register, handler } = setup();
|
|
97
|
+
const pair = await connectPair(register, { elicit: () => ({ action: 'accept', content: { confirm: false } }) });
|
|
98
|
+
const res = await pair.call('wipe', { id: 't1' });
|
|
99
|
+
expect(pair.questions[0].requestedSchema).toEqual({
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: { confirm: { type: 'boolean', title: 'Proceed', description: 'Ticked already: press Enter twice to approve (Space unticks).', default: true } },
|
|
102
|
+
required: ['confirm'],
|
|
103
|
+
});
|
|
104
|
+
expect(res.body).toMatchObject({ cancelled: true, reason: 'Proceed was not ticked. Nothing was changed.' });
|
|
105
|
+
expect(handler).not.toHaveBeenCalled();
|
|
106
|
+
await pair.close();
|
|
107
|
+
});
|
|
108
|
+
|
|
95
109
|
it('names things in the approval instead of showing their ids', async () => {
|
|
96
110
|
const handler = vi.fn(async () => ({ content: [{ type: 'text' as const, text: '{}' }] }));
|
|
97
111
|
const pair = await connectPair((server) => {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { on, recordFetch } from '../helpers/fetch-recorder.js';
|
|
3
|
+
import { cleanEnv } from '../helpers/config-home.js';
|
|
4
|
+
import { connectPair } from '../helpers/mcp-pair.js';
|
|
5
|
+
import { IndemnClient } from '../../src/sdk/client.js';
|
|
6
|
+
import { registerTools } from '../../src/mcp/tools.js';
|
|
7
|
+
|
|
8
|
+
const ORG = 'aaaaaaaaaaaaaaaaaaaaaaaa';
|
|
9
|
+
const created = on('POST', '/organization/sandbox', { json: { success: true, id_organization: ORG, id_project: 'p1', id_plan: 'plan1' } });
|
|
10
|
+
|
|
11
|
+
async function pair(elicit?: () => { action: 'accept' | 'decline'; content?: { confirm: boolean } }) {
|
|
12
|
+
cleanEnv();
|
|
13
|
+
const client = new IndemnClient({ apiKey: `ind_dev_${'a'.repeat(64)}`, environment: 'dev' });
|
|
14
|
+
return connectPair((server) => { registerTools(server, client); }, { elicit });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('create_sandbox_org', () => {
|
|
18
|
+
let p: Awaited<ReturnType<typeof pair>> | undefined;
|
|
19
|
+
afterEach(async () => { await p?.close(); p = undefined; });
|
|
20
|
+
|
|
21
|
+
it('creates the org once the person approves, sending only its name', async () => {
|
|
22
|
+
const { calls } = recordFetch([created]);
|
|
23
|
+
p = await pair(() => ({ action: 'accept', content: { confirm: true } }));
|
|
24
|
+
const res = await p.call('create_sandbox_org', { name: 'Acme Insurance' });
|
|
25
|
+
expect(res.isError).toBe(false);
|
|
26
|
+
expect(p.questions[0]?.message).toContain('Acme Insurance');
|
|
27
|
+
expect(calls.find((c) => c.method === 'POST')?.body).toEqual({ organizationName: 'Acme Insurance' });
|
|
28
|
+
expect(res.body.data).toMatchObject({ organization_id: ORG, url: `https://devcopilot.indemn.ai/#/organization/${ORG}/ai-studio/bots` });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('creates nothing when the person declines', async () => {
|
|
32
|
+
const { calls } = recordFetch([created]);
|
|
33
|
+
p = await pair(() => ({ action: 'decline' }));
|
|
34
|
+
await p.call('create_sandbox_org', { name: 'Acme' });
|
|
35
|
+
expect(calls.some((c) => c.method === 'POST')).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('refuses the model\'s own confirm when the host cannot ask the person', async () => {
|
|
39
|
+
const { calls } = recordFetch([created]);
|
|
40
|
+
p = await pair();
|
|
41
|
+
const res = await p.call('create_sandbox_org', { name: 'Acme', confirm: true });
|
|
42
|
+
expect(JSON.stringify(res.body)).toContain('needs a person to approve it');
|
|
43
|
+
expect(calls.some((c) => c.method === 'POST')).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { registerTools } from '../../src/mcp/tools.js';
|
|
3
|
+
import { TOOL_META } from '../../src/mcp/tool-meta.js';
|
|
3
4
|
import { IndemnClient } from '../../src/sdk/client.js';
|
|
4
5
|
import { connectPair } from '../helpers/mcp-pair.js';
|
|
5
6
|
import { cleanEnv } from '../helpers/config-home.js';
|
|
@@ -16,7 +17,7 @@ describe('MCP tool list', () => {
|
|
|
16
17
|
expect(typeof tool.annotations?.readOnlyHint, tool.name).toBe('boolean');
|
|
17
18
|
}
|
|
18
19
|
expect(tools.map((t) => t.name).sort()).toMatchSnapshot();
|
|
19
|
-
expect(tools).toHaveLength(
|
|
20
|
+
expect(tools).toHaveLength(141); // +numbers // +set_agent_transcriber // +set_org_admin_settings // +remove_holiday // +create_sandbox_org
|
|
20
21
|
const props = (name: string) => Object.keys((tools.find((t) => t.name === name)!.inputSchema as { properties?: object }).properties ?? {});
|
|
21
22
|
for (const name of ['list_agents', 'delete_agent', 'set_prompt', 'list_org_members', 'list_channels', 'chat_with_agent']) {
|
|
22
23
|
expect(props(name), name).toContain('org_id');
|
|
@@ -28,6 +29,9 @@ describe('MCP tool list', () => {
|
|
|
28
29
|
for (const name of ['delete_agent', 'create_function', 'test_function', 'add_kb_data', 'scrape_url', 'upload_kb_file']) {
|
|
29
30
|
expect(props(name), name).toContain('confirm');
|
|
30
31
|
}
|
|
32
|
+
// The person chooses how much of a website is crawled: only scrape_url crawls, and it needs their choice.
|
|
33
|
+
expect((tools.find((t) => t.name === 'scrape_url')!.inputSchema as { required?: string[] }).required).toContain('crawl_mode');
|
|
34
|
+
expect(props('add_kb_data')).not.toContain('source_url');
|
|
31
35
|
// Test items and rubric rules have declared fields (Gemini cannot fill open objects).
|
|
32
36
|
const itemsOf = (name: string, field: string) => ((tools.find((t) => t.name === name)!.inputSchema as { properties: Record<string, { items?: { properties?: object } }> }).properties[field].items?.properties ?? {});
|
|
33
37
|
// ... and no nested objects: Gemini writes constructors for them that Google rejects.
|
|
@@ -43,4 +47,10 @@ describe('MCP tool list', () => {
|
|
|
43
47
|
expect(skillProps).not.toContain('authorization');
|
|
44
48
|
await pair.close();
|
|
45
49
|
});
|
|
50
|
+
|
|
51
|
+
it('says in the crawl approval whether it is the whole site or one page', () => {
|
|
52
|
+
const describe = TOOL_META.scrape_url.describe!;
|
|
53
|
+
expect(describe({ kb_id: 'kb1', url: 'https://www.aaa.com' })).toMatch(/^This will crawl the whole site from https:\/\/www\.aaa\.com \(up to 100 pages\) into knowledge base kb1;/);
|
|
54
|
+
expect(describe({ kb_id: 'kb1', url: 'https://www.aaa.com/pricing', crawl_mode: 'selected' })).toMatch(/^This will import only the page https:\/\/www\.aaa\.com\/pricing into knowledge base kb1;/);
|
|
55
|
+
});
|
|
46
56
|
});
|
package/test/sdk/orgs.test.ts
CHANGED
|
@@ -58,6 +58,21 @@ describe('OrgsSDK', () => {
|
|
|
58
58
|
await expect(new OrgsSDK(new IndemnClient({ orgId: A })).listMembers()).resolves.toEqual([]);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
it('creates a sandbox org with only its name and links to it', async () => {
|
|
62
|
+
const { calls } = recordFetch([on('POST', '/organization/sandbox', { json: { success: true, id_organization: A, id_project: 'p1', id_plan: 'plan1' } })]);
|
|
63
|
+
await expect(new OrgsSDK(new IndemnClient()).createSandbox('Acme Insurance')).resolves.toEqual({
|
|
64
|
+
organization_id: A, name: 'Acme Insurance', project_id: 'p1', plan_id: 'plan1',
|
|
65
|
+
url: `https://devcopilot.indemn.ai/#/organization/${A}/ai-studio/bots`,
|
|
66
|
+
});
|
|
67
|
+
expect(new URL(calls[0].url).pathname).toBe('/api/organization/sandbox');
|
|
68
|
+
expect(calls[0].body).toEqual({ organizationName: 'Acme Insurance' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('keeps copilot\'s reason when someone other than support asks for a sandbox', async () => {
|
|
72
|
+
recordFetch([on('POST', '/organization/sandbox', { status: 403, json: { success: false, msg: 'Only the Indemn support account can create sandbox organizations.' } })]);
|
|
73
|
+
await expect(new OrgsSDK(new IndemnClient()).createSandbox('Acme')).rejects.toThrow('Not allowed: Only the Indemn support account can create sandbox organizations.');
|
|
74
|
+
});
|
|
75
|
+
|
|
61
76
|
it('lists channels', async () => {
|
|
62
77
|
const { calls } = recordFetch([on('GET', '/ai-studio/channels', { json: { success: true, data: [{ _id: 'c1', name: 'WEB', type: 'web' }] } })]);
|
|
63
78
|
await expect(new OrgsSDK(new IndemnClient({ orgId: A })).listChannels()).resolves.toEqual([{ _id: 'c1', name: 'WEB', type: 'web', description: undefined }]);
|