@indemn/cli 1.5.0 → 1.7.0
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 +3 -3
- package/.claude-plugin/plugin.json +9 -4
- package/.github/workflows/ci.yml +10 -0
- package/.github/workflows/publish.yml +2 -2
- package/.mcp.json +1 -4
- package/CONTRIBUTING.md +3 -1
- package/README.md +69 -176
- package/dist/cli/agents.d.ts.map +1 -1
- package/dist/cli/agents.js +226 -18
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/analytics.d.ts +3 -0
- package/dist/cli/analytics.d.ts.map +1 -0
- package/dist/cli/analytics.js +133 -0
- package/dist/cli/analytics.js.map +1 -0
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +62 -17
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/billing.d.ts +3 -0
- package/dist/cli/billing.d.ts.map +1 -0
- package/dist/cli/billing.js +23 -0
- package/dist/cli/billing.js.map +1 -0
- package/dist/cli/campaigns.d.ts +3 -0
- package/dist/cli/campaigns.d.ts.map +1 -0
- package/dist/cli/campaigns.js +34 -0
- package/dist/cli/campaigns.js.map +1 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +69 -102
- package/dist/cli/chat.js.map +1 -1
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +71 -27
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/conversations.d.ts +3 -0
- package/dist/cli/conversations.d.ts.map +1 -0
- package/dist/cli/conversations.js +129 -0
- package/dist/cli/conversations.js.map +1 -0
- package/dist/cli/distribution.d.ts +3 -0
- package/dist/cli/distribution.d.ts.map +1 -0
- package/dist/cli/distribution.js +122 -0
- package/dist/cli/distribution.js.map +1 -0
- package/dist/cli/eval.d.ts.map +1 -1
- package/dist/cli/eval.js +315 -71
- package/dist/cli/eval.js.map +1 -1
- package/dist/cli/functions.d.ts.map +1 -1
- package/dist/cli/functions.js +125 -35
- package/dist/cli/functions.js.map +1 -1
- package/dist/cli/index.js +63 -54
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +27 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +152 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/kb.d.ts.map +1 -1
- package/dist/cli/kb.js +141 -15
- package/dist/cli/kb.js.map +1 -1
- package/dist/cli/link.d.ts +21 -0
- package/dist/cli/link.d.ts.map +1 -0
- package/dist/cli/link.js +112 -0
- package/dist/cli/link.js.map +1 -0
- package/dist/cli/org.d.ts.map +1 -1
- package/dist/cli/org.js +79 -67
- package/dist/cli/org.js.map +1 -1
- package/dist/cli/platform.d.ts +3 -0
- package/dist/cli/platform.d.ts.map +1 -0
- package/dist/cli/platform.js +133 -0
- package/dist/cli/platform.js.map +1 -0
- package/dist/cli/tickets.d.ts +3 -0
- package/dist/cli/tickets.d.ts.map +1 -0
- package/dist/cli/tickets.js +125 -0
- package/dist/cli/tickets.js.map +1 -0
- package/dist/cli/utils/output.d.ts +15 -9
- package/dist/cli/utils/output.d.ts.map +1 -1
- package/dist/cli/utils/output.js +53 -46
- package/dist/cli/utils/output.js.map +1 -1
- package/dist/cli/voice.d.ts +3 -0
- package/dist/cli/voice.d.ts.map +1 -0
- package/dist/cli/voice.js +206 -0
- package/dist/cli/voice.js.map +1 -0
- package/dist/core/diff.d.ts +16 -0
- package/dist/core/diff.d.ts.map +1 -0
- package/dist/core/diff.js +57 -0
- package/dist/core/diff.js.map +1 -0
- package/dist/core/org-resolver.d.ts +37 -0
- package/dist/core/org-resolver.d.ts.map +1 -0
- package/dist/core/org-resolver.js +73 -0
- package/dist/core/org-resolver.js.map +1 -0
- package/dist/core/runtime.d.ts +36 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +62 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/core/scope.d.ts +33 -0
- package/dist/core/scope.d.ts.map +1 -0
- package/dist/core/scope.js +37 -0
- package/dist/core/scope.js.map +1 -0
- package/dist/mcp/registry.d.ts +67 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/registry.js +195 -0
- package/dist/mcp/registry.js.map +1 -0
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +87 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-meta.d.ts +21 -0
- package/dist/mcp/tool-meta.d.ts.map +1 -0
- package/dist/mcp/tool-meta.js +203 -0
- package/dist/mcp/tool-meta.js.map +1 -0
- package/dist/mcp/tools.d.ts +2 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +1462 -215
- package/dist/mcp/tools.js.map +1 -1
- package/dist/sdk/agent-bundle.d.ts +99 -0
- package/dist/sdk/agent-bundle.d.ts.map +1 -0
- package/dist/sdk/agent-bundle.js +211 -0
- package/dist/sdk/agent-bundle.js.map +1 -0
- package/dist/sdk/agents.d.ts +54 -1
- package/dist/sdk/agents.d.ts.map +1 -1
- package/dist/sdk/agents.js +59 -5
- package/dist/sdk/agents.js.map +1 -1
- package/dist/sdk/analytics.d.ts +158 -0
- package/dist/sdk/analytics.d.ts.map +1 -0
- package/dist/sdk/analytics.js +185 -0
- package/dist/sdk/analytics.js.map +1 -0
- package/dist/sdk/auth.d.ts +34 -12
- package/dist/sdk/auth.d.ts.map +1 -1
- package/dist/sdk/auth.js +60 -77
- package/dist/sdk/auth.js.map +1 -1
- package/dist/sdk/billing.d.ts +25 -0
- package/dist/sdk/billing.d.ts.map +1 -0
- package/dist/sdk/billing.js +61 -0
- package/dist/sdk/billing.js.map +1 -0
- package/dist/sdk/campaigns.d.ts +35 -0
- package/dist/sdk/campaigns.d.ts.map +1 -0
- package/dist/sdk/campaigns.js +59 -0
- package/dist/sdk/campaigns.js.map +1 -0
- package/dist/sdk/chat.d.ts +25 -2
- package/dist/sdk/chat.d.ts.map +1 -1
- package/dist/sdk/chat.js +37 -10
- package/dist/sdk/chat.js.map +1 -1
- package/dist/sdk/client.d.ts +93 -5
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +362 -100
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/config-change.d.ts +74 -0
- package/dist/sdk/config-change.d.ts.map +1 -0
- package/dist/sdk/config-change.js +117 -0
- package/dist/sdk/config-change.js.map +1 -0
- package/dist/sdk/config.d.ts.map +1 -1
- package/dist/sdk/config.js +2 -1
- package/dist/sdk/config.js.map +1 -1
- package/dist/sdk/conversations.d.ts +101 -0
- package/dist/sdk/conversations.d.ts.map +1 -0
- package/dist/sdk/conversations.js +155 -0
- package/dist/sdk/conversations.js.map +1 -0
- package/dist/sdk/credentials.d.ts +25 -0
- package/dist/sdk/credentials.d.ts.map +1 -0
- package/dist/sdk/credentials.js +44 -0
- package/dist/sdk/credentials.js.map +1 -0
- package/dist/sdk/distribution.d.ts +94 -0
- package/dist/sdk/distribution.d.ts.map +1 -0
- package/dist/sdk/distribution.js +100 -0
- package/dist/sdk/distribution.js.map +1 -0
- package/dist/sdk/eval.d.ts +81 -0
- package/dist/sdk/eval.d.ts.map +1 -1
- package/dist/sdk/eval.js +117 -0
- package/dist/sdk/eval.js.map +1 -1
- package/dist/sdk/functions.d.ts +14 -1
- package/dist/sdk/functions.d.ts.map +1 -1
- package/dist/sdk/functions.js +48 -5
- package/dist/sdk/functions.js.map +1 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +2 -0
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/jarvis-chat.d.ts +28 -0
- package/dist/sdk/jarvis-chat.d.ts.map +1 -0
- package/dist/sdk/jarvis-chat.js +117 -0
- package/dist/sdk/jarvis-chat.js.map +1 -0
- package/dist/sdk/kb.d.ts +69 -1
- package/dist/sdk/kb.d.ts.map +1 -1
- package/dist/sdk/kb.js +111 -24
- package/dist/sdk/kb.js.map +1 -1
- package/dist/sdk/orgs.d.ts +13 -0
- package/dist/sdk/orgs.d.ts.map +1 -0
- package/dist/sdk/orgs.js +59 -0
- package/dist/sdk/orgs.js.map +1 -0
- package/dist/sdk/platform.d.ts +62 -0
- package/dist/sdk/platform.d.ts.map +1 -0
- package/dist/sdk/platform.js +95 -0
- package/dist/sdk/platform.js.map +1 -0
- package/dist/sdk/session.d.ts +25 -0
- package/dist/sdk/session.d.ts.map +1 -0
- package/dist/sdk/session.js +69 -0
- package/dist/sdk/session.js.map +1 -0
- package/dist/sdk/skills.d.ts +46 -0
- package/dist/sdk/skills.d.ts.map +1 -0
- package/dist/sdk/skills.js +203 -0
- package/dist/sdk/skills.js.map +1 -0
- package/dist/sdk/test-chat.d.ts +52 -0
- package/dist/sdk/test-chat.d.ts.map +1 -0
- package/dist/sdk/test-chat.js +134 -0
- package/dist/sdk/test-chat.js.map +1 -0
- package/dist/sdk/tickets.d.ts +98 -0
- package/dist/sdk/tickets.d.ts.map +1 -0
- package/dist/sdk/tickets.js +91 -0
- package/dist/sdk/tickets.js.map +1 -0
- package/dist/sdk/types.d.ts +91 -6
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/voice.d.ts +147 -0
- package/dist/sdk/voice.d.ts.map +1 -0
- package/dist/sdk/voice.js +236 -0
- package/dist/sdk/voice.js.map +1 -0
- package/package.json +22 -3
- package/scripts/check-skills.mjs +25 -0
- package/skills/agent-builder/SKILL.md +89 -252
- package/skills/evaluations/SKILL.md +23 -234
- package/skills/evaluations/references/analysis.md +95 -0
- package/skills/evaluations/references/cli-reference.md +28 -0
- package/skills/evaluations/references/json-formats.md +100 -0
- package/src/cli/agents.ts +202 -22
- package/src/cli/analytics.ts +138 -0
- package/src/cli/auth.ts +56 -18
- package/src/cli/billing.ts +25 -0
- package/src/cli/campaigns.ts +33 -0
- package/src/cli/chat.ts +66 -115
- package/src/cli/config.ts +65 -31
- package/src/cli/conversations.ts +133 -0
- package/src/cli/distribution.ts +116 -0
- package/src/cli/eval.ts +254 -70
- package/src/cli/functions.ts +122 -36
- package/src/cli/index.ts +58 -55
- package/src/cli/init.ts +154 -0
- package/src/cli/kb.ts +120 -17
- package/src/cli/link.ts +117 -0
- package/src/cli/org.ts +81 -75
- package/src/cli/platform.ts +128 -0
- package/src/cli/tickets.ts +130 -0
- package/src/cli/utils/output.ts +51 -42
- package/src/cli/voice.ts +204 -0
- package/src/core/diff.ts +52 -0
- package/src/core/org-resolver.ts +113 -0
- package/src/core/runtime.ts +73 -0
- package/src/core/scope.ts +64 -0
- package/src/mcp/registry.ts +236 -0
- package/src/mcp/server.ts +84 -2
- package/src/mcp/tool-meta.ts +241 -0
- package/src/mcp/tools.ts +1529 -265
- package/src/sdk/agent-bundle.ts +245 -0
- package/src/sdk/agents.ts +74 -5
- package/src/sdk/analytics.ts +220 -0
- package/src/sdk/auth.ts +87 -107
- package/src/sdk/billing.ts +59 -0
- package/src/sdk/campaigns.ts +60 -0
- package/src/sdk/chat.ts +44 -10
- package/src/sdk/client.ts +373 -102
- package/src/sdk/config-change.ts +148 -0
- package/src/sdk/config.ts +2 -1
- package/src/sdk/conversations.ts +199 -0
- package/src/sdk/credentials.ts +53 -0
- package/src/sdk/distribution.ts +132 -0
- package/src/sdk/eval.ts +140 -0
- package/src/sdk/functions.ts +52 -6
- package/src/sdk/index.ts +2 -0
- package/src/sdk/jarvis-chat.ts +112 -0
- package/src/sdk/kb.ts +116 -29
- package/src/sdk/orgs.ts +62 -0
- package/src/sdk/platform.ts +115 -0
- package/src/sdk/session.ts +90 -0
- package/src/sdk/skills.ts +188 -0
- package/src/sdk/test-chat.ts +151 -0
- package/src/sdk/tickets.ts +120 -0
- package/src/sdk/types.ts +88 -6
- package/src/sdk/voice.ts +262 -0
- package/test/cli/entry.test.ts +34 -0
- package/test/cli/init.test.ts +66 -0
- package/test/cli/link.test.ts +57 -0
- package/test/core/org-resolver.test.ts +92 -0
- package/test/core/runtime.test.ts +30 -0
- package/test/core/scope.test.ts +93 -0
- package/test/harness.test.ts +24 -0
- package/test/helpers/config-home.ts +22 -0
- package/test/helpers/fake-middleware.ts +56 -0
- package/test/helpers/fetch-recorder.ts +75 -0
- package/test/helpers/jwt.ts +9 -0
- package/test/helpers/mcp-pair.ts +38 -0
- package/test/mcp/__snapshots__/tools-snapshot.test.ts.snap +145 -0
- package/test/mcp/askers.test.ts +60 -0
- package/test/mcp/elicit-timeout.test.ts +29 -0
- package/test/mcp/registry.test.ts +169 -0
- package/test/mcp/shaping.test.ts +41 -0
- package/test/mcp/tools-snapshot.test.ts +42 -0
- package/test/sdk/agents.test.ts +232 -0
- package/test/sdk/analytics.test.ts +89 -0
- package/test/sdk/auth.test.ts +70 -0
- package/test/sdk/billing.test.ts +47 -0
- package/test/sdk/campaigns.test.ts +43 -0
- package/test/sdk/client.test.ts +80 -0
- package/test/sdk/config-change.test.ts +142 -0
- package/test/sdk/conversations.test.ts +80 -0
- package/test/sdk/credentials.test.ts +33 -0
- package/test/sdk/distribution.test.ts +55 -0
- package/test/sdk/eval.test.ts +155 -0
- package/test/sdk/kb-upload.test.ts +50 -0
- package/test/sdk/kb.test.ts +95 -0
- package/test/sdk/orgs.test.ts +48 -0
- package/test/sdk/platform.test.ts +89 -0
- package/test/sdk/renewal.test.ts +115 -0
- package/test/sdk/session.test.ts +60 -0
- package/test/sdk/skills.test.ts +235 -0
- package/test/sdk/test-chat.test.ts +112 -0
- package/test/sdk/tickets.test.ts +42 -0
- package/test/sdk/voice.test.ts +147 -0
- package/vitest.config.ts +11 -0
package/src/cli/functions.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import ora from 'ora';
|
|
4
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { confirmOrExit } from './link.js';
|
|
6
|
+
import { CliError, EXIT } from '../core/runtime.js';
|
|
7
|
+
import { proposedSkills, resolveSecretArg, transferDestination } from '../sdk/skills.js';
|
|
5
8
|
import { IndemnClient } from '../sdk/client.js';
|
|
6
9
|
import { FunctionsSDK } from '../sdk/functions.js';
|
|
7
10
|
import { formatTable, printJson, printError, printSuccess, confirm } from './utils/output.js';
|
|
8
11
|
|
|
9
12
|
const REST_API_TYPE = 'rest_api';
|
|
10
13
|
const CUSTOM_TOOL_TYPE = 'custom_tool';
|
|
14
|
+
const TRANSFER_CALL_TYPE = 'transfer_call';
|
|
11
15
|
// Server expects kebab-case auth types (see copilot-server/utils/constants.js
|
|
12
16
|
// AI_FUNCTIONS_REST_API_HEADERS). Snake_case values pass validation but silently
|
|
13
17
|
// produce no Authorization header.
|
|
@@ -29,6 +33,7 @@ interface ConfigSchemaOptions {
|
|
|
29
33
|
authUsername?: string;
|
|
30
34
|
authPassword?: string;
|
|
31
35
|
authToken?: string;
|
|
36
|
+
handoffNumber?: string;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
function loadConfigFile(filePath: string): Record<string, unknown> {
|
|
@@ -80,19 +85,19 @@ function buildAuthorization(opts: ConfigSchemaOptions): Record<string, unknown>
|
|
|
80
85
|
if (!opts.authKey || !opts.authValue) {
|
|
81
86
|
throw new Error('--auth-key and --auth-value are required when --auth-type is api-key');
|
|
82
87
|
}
|
|
83
|
-
return { type: 'api-key', key: opts.authKey, value: opts.authValue };
|
|
88
|
+
return { type: 'api-key', key: opts.authKey, value: resolveSecretArg('--auth-value', opts.authValue) };
|
|
84
89
|
}
|
|
85
90
|
if (authType === 'basic-auth') {
|
|
86
91
|
if (!opts.authUsername || !opts.authPassword) {
|
|
87
92
|
throw new Error('--auth-username and --auth-password are required when --auth-type is basic-auth');
|
|
88
93
|
}
|
|
89
|
-
return { type: 'basic-auth', username: opts.authUsername, password: opts.authPassword };
|
|
94
|
+
return { type: 'basic-auth', username: opts.authUsername, password: resolveSecretArg('--auth-password', opts.authPassword) };
|
|
90
95
|
}
|
|
91
96
|
if (authType === 'bearer-token') {
|
|
92
97
|
if (!opts.authToken) {
|
|
93
98
|
throw new Error('--auth-token is required when --auth-type is bearer-token');
|
|
94
99
|
}
|
|
95
|
-
return { type: 'bearer-token', token: opts.authToken };
|
|
100
|
+
return { type: 'bearer-token', token: resolveSecretArg('--auth-token', opts.authToken) };
|
|
96
101
|
}
|
|
97
102
|
return undefined;
|
|
98
103
|
}
|
|
@@ -119,6 +124,9 @@ function validateTypeFlagCompatibility(type: string, opts: ConfigSchemaOptions):
|
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
}
|
|
127
|
+
if (type !== TRANSFER_CALL_TYPE && opts.handoffNumber !== undefined) {
|
|
128
|
+
throw new Error('--handoff-number is only valid for --type transfer_call. Use --config for other function types.');
|
|
129
|
+
}
|
|
122
130
|
if (opts.method !== undefined) {
|
|
123
131
|
const upper = opts.method.toUpperCase();
|
|
124
132
|
if (!(VALID_HTTP_METHODS as readonly string[]).includes(upper)) {
|
|
@@ -151,6 +159,14 @@ function buildConfigurationSchema(opts: ConfigSchemaOptions, type: string): Reco
|
|
|
151
159
|
if (opts.craftResponse !== undefined) result.craftResponse = !!opts.craftResponse;
|
|
152
160
|
}
|
|
153
161
|
|
|
162
|
+
if (type === TRANSFER_CALL_TYPE) {
|
|
163
|
+
// Server's validateTransferCallPayload requires `handoffNumber` (the destination
|
|
164
|
+
// phone number for the call transfer). copilot-server/services/botToolsValidationService.js:52
|
|
165
|
+
// Saved the way the dashboard saves it (phoneNumber + intlTelObj, or sipUri via --config).
|
|
166
|
+
if (opts.handoffNumber !== undefined) result.phoneNumber = opts.handoffNumber;
|
|
167
|
+
if (opts.outputInstructions !== undefined) result.outputInstructions = opts.outputInstructions;
|
|
168
|
+
}
|
|
169
|
+
|
|
154
170
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
155
171
|
}
|
|
156
172
|
|
|
@@ -166,15 +182,16 @@ function applyConfigSchemaOptions(cmd: Command): Command {
|
|
|
166
182
|
.option('--no-craft-response', 'Return data directly without LLM crafting')
|
|
167
183
|
.option('--auth-type <type>', 'Auth type: api-key | basic-auth | bearer-token | no-auth')
|
|
168
184
|
.option('--auth-key <name>', 'Header name for api-key auth (e.g. X-API-Key)')
|
|
169
|
-
.option('--auth-value <value>', 'Header value for api-key auth')
|
|
185
|
+
.option('--auth-value <value>', 'Header value for api-key auth: @file, env:NAME or - (stdin) keep it out of shell history')
|
|
170
186
|
.option('--auth-username <user>', 'Username for basic-auth')
|
|
171
|
-
.option('--auth-password <pass>', 'Password for basic-auth')
|
|
172
|
-
.option('--auth-token <token>', 'Token for bearer-token auth')
|
|
187
|
+
.option('--auth-password <pass>', 'Password for basic-auth: @file, env:NAME or - (stdin)')
|
|
188
|
+
.option('--auth-token <token>', 'Token for bearer-token auth: @file, env:NAME or - (stdin)')
|
|
189
|
+
.option('--handoff-number <number>', 'Destination phone number for --type transfer_call (E.164, e.g. +15551234567)')
|
|
173
190
|
.option('--config <path>', 'JSON file with full configurationSchema (individual flags override file values)');
|
|
174
191
|
}
|
|
175
192
|
|
|
176
193
|
export function registerFunctionCommands(program: Command): void {
|
|
177
|
-
const functions = program.command('functions').description('Manage agent functions
|
|
194
|
+
const functions = program.command('skills').alias('functions').description('Manage agent skills, the actions an agent can take (alias: functions)');
|
|
178
195
|
|
|
179
196
|
functions
|
|
180
197
|
.command('list')
|
|
@@ -248,6 +265,13 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
248
265
|
}
|
|
249
266
|
}
|
|
250
267
|
|
|
268
|
+
// For transfer_call on create, require handoffNumber (server validation rejects without it).
|
|
269
|
+
if (options.type === TRANSFER_CALL_TYPE) {
|
|
270
|
+
if (!configurationSchema || !(configurationSchema.phoneNumber || configurationSchema.handoffNumber || configurationSchema.sipUri)) {
|
|
271
|
+
throw new Error('--handoff-number (or --config with phoneNumber or sipUri) is required for --type transfer_call');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
251
275
|
const client = new IndemnClient();
|
|
252
276
|
const sdk = new FunctionsSDK(client);
|
|
253
277
|
const spinner = ora('Creating function...').start();
|
|
@@ -268,7 +292,7 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
268
292
|
finalFunc = await sdk.updateFunction(agentId, func._id, {
|
|
269
293
|
type: options.type,
|
|
270
294
|
description: options.description || '',
|
|
271
|
-
configurationSchema,
|
|
295
|
+
configurationSchema: options.type === TRANSFER_CALL_TYPE ? transferDestination({}, configurationSchema) : configurationSchema,
|
|
272
296
|
});
|
|
273
297
|
} catch (err) {
|
|
274
298
|
spinner.fail(`Function created (${func._id}) but configuration step failed.`);
|
|
@@ -327,10 +351,10 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
327
351
|
// otherwise `update --endpoint X` would wipe everything else.
|
|
328
352
|
const patch = buildConfigurationSchema(options, current.type);
|
|
329
353
|
if (patch) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
354
|
+
// A new transfer destination must replace the saved one of either kind (copilot prefers sipUri, then phoneNumber).
|
|
355
|
+
input.configurationSchema = current.type === TRANSFER_CALL_TYPE
|
|
356
|
+
? transferDestination(current.configurationSchema || {}, patch)
|
|
357
|
+
: { ...(current.configurationSchema || {}), ...patch };
|
|
334
358
|
}
|
|
335
359
|
|
|
336
360
|
// Anything actually changing? type+description are always set; require at least one explicit option.
|
|
@@ -357,24 +381,20 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
357
381
|
functions
|
|
358
382
|
.command('delete')
|
|
359
383
|
.argument('<agent-id>', 'Agent ID')
|
|
360
|
-
.argument('<func-id>', '
|
|
361
|
-
.
|
|
362
|
-
.
|
|
384
|
+
.argument('<func-id>', 'Skill ID')
|
|
385
|
+
.option('--yes', 'Do not ask for confirmation')
|
|
386
|
+
.description('Delete a skill')
|
|
387
|
+
.action(async (agentId: string, funcId: string, options) => {
|
|
363
388
|
try {
|
|
364
|
-
const confirmed = await confirm(`Delete function ${funcId}? This cannot be undone.`);
|
|
365
|
-
if (!confirmed) {
|
|
366
|
-
console.log('Cancelled.');
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
389
|
const client = new IndemnClient();
|
|
371
390
|
const sdk = new FunctionsSDK(client);
|
|
372
|
-
const
|
|
373
|
-
|
|
391
|
+
const skill = (await sdk.listFunctions(agentId)).find((f) => f._id === funcId);
|
|
392
|
+
if (!skill) throw new Error(`Skill ${funcId} is not on agent ${agentId}.`);
|
|
393
|
+
await confirmOrExit(`Delete skill "${skill.name ?? skill.type}" (${funcId}) from agent ${agentId}?`, Boolean(options.yes));
|
|
394
|
+
const spinner = ora('Deleting skill...').start();
|
|
374
395
|
await sdk.deleteFunction(agentId, funcId);
|
|
375
396
|
spinner.stop();
|
|
376
|
-
|
|
377
|
-
printSuccess(`Function ${funcId} deleted.`);
|
|
397
|
+
printSuccess(`Skill ${funcId} deleted.`);
|
|
378
398
|
} catch (err) {
|
|
379
399
|
printError(err);
|
|
380
400
|
process.exit(1);
|
|
@@ -446,18 +466,23 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
446
466
|
functions
|
|
447
467
|
.command('import')
|
|
448
468
|
.argument('<agent-id>', 'Agent ID')
|
|
449
|
-
.description('Import
|
|
450
|
-
.requiredOption('--url <url>', 'URL
|
|
469
|
+
.description('Import skills from a URL. REPLACES every skill of the agent, including the built-in handoff and FAQ retriever')
|
|
470
|
+
.requiredOption('--url <url>', 'URL of a skills JSON file (made by `skills export`); copilot downloads it itself')
|
|
471
|
+
.option('--yes', 'Do not ask for confirmation')
|
|
451
472
|
.action(async (agentId: string, options) => {
|
|
452
473
|
try {
|
|
453
474
|
const client = new IndemnClient();
|
|
454
475
|
const sdk = new FunctionsSDK(client);
|
|
455
|
-
const
|
|
456
|
-
|
|
476
|
+
const current = await sdk.listFunctions(agentId);
|
|
477
|
+
const builtIn = current.filter((f) => ['human_handoff', 'faqs-retriever'].includes(f.type)).length;
|
|
478
|
+
await confirmOrExit(
|
|
479
|
+
`Replace all ${current.length} skill(s) of agent ${agentId} (${builtIn} built-in) with the file's skills? There is no undo; export first with \`indemn skills export ${agentId}\`.`,
|
|
480
|
+
Boolean(options.yes),
|
|
481
|
+
);
|
|
482
|
+
const spinner = ora('Importing skills...').start();
|
|
457
483
|
const result = await sdk.importFunctions(agentId, options.url);
|
|
458
484
|
spinner.stop();
|
|
459
|
-
|
|
460
|
-
printSuccess('Functions imported successfully.');
|
|
485
|
+
printSuccess('Skills imported.');
|
|
461
486
|
printJson(result);
|
|
462
487
|
} catch (err) {
|
|
463
488
|
printError(err);
|
|
@@ -465,6 +490,63 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
465
490
|
}
|
|
466
491
|
});
|
|
467
492
|
|
|
493
|
+
functions
|
|
494
|
+
.command('generate')
|
|
495
|
+
.argument('<agent-id>', 'Agent ID')
|
|
496
|
+
.description('Propose skills from an API document or pasted text. Creates nothing unless --create')
|
|
497
|
+
.option('--file <path>', 'Document to read (for example an API description)')
|
|
498
|
+
.option('--text <text>', 'Text to read instead of a file')
|
|
499
|
+
.option('-o, --output <file>', 'Save the proposal to review and edit, then use `skills bulk-create --file`')
|
|
500
|
+
.option('--create', 'Create the proposed skills right away')
|
|
501
|
+
.option('--yes', 'With --create: do not ask for confirmation')
|
|
502
|
+
.action(async (agentId: string, options) => {
|
|
503
|
+
try {
|
|
504
|
+
if (!options.file && !options.text) throw new CliError('Give --file <path> or --text <text>.', EXIT.NEEDS_INPUT);
|
|
505
|
+
const client = new IndemnClient();
|
|
506
|
+
const sdk = new FunctionsSDK(client);
|
|
507
|
+
const spinner = ora('Reading the document...').start();
|
|
508
|
+
const proposal = await sdk.generateFromDoc(agentId, { filePath: options.file, text: options.text });
|
|
509
|
+
spinner.stop();
|
|
510
|
+
const skills = proposedSkills(proposal);
|
|
511
|
+
if (options.output) {
|
|
512
|
+
writeFileSync(options.output, JSON.stringify({ functions: skills ?? proposal }, null, 2) + '\n');
|
|
513
|
+
printSuccess(`Wrote ${options.output} (${skills?.length ?? 'unknown number of'} skill(s)).`);
|
|
514
|
+
}
|
|
515
|
+
if (!options.create) {
|
|
516
|
+
if (!options.output) printJson(proposal);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (!skills?.length) throw new CliError('Could not find a list of skills in the answer. Save it with -o, fix it, then use `skills bulk-create --file`.');
|
|
520
|
+
await confirmOrExit(`Create ${skills.length} skill(s) on agent ${agentId}?`, Boolean(options.yes));
|
|
521
|
+
const created = await sdk.bulkCreate(agentId, skills);
|
|
522
|
+
printSuccess(`Created ${created.length} skill(s).`);
|
|
523
|
+
} catch (err) {
|
|
524
|
+
printError(err);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
functions
|
|
530
|
+
.command('bulk-create')
|
|
531
|
+
.argument('<agent-id>', 'Agent ID')
|
|
532
|
+
.requiredOption('--file <path>', 'JSON: { "functions": [...] } or a list of skills')
|
|
533
|
+
.option('--yes', 'Do not ask for confirmation')
|
|
534
|
+
.description('Create several skills at once (names are checked first; a failure part-way can leave some created)')
|
|
535
|
+
.action(async (agentId: string, options) => {
|
|
536
|
+
try {
|
|
537
|
+
const raw = JSON.parse(readFileSync(options.file, 'utf8')) as unknown;
|
|
538
|
+
const skills = proposedSkills(raw);
|
|
539
|
+
if (!skills?.length) throw new CliError(`${options.file} has no list of skills.`);
|
|
540
|
+
const sdk = new FunctionsSDK(new IndemnClient());
|
|
541
|
+
await confirmOrExit(`Create ${skills.length} skill(s) on agent ${agentId}?`, Boolean(options.yes));
|
|
542
|
+
const created = await sdk.bulkCreate(agentId, skills);
|
|
543
|
+
printSuccess(`Created ${created.length} skill(s).`);
|
|
544
|
+
} catch (err) {
|
|
545
|
+
printError(err);
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
468
550
|
const params = functions.command('params').description('Manage function parameters');
|
|
469
551
|
|
|
470
552
|
params
|
|
@@ -496,6 +578,7 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
496
578
|
.requiredOption('--type <type>', 'Parameter type (e.g. string, number, boolean)')
|
|
497
579
|
.option('--description <desc>', 'Parameter description')
|
|
498
580
|
.option('--required', 'Mark as required')
|
|
581
|
+
.option('--allowed-values <list>', 'Fixed choices, comma-separated (shown as buttons)')
|
|
499
582
|
.action(async (agentId: string, funcId: string, options) => {
|
|
500
583
|
try {
|
|
501
584
|
const client = new IndemnClient();
|
|
@@ -504,6 +587,7 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
504
587
|
const result = await sdk.addParameter(agentId, funcId, {
|
|
505
588
|
name: options.name, type: options.type,
|
|
506
589
|
description: options.description, required: options.required || false,
|
|
590
|
+
allowedValues: options.allowedValues ? String(options.allowedValues).split(',').map((v: string) => v.trim()).filter(Boolean) : undefined,
|
|
507
591
|
});
|
|
508
592
|
spinner.stop();
|
|
509
593
|
// Server returns array of upserted params; extract the first one
|
|
@@ -522,6 +606,7 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
522
606
|
.option('--type <type>', 'Parameter type')
|
|
523
607
|
.option('--description <desc>', 'Parameter description')
|
|
524
608
|
.option('--required', 'Mark as required')
|
|
609
|
+
.option('--allowed-values <list>', 'New fixed choices, comma-separated ("" removes them)')
|
|
525
610
|
.action(async (agentId: string, funcId: string, paramId: string, options) => {
|
|
526
611
|
try {
|
|
527
612
|
const input: Record<string, unknown> = {};
|
|
@@ -529,6 +614,7 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
529
614
|
if (options.type) input.type = options.type;
|
|
530
615
|
if (options.description) input.description = options.description;
|
|
531
616
|
if (options.required !== undefined) input.required = options.required;
|
|
617
|
+
if (options.allowedValues !== undefined) input.allowedValues = String(options.allowedValues).split(',').map((v: string) => v.trim()).filter(Boolean);
|
|
532
618
|
if (Object.keys(input).length === 0) { printError('No options provided.'); process.exit(1); }
|
|
533
619
|
const client = new IndemnClient();
|
|
534
620
|
const sdk = new FunctionsSDK(client);
|
|
@@ -544,11 +630,11 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
544
630
|
.argument('<agent-id>', 'Agent ID')
|
|
545
631
|
.argument('<func-id>', 'Function ID')
|
|
546
632
|
.argument('<param-id>', 'Parameter ID')
|
|
547
|
-
.description('Delete a
|
|
548
|
-
.
|
|
633
|
+
.description('Delete a skill parameter')
|
|
634
|
+
.option('--yes', 'Do not ask for confirmation')
|
|
635
|
+
.action(async (agentId: string, funcId: string, paramId: string, options) => {
|
|
549
636
|
try {
|
|
550
|
-
|
|
551
|
-
if (!confirmed) { console.log('Cancelled.'); return; }
|
|
637
|
+
await confirmOrExit(`Delete parameter ${paramId} of skill ${funcId}?`, Boolean(options.yes));
|
|
552
638
|
const client = new IndemnClient();
|
|
553
639
|
const sdk = new FunctionsSDK(client);
|
|
554
640
|
const spinner = ora('Deleting parameter...').start();
|
package/src/cli/index.ts
CHANGED
|
@@ -11,16 +11,29 @@ import { registerKBCommands } from './kb.js';
|
|
|
11
11
|
import { registerEvalCommands } from './eval.js';
|
|
12
12
|
import { registerChatCommand } from './chat.js';
|
|
13
13
|
import { registerOrgCommands } from './org.js';
|
|
14
|
-
import {
|
|
14
|
+
import { registerLinkCommands } from './link.js';
|
|
15
|
+
import { registerConversationCommands } from './conversations.js';
|
|
16
|
+
import { registerVoiceCommands } from './voice.js';
|
|
17
|
+
import { registerDistributionCommands } from './distribution.js';
|
|
18
|
+
import { registerAnalyticsCommands } from './analytics.js';
|
|
19
|
+
import { registerTicketCommands } from './tickets.js';
|
|
20
|
+
import { registerBillingCommands } from './billing.js';
|
|
21
|
+
import { registerCampaignCommands } from './campaigns.js';
|
|
22
|
+
import { registerPlatformCommands } from './platform.js';
|
|
23
|
+
import { registerInitCommand } from './init.js';
|
|
24
|
+
import { askMfaInTerminal, askOrgInTerminal, installBanner } from './utils/output.js';
|
|
25
|
+
import { setAskers, setOrgFlag } from '../core/scope.js';
|
|
26
|
+
import { cliVersion, isNonInteractive } from '../core/runtime.js';
|
|
15
27
|
|
|
16
28
|
const program = new Command();
|
|
17
29
|
|
|
18
30
|
program
|
|
19
31
|
.name('indemn')
|
|
20
32
|
.description('CLI for managing Indemn AI agents')
|
|
21
|
-
.version(
|
|
33
|
+
.version(cliVersion());
|
|
22
34
|
|
|
23
|
-
program.option('--
|
|
35
|
+
program.option('--debug', 'Print every request and response status to stderr');
|
|
36
|
+
program.option('--org <id>', 'Organization to act on (otherwise: linked folder, your only org, or you are asked)');
|
|
24
37
|
program.option('--no-banner', 'Suppress the active-org banner');
|
|
25
38
|
|
|
26
39
|
// Commands that already surface org identity themselves — no banner needed.
|
|
@@ -30,7 +43,10 @@ const BANNER_SKIP = new Set([
|
|
|
30
43
|
'whoami',
|
|
31
44
|
'init',
|
|
32
45
|
'org list',
|
|
46
|
+
'org current',
|
|
33
47
|
'org switch',
|
|
48
|
+
'link',
|
|
49
|
+
'unlink',
|
|
34
50
|
'mcp serve',
|
|
35
51
|
]);
|
|
36
52
|
|
|
@@ -47,8 +63,15 @@ function fullCommandName(cmd: Command): string {
|
|
|
47
63
|
// Hook runs before any command action
|
|
48
64
|
program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
49
65
|
const globalOpts = thisCommand.opts();
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
// The environment is fixed by `indemn login --env`; there is no per-command switch.
|
|
67
|
+
if (globalOpts.debug) {
|
|
68
|
+
process.env.INDEMN_DEBUG = '1';
|
|
69
|
+
}
|
|
70
|
+
setOrgFlag(globalOpts.org);
|
|
71
|
+
|
|
72
|
+
// Ask in the terminal only when a person can answer; agents and CI get exit code 2 instead.
|
|
73
|
+
if (fullCommandName(actionCommand) !== 'mcp serve' && !isNonInteractive()) {
|
|
74
|
+
setAskers({ chooseOrg: askOrgInTerminal, mfaCode: askMfaInTerminal });
|
|
52
75
|
}
|
|
53
76
|
|
|
54
77
|
// Banner suppression: global flag, env var, command opts-out, or JSON output mode.
|
|
@@ -58,18 +81,45 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
|
58
81
|
if (actionOpts.json) return;
|
|
59
82
|
if (BANNER_SKIP.has(fullCommandName(actionCommand))) return;
|
|
60
83
|
|
|
61
|
-
|
|
84
|
+
installBanner();
|
|
62
85
|
});
|
|
63
86
|
|
|
64
87
|
// Register all command groups
|
|
65
88
|
registerAuthCommands(program);
|
|
89
|
+
// Groups other modules extend (org, agents) are registered first.
|
|
90
|
+
registerOrgCommands(program);
|
|
91
|
+
registerLinkCommands(program);
|
|
66
92
|
registerAgentCommands(program);
|
|
67
93
|
registerConfigCommands(program);
|
|
68
94
|
registerFunctionCommands(program);
|
|
69
95
|
registerKBCommands(program);
|
|
70
96
|
registerEvalCommands(program);
|
|
71
97
|
registerChatCommand(program);
|
|
72
|
-
|
|
98
|
+
registerConversationCommands(program);
|
|
99
|
+
registerVoiceCommands(program);
|
|
100
|
+
registerDistributionCommands(program);
|
|
101
|
+
registerAnalyticsCommands(program);
|
|
102
|
+
registerTicketCommands(program);
|
|
103
|
+
registerBillingCommands(program);
|
|
104
|
+
registerCampaignCommands(program);
|
|
105
|
+
registerPlatformCommands(program);
|
|
106
|
+
|
|
107
|
+
program
|
|
108
|
+
.command('channels')
|
|
109
|
+
.description('Channels an agent can be published on')
|
|
110
|
+
.command('list')
|
|
111
|
+
.description('List channels')
|
|
112
|
+
.option('--json', 'Output as JSON')
|
|
113
|
+
.action(async (options) => {
|
|
114
|
+
const { IndemnClient } = await import('../sdk/client.js');
|
|
115
|
+
const { OrgsSDK } = await import('../sdk/orgs.js');
|
|
116
|
+
const { formatTable, printJson, printError } = await import('./utils/output.js');
|
|
117
|
+
try {
|
|
118
|
+
const channels = await new OrgsSDK(new IndemnClient()).listChannels();
|
|
119
|
+
if (options.json) { printJson(channels); return; }
|
|
120
|
+
console.log(formatTable(['ID', 'Name', 'Type', 'Description'], channels.map((c) => [c._id, c.name, c.type ?? '', c.description ?? ''])));
|
|
121
|
+
} catch (err) { printError(err); process.exit(1); }
|
|
122
|
+
});
|
|
73
123
|
|
|
74
124
|
// MCP serve command
|
|
75
125
|
const mcp = program.command('mcp').description('MCP server commands');
|
|
@@ -102,53 +152,6 @@ program
|
|
|
102
152
|
} catch (err) { printError(err); process.exit(1); }
|
|
103
153
|
});
|
|
104
154
|
|
|
105
|
-
|
|
106
|
-
program
|
|
107
|
-
.command('init')
|
|
108
|
-
.description('Set up Claude Code skills in the current project')
|
|
109
|
-
.option('--force', 'Overwrite existing files')
|
|
110
|
-
.action(async (options) => {
|
|
111
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
112
|
-
const __dirname = dirname(__filename);
|
|
113
|
-
const packageRoot = resolve(__dirname, '..', '..');
|
|
114
|
-
const skillsSource = join(packageRoot, 'skills');
|
|
115
|
-
const cwd = process.cwd();
|
|
116
|
-
const skillsTarget = join(cwd, '.claude', 'skills', 'indemn');
|
|
117
|
-
const rulesTarget = join(cwd, '.claude', 'rules');
|
|
118
|
-
|
|
119
|
-
if (existsSync(skillsTarget) && !options.force) {
|
|
120
|
-
console.log('Indemn skills already installed. Use --force to overwrite.');
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Copy skills
|
|
125
|
-
mkdirSync(skillsTarget, { recursive: true });
|
|
126
|
-
cpSync(skillsSource, skillsTarget, { recursive: true });
|
|
127
|
-
|
|
128
|
-
// Write rules file
|
|
129
|
-
mkdirSync(rulesTarget, { recursive: true });
|
|
130
|
-
const rulesPath = join(rulesTarget, 'indemn.md');
|
|
131
|
-
if (!existsSync(rulesPath) || options.force) {
|
|
132
|
-
writeFileSync(rulesPath, [
|
|
133
|
-
'# Indemn CLI',
|
|
134
|
-
'',
|
|
135
|
-
'The `indemn` CLI is available for managing AI agents. Run `indemn whoami` to verify authentication.',
|
|
136
|
-
'',
|
|
137
|
-
'## Skills',
|
|
138
|
-
'',
|
|
139
|
-
'- **agent-builder** — Create, configure, and test agents (prompts, functions, knowledge bases, chat)',
|
|
140
|
-
'- **evaluations** — Create rubrics and test sets, run evaluations, analyze results, export reports',
|
|
141
|
-
'',
|
|
142
|
-
'Use these skills for any agent management or evaluation task. They contain the complete command reference.',
|
|
143
|
-
'',
|
|
144
|
-
].join('\n'));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
console.log('Indemn CLI integration set up:');
|
|
148
|
-
console.log(` Skills: ${skillsTarget}/`);
|
|
149
|
-
console.log(` Rules: ${rulesPath}`);
|
|
150
|
-
console.log('');
|
|
151
|
-
console.log('Claude Code will now discover the indemn skills automatically.');
|
|
152
|
-
});
|
|
155
|
+
registerInitCommand(program);
|
|
153
156
|
|
|
154
157
|
program.parse();
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { TOOL_META } from '../mcp/tool-meta.js';
|
|
7
|
+
import { IndemnClient } from '../sdk/client.js';
|
|
8
|
+
import { findLinkFile } from '../core/org-resolver.js';
|
|
9
|
+
import { printError, printSuccess } from './utils/output.js';
|
|
10
|
+
|
|
11
|
+
export const MCP_SERVER_ENTRY = { command: 'npx', args: ['-y', '@indemn/cli', 'mcp', 'serve'] };
|
|
12
|
+
|
|
13
|
+
/** Claude Code permission rules: read-only tools run without asking, anything that changes the platform asks every time. */
|
|
14
|
+
export function permissionBlock(): { allow: string[]; ask: string[] } {
|
|
15
|
+
const allow: string[] = [];
|
|
16
|
+
const ask: string[] = [];
|
|
17
|
+
for (const [name, meta] of Object.entries(TOOL_META)) {
|
|
18
|
+
const rule = `mcp__indemn__${name}`;
|
|
19
|
+
if (meta.annotations.readOnlyHint && !meta.guard) allow.push(rule);
|
|
20
|
+
else ask.push(rule);
|
|
21
|
+
}
|
|
22
|
+
return { allow: allow.sort(), ask: ask.sort() };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readJson(path: string): Record<string, any> {
|
|
26
|
+
if (!existsSync(path)) return {};
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error(`${path} is not valid JSON; fix it or move it away, then run init again.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const RULES = `# Indemn CLI
|
|
35
|
+
|
|
36
|
+
The \`indemn\` CLI and its MCP server manage Indemn AI agents. Run \`indemn whoami\` to see the platform (dev or production), the user and which org commands here act on.
|
|
37
|
+
|
|
38
|
+
- Every chat with an agent is a test conversation: never billed, never in analytics, shown with the tester's name.
|
|
39
|
+
- Actions that change or delete something are approved by the user first. When an approval form appears, that form is the question: call the tool and do not ask again in chat. When a tool returns a dry_run instead, show it and pass confirm: true only after the user agreed to that exact change.
|
|
40
|
+
- If a tool returns needs_input (for example which org), ask the user and call again with their answer.
|
|
41
|
+
- Talk in names: call agents, knowledge bases, test sets and runs by their names; show ids only when asked or to tell two items apart. Plain words, short answers, details on request.
|
|
42
|
+
|
|
43
|
+
## Skills
|
|
44
|
+
|
|
45
|
+
- **agent-builder**: create, configure and test agents (prompt, skills, knowledge bases, voice, chat).
|
|
46
|
+
- **evaluations**: rubrics, test sets, runs, CI gates, reports.
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
export interface InitReport {
|
|
50
|
+
skills: string;
|
|
51
|
+
rules: string;
|
|
52
|
+
mcp: 'added' | 'kept' | 'skipped';
|
|
53
|
+
permissions: 'added' | 'merged' | 'skipped';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Set up Claude Code in `cwd`. Existing files are merged, never overwritten, unless force is set for skills and rules. */
|
|
57
|
+
export function initProject(cwd: string, opts: { force?: boolean; mcp?: boolean; permissions?: boolean; packageRoot?: string } = {}): InitReport {
|
|
58
|
+
const packageRoot = opts.packageRoot ?? resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
59
|
+
// Claude Code finds project skills at .claude/skills/<name>/SKILL.md, one folder per skill.
|
|
60
|
+
const skillsTarget = join(cwd, '.claude', 'skills');
|
|
61
|
+
for (const skill of readdirSync(join(packageRoot, 'skills'), { withFileTypes: true }).filter((d) => d.isDirectory())) {
|
|
62
|
+
const target = join(skillsTarget, skill.name);
|
|
63
|
+
if (!existsSync(target) || opts.force) {
|
|
64
|
+
mkdirSync(target, { recursive: true });
|
|
65
|
+
cpSync(join(packageRoot, 'skills', skill.name), target, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const rulesPath = join(cwd, '.claude', 'rules', 'indemn.md');
|
|
69
|
+
if (!existsSync(rulesPath) || opts.force) {
|
|
70
|
+
mkdirSync(dirname(rulesPath), { recursive: true });
|
|
71
|
+
writeFileSync(rulesPath, RULES);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let mcp: InitReport['mcp'] = 'skipped';
|
|
75
|
+
if (opts.mcp !== false) {
|
|
76
|
+
const path = join(cwd, '.mcp.json');
|
|
77
|
+
const cfg = readJson(path);
|
|
78
|
+
cfg.mcpServers = cfg.mcpServers ?? {};
|
|
79
|
+
if (cfg.mcpServers.indemn) {
|
|
80
|
+
mcp = 'kept';
|
|
81
|
+
} else {
|
|
82
|
+
cfg.mcpServers.indemn = MCP_SERVER_ENTRY;
|
|
83
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + '\n');
|
|
84
|
+
mcp = 'added';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let permissions: InitReport['permissions'] = 'skipped';
|
|
89
|
+
if (opts.permissions !== false) {
|
|
90
|
+
const path = join(cwd, '.claude', 'settings.json');
|
|
91
|
+
const cfg = readJson(path);
|
|
92
|
+
const had = Boolean(cfg.permissions);
|
|
93
|
+
cfg.permissions = cfg.permissions ?? {};
|
|
94
|
+
const block = permissionBlock();
|
|
95
|
+
const denied = new Set<string>(cfg.permissions.deny ?? []);
|
|
96
|
+
const askSet = new Set<string>([...(cfg.permissions.ask ?? []), ...block.ask]);
|
|
97
|
+
// A tool the user already restricted is never loosened.
|
|
98
|
+
const allowSet = new Set<string>([...(cfg.permissions.allow ?? []), ...block.allow.filter((r) => !askSet.has(r) && !denied.has(r))]);
|
|
99
|
+
cfg.permissions.allow = [...allowSet].sort();
|
|
100
|
+
cfg.permissions.ask = [...askSet].filter((r) => !denied.has(r)).sort();
|
|
101
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
102
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + '\n');
|
|
103
|
+
permissions = had ? 'merged' : 'added';
|
|
104
|
+
}
|
|
105
|
+
return { skills: skillsTarget, rules: rulesPath, mcp, permissions };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** What is set up in `cwd`, for `indemn init --status`. */
|
|
109
|
+
export function initStatus(cwd: string): Array<[string, boolean, string]> {
|
|
110
|
+
const mcp = readJson(join(cwd, '.mcp.json'));
|
|
111
|
+
const settings = readJson(join(cwd, '.claude', 'settings.json'));
|
|
112
|
+
const login = IndemnClient.loadConfig();
|
|
113
|
+
let link: string | null = null;
|
|
114
|
+
try { link = findLinkFile(cwd)?.path ?? null; } catch { link = null; }
|
|
115
|
+
return [
|
|
116
|
+
['Skills', existsSync(join(cwd, '.claude', 'skills', 'agent-builder', 'SKILL.md')), '.claude/skills/agent-builder, evaluations'],
|
|
117
|
+
['Rules', existsSync(join(cwd, '.claude', 'rules', 'indemn.md')), '.claude/rules/indemn.md'],
|
|
118
|
+
['MCP server', Boolean(mcp.mcpServers?.indemn), '.mcp.json'],
|
|
119
|
+
['Permissions', (settings.permissions?.ask ?? []).some((r: string) => r.startsWith('mcp__indemn__')), '.claude/settings.json'],
|
|
120
|
+
['Logged in', Boolean(login), login ? `${login.email ?? login.user_id} on ${login.environment}` : 'run `indemn login`'],
|
|
121
|
+
['Folder linked', Boolean(link), link ?? 'optional: `indemn link --org <id>`'],
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function registerInitCommand(program: Command): void {
|
|
126
|
+
program
|
|
127
|
+
.command('init')
|
|
128
|
+
.description('Set up Claude Code here: skills, rules, the MCP server in .mcp.json, and permissions (reads allowed, changes ask)')
|
|
129
|
+
.option('--force', 'Overwrite the installed skills and rules')
|
|
130
|
+
.option('--no-mcp', 'Do not add the MCP server to .mcp.json')
|
|
131
|
+
.option('--no-permissions', 'Do not add permission rules to .claude/settings.json')
|
|
132
|
+
.option('--status', 'Show what is set up and stop')
|
|
133
|
+
.action((options) => {
|
|
134
|
+
try {
|
|
135
|
+
const cwd = process.cwd();
|
|
136
|
+
if (options.status) {
|
|
137
|
+
for (const [what, ok, detail] of initStatus(cwd)) console.log(`${ok ? chalk.green('✓') : chalk.yellow('·')} ${what.padEnd(14)} ${chalk.dim(detail)}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const r = initProject(cwd, { force: options.force, mcp: options.mcp, permissions: options.permissions });
|
|
141
|
+
printSuccess('Indemn CLI set up for Claude Code:');
|
|
142
|
+
console.log(` Skills: ${r.skills}`);
|
|
143
|
+
console.log(` Rules: ${r.rules}`);
|
|
144
|
+
console.log(` MCP server: ${r.mcp === 'added' ? 'added to .mcp.json' : r.mcp === 'kept' ? 'already in .mcp.json (kept)' : 'skipped'}`);
|
|
145
|
+
console.log(` Permissions: ${r.permissions === 'skipped' ? 'skipped' : `${r.permissions} in .claude/settings.json (reads allowed, changes ask)`}`);
|
|
146
|
+
const login = IndemnClient.loadConfig();
|
|
147
|
+
if (!login) console.log(chalk.yellow('Next: run `indemn login --env dev` (or production).'));
|
|
148
|
+
else if ((login.orgs?.length ?? 0) > 1 && !findLinkFile(cwd)) console.log(chalk.dim('Tip: pin this folder to one org with `indemn link --org <id>`.'));
|
|
149
|
+
} catch (err) {
|
|
150
|
+
printError(err);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|