@indemn/cli 1.6.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 +109 -38
- 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 +105 -39
- 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,7 +1,10 @@
|
|
|
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';
|
|
@@ -82,19 +85,19 @@ function buildAuthorization(opts: ConfigSchemaOptions): Record<string, unknown>
|
|
|
82
85
|
if (!opts.authKey || !opts.authValue) {
|
|
83
86
|
throw new Error('--auth-key and --auth-value are required when --auth-type is api-key');
|
|
84
87
|
}
|
|
85
|
-
return { type: 'api-key', key: opts.authKey, value: opts.authValue };
|
|
88
|
+
return { type: 'api-key', key: opts.authKey, value: resolveSecretArg('--auth-value', opts.authValue) };
|
|
86
89
|
}
|
|
87
90
|
if (authType === 'basic-auth') {
|
|
88
91
|
if (!opts.authUsername || !opts.authPassword) {
|
|
89
92
|
throw new Error('--auth-username and --auth-password are required when --auth-type is basic-auth');
|
|
90
93
|
}
|
|
91
|
-
return { type: 'basic-auth', username: opts.authUsername, password: opts.authPassword };
|
|
94
|
+
return { type: 'basic-auth', username: opts.authUsername, password: resolveSecretArg('--auth-password', opts.authPassword) };
|
|
92
95
|
}
|
|
93
96
|
if (authType === 'bearer-token') {
|
|
94
97
|
if (!opts.authToken) {
|
|
95
98
|
throw new Error('--auth-token is required when --auth-type is bearer-token');
|
|
96
99
|
}
|
|
97
|
-
return { type: 'bearer-token', token: opts.authToken };
|
|
100
|
+
return { type: 'bearer-token', token: resolveSecretArg('--auth-token', opts.authToken) };
|
|
98
101
|
}
|
|
99
102
|
return undefined;
|
|
100
103
|
}
|
|
@@ -159,7 +162,8 @@ function buildConfigurationSchema(opts: ConfigSchemaOptions, type: string): Reco
|
|
|
159
162
|
if (type === TRANSFER_CALL_TYPE) {
|
|
160
163
|
// Server's validateTransferCallPayload requires `handoffNumber` (the destination
|
|
161
164
|
// phone number for the call transfer). copilot-server/services/botToolsValidationService.js:52
|
|
162
|
-
|
|
165
|
+
// Saved the way the dashboard saves it (phoneNumber + intlTelObj, or sipUri via --config).
|
|
166
|
+
if (opts.handoffNumber !== undefined) result.phoneNumber = opts.handoffNumber;
|
|
163
167
|
if (opts.outputInstructions !== undefined) result.outputInstructions = opts.outputInstructions;
|
|
164
168
|
}
|
|
165
169
|
|
|
@@ -178,16 +182,16 @@ function applyConfigSchemaOptions(cmd: Command): Command {
|
|
|
178
182
|
.option('--no-craft-response', 'Return data directly without LLM crafting')
|
|
179
183
|
.option('--auth-type <type>', 'Auth type: api-key | basic-auth | bearer-token | no-auth')
|
|
180
184
|
.option('--auth-key <name>', 'Header name for api-key auth (e.g. X-API-Key)')
|
|
181
|
-
.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')
|
|
182
186
|
.option('--auth-username <user>', 'Username for basic-auth')
|
|
183
|
-
.option('--auth-password <pass>', 'Password for basic-auth')
|
|
184
|
-
.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)')
|
|
185
189
|
.option('--handoff-number <number>', 'Destination phone number for --type transfer_call (E.164, e.g. +15551234567)')
|
|
186
190
|
.option('--config <path>', 'JSON file with full configurationSchema (individual flags override file values)');
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
export function registerFunctionCommands(program: Command): void {
|
|
190
|
-
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)');
|
|
191
195
|
|
|
192
196
|
functions
|
|
193
197
|
.command('list')
|
|
@@ -263,8 +267,8 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
263
267
|
|
|
264
268
|
// For transfer_call on create, require handoffNumber (server validation rejects without it).
|
|
265
269
|
if (options.type === TRANSFER_CALL_TYPE) {
|
|
266
|
-
if (!configurationSchema || !configurationSchema.handoffNumber) {
|
|
267
|
-
throw new Error('--handoff-number (or --config with
|
|
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');
|
|
268
272
|
}
|
|
269
273
|
}
|
|
270
274
|
|
|
@@ -288,7 +292,7 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
288
292
|
finalFunc = await sdk.updateFunction(agentId, func._id, {
|
|
289
293
|
type: options.type,
|
|
290
294
|
description: options.description || '',
|
|
291
|
-
configurationSchema,
|
|
295
|
+
configurationSchema: options.type === TRANSFER_CALL_TYPE ? transferDestination({}, configurationSchema) : configurationSchema,
|
|
292
296
|
});
|
|
293
297
|
} catch (err) {
|
|
294
298
|
spinner.fail(`Function created (${func._id}) but configuration step failed.`);
|
|
@@ -347,10 +351,10 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
347
351
|
// otherwise `update --endpoint X` would wipe everything else.
|
|
348
352
|
const patch = buildConfigurationSchema(options, current.type);
|
|
349
353
|
if (patch) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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 };
|
|
354
358
|
}
|
|
355
359
|
|
|
356
360
|
// Anything actually changing? type+description are always set; require at least one explicit option.
|
|
@@ -377,24 +381,20 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
377
381
|
functions
|
|
378
382
|
.command('delete')
|
|
379
383
|
.argument('<agent-id>', 'Agent ID')
|
|
380
|
-
.argument('<func-id>', '
|
|
381
|
-
.
|
|
382
|
-
.
|
|
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) => {
|
|
383
388
|
try {
|
|
384
|
-
const confirmed = await confirm(`Delete function ${funcId}? This cannot be undone.`);
|
|
385
|
-
if (!confirmed) {
|
|
386
|
-
console.log('Cancelled.');
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
389
|
const client = new IndemnClient();
|
|
391
390
|
const sdk = new FunctionsSDK(client);
|
|
392
|
-
const
|
|
393
|
-
|
|
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();
|
|
394
395
|
await sdk.deleteFunction(agentId, funcId);
|
|
395
396
|
spinner.stop();
|
|
396
|
-
|
|
397
|
-
printSuccess(`Function ${funcId} deleted.`);
|
|
397
|
+
printSuccess(`Skill ${funcId} deleted.`);
|
|
398
398
|
} catch (err) {
|
|
399
399
|
printError(err);
|
|
400
400
|
process.exit(1);
|
|
@@ -466,18 +466,23 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
466
466
|
functions
|
|
467
467
|
.command('import')
|
|
468
468
|
.argument('<agent-id>', 'Agent ID')
|
|
469
|
-
.description('Import
|
|
470
|
-
.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')
|
|
471
472
|
.action(async (agentId: string, options) => {
|
|
472
473
|
try {
|
|
473
474
|
const client = new IndemnClient();
|
|
474
475
|
const sdk = new FunctionsSDK(client);
|
|
475
|
-
const
|
|
476
|
-
|
|
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();
|
|
477
483
|
const result = await sdk.importFunctions(agentId, options.url);
|
|
478
484
|
spinner.stop();
|
|
479
|
-
|
|
480
|
-
printSuccess('Functions imported successfully.');
|
|
485
|
+
printSuccess('Skills imported.');
|
|
481
486
|
printJson(result);
|
|
482
487
|
} catch (err) {
|
|
483
488
|
printError(err);
|
|
@@ -485,6 +490,63 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
485
490
|
}
|
|
486
491
|
});
|
|
487
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
|
+
|
|
488
550
|
const params = functions.command('params').description('Manage function parameters');
|
|
489
551
|
|
|
490
552
|
params
|
|
@@ -516,6 +578,7 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
516
578
|
.requiredOption('--type <type>', 'Parameter type (e.g. string, number, boolean)')
|
|
517
579
|
.option('--description <desc>', 'Parameter description')
|
|
518
580
|
.option('--required', 'Mark as required')
|
|
581
|
+
.option('--allowed-values <list>', 'Fixed choices, comma-separated (shown as buttons)')
|
|
519
582
|
.action(async (agentId: string, funcId: string, options) => {
|
|
520
583
|
try {
|
|
521
584
|
const client = new IndemnClient();
|
|
@@ -524,6 +587,7 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
524
587
|
const result = await sdk.addParameter(agentId, funcId, {
|
|
525
588
|
name: options.name, type: options.type,
|
|
526
589
|
description: options.description, required: options.required || false,
|
|
590
|
+
allowedValues: options.allowedValues ? String(options.allowedValues).split(',').map((v: string) => v.trim()).filter(Boolean) : undefined,
|
|
527
591
|
});
|
|
528
592
|
spinner.stop();
|
|
529
593
|
// Server returns array of upserted params; extract the first one
|
|
@@ -542,6 +606,7 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
542
606
|
.option('--type <type>', 'Parameter type')
|
|
543
607
|
.option('--description <desc>', 'Parameter description')
|
|
544
608
|
.option('--required', 'Mark as required')
|
|
609
|
+
.option('--allowed-values <list>', 'New fixed choices, comma-separated ("" removes them)')
|
|
545
610
|
.action(async (agentId: string, funcId: string, paramId: string, options) => {
|
|
546
611
|
try {
|
|
547
612
|
const input: Record<string, unknown> = {};
|
|
@@ -549,6 +614,7 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
549
614
|
if (options.type) input.type = options.type;
|
|
550
615
|
if (options.description) input.description = options.description;
|
|
551
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);
|
|
552
618
|
if (Object.keys(input).length === 0) { printError('No options provided.'); process.exit(1); }
|
|
553
619
|
const client = new IndemnClient();
|
|
554
620
|
const sdk = new FunctionsSDK(client);
|
|
@@ -564,11 +630,11 @@ export function registerFunctionCommands(program: Command): void {
|
|
|
564
630
|
.argument('<agent-id>', 'Agent ID')
|
|
565
631
|
.argument('<func-id>', 'Function ID')
|
|
566
632
|
.argument('<param-id>', 'Parameter ID')
|
|
567
|
-
.description('Delete a
|
|
568
|
-
.
|
|
633
|
+
.description('Delete a skill parameter')
|
|
634
|
+
.option('--yes', 'Do not ask for confirmation')
|
|
635
|
+
.action(async (agentId: string, funcId: string, paramId: string, options) => {
|
|
569
636
|
try {
|
|
570
|
-
|
|
571
|
-
if (!confirmed) { console.log('Cancelled.'); return; }
|
|
637
|
+
await confirmOrExit(`Delete parameter ${paramId} of skill ${funcId}?`, Boolean(options.yes));
|
|
572
638
|
const client = new IndemnClient();
|
|
573
639
|
const sdk = new FunctionsSDK(client);
|
|
574
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
|
+
}
|