@indemn/cli 1.3.1 → 1.5.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.
Files changed (41) hide show
  1. package/.github/workflows/ci.yml +20 -0
  2. package/.github/workflows/publish.yml +24 -0
  3. package/CONTRIBUTING.md +60 -0
  4. package/dist/cli/functions.d.ts.map +1 -1
  5. package/dist/cli/functions.js +235 -24
  6. package/dist/cli/functions.js.map +1 -1
  7. package/dist/cli/index.js +38 -6
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/org.d.ts.map +1 -1
  10. package/dist/cli/org.js +7 -2
  11. package/dist/cli/org.js.map +1 -1
  12. package/dist/cli/utils/output.d.ts +11 -0
  13. package/dist/cli/utils/output.d.ts.map +1 -1
  14. package/dist/cli/utils/output.js +49 -0
  15. package/dist/cli/utils/output.js.map +1 -1
  16. package/dist/sdk/auth.d.ts.map +1 -1
  17. package/dist/sdk/auth.js +18 -0
  18. package/dist/sdk/auth.js.map +1 -1
  19. package/dist/sdk/client.d.ts.map +1 -1
  20. package/dist/sdk/client.js +1 -0
  21. package/dist/sdk/client.js.map +1 -1
  22. package/dist/sdk/types.d.ts +6 -0
  23. package/dist/sdk/types.d.ts.map +1 -1
  24. package/package.json +2 -2
  25. package/src/cli/functions.ts +272 -33
  26. package/src/cli/index.ts +38 -6
  27. package/src/cli/org.ts +9 -2
  28. package/src/cli/utils/output.ts +49 -0
  29. package/src/sdk/auth.ts +18 -0
  30. package/src/sdk/client.ts +1 -0
  31. package/src/sdk/types.ts +9 -0
  32. package/dist/report/assets/Indemn_PrimaryLogo_Iris.png +0 -0
  33. package/dist/report/assets/assets/Indemn_PrimaryLogo_Iris.png +0 -0
  34. package/dist/report/assets/assets/fonts/Barlow-Bold.ttf +0 -0
  35. package/dist/report/assets/assets/fonts/Barlow-Medium.ttf +0 -0
  36. package/dist/report/assets/assets/fonts/Barlow-Regular.ttf +0 -0
  37. package/dist/report/assets/assets/fonts/Barlow-SemiBold.ttf +0 -0
  38. package/dist/report/assets/fonts/Barlow-Bold.ttf +0 -0
  39. package/dist/report/assets/fonts/Barlow-Medium.ttf +0 -0
  40. package/dist/report/assets/fonts/Barlow-Regular.ttf +0 -0
  41. package/dist/report/assets/fonts/Barlow-SemiBold.ttf +0 -0
@@ -1,9 +1,178 @@
1
1
  import { Command } from 'commander';
2
+ import chalk from 'chalk';
2
3
  import ora from 'ora';
4
+ import { readFileSync, existsSync } from 'node:fs';
3
5
  import { IndemnClient } from '../sdk/client.js';
4
6
  import { FunctionsSDK } from '../sdk/functions.js';
5
7
  import { formatTable, printJson, printError, printSuccess, confirm } from './utils/output.js';
6
8
 
9
+ const REST_API_TYPE = 'rest_api';
10
+ const CUSTOM_TOOL_TYPE = 'custom_tool';
11
+ // Server expects kebab-case auth types (see copilot-server/utils/constants.js
12
+ // AI_FUNCTIONS_REST_API_HEADERS). Snake_case values pass validation but silently
13
+ // produce no Authorization header.
14
+ const VALID_AUTH_TYPES = ['api-key', 'basic-auth', 'bearer-token', 'no-auth'] as const;
15
+ const VALID_HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const;
16
+
17
+ interface ConfigSchemaOptions {
18
+ config?: string;
19
+ endpoint?: string;
20
+ method?: string;
21
+ payload?: string;
22
+ payloadFile?: string;
23
+ responseField?: string;
24
+ outputInstructions?: string;
25
+ craftResponse?: boolean;
26
+ authType?: string;
27
+ authKey?: string;
28
+ authValue?: string;
29
+ authUsername?: string;
30
+ authPassword?: string;
31
+ authToken?: string;
32
+ }
33
+
34
+ function loadConfigFile(filePath: string): Record<string, unknown> {
35
+ if (!existsSync(filePath)) {
36
+ throw new Error(`--config file not found: ${filePath}`);
37
+ }
38
+ const content = readFileSync(filePath, 'utf-8');
39
+ let parsed: unknown;
40
+ try {
41
+ parsed = JSON.parse(content);
42
+ } catch {
43
+ throw new Error(`--config file is not valid JSON: ${filePath}`);
44
+ }
45
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
46
+ throw new Error(`--config file must contain a JSON object: ${filePath}`);
47
+ }
48
+ const obj = parsed as Record<string, unknown>;
49
+ // Auto-unwrap if the file is a full BotFunction dump rather than just configurationSchema contents.
50
+ // (e.g. user piped `functions list --json` into a file). Detected by presence of the wrapping key.
51
+ if (obj.configurationSchema && typeof obj.configurationSchema === 'object' && !Array.isArray(obj.configurationSchema)) {
52
+ return obj.configurationSchema as Record<string, unknown>;
53
+ }
54
+ return obj;
55
+ }
56
+
57
+ function loadPayloadString(opts: ConfigSchemaOptions): string | undefined {
58
+ if (opts.payload !== undefined && opts.payloadFile !== undefined) {
59
+ throw new Error('--payload and --payload-file are mutually exclusive');
60
+ }
61
+ if (opts.payload !== undefined) return opts.payload;
62
+ if (opts.payloadFile !== undefined) {
63
+ if (!existsSync(opts.payloadFile)) {
64
+ throw new Error(`--payload-file not found: ${opts.payloadFile}`);
65
+ }
66
+ return readFileSync(opts.payloadFile, 'utf-8');
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ function buildAuthorization(opts: ConfigSchemaOptions): Record<string, unknown> | undefined {
72
+ if (opts.authType === undefined) return undefined;
73
+ // Accept snake_case as a user-friendly alias for kebab-case (idiomatic CLI variants).
74
+ const authType = opts.authType.replace(/_/g, '-');
75
+ if (!(VALID_AUTH_TYPES as readonly string[]).includes(authType)) {
76
+ throw new Error(`--auth-type must be one of: ${VALID_AUTH_TYPES.join(', ')}`);
77
+ }
78
+ if (authType === 'no-auth') return { type: 'no-auth' };
79
+ if (authType === 'api-key') {
80
+ if (!opts.authKey || !opts.authValue) {
81
+ throw new Error('--auth-key and --auth-value are required when --auth-type is api-key');
82
+ }
83
+ return { type: 'api-key', key: opts.authKey, value: opts.authValue };
84
+ }
85
+ if (authType === 'basic-auth') {
86
+ if (!opts.authUsername || !opts.authPassword) {
87
+ throw new Error('--auth-username and --auth-password are required when --auth-type is basic-auth');
88
+ }
89
+ return { type: 'basic-auth', username: opts.authUsername, password: opts.authPassword };
90
+ }
91
+ if (authType === 'bearer-token') {
92
+ if (!opts.authToken) {
93
+ throw new Error('--auth-token is required when --auth-type is bearer-token');
94
+ }
95
+ return { type: 'bearer-token', token: opts.authToken };
96
+ }
97
+ return undefined;
98
+ }
99
+
100
+ function validateTypeFlagCompatibility(type: string, opts: ConfigSchemaOptions): void {
101
+ // REST-only flags must not be used with non-rest_api types — point them at --config instead.
102
+ const restOnlyFlags: Array<[keyof ConfigSchemaOptions, string]> = [
103
+ ['endpoint', '--endpoint'],
104
+ ['method', '--method'],
105
+ ['payload', '--payload'],
106
+ ['payloadFile', '--payload-file'],
107
+ ['responseField', '--response-field'],
108
+ ['authType', '--auth-type'],
109
+ ['authKey', '--auth-key'],
110
+ ['authValue', '--auth-value'],
111
+ ['authUsername', '--auth-username'],
112
+ ['authPassword', '--auth-password'],
113
+ ['authToken', '--auth-token'],
114
+ ];
115
+ if (type !== REST_API_TYPE) {
116
+ for (const [opt, flag] of restOnlyFlags) {
117
+ if (opts[opt] !== undefined) {
118
+ throw new Error(`${flag} is only valid for --type rest_api. Use --config for other function types.`);
119
+ }
120
+ }
121
+ }
122
+ if (opts.method !== undefined) {
123
+ const upper = opts.method.toUpperCase();
124
+ if (!(VALID_HTTP_METHODS as readonly string[]).includes(upper)) {
125
+ throw new Error(`--method must be one of: ${VALID_HTTP_METHODS.join(', ')}`);
126
+ }
127
+ }
128
+ }
129
+
130
+ function buildConfigurationSchema(opts: ConfigSchemaOptions, type: string): Record<string, unknown> | undefined {
131
+ // Start from --config file if provided; layer individual flags on top so CLI wins.
132
+ const result: Record<string, unknown> = opts.config ? loadConfigFile(opts.config) : {};
133
+
134
+ if (type === REST_API_TYPE) {
135
+ if (opts.endpoint !== undefined) result.endpoint = opts.endpoint;
136
+ if (opts.method !== undefined) result.method = opts.method.toUpperCase();
137
+ const payloadStr = loadPayloadString(opts);
138
+ if (payloadStr !== undefined) result.payload = payloadStr;
139
+ if (opts.responseField !== undefined) result.responseField = opts.responseField;
140
+ const auth = buildAuthorization(opts);
141
+ if (auth !== undefined) result.authorization = auth;
142
+ // Server's validateRestApiPayload requires authorization.type. Default to "no-auth"
143
+ // so callers don't have to pass --auth-type explicitly for unauthenticated APIs.
144
+ if (result.authorization === undefined) {
145
+ result.authorization = { type: 'no-auth' };
146
+ }
147
+ }
148
+
149
+ if (type === REST_API_TYPE || type === CUSTOM_TOOL_TYPE) {
150
+ if (opts.outputInstructions !== undefined) result.outputInstructions = opts.outputInstructions;
151
+ if (opts.craftResponse !== undefined) result.craftResponse = !!opts.craftResponse;
152
+ }
153
+
154
+ return Object.keys(result).length > 0 ? result : undefined;
155
+ }
156
+
157
+ function applyConfigSchemaOptions(cmd: Command): Command {
158
+ return cmd
159
+ .option('--endpoint <url>', 'REST API endpoint URL')
160
+ .option('--method <method>', 'HTTP method (GET|POST|PUT|DELETE|PATCH)')
161
+ .option('--payload <json>', 'Request body as JSON string (server JSON.parses at execution)')
162
+ .option('--payload-file <path>', 'Path to file containing request body JSON (alt to --payload)')
163
+ .option('--response-field <path>', 'Dot-path for extracting from response (e.g. data.result)')
164
+ .option('--output-instructions <text>', 'Instructions for how the LLM uses the response')
165
+ .option('--craft-response', 'Have LLM craft response (use --no-craft-response to return data directly)')
166
+ .option('--no-craft-response', 'Return data directly without LLM crafting')
167
+ .option('--auth-type <type>', 'Auth type: api-key | basic-auth | bearer-token | no-auth')
168
+ .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')
170
+ .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')
173
+ .option('--config <path>', 'JSON file with full configurationSchema (individual flags override file values)');
174
+ }
175
+
7
176
  export function registerFunctionCommands(program: Command): void {
8
177
  const functions = program.command('functions').description('Manage agent functions/tools');
9
178
 
@@ -55,65 +224,126 @@ export function registerFunctionCommands(program: Command): void {
55
224
  }
56
225
  });
57
226
 
58
- functions
59
- .command('create')
60
- .argument('<agent-id>', 'Agent ID')
61
- .description('Create a function for an agent')
62
- .requiredOption('--type <type>', 'Function type')
63
- .option('--name <name>', 'Function name')
64
- .option('--description <description>', 'Function description')
227
+ applyConfigSchemaOptions(
228
+ functions
229
+ .command('create')
230
+ .argument('<agent-id>', 'Agent ID')
231
+ .description('Create a function for an agent')
232
+ .requiredOption('--type <type>', 'Function type')
233
+ .option('--name <name>', 'Function name')
234
+ .option('--description <description>', 'Function description')
235
+ )
65
236
  .action(async (agentId: string, options) => {
66
237
  try {
238
+ validateTypeFlagCompatibility(options.type, options);
239
+ const configurationSchema = buildConfigurationSchema(options, options.type);
240
+
241
+ // For rest_api on create, require endpoint+method (unless supplied via --config).
242
+ if (options.type === REST_API_TYPE) {
243
+ if (!configurationSchema || !configurationSchema.endpoint) {
244
+ throw new Error('--endpoint (or --config with endpoint) is required for --type rest_api');
245
+ }
246
+ if (!configurationSchema.method) {
247
+ throw new Error('--method (or --config with method) is required for --type rest_api');
248
+ }
249
+ }
250
+
67
251
  const client = new IndemnClient();
68
252
  const sdk = new FunctionsSDK(client);
69
253
  const spinner = ora('Creating function...').start();
70
254
 
255
+ // Step 1: POST skeleton (server's createBotTool doesn't convert configurationSchema —
256
+ // only the update path does. So a two-step create is needed for the function to be executable.)
71
257
  const func = await sdk.createFunction(agentId, {
72
258
  type: options.type,
73
259
  name: options.name,
74
260
  description: options.description,
75
261
  });
262
+
263
+ // Step 2: PUT configurationSchema so the server converts it into executionSchema.
264
+ let finalFunc = func;
265
+ if (configurationSchema) {
266
+ spinner.text = 'Configuring function...';
267
+ try {
268
+ finalFunc = await sdk.updateFunction(agentId, func._id, {
269
+ type: options.type,
270
+ description: options.description || '',
271
+ configurationSchema,
272
+ });
273
+ } catch (err) {
274
+ spinner.fail(`Function created (${func._id}) but configuration step failed.`);
275
+ printError(err);
276
+ console.log(chalk.dim(`Retry: indemn functions update <agent-id> ${func._id} ...`));
277
+ console.log(chalk.dim(`Or remove: indemn functions delete <agent-id> ${func._id}`));
278
+ process.exit(1);
279
+ }
280
+ }
76
281
  spinner.stop();
77
282
 
78
- printSuccess(`Function created: ${func._id}`);
79
- console.log(` Name: ${func.name || '(none)'}`);
80
- console.log(` Type: ${func.type}`);
283
+ printSuccess(`Function created: ${finalFunc._id}`);
284
+ console.log(` Name: ${finalFunc.name || '(none)'}`);
285
+ console.log(` Type: ${finalFunc.type}`);
81
286
  } catch (err) {
82
287
  printError(err);
83
288
  process.exit(1);
84
289
  }
85
290
  });
86
291
 
87
- functions
88
- .command('update')
89
- .argument('<agent-id>', 'Agent ID')
90
- .argument('<func-id>', 'Function ID')
91
- .description('Update a function')
92
- .option('--name <name>', 'Function name')
93
- .option('--description <description>', 'Function description')
292
+ applyConfigSchemaOptions(
293
+ functions
294
+ .command('update')
295
+ .argument('<agent-id>', 'Agent ID')
296
+ .argument('<func-id>', 'Function ID')
297
+ .description('Update a function')
298
+ .option('--name <name>', 'Function name')
299
+ .option('--description <description>', 'Function description')
300
+ )
94
301
  .action(async (agentId: string, funcId: string, options) => {
95
302
  try {
96
- const input: Record<string, unknown> = {};
97
- if (options.name) input.name = options.name;
98
- if (options.description) input.description = options.description;
99
-
100
- if (Object.keys(input).length === 0) {
101
- printError('No update options provided. Use --name or --description.');
102
- process.exit(1);
103
- }
104
-
105
303
  const client = new IndemnClient();
106
304
  const sdk = new FunctionsSDK(client);
107
- const spinner = ora('Updating function...').start();
108
305
 
109
- // Server requires type and description on updates — fetch current values as defaults
306
+ // Fetch current function — required so we can preserve type/description and merge configurationSchema.
307
+ const fetchSpinner = ora('Fetching current function...').start();
110
308
  const functions = await sdk.listFunctions(agentId);
111
309
  const current = functions.find((f) => f._id === funcId);
112
- if (current) {
113
- if (!input.type) input.type = current.type;
114
- if (!input.description) input.description = current.description;
310
+ fetchSpinner.stop();
311
+ if (!current) {
312
+ printError(`Function ${funcId} not found.`);
313
+ process.exit(1);
115
314
  }
116
315
 
316
+ validateTypeFlagCompatibility(current.type, options);
317
+
318
+ const input: Record<string, unknown> = {
319
+ // Server requires type + description on every update.
320
+ type: current.type,
321
+ description: options.description || current.description || '',
322
+ };
323
+ if (options.name) input.name = options.name;
324
+
325
+ // Build configurationSchema patch from flags + --config, then shallow-merge over current.
326
+ // Server replaces executionSchema wholesale on each update, so we must send the full merged schema —
327
+ // otherwise `update --endpoint X` would wipe everything else.
328
+ const patch = buildConfigurationSchema(options, current.type);
329
+ if (patch) {
330
+ input.configurationSchema = {
331
+ ...(current.configurationSchema || {}),
332
+ ...patch,
333
+ };
334
+ }
335
+
336
+ // Anything actually changing? type+description are always set; require at least one explicit option.
337
+ const hasUserChanges =
338
+ options.name !== undefined ||
339
+ options.description !== undefined ||
340
+ patch !== undefined;
341
+ if (!hasUserChanges) {
342
+ printError('No update options provided. Use --name, --description, --endpoint, --config, etc.');
343
+ process.exit(1);
344
+ }
345
+
346
+ const spinner = ora('Updating function...').start();
117
347
  await sdk.updateFunction(agentId, funcId, input);
118
348
  spinner.stop();
119
349
 
@@ -161,9 +391,18 @@ export function registerFunctionCommands(program: Command): void {
161
391
  try {
162
392
  const client = new IndemnClient();
163
393
  const sdk = new FunctionsSDK(client);
164
- const spinner = ora('Testing function...').start();
165
394
 
166
- const result = await sdk.testFunction(agentId, funcId);
395
+ // Server's /test-rest-api endpoint expects the configurationSchema in the request body
396
+ // (it re-converts via convertPayloadToExecutionSchema). It does NOT read the stored
397
+ // schema from the DB. So we fetch the function's current configurationSchema and send it.
398
+ const spinner = ora('Testing function...').start();
399
+ const all = await sdk.listFunctions(agentId);
400
+ const current = all.find((f) => f._id === funcId);
401
+ if (!current) {
402
+ spinner.fail(`Function ${funcId} not found.`);
403
+ process.exit(1);
404
+ }
405
+ const result = await sdk.testFunction(agentId, funcId, current.configurationSchema || {});
167
406
  spinner.stop();
168
407
 
169
408
  if (options.json) {
package/src/cli/index.ts CHANGED
@@ -11,22 +11,54 @@ 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 { printBanner } from './utils/output.js';
14
15
 
15
16
  const program = new Command();
16
17
 
17
18
  program
18
19
  .name('indemn')
19
20
  .description('CLI for managing Indemn AI agents')
20
- .version('1.2.0');
21
+ .version('1.5.0');
21
22
 
22
23
  program.option('--env <environment>', 'Environment (dev or production)');
24
+ program.option('--no-banner', 'Suppress the active-org banner');
23
25
 
24
- // Hook to set environment before any command runs
25
- program.hook('preAction', (thisCommand) => {
26
- const opts = thisCommand.opts();
27
- if (opts.env) {
28
- process.env.INDEMN_ENV = opts.env;
26
+ // Commands that already surface org identity themselves — no banner needed.
27
+ const BANNER_SKIP = new Set([
28
+ 'login',
29
+ 'logout',
30
+ 'whoami',
31
+ 'init',
32
+ 'org list',
33
+ 'org switch',
34
+ 'mcp serve',
35
+ ]);
36
+
37
+ function fullCommandName(cmd: Command): string {
38
+ const parts: string[] = [cmd.name()];
39
+ let p: Command | null = cmd.parent;
40
+ while (p && p.name() !== 'indemn') {
41
+ parts.unshift(p.name());
42
+ p = p.parent;
43
+ }
44
+ return parts.join(' ');
45
+ }
46
+
47
+ // Hook runs before any command action
48
+ program.hook('preAction', async (thisCommand, actionCommand) => {
49
+ const globalOpts = thisCommand.opts();
50
+ if (globalOpts.env) {
51
+ process.env.INDEMN_ENV = globalOpts.env;
29
52
  }
53
+
54
+ // Banner suppression: global flag, env var, command opts-out, or JSON output mode.
55
+ if (globalOpts.banner === false) return;
56
+ if (process.env.INDEMN_NO_BANNER === '1') return;
57
+ const actionOpts = actionCommand.opts();
58
+ if (actionOpts.json) return;
59
+ if (BANNER_SKIP.has(fullCommandName(actionCommand))) return;
60
+
61
+ await printBanner();
30
62
  });
31
63
 
32
64
  // Register all command groups
package/src/cli/org.ts CHANGED
@@ -56,6 +56,13 @@ export function registerOrgCommands(program: Command): void {
56
56
  }
57
57
 
58
58
  const currentOrgId = client.getOrgId();
59
+
60
+ // Opportunistically sync the cached org_name while we have fresh data.
61
+ const activeOrg = orgs.find(o => o._id === currentOrgId);
62
+ if (activeOrg) {
63
+ IndemnClient.saveConfig({ org_name: activeOrg.name });
64
+ }
65
+
59
66
  console.log(chalk.bold('Organizations'));
60
67
  console.log('');
61
68
  for (const o of orgs.sort((a, b) => a.name.localeCompare(b.name))) {
@@ -95,8 +102,8 @@ export function registerOrgCommands(program: Command): void {
95
102
  const data = await orgRes.json() as { organization: { _id: string; name: string } };
96
103
  const orgName = data.organization.name;
97
104
 
98
- // Update config
99
- IndemnClient.saveConfig({ org_id: orgId });
105
+ // Update config — cache both org_id and org_name so the banner can render without a lookup.
106
+ IndemnClient.saveConfig({ org_id: orgId, org_name: orgName });
100
107
 
101
108
  spinner.succeed(`Switched to ${orgName} (${orgId})`);
102
109
  } catch (err) {
@@ -1,6 +1,7 @@
1
1
  import Table from 'cli-table3';
2
2
  import chalk from 'chalk';
3
3
  import * as readline from 'readline';
4
+ import { IndemnClient } from '../../sdk/client.js';
4
5
 
5
6
  export function formatTable(headers: string[], rows: string[][]): string {
6
7
  const table = new Table({
@@ -27,6 +28,54 @@ export function printSuccess(message: string): void {
27
28
  console.log(chalk.green(message));
28
29
  }
29
30
 
31
+ /**
32
+ * Render the active-org banner: [Voice Demos · 69a40cd9…6807f · dev]
33
+ *
34
+ * Printed to stderr so it never pollutes stdout pipes (JSON output stays clean
35
+ * without --json suppression). Rendered red for prod to reinforce blast-radius
36
+ * awareness. Silent if not logged in or if the caller opts out.
37
+ *
38
+ * Self-heals when org_name is missing from config (e.g., after upgrading from
39
+ * a pre-1.4 install) by fetching /organization/:id once and caching.
40
+ */
41
+ export async function printBanner(): Promise<void> {
42
+ if (process.env.INDEMN_NO_BANNER === '1') return;
43
+
44
+ const config = IndemnClient.loadConfig();
45
+ if (!config || !config.api_key || !config.org_id) return;
46
+
47
+ let orgName = config.org_name;
48
+ if (!orgName) {
49
+ // Lazy one-time fetch so post-upgrade installs don't render "(unknown)" forever.
50
+ try {
51
+ const client = new IndemnClient();
52
+ const orgRes = await fetch(`${client.getHosts().copilot}/organization/${config.org_id}`, {
53
+ headers: {
54
+ 'Authorization': `Bearer ${client.getApiKey()}`,
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ });
58
+ if (orgRes.ok) {
59
+ const data = await orgRes.json() as { organization?: { name?: string } };
60
+ orgName = data.organization?.name;
61
+ if (orgName) IndemnClient.saveConfig({ org_name: orgName });
62
+ }
63
+ } catch {
64
+ // Silent — banner degrades to ID-only display
65
+ }
66
+ }
67
+
68
+ const shortId = config.org_id.length > 12
69
+ ? `${config.org_id.slice(0, 8)}…${config.org_id.slice(-4)}`
70
+ : config.org_id;
71
+ const label = orgName || '(unknown org)';
72
+ const text = `[${label} · ${shortId} · ${config.environment}]`;
73
+ const colored = config.environment === 'production'
74
+ ? chalk.red.bold(text)
75
+ : chalk.yellow.dim(text);
76
+ process.stderr.write(colored + '\n');
77
+ }
78
+
30
79
  export async function confirm(message: string): Promise<boolean> {
31
80
  const rl = readline.createInterface({
32
81
  input: process.stdin,
package/src/sdk/auth.ts CHANGED
@@ -91,9 +91,27 @@ export async function login(email: string, password: string, env: Environment =
91
91
  throw new Error('Login succeeded but no organization found. Contact your admin to be added to an organization.');
92
92
  }
93
93
 
94
+ // Step 3.5: Fetch the org name so the banner can display it without a lookup on every command.
95
+ let orgName: string | undefined;
96
+ try {
97
+ const orgRes = await fetch(`${client.getHosts().copilot}/organization/${apiKeyResponse.org_id}`, {
98
+ headers: {
99
+ 'Authorization': `Bearer ${apiKeyResponse.key}`,
100
+ 'Content-Type': 'application/json',
101
+ },
102
+ });
103
+ if (orgRes.ok) {
104
+ const data = await orgRes.json() as { organization?: { name?: string } };
105
+ orgName = data.organization?.name;
106
+ }
107
+ } catch {
108
+ // Non-fatal — banner falls back to org ID if name unavailable
109
+ }
110
+
94
111
  const config = {
95
112
  api_key: apiKeyResponse.key,
96
113
  org_id: apiKeyResponse.org_id,
114
+ org_name: orgName,
97
115
  user_id: userId,
98
116
  environment: env,
99
117
  };
package/src/sdk/client.ts CHANGED
@@ -53,6 +53,7 @@ export class IndemnClient {
53
53
  return {
54
54
  api_key: apiKey,
55
55
  org_id: store.get('org_id', ''),
56
+ org_name: store.get('org_name', '') || undefined,
56
57
  user_id: store.get('user_id', ''),
57
58
  environment: store.get('environment', 'dev'),
58
59
  hosts: store.get('hosts', DEFAULT_HOSTS),
package/src/sdk/types.ts CHANGED
@@ -11,6 +11,7 @@ export interface EnvironmentHosts {
11
11
  export interface IndemnConfig {
12
12
  api_key: string;
13
13
  org_id: string;
14
+ org_name?: string;
14
15
  user_id: string;
15
16
  environment: Environment;
16
17
  hosts: Record<Environment, EnvironmentHosts>;
@@ -102,6 +103,8 @@ export interface BotFunction {
102
103
  headers?: Record<string, string>;
103
104
  body?: Record<string, unknown>;
104
105
  parameters?: FunctionParameter[];
106
+ configurationSchema?: Record<string, unknown>;
107
+ executionSchema?: Record<string, unknown>;
105
108
  id_bot?: string;
106
109
  createdAt?: string;
107
110
  updatedAt?: string;
@@ -119,6 +122,10 @@ export interface CreateFunctionInput {
119
122
  type: string;
120
123
  description?: string;
121
124
  name?: string;
125
+ // Server stores configurationSchema and converts to executionSchema in the update path.
126
+ // Note: url/method/headers/body at the root are unused legacy fields — actual REST API
127
+ // config lives inside configurationSchema. See AI-369.
128
+ configurationSchema?: Record<string, unknown>;
122
129
  url?: string;
123
130
  method?: string;
124
131
  headers?: Record<string, string>;
@@ -126,8 +133,10 @@ export interface CreateFunctionInput {
126
133
  }
127
134
 
128
135
  export interface UpdateFunctionInput {
136
+ type?: string;
129
137
  description?: string;
130
138
  name?: string;
139
+ configurationSchema?: Record<string, unknown>;
131
140
  url?: string;
132
141
  method?: string;
133
142
  headers?: Record<string, string>;