@indemn/cli 1.7.4 → 1.7.6
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 +4 -4
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +10 -6
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/eval.d.ts.map +1 -1
- package/dist/cli/eval.js +8 -2
- package/dist/cli/eval.js.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +17 -6
- 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/cli/utils/output.d.ts +0 -1
- package/dist/cli/utils/output.d.ts.map +1 -1
- package/dist/cli/utils/output.js +0 -4
- package/dist/cli/utils/output.js.map +1 -1
- package/dist/cli/voice.d.ts.map +1 -1
- package/dist/cli/voice.js +4 -1
- package/dist/cli/voice.js.map +1 -1
- package/dist/core/org-resolver.d.ts +4 -0
- package/dist/core/org-resolver.d.ts.map +1 -1
- package/dist/core/org-resolver.js +11 -0
- package/dist/core/org-resolver.js.map +1 -1
- package/dist/core/runtime.d.ts +9 -0
- package/dist/core/runtime.d.ts.map +1 -1
- package/dist/core/runtime.js +49 -0
- package/dist/core/runtime.js.map +1 -1
- package/dist/core/scope.d.ts +0 -1
- package/dist/core/scope.d.ts.map +1 -1
- package/dist/core/scope.js.map +1 -1
- package/dist/mcp/registry.d.ts.map +1 -1
- package/dist/mcp/registry.js +15 -2
- package/dist/mcp/registry.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +2 -19
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-meta.d.ts.map +1 -1
- package/dist/mcp/tool-meta.js +11 -5
- package/dist/mcp/tool-meta.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +12 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/report/export.d.ts.map +1 -1
- package/dist/report/export.js +2 -3
- package/dist/report/export.js.map +1 -1
- package/dist/report/generate.d.ts.map +1 -1
- package/dist/report/generate.js +3 -3
- package/dist/report/generate.js.map +1 -1
- package/dist/sdk/auth.d.ts +8 -3
- package/dist/sdk/auth.d.ts.map +1 -1
- package/dist/sdk/auth.js +13 -5
- package/dist/sdk/auth.js.map +1 -1
- package/dist/sdk/chat.d.ts.map +1 -1
- package/dist/sdk/chat.js +1 -0
- package/dist/sdk/chat.js.map +1 -1
- package/dist/sdk/client.d.ts +11 -9
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +50 -67
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/config-change.js +1 -1
- package/dist/sdk/config-change.js.map +1 -1
- package/dist/sdk/conversations.js +1 -1
- package/dist/sdk/conversations.js.map +1 -1
- package/dist/sdk/functions.js +2 -2
- package/dist/sdk/functions.js.map +1 -1
- package/dist/sdk/jarvis-chat.js +2 -2
- package/dist/sdk/jarvis-chat.js.map +1 -1
- package/dist/sdk/kb.d.ts.map +1 -1
- package/dist/sdk/kb.js +3 -2
- package/dist/sdk/kb.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 +16 -6
- package/dist/sdk/orgs.js.map +1 -1
- package/dist/sdk/session.d.ts +2 -9
- package/dist/sdk/session.d.ts.map +1 -1
- package/dist/sdk/session.js +2 -27
- package/dist/sdk/session.js.map +1 -1
- package/dist/sdk/test-chat.d.ts.map +1 -1
- package/dist/sdk/test-chat.js +17 -6
- package/dist/sdk/test-chat.js.map +1 -1
- package/package.json +1 -1
- package/skills/sandbox/SKILL.md +26 -0
- package/src/cli/auth.ts +9 -6
- package/src/cli/eval.ts +6 -2
- package/src/cli/index.ts +2 -2
- package/src/cli/init.ts +16 -7
- package/src/cli/org.ts +29 -0
- package/src/cli/utils/output.ts +0 -4
- package/src/cli/voice.ts +3 -1
- package/src/core/org-resolver.ts +18 -0
- package/src/core/runtime.ts +46 -0
- package/src/core/scope.ts +0 -1
- package/src/mcp/registry.ts +17 -2
- package/src/mcp/server.ts +2 -19
- package/src/mcp/tool-meta.ts +11 -5
- package/src/mcp/tools.ts +14 -1
- package/src/report/export.ts +2 -1
- package/src/report/generate.tsx +3 -2
- package/src/sdk/auth.ts +15 -5
- package/src/sdk/chat.ts +1 -0
- package/src/sdk/client.ts +50 -72
- package/src/sdk/config-change.ts +1 -1
- package/src/sdk/conversations.ts +1 -1
- package/src/sdk/functions.ts +2 -2
- package/src/sdk/jarvis-chat.ts +2 -2
- package/src/sdk/kb.ts +3 -2
- package/src/sdk/orgs.ts +28 -4
- package/src/sdk/session.ts +2 -25
- package/src/sdk/test-chat.ts +16 -6
- package/test/cli/init.test.ts +14 -0
- package/test/core/org-resolver.test.ts +38 -2
- package/test/core/scope.test.ts +12 -0
- package/test/helpers/mcp-pair.ts +10 -1
- package/test/mcp/__snapshots__/tools-snapshot.test.ts.snap +1 -0
- package/test/mcp/darwin-fixes.test.ts +18 -17
- package/test/mcp/registry.test.ts +11 -0
- package/test/mcp/sandbox.test.ts +45 -0
- package/test/mcp/shaping.test.ts +18 -0
- package/test/mcp/tools-snapshot.test.ts +5 -1
- package/test/sdk/auth.test.ts +11 -2
- package/test/sdk/client.test.ts +13 -0
- package/test/sdk/config-change.test.ts +1 -1
- package/test/sdk/kb-upload.test.ts +21 -0
- package/test/sdk/orgs.test.ts +33 -0
- package/test/sdk/renewal.test.ts +43 -64
- package/test/sdk/session.test.ts +1 -23
- package/test/sdk/skills.test.ts +20 -18
- package/test/sdk/test-chat.test.ts +29 -0
package/dist/sdk/test-chat.js
CHANGED
|
@@ -59,6 +59,13 @@ export async function openTestChat(client, agentId, opts = {}) {
|
|
|
59
59
|
// The middleware greets with customData.first_message, else a stock line; the dashboard preview sends the saved one.
|
|
60
60
|
const firstMessage = opts.sessionId ? undefined : (await new ConfigSDK(client).getConfig(agent._id).catch(() => undefined))?.first_message;
|
|
61
61
|
const customData = firstMessage?.text ? { first_message: firstMessage } : undefined;
|
|
62
|
+
// A conversation named by the caller may be a customer's live one: it is checked before anything is sent to it,
|
|
63
|
+
// and never ended here.
|
|
64
|
+
if (opts.sessionId) {
|
|
65
|
+
if (!projectId)
|
|
66
|
+
throw new Error(`Agent ${agent._id} has no project; cannot confirm the conversation is a test. Nothing was sent.`);
|
|
67
|
+
await new ConversationsSDK(client).verifyTestConversation(projectId, opts.sessionId, orgId, { timeoutMs: 0 });
|
|
68
|
+
}
|
|
62
69
|
const session = new ChatSession(agent._id, client.getHosts().middleware, opts.sessionId, { tester, preview, customData });
|
|
63
70
|
const transcript = [];
|
|
64
71
|
let receiving = false;
|
|
@@ -77,7 +84,7 @@ export async function openTestChat(client, agentId, opts = {}) {
|
|
|
77
84
|
await new ConversationsSDK(client).verifyTestConversation(projectId, sessionId, orgId, { timeoutMs: opts.verifyTimeoutMs });
|
|
78
85
|
}
|
|
79
86
|
catch (err) {
|
|
80
|
-
await session.disconnect();
|
|
87
|
+
await session.disconnect({ end: !opts.sessionId });
|
|
81
88
|
throw err;
|
|
82
89
|
}
|
|
83
90
|
return {
|
|
@@ -113,19 +120,23 @@ function until(check, timeoutMs, stepMs = 100) {
|
|
|
113
120
|
* `settleMs` after its last message.
|
|
114
121
|
*/
|
|
115
122
|
export async function exchange(chat, message, opts = {}) {
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
const timeoutMs = opts.timeoutMs ?? 60_000;
|
|
124
|
+
// A new conversation's greeting must be complete first, or it would be taken for the reply.
|
|
125
|
+
if (!chat.resumed && !(await until(() => chat.transcript.length > 0 && !chat.isReceiving(), opts.greetingWaitMs ?? timeoutMs))) {
|
|
126
|
+
throw new Error(`The agent's greeting did not finish within ${Math.round((opts.greetingWaitMs ?? timeoutMs) / 1000)} seconds; nothing was sent.`);
|
|
118
127
|
}
|
|
119
128
|
const greeting = chat.transcript.join('\n');
|
|
120
129
|
const start = chat.transcript.length;
|
|
130
|
+
const deadline = Date.now() + timeoutMs;
|
|
121
131
|
chat.session.send(message);
|
|
122
|
-
const gotReply = await until(() => chat.transcript.length > start && !chat.isReceiving(),
|
|
132
|
+
const gotReply = await until(() => chat.transcript.length > start && !chat.isReceiving(), timeoutMs);
|
|
123
133
|
if (!gotReply)
|
|
124
|
-
throw new Error(`Agent did not respond within ${Math.round(
|
|
134
|
+
throw new Error(`Agent did not respond within ${Math.round(timeoutMs / 1000)} seconds.`);
|
|
125
135
|
let seen = chat.transcript.length;
|
|
126
136
|
for (;;) {
|
|
127
137
|
await new Promise((resolve) => setTimeout(resolve, opts.settleMs ?? 1_500));
|
|
128
|
-
|
|
138
|
+
// A stream that never finishes ends the wait at the deadline, with what arrived.
|
|
139
|
+
if ((chat.transcript.length === seen && !chat.isReceiving()) || Date.now() >= deadline)
|
|
129
140
|
break;
|
|
130
141
|
seen = chat.transcript.length;
|
|
131
142
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-chat.js","sourceRoot":"","sources":["../../src/sdk/test-chat.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"test-chat.js","sourceRoot":"","sources":["../../src/sdk/test-chat.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/auth.ts
CHANGED
|
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import * as readline from 'readline';
|
|
5
5
|
import { Writable } from 'node:stream';
|
|
6
|
-
import { login, logout, whoami, type WhoAmI } from '../sdk/auth.js';
|
|
6
|
+
import { customHosts, login, logout, whoami, type WhoAmI } from '../sdk/auth.js';
|
|
7
7
|
import { findLinkFile } from '../core/org-resolver.js';
|
|
8
8
|
import { getOrgFlag } from '../core/scope.js';
|
|
9
9
|
import { SessionExpiredError } from '../core/runtime.js';
|
|
@@ -103,7 +103,7 @@ export function registerAuthCommands(program: Command): void {
|
|
|
103
103
|
});
|
|
104
104
|
spinner.succeed(`Logged in as ${result.email} on ${result.environment}`);
|
|
105
105
|
if (result.expiresAt) {
|
|
106
|
-
console.log(chalk.dim(` Session valid until ${new Date(result.expiresAt).toLocaleString()};
|
|
106
|
+
console.log(chalk.dim(` Session valid until ${new Date(result.expiresAt).toLocaleString()}; run \`indemn login\` again after that.`));
|
|
107
107
|
}
|
|
108
108
|
if (result.orgs.length === 0) {
|
|
109
109
|
console.log(chalk.yellow(' You are not a member of any organization yet. Ask an admin to add you.'));
|
|
@@ -164,16 +164,19 @@ export function registerAuthCommands(program: Command): void {
|
|
|
164
164
|
if (info.email) console.log(` User: ${info.email}`);
|
|
165
165
|
console.log(` Credential: ${describeCredential(info)}`);
|
|
166
166
|
if (info.sessionExpiresAt) {
|
|
167
|
-
console.log(` Session: valid until ${new Date(info.sessionExpiresAt).toLocaleString()} (
|
|
167
|
+
console.log(` Session: valid until ${new Date(info.sessionExpiresAt).toLocaleString()} (then \`indemn login\`)`);
|
|
168
168
|
}
|
|
169
169
|
console.log(` Org here: ${describeOrgRule(info)}`);
|
|
170
170
|
if (info.orgs.length) {
|
|
171
171
|
console.log(` Orgs:`);
|
|
172
172
|
for (const o of info.orgs) console.log(` ${o.id} ${o.name}`);
|
|
173
173
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
// The servers only when one is not the platform's default: the usual ones are noise.
|
|
175
|
+
const custom = customHosts(info);
|
|
176
|
+
if (custom.length) {
|
|
177
|
+
console.log(` Custom hosts (not the ${info.environment} defaults):`);
|
|
178
|
+
for (const [label, url] of custom) console.log(` ${label.padEnd(12)} ${url}`);
|
|
179
|
+
}
|
|
177
180
|
} catch (err) {
|
|
178
181
|
printError(err);
|
|
179
182
|
process.exit(1);
|
package/src/cli/eval.ts
CHANGED
|
@@ -78,11 +78,15 @@ export function registerEvalCommands(program: Command): void {
|
|
|
78
78
|
|
|
79
79
|
const items = (testSet as { items?: unknown[] }).items?.length ?? 0;
|
|
80
80
|
const count = input.mode === 'transcript' ? 'real conversations' : `${input.limit ? Math.min(input.limit, items) : items} item(s)`;
|
|
81
|
+
const minPassRate = options.minPassRate !== undefined ? Number(options.minPassRate) : undefined;
|
|
82
|
+
const maxP95Ms = options.maxP95Ms !== undefined ? Number(options.maxP95Ms) : undefined;
|
|
83
|
+
if (minPassRate !== undefined && !(minPassRate >= 0 && minPassRate <= 1)) throw new CliError('--min-pass-rate is a fraction from 0 to 1 (0.9 means 90%).', EXIT.NEEDS_INPUT);
|
|
84
|
+
if (maxP95Ms !== undefined && !(maxP95Ms >= 0)) throw new CliError('--max-p95-ms is a number of milliseconds, 0 or more.', EXIT.NEEDS_INPUT);
|
|
81
85
|
await confirmOrExit(`Run an evaluation of agent ${agentId} on ${count} with concurrency ${input.concurrency ?? 1}? It uses paid model calls and cannot be cancelled.`, Boolean(options.yes));
|
|
82
86
|
|
|
83
87
|
const gate: GateOptions = {
|
|
84
|
-
minPassRate
|
|
85
|
-
maxP95Ms
|
|
88
|
+
minPassRate,
|
|
89
|
+
maxP95Ms,
|
|
86
90
|
errorPolicy: options.errorPolicy === 'warn' ? 'warn' : 'block',
|
|
87
91
|
failOnError: Boolean(options.failOnError),
|
|
88
92
|
};
|
package/src/cli/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { registerBillingCommands } from './billing.js';
|
|
|
21
21
|
import { registerCampaignCommands } from './campaigns.js';
|
|
22
22
|
import { registerPlatformCommands } from './platform.js';
|
|
23
23
|
import { registerInitCommand } from './init.js';
|
|
24
|
-
import {
|
|
24
|
+
import { askOrgInTerminal, installBanner } from './utils/output.js';
|
|
25
25
|
import { setAskers, setOrgFlag } from '../core/scope.js';
|
|
26
26
|
import { cliVersion, isNonInteractive } from '../core/runtime.js';
|
|
27
27
|
|
|
@@ -71,7 +71,7 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
|
71
71
|
|
|
72
72
|
// Ask in the terminal only when a person can answer; agents and CI get exit code 2 instead.
|
|
73
73
|
if (fullCommandName(actionCommand) !== 'mcp serve' && !isNonInteractive()) {
|
|
74
|
-
setAskers({ chooseOrg: askOrgInTerminal
|
|
74
|
+
setAskers({ chooseOrg: askOrgInTerminal });
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// Banner suppression: global flag, env var, command opts-out, or JSON output mode.
|
package/src/cli/init.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { cliVersion } from '../core/runtime.js';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
5
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -8,7 +9,9 @@ import { IndemnClient } from '../sdk/client.js';
|
|
|
8
9
|
import { findLinkFile } from '../core/org-resolver.js';
|
|
9
10
|
import { printError, printSuccess } from './utils/output.js';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
// Pinned to this CLI's own version: an unpinned npx entry runs whatever is newest on npm at each start.
|
|
13
|
+
const PACKAGE = `@indemn/cli@${cliVersion()}`;
|
|
14
|
+
export const MCP_SERVER_ENTRY = { command: 'npx', args: ['-y', PACKAGE, 'mcp', 'serve'] };
|
|
12
15
|
|
|
13
16
|
/** Claude Code permission rules: read-only tools run without asking, anything that changes the platform asks every time. */
|
|
14
17
|
export function permissionBlock(): { allow: string[]; ask: string[] } {
|
|
@@ -44,12 +47,13 @@ The \`indemn\` CLI and its MCP server manage Indemn AI agents. Run \`indemn whoa
|
|
|
44
47
|
|
|
45
48
|
- **agent-builder**: create, configure and test agents (prompt, skills, knowledge bases, voice, chat).
|
|
46
49
|
- **evaluations**: rubrics, test sets, runs, CI gates, reports.
|
|
50
|
+
- **sandbox**: create a sandbox organization for a prospect (support account only).
|
|
47
51
|
`;
|
|
48
52
|
|
|
49
53
|
export interface InitReport {
|
|
50
54
|
skills: string;
|
|
51
55
|
rules: string;
|
|
52
|
-
mcp: 'added' | 'kept' | 'skipped';
|
|
56
|
+
mcp: 'added' | 'updated' | 'kept' | 'skipped';
|
|
53
57
|
permissions: 'added' | 'merged' | 'skipped';
|
|
54
58
|
}
|
|
55
59
|
|
|
@@ -76,12 +80,17 @@ export function initProject(cwd: string, opts: { force?: boolean; mcp?: boolean;
|
|
|
76
80
|
const path = join(cwd, '.mcp.json');
|
|
77
81
|
const cfg = readJson(path);
|
|
78
82
|
cfg.mcpServers = cfg.mcpServers ?? {};
|
|
79
|
-
|
|
83
|
+
// An npx @indemn/cli entry (any version) moves to this version, so the server matches the skills just installed;
|
|
84
|
+
// only the package's version changes (an --org, env and the rest stay). Any other entry is the user's own and is kept.
|
|
85
|
+
const had = cfg.mcpServers.indemn;
|
|
86
|
+
const at = had?.command === 'npx' && Array.isArray(had.args) ? had.args.findIndex((a: unknown) => typeof a === 'string' && /^@indemn\/cli(@|$)/.test(a)) : -1;
|
|
87
|
+
if (had && (at < 0 || had.args[at] === PACKAGE)) {
|
|
80
88
|
mcp = 'kept';
|
|
81
89
|
} else {
|
|
82
|
-
|
|
90
|
+
if (had) had.args[at] = PACKAGE;
|
|
91
|
+
else cfg.mcpServers.indemn = MCP_SERVER_ENTRY;
|
|
83
92
|
writeFileSync(path, JSON.stringify(cfg, null, 2) + '\n');
|
|
84
|
-
mcp = 'added';
|
|
93
|
+
mcp = had ? 'updated' : 'added';
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
96
|
|
|
@@ -113,7 +122,7 @@ export function initStatus(cwd: string): Array<[string, boolean, string]> {
|
|
|
113
122
|
let link: string | null = null;
|
|
114
123
|
try { link = findLinkFile(cwd)?.path ?? null; } catch { link = null; }
|
|
115
124
|
return [
|
|
116
|
-
['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'],
|
|
117
126
|
['Rules', existsSync(join(cwd, '.claude', 'rules', 'indemn.md')), '.claude/rules/indemn.md'],
|
|
118
127
|
['MCP server', Boolean(mcp.mcpServers?.indemn), '.mcp.json'],
|
|
119
128
|
['Permissions', (settings.permissions?.ask ?? []).some((r: string) => r.startsWith('mcp__indemn__')), '.claude/settings.json'],
|
|
@@ -141,7 +150,7 @@ export function registerInitCommand(program: Command): void {
|
|
|
141
150
|
printSuccess('Indemn CLI set up for Claude Code:');
|
|
142
151
|
console.log(` Skills: ${r.skills}`);
|
|
143
152
|
console.log(` Rules: ${r.rules}`);
|
|
144
|
-
console.log(` MCP server: ${
|
|
153
|
+
console.log(` MCP server: ${{ added: 'added to .mcp.json', updated: `updated to ${cliVersion()} in .mcp.json`, kept: 'already in .mcp.json (kept)', skipped: 'skipped' }[r.mcp]}`);
|
|
145
154
|
console.log(` Permissions: ${r.permissions === 'skipped' ? 'skipped' : `${r.permissions} in .claude/settings.json (reads allowed, changes ask)`}`);
|
|
146
155
|
const login = IndemnClient.loadConfig();
|
|
147
156
|
if (!login) console.log(chalk.yellow('Next: run `indemn login --env dev` (or production).'));
|
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/cli/utils/output.ts
CHANGED
|
@@ -80,10 +80,6 @@ export async function askOrgInTerminal(choices: OrgChoice[]): Promise<string | u
|
|
|
80
80
|
return choices.find((c) => c.id === answer)?.id;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export async function askMfaInTerminal(): Promise<string> {
|
|
84
|
-
process.stderr.write(chalk.yellow('Your session needs renewing and your account uses MFA. A code was emailed to you.\n'));
|
|
85
|
-
return askLine('MFA code: ');
|
|
86
|
-
}
|
|
87
83
|
|
|
88
84
|
export async function confirm(message: string): Promise<boolean> {
|
|
89
85
|
const rl = readline.createInterface({
|
package/src/cli/voice.ts
CHANGED
|
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { writeFileSync } from 'node:fs';
|
|
4
4
|
import { IndemnClient } from '../sdk/client.js';
|
|
5
5
|
import { VoiceSDK, toE164 } from '../sdk/voice.js';
|
|
6
|
-
import { CliError, EXIT } from '../core/runtime.js';
|
|
6
|
+
import { CliError, EXIT, isNonInteractive } from '../core/runtime.js';
|
|
7
7
|
import { confirmOrExit } from './link.js';
|
|
8
8
|
import { formatTable, printError, printJson, printSuccess } from './utils/output.js';
|
|
9
9
|
|
|
@@ -175,6 +175,8 @@ export function registerVoiceCommands(program: Command): void {
|
|
|
175
175
|
.description('Buy an available number and attach it to a voice agent (costs money every month)')
|
|
176
176
|
.action((agentId: string, number: string, options) => run(async () => {
|
|
177
177
|
const phone = toE164(number, String(options.country).toUpperCase());
|
|
178
|
+
// As the MCP tool: no purchase without a person (an AI host, CI or a script cannot answer for one).
|
|
179
|
+
if (isNonInteractive()) throw new CliError('Buying a number costs money every month: run this yourself at a terminal.', EXIT.NEEDS_INPUT);
|
|
178
180
|
if (options.yes && !options.confirmPurchase) throw new CliError('Buying a number costs money: pass --confirm-purchase with --yes, or run without --yes to be asked.', EXIT.NEEDS_INPUT);
|
|
179
181
|
await confirmOrExit(`Buy ${phone} and attach it to agent ${agentId}? The org pays for it every month, and it cannot be undone from the CLI.`, Boolean(options.yes));
|
|
180
182
|
const res = await new VoiceSDK(new IndemnClient()).buyNumber(agentId, number, { country: options.country, tollFree: Boolean(options.tollFree) });
|
package/src/core/org-resolver.ts
CHANGED
|
@@ -72,6 +72,10 @@ export async function resolveOrg(input: {
|
|
|
72
72
|
link: LinkFile | null;
|
|
73
73
|
environment: Environment;
|
|
74
74
|
memberships: () => Promise<OrgChoice[]>;
|
|
75
|
+
/** The orgs saved at `indemn login` (none for a key or a host's session): an explicit org is checked against them. */
|
|
76
|
+
known?: OrgChoice[];
|
|
77
|
+
/** The orgs as they are now, for an explicit org missing from `known` (one joined since login). */
|
|
78
|
+
refresh?: () => Promise<OrgChoice[]>;
|
|
75
79
|
ask?: (choices: OrgChoice[]) => Promise<string | undefined>;
|
|
76
80
|
}): Promise<ResolvedOrg> {
|
|
77
81
|
const { link } = input;
|
|
@@ -91,6 +95,20 @@ export async function resolveOrg(input: {
|
|
|
91
95
|
EXIT.NEEDS_INPUT,
|
|
92
96
|
);
|
|
93
97
|
}
|
|
98
|
+
// Checked against the orgs saved at login (no request), so a typo names them rather than a bare "Access denied".
|
|
99
|
+
// One missing from them is looked up again (an org joined since login). Without them (an API key, a host's
|
|
100
|
+
// session), or when the look-up fails, copilot's own check decides, as before.
|
|
101
|
+
const known = input.known ?? [];
|
|
102
|
+
if (known.length && !known.some((o) => o.id === explicit)) {
|
|
103
|
+
const orgs = input.refresh ? await input.refresh().catch(() => undefined) : known;
|
|
104
|
+
if (orgs && !orgs.some((o) => o.id === explicit)) {
|
|
105
|
+
throw new CliError(
|
|
106
|
+
`You are not a member of org ${explicit}` +
|
|
107
|
+
(orgs.length ? `. Your orgs: ${orgs.map((o) => `${o.name} (${o.id})`).join(', ')}.` : ', or of any organization on this platform.'),
|
|
108
|
+
EXIT.NEEDS_INPUT,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
94
112
|
return { id: explicit, source: input.flag ? 'flag' : 'env', link };
|
|
95
113
|
}
|
|
96
114
|
|
package/src/core/runtime.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, join, sep } from 'node:path';
|
|
1
4
|
import { createRequire } from 'node:module';
|
|
2
5
|
|
|
3
6
|
/** Process exit codes. Scripts and agents branch on these. */
|
|
@@ -71,3 +74,46 @@ export function cliVersion(): string {
|
|
|
71
74
|
|
|
72
75
|
/** How long a question to a person (through an MCP host) may stay open. The MCP default of 60 s is too short to read a diff. */
|
|
73
76
|
export const PERSON_TIMEOUT_MS = 10 * 60_000;
|
|
77
|
+
|
|
78
|
+
/** Only Indemn's own hosts (https/wss on indemn.ai), or this machine for local development, ever get the session. */
|
|
79
|
+
export function indemnHost(url: string, name: string): string {
|
|
80
|
+
let u: URL;
|
|
81
|
+
try {
|
|
82
|
+
u = new URL(url);
|
|
83
|
+
} catch {
|
|
84
|
+
throw new CliError(`${name} is not a valid URL: ${url}`);
|
|
85
|
+
}
|
|
86
|
+
const local = ['localhost', '127.0.0.1', '[::1]'].includes(u.hostname);
|
|
87
|
+
const indemn = (u.protocol === 'https:' || u.protocol === 'wss:') && (u.hostname === 'indemn.ai' || u.hostname.endsWith('.indemn.ai'));
|
|
88
|
+
if (local || indemn) return url;
|
|
89
|
+
throw new CliError(`${name} points to ${u.protocol}//${u.host}: the CLI sends your session only to Indemn's hosts (https://*.indemn.ai) or localhost.`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Where secrets live in the home folder: the CLI's own session and the usual credential folders.
|
|
93
|
+
const SECRET_DIRS = ['.indemn', '.ssh', '.aws', '.azure', '.gnupg', '.docker', '.kube', '.config'];
|
|
94
|
+
|
|
95
|
+
/** A file the CLI may send off the machine: never a dotfile (.env, .npmrc…), one in a secret folder above, or /proc. */
|
|
96
|
+
export function uploadable(filePath: string): string {
|
|
97
|
+
const real = realpathSync(filePath);
|
|
98
|
+
let home = homedir();
|
|
99
|
+
try { home = realpathSync(home); } catch { /* as given */ }
|
|
100
|
+
if (real.startsWith('/proc/') || basename(real).startsWith('.') || SECRET_DIRS.some((dir) => real.startsWith(join(home, dir) + sep))) {
|
|
101
|
+
throw new CliError(`Refusing to upload ${filePath}: dotfiles and secret folders (such as ~/.indemn, ~/.ssh, ~/.aws) are never sent.`);
|
|
102
|
+
}
|
|
103
|
+
return real;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Write a report. A path the caller chose must be a new file of the report's own type (never over an existing
|
|
108
|
+
* file); the default name in the current folder is replaced, as before.
|
|
109
|
+
*/
|
|
110
|
+
export function writeReport(filePath: string, data: string | Uint8Array, ext: '.md' | '.pdf', chosen: boolean): void {
|
|
111
|
+
if (chosen && !filePath.toLowerCase().endsWith(ext)) throw new CliError(`This report is a ${ext} file: give an output path ending in ${ext}.`);
|
|
112
|
+
try {
|
|
113
|
+
writeFileSync(filePath, data, { flag: chosen ? 'wx' : 'w' });
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if ((err as NodeJS.ErrnoException).code === 'EEXIST') throw new CliError(`${filePath} already exists; nothing was replaced. Choose another output path.`);
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
package/src/core/scope.ts
CHANGED
|
@@ -35,7 +35,6 @@ export interface FormField {
|
|
|
35
35
|
|
|
36
36
|
export interface Askers {
|
|
37
37
|
chooseOrg?: (choices: OrgChoice[]) => Promise<string | undefined>;
|
|
38
|
-
mfaCode?: () => Promise<string>;
|
|
39
38
|
/** Ask for several text fields at once; undefined when declined or nobody can answer. */
|
|
40
39
|
form?: (message: string, fields: FormField[]) => Promise<Record<string, string> | undefined>;
|
|
41
40
|
}
|
package/src/mcp/registry.ts
CHANGED
|
@@ -40,7 +40,14 @@ function fit(body: { context: ResultContext; data: unknown }): CallToolResult {
|
|
|
40
40
|
const hint = 'Result was too large. Ask for less: use limit/page, a filter, or response_format "concise".';
|
|
41
41
|
const list = Array.isArray(body.data) ? body.data : (body.data as { items?: unknown[] } | null)?.items;
|
|
42
42
|
if (Array.isArray(list)) {
|
|
43
|
-
|
|
43
|
+
// A page keeps its page, limit, total, has_more and next: only its items are trimmed.
|
|
44
|
+
const page = Array.isArray(body.data) ? {} : (body.data as Record<string, unknown>);
|
|
45
|
+
// A trimmed page left items out: it is never "complete", and the way on starts where this page did, at a
|
|
46
|
+
// limit that fits (no item skipped; a few may come again).
|
|
47
|
+
const rest = (keep: number) => typeof page.page === 'number' && typeof page.limit === 'number'
|
|
48
|
+
? { has_more: true, next: `Only ${keep} of this page's ${list.length} items fit: call again with limit ${Math.max(keep, 1)} and page ${Math.floor(((page.page - 1) * page.limit) / Math.max(keep, 1)) + 1}, then the pages after it.` }
|
|
49
|
+
: {};
|
|
50
|
+
const shaped = (keep: number) => ({ context: body.context, data: { total: list.length, ...page, items: list.slice(0, keep), returned: keep, truncated: true, ...rest(keep), hint } });
|
|
44
51
|
let keep = list.length;
|
|
45
52
|
while (keep > 0 && JSON.stringify(shaped(keep)).length > MAX_RESULT_CHARS) keep = Math.floor(keep * 0.7);
|
|
46
53
|
return text(shaped(keep));
|
|
@@ -92,6 +99,12 @@ export function fitForm(text: string, maxLines = 8, maxLine = 140): string {
|
|
|
92
99
|
return cut ? `${shown.join('\n')}\n(Shortened to fit; ask to see the full change before approving.)` : text;
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
// Dry runs shown to the caller, by tool and arguments: on a host without approval forms, `confirm: true` is taken
|
|
103
|
+
// only for a change that was previewed first (the model cannot approve a change nobody was shown).
|
|
104
|
+
const previewed = new Set<string>();
|
|
105
|
+
const previewKey = (toolName: string, args: Args) =>
|
|
106
|
+
`${toolName}:${JSON.stringify(Object.keys(args).filter((k) => k !== 'confirm').sort().map((k) => [k, args[k]]))}`;
|
|
107
|
+
|
|
95
108
|
export async function guardDestructive(
|
|
96
109
|
server: McpServer,
|
|
97
110
|
toolName: string,
|
|
@@ -120,7 +133,9 @@ export async function guardDestructive(
|
|
|
120
133
|
return { proceed: false, result: text({ cancelled: true, action: toolName, would: preview, reason: 'The approval form could not be shown, so nothing was changed. Tell the person; they can ask again.' }) };
|
|
121
134
|
}
|
|
122
135
|
}
|
|
123
|
-
|
|
136
|
+
const key = previewKey(toolName, args);
|
|
137
|
+
if (args.confirm === true && previewed.delete(key)) return { proceed: true, byPerson: false };
|
|
138
|
+
previewed.add(key);
|
|
124
139
|
return {
|
|
125
140
|
proceed: false,
|
|
126
141
|
result: text({
|
package/src/mcp/server.ts
CHANGED
|
@@ -3,13 +3,13 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
3
3
|
import { IndemnClient } from '../sdk/client.js';
|
|
4
4
|
import { registerTools } from './tools.js';
|
|
5
5
|
import { setAskers, type Askers } from '../core/scope.js';
|
|
6
|
-
import { cliVersion,
|
|
6
|
+
import { cliVersion, PERSON_TIMEOUT_MS } from '../core/runtime.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Questions the CLI may need answered during a tool call, asked through the
|
|
10
10
|
* host (Claude Code shows a dialog). Stdin carries the MCP protocol, so the
|
|
11
11
|
* terminal is never used. Without elicitation: the org question becomes a
|
|
12
|
-
* needs_input result
|
|
12
|
+
* needs_input result.
|
|
13
13
|
*/
|
|
14
14
|
export function mcpAskers(server: McpServer): Askers {
|
|
15
15
|
const canAsk = () => Boolean(server.server.getClientCapabilities()?.elicitation);
|
|
@@ -55,23 +55,6 @@ export function mcpAskers(server: McpServer): Askers {
|
|
|
55
55
|
return undefined;
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
|
-
mfaCode: async () => {
|
|
59
|
-
if (!canAsk()) {
|
|
60
|
-
throw new SessionExpiredError('The session needs renewing and your account uses MFA. Run `indemn login` in a terminal, then retry.');
|
|
61
|
-
}
|
|
62
|
-
const answer = await ask({
|
|
63
|
-
message: 'Your Indemn session needs renewing. Enter the MFA code that was just emailed to you.',
|
|
64
|
-
requestedSchema: {
|
|
65
|
-
type: 'object',
|
|
66
|
-
properties: { code: { type: 'string', title: 'MFA code', description: '6 digits', minLength: 6, maxLength: 6 } },
|
|
67
|
-
required: ['code'],
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
if (answer.action !== 'accept' || typeof answer.content?.code !== 'string') {
|
|
71
|
-
throw new SessionExpiredError('MFA code not entered. Run `indemn login` in a terminal, then retry.');
|
|
72
|
-
}
|
|
73
|
-
return answer.content.code;
|
|
74
|
-
},
|
|
75
58
|
};
|
|
76
59
|
}
|
|
77
60
|
|
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
|
|
|
@@ -76,10 +82,10 @@ export const TOOL_META: Record<string, ToolMeta> = {
|
|
|
76
82
|
|
|
77
83
|
// Skills (the CLI still calls them functions)
|
|
78
84
|
list_functions: { ...read('List skills'), paginate: true },
|
|
79
|
-
create_function: write('Create skill'),
|
|
85
|
+
create_function: { ...write('Create skill'), guard: true, describe: (a) => `This will add skill "${a.name}" to agent ${a.agent_id}; the agent can use it at once.` },
|
|
80
86
|
update_function: { ...write('Update skill', { idempotentHint: true }), guard: true, describe: (a) => `This will change skill ${a.function_id} of agent ${a.agent_id}.` },
|
|
81
87
|
delete_function: { ...destroy('Delete skill'), guard: true, describe: (a) => `This will delete skill ${a.function_id} from agent ${a.agent_id}.` },
|
|
82
|
-
test_function: write('Test skill', { openWorldHint: true }),
|
|
88
|
+
test_function: { ...write('Test skill', { openWorldHint: true }), guard: true, describe: (a) => `This will call the real endpoint of skill ${a.function_id} on agent ${a.agent_id}.` },
|
|
83
89
|
export_functions: read('Export skills'),
|
|
84
90
|
import_functions: {
|
|
85
91
|
...destroy('Import skills, replacing existing ones', { openWorldHint: true }),
|
|
@@ -107,9 +113,9 @@ export const TOOL_META: Record<string, ToolMeta> = {
|
|
|
107
113
|
update_knowledge_base: write('Update knowledge base', { idempotentHint: true }),
|
|
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
|
-
add_kb_data: write('Add knowledge base entry'),
|
|
111
|
-
scrape_url: write('Scrape a website into a knowledge base', { openWorldHint: true }),
|
|
112
|
-
upload_kb_file: write('Upload file to knowledge base'),
|
|
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.` },
|
|
117
|
+
scrape_url: { ...write('Scrape a website into a knowledge base', { openWorldHint: true }), guard: true, describe: (a) => `This will crawl ${a.url} into knowledge base ${a.kb_id}; agents using it answer from it once it is imported.` },
|
|
118
|
+
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
119
|
update_kb_data: {
|
|
114
120
|
...write('Update knowledge base entry', { idempotentHint: true }),
|
|
115
121
|
guard: 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.',
|
|
@@ -385,7 +398,7 @@ export function registerTools(server: McpServer, client: IndemnClient): Registry
|
|
|
385
398
|
);
|
|
386
399
|
|
|
387
400
|
// Lifecycle lives on the V2 platform; "unknown" when it cannot be read (treated as published: mayBeLive).
|
|
388
|
-
const lifecycleOf = (id: string) => new AgentInsightsSDK(client).detail(id).then((d) => d.lifecycle ?? '
|
|
401
|
+
const lifecycleOf = (id: string) => new AgentInsightsSDK(client).detail(id).then((d) => d.lifecycle ?? 'unknown', () => 'unknown');
|
|
389
402
|
// A change to an agent that may be live asks first; the approval names the fields, never their values (skill auth).
|
|
390
403
|
const liveChange = (what: string, skip: string[]) => ({
|
|
391
404
|
guardWhen: async (a: Record<string, unknown>) => mayBeLive(await lifecycleOf(String(a.agent_id))),
|
package/src/report/export.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Shared export logic used by both CLI commands and MCP tools
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import { writeReport } from '../core/runtime.js';
|
|
4
5
|
import { IndemnClient } from '../sdk/client.js';
|
|
5
6
|
import { EvalSDK } from '../sdk/eval.js';
|
|
6
7
|
import { RubricsSDK } from '../sdk/rubrics.js';
|
|
@@ -45,7 +46,7 @@ export async function exportEvalMarkdown(
|
|
|
45
46
|
const defaultFilename = `Indemn_Evaluation_${cleanName}_${cleanDate}.md`;
|
|
46
47
|
|
|
47
48
|
const resolvedPath = path.resolve(outputPath || defaultFilename);
|
|
48
|
-
|
|
49
|
+
writeReport(resolvedPath, markdown, '.md', Boolean(outputPath));
|
|
49
50
|
|
|
50
51
|
return {
|
|
51
52
|
file_path: resolvedPath,
|