@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.
Files changed (311) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +9 -4
  3. package/.github/workflows/ci.yml +10 -0
  4. package/.github/workflows/publish.yml +2 -2
  5. package/.mcp.json +1 -4
  6. package/CONTRIBUTING.md +3 -1
  7. package/README.md +69 -176
  8. package/dist/cli/agents.d.ts.map +1 -1
  9. package/dist/cli/agents.js +226 -18
  10. package/dist/cli/agents.js.map +1 -1
  11. package/dist/cli/analytics.d.ts +3 -0
  12. package/dist/cli/analytics.d.ts.map +1 -0
  13. package/dist/cli/analytics.js +133 -0
  14. package/dist/cli/analytics.js.map +1 -0
  15. package/dist/cli/auth.d.ts.map +1 -1
  16. package/dist/cli/auth.js +62 -17
  17. package/dist/cli/auth.js.map +1 -1
  18. package/dist/cli/billing.d.ts +3 -0
  19. package/dist/cli/billing.d.ts.map +1 -0
  20. package/dist/cli/billing.js +23 -0
  21. package/dist/cli/billing.js.map +1 -0
  22. package/dist/cli/campaigns.d.ts +3 -0
  23. package/dist/cli/campaigns.d.ts.map +1 -0
  24. package/dist/cli/campaigns.js +34 -0
  25. package/dist/cli/campaigns.js.map +1 -0
  26. package/dist/cli/chat.d.ts.map +1 -1
  27. package/dist/cli/chat.js +69 -102
  28. package/dist/cli/chat.js.map +1 -1
  29. package/dist/cli/config.d.ts.map +1 -1
  30. package/dist/cli/config.js +71 -27
  31. package/dist/cli/config.js.map +1 -1
  32. package/dist/cli/conversations.d.ts +3 -0
  33. package/dist/cli/conversations.d.ts.map +1 -0
  34. package/dist/cli/conversations.js +129 -0
  35. package/dist/cli/conversations.js.map +1 -0
  36. package/dist/cli/distribution.d.ts +3 -0
  37. package/dist/cli/distribution.d.ts.map +1 -0
  38. package/dist/cli/distribution.js +122 -0
  39. package/dist/cli/distribution.js.map +1 -0
  40. package/dist/cli/eval.d.ts.map +1 -1
  41. package/dist/cli/eval.js +315 -71
  42. package/dist/cli/eval.js.map +1 -1
  43. package/dist/cli/functions.d.ts.map +1 -1
  44. package/dist/cli/functions.js +125 -35
  45. package/dist/cli/functions.js.map +1 -1
  46. package/dist/cli/index.js +63 -54
  47. package/dist/cli/index.js.map +1 -1
  48. package/dist/cli/init.d.ts +27 -0
  49. package/dist/cli/init.d.ts.map +1 -0
  50. package/dist/cli/init.js +152 -0
  51. package/dist/cli/init.js.map +1 -0
  52. package/dist/cli/kb.d.ts.map +1 -1
  53. package/dist/cli/kb.js +141 -15
  54. package/dist/cli/kb.js.map +1 -1
  55. package/dist/cli/link.d.ts +21 -0
  56. package/dist/cli/link.d.ts.map +1 -0
  57. package/dist/cli/link.js +112 -0
  58. package/dist/cli/link.js.map +1 -0
  59. package/dist/cli/org.d.ts.map +1 -1
  60. package/dist/cli/org.js +79 -67
  61. package/dist/cli/org.js.map +1 -1
  62. package/dist/cli/platform.d.ts +3 -0
  63. package/dist/cli/platform.d.ts.map +1 -0
  64. package/dist/cli/platform.js +133 -0
  65. package/dist/cli/platform.js.map +1 -0
  66. package/dist/cli/tickets.d.ts +3 -0
  67. package/dist/cli/tickets.d.ts.map +1 -0
  68. package/dist/cli/tickets.js +125 -0
  69. package/dist/cli/tickets.js.map +1 -0
  70. package/dist/cli/utils/output.d.ts +15 -9
  71. package/dist/cli/utils/output.d.ts.map +1 -1
  72. package/dist/cli/utils/output.js +53 -46
  73. package/dist/cli/utils/output.js.map +1 -1
  74. package/dist/cli/voice.d.ts +3 -0
  75. package/dist/cli/voice.d.ts.map +1 -0
  76. package/dist/cli/voice.js +206 -0
  77. package/dist/cli/voice.js.map +1 -0
  78. package/dist/core/diff.d.ts +16 -0
  79. package/dist/core/diff.d.ts.map +1 -0
  80. package/dist/core/diff.js +57 -0
  81. package/dist/core/diff.js.map +1 -0
  82. package/dist/core/org-resolver.d.ts +37 -0
  83. package/dist/core/org-resolver.d.ts.map +1 -0
  84. package/dist/core/org-resolver.js +73 -0
  85. package/dist/core/org-resolver.js.map +1 -0
  86. package/dist/core/runtime.d.ts +36 -0
  87. package/dist/core/runtime.d.ts.map +1 -0
  88. package/dist/core/runtime.js +62 -0
  89. package/dist/core/runtime.js.map +1 -0
  90. package/dist/core/scope.d.ts +33 -0
  91. package/dist/core/scope.d.ts.map +1 -0
  92. package/dist/core/scope.js +37 -0
  93. package/dist/core/scope.js.map +1 -0
  94. package/dist/mcp/registry.d.ts +67 -0
  95. package/dist/mcp/registry.d.ts.map +1 -0
  96. package/dist/mcp/registry.js +195 -0
  97. package/dist/mcp/registry.js.map +1 -0
  98. package/dist/mcp/server.d.ts +9 -0
  99. package/dist/mcp/server.d.ts.map +1 -1
  100. package/dist/mcp/server.js +87 -2
  101. package/dist/mcp/server.js.map +1 -1
  102. package/dist/mcp/tool-meta.d.ts +21 -0
  103. package/dist/mcp/tool-meta.d.ts.map +1 -0
  104. package/dist/mcp/tool-meta.js +203 -0
  105. package/dist/mcp/tool-meta.js.map +1 -0
  106. package/dist/mcp/tools.d.ts +2 -1
  107. package/dist/mcp/tools.d.ts.map +1 -1
  108. package/dist/mcp/tools.js +1462 -215
  109. package/dist/mcp/tools.js.map +1 -1
  110. package/dist/sdk/agent-bundle.d.ts +99 -0
  111. package/dist/sdk/agent-bundle.d.ts.map +1 -0
  112. package/dist/sdk/agent-bundle.js +211 -0
  113. package/dist/sdk/agent-bundle.js.map +1 -0
  114. package/dist/sdk/agents.d.ts +54 -1
  115. package/dist/sdk/agents.d.ts.map +1 -1
  116. package/dist/sdk/agents.js +59 -5
  117. package/dist/sdk/agents.js.map +1 -1
  118. package/dist/sdk/analytics.d.ts +158 -0
  119. package/dist/sdk/analytics.d.ts.map +1 -0
  120. package/dist/sdk/analytics.js +185 -0
  121. package/dist/sdk/analytics.js.map +1 -0
  122. package/dist/sdk/auth.d.ts +34 -12
  123. package/dist/sdk/auth.d.ts.map +1 -1
  124. package/dist/sdk/auth.js +60 -77
  125. package/dist/sdk/auth.js.map +1 -1
  126. package/dist/sdk/billing.d.ts +25 -0
  127. package/dist/sdk/billing.d.ts.map +1 -0
  128. package/dist/sdk/billing.js +61 -0
  129. package/dist/sdk/billing.js.map +1 -0
  130. package/dist/sdk/campaigns.d.ts +35 -0
  131. package/dist/sdk/campaigns.d.ts.map +1 -0
  132. package/dist/sdk/campaigns.js +59 -0
  133. package/dist/sdk/campaigns.js.map +1 -0
  134. package/dist/sdk/chat.d.ts +25 -2
  135. package/dist/sdk/chat.d.ts.map +1 -1
  136. package/dist/sdk/chat.js +37 -10
  137. package/dist/sdk/chat.js.map +1 -1
  138. package/dist/sdk/client.d.ts +93 -5
  139. package/dist/sdk/client.d.ts.map +1 -1
  140. package/dist/sdk/client.js +362 -100
  141. package/dist/sdk/client.js.map +1 -1
  142. package/dist/sdk/config-change.d.ts +74 -0
  143. package/dist/sdk/config-change.d.ts.map +1 -0
  144. package/dist/sdk/config-change.js +117 -0
  145. package/dist/sdk/config-change.js.map +1 -0
  146. package/dist/sdk/config.d.ts.map +1 -1
  147. package/dist/sdk/config.js +2 -1
  148. package/dist/sdk/config.js.map +1 -1
  149. package/dist/sdk/conversations.d.ts +101 -0
  150. package/dist/sdk/conversations.d.ts.map +1 -0
  151. package/dist/sdk/conversations.js +155 -0
  152. package/dist/sdk/conversations.js.map +1 -0
  153. package/dist/sdk/credentials.d.ts +25 -0
  154. package/dist/sdk/credentials.d.ts.map +1 -0
  155. package/dist/sdk/credentials.js +44 -0
  156. package/dist/sdk/credentials.js.map +1 -0
  157. package/dist/sdk/distribution.d.ts +94 -0
  158. package/dist/sdk/distribution.d.ts.map +1 -0
  159. package/dist/sdk/distribution.js +100 -0
  160. package/dist/sdk/distribution.js.map +1 -0
  161. package/dist/sdk/eval.d.ts +81 -0
  162. package/dist/sdk/eval.d.ts.map +1 -1
  163. package/dist/sdk/eval.js +117 -0
  164. package/dist/sdk/eval.js.map +1 -1
  165. package/dist/sdk/functions.d.ts +14 -1
  166. package/dist/sdk/functions.d.ts.map +1 -1
  167. package/dist/sdk/functions.js +48 -5
  168. package/dist/sdk/functions.js.map +1 -1
  169. package/dist/sdk/index.d.ts +2 -0
  170. package/dist/sdk/index.d.ts.map +1 -1
  171. package/dist/sdk/index.js +2 -0
  172. package/dist/sdk/index.js.map +1 -1
  173. package/dist/sdk/jarvis-chat.d.ts +28 -0
  174. package/dist/sdk/jarvis-chat.d.ts.map +1 -0
  175. package/dist/sdk/jarvis-chat.js +117 -0
  176. package/dist/sdk/jarvis-chat.js.map +1 -0
  177. package/dist/sdk/kb.d.ts +69 -1
  178. package/dist/sdk/kb.d.ts.map +1 -1
  179. package/dist/sdk/kb.js +111 -24
  180. package/dist/sdk/kb.js.map +1 -1
  181. package/dist/sdk/orgs.d.ts +13 -0
  182. package/dist/sdk/orgs.d.ts.map +1 -0
  183. package/dist/sdk/orgs.js +59 -0
  184. package/dist/sdk/orgs.js.map +1 -0
  185. package/dist/sdk/platform.d.ts +62 -0
  186. package/dist/sdk/platform.d.ts.map +1 -0
  187. package/dist/sdk/platform.js +95 -0
  188. package/dist/sdk/platform.js.map +1 -0
  189. package/dist/sdk/session.d.ts +25 -0
  190. package/dist/sdk/session.d.ts.map +1 -0
  191. package/dist/sdk/session.js +69 -0
  192. package/dist/sdk/session.js.map +1 -0
  193. package/dist/sdk/skills.d.ts +46 -0
  194. package/dist/sdk/skills.d.ts.map +1 -0
  195. package/dist/sdk/skills.js +203 -0
  196. package/dist/sdk/skills.js.map +1 -0
  197. package/dist/sdk/test-chat.d.ts +52 -0
  198. package/dist/sdk/test-chat.d.ts.map +1 -0
  199. package/dist/sdk/test-chat.js +134 -0
  200. package/dist/sdk/test-chat.js.map +1 -0
  201. package/dist/sdk/tickets.d.ts +98 -0
  202. package/dist/sdk/tickets.d.ts.map +1 -0
  203. package/dist/sdk/tickets.js +91 -0
  204. package/dist/sdk/tickets.js.map +1 -0
  205. package/dist/sdk/types.d.ts +91 -6
  206. package/dist/sdk/types.d.ts.map +1 -1
  207. package/dist/sdk/voice.d.ts +147 -0
  208. package/dist/sdk/voice.d.ts.map +1 -0
  209. package/dist/sdk/voice.js +236 -0
  210. package/dist/sdk/voice.js.map +1 -0
  211. package/package.json +22 -3
  212. package/scripts/check-skills.mjs +25 -0
  213. package/skills/agent-builder/SKILL.md +89 -252
  214. package/skills/evaluations/SKILL.md +23 -234
  215. package/skills/evaluations/references/analysis.md +95 -0
  216. package/skills/evaluations/references/cli-reference.md +28 -0
  217. package/skills/evaluations/references/json-formats.md +100 -0
  218. package/src/cli/agents.ts +202 -22
  219. package/src/cli/analytics.ts +138 -0
  220. package/src/cli/auth.ts +56 -18
  221. package/src/cli/billing.ts +25 -0
  222. package/src/cli/campaigns.ts +33 -0
  223. package/src/cli/chat.ts +66 -115
  224. package/src/cli/config.ts +65 -31
  225. package/src/cli/conversations.ts +133 -0
  226. package/src/cli/distribution.ts +116 -0
  227. package/src/cli/eval.ts +254 -70
  228. package/src/cli/functions.ts +122 -36
  229. package/src/cli/index.ts +58 -55
  230. package/src/cli/init.ts +154 -0
  231. package/src/cli/kb.ts +120 -17
  232. package/src/cli/link.ts +117 -0
  233. package/src/cli/org.ts +81 -75
  234. package/src/cli/platform.ts +128 -0
  235. package/src/cli/tickets.ts +130 -0
  236. package/src/cli/utils/output.ts +51 -42
  237. package/src/cli/voice.ts +204 -0
  238. package/src/core/diff.ts +52 -0
  239. package/src/core/org-resolver.ts +113 -0
  240. package/src/core/runtime.ts +73 -0
  241. package/src/core/scope.ts +64 -0
  242. package/src/mcp/registry.ts +236 -0
  243. package/src/mcp/server.ts +84 -2
  244. package/src/mcp/tool-meta.ts +241 -0
  245. package/src/mcp/tools.ts +1529 -265
  246. package/src/sdk/agent-bundle.ts +245 -0
  247. package/src/sdk/agents.ts +74 -5
  248. package/src/sdk/analytics.ts +220 -0
  249. package/src/sdk/auth.ts +87 -107
  250. package/src/sdk/billing.ts +59 -0
  251. package/src/sdk/campaigns.ts +60 -0
  252. package/src/sdk/chat.ts +44 -10
  253. package/src/sdk/client.ts +373 -102
  254. package/src/sdk/config-change.ts +148 -0
  255. package/src/sdk/config.ts +2 -1
  256. package/src/sdk/conversations.ts +199 -0
  257. package/src/sdk/credentials.ts +53 -0
  258. package/src/sdk/distribution.ts +132 -0
  259. package/src/sdk/eval.ts +140 -0
  260. package/src/sdk/functions.ts +52 -6
  261. package/src/sdk/index.ts +2 -0
  262. package/src/sdk/jarvis-chat.ts +112 -0
  263. package/src/sdk/kb.ts +116 -29
  264. package/src/sdk/orgs.ts +62 -0
  265. package/src/sdk/platform.ts +115 -0
  266. package/src/sdk/session.ts +90 -0
  267. package/src/sdk/skills.ts +188 -0
  268. package/src/sdk/test-chat.ts +151 -0
  269. package/src/sdk/tickets.ts +120 -0
  270. package/src/sdk/types.ts +88 -6
  271. package/src/sdk/voice.ts +262 -0
  272. package/test/cli/entry.test.ts +34 -0
  273. package/test/cli/init.test.ts +66 -0
  274. package/test/cli/link.test.ts +57 -0
  275. package/test/core/org-resolver.test.ts +92 -0
  276. package/test/core/runtime.test.ts +30 -0
  277. package/test/core/scope.test.ts +93 -0
  278. package/test/harness.test.ts +24 -0
  279. package/test/helpers/config-home.ts +22 -0
  280. package/test/helpers/fake-middleware.ts +56 -0
  281. package/test/helpers/fetch-recorder.ts +75 -0
  282. package/test/helpers/jwt.ts +9 -0
  283. package/test/helpers/mcp-pair.ts +38 -0
  284. package/test/mcp/__snapshots__/tools-snapshot.test.ts.snap +145 -0
  285. package/test/mcp/askers.test.ts +60 -0
  286. package/test/mcp/elicit-timeout.test.ts +29 -0
  287. package/test/mcp/registry.test.ts +169 -0
  288. package/test/mcp/shaping.test.ts +41 -0
  289. package/test/mcp/tools-snapshot.test.ts +42 -0
  290. package/test/sdk/agents.test.ts +232 -0
  291. package/test/sdk/analytics.test.ts +89 -0
  292. package/test/sdk/auth.test.ts +70 -0
  293. package/test/sdk/billing.test.ts +47 -0
  294. package/test/sdk/campaigns.test.ts +43 -0
  295. package/test/sdk/client.test.ts +80 -0
  296. package/test/sdk/config-change.test.ts +142 -0
  297. package/test/sdk/conversations.test.ts +80 -0
  298. package/test/sdk/credentials.test.ts +33 -0
  299. package/test/sdk/distribution.test.ts +55 -0
  300. package/test/sdk/eval.test.ts +155 -0
  301. package/test/sdk/kb-upload.test.ts +50 -0
  302. package/test/sdk/kb.test.ts +95 -0
  303. package/test/sdk/orgs.test.ts +48 -0
  304. package/test/sdk/platform.test.ts +89 -0
  305. package/test/sdk/renewal.test.ts +115 -0
  306. package/test/sdk/session.test.ts +60 -0
  307. package/test/sdk/skills.test.ts +235 -0
  308. package/test/sdk/test-chat.test.ts +112 -0
  309. package/test/sdk/tickets.test.ts +42 -0
  310. package/test/sdk/voice.test.ts +147 -0
  311. package/vitest.config.ts +11 -0
package/src/cli/agents.ts CHANGED
@@ -1,7 +1,17 @@
1
1
  import { Command } from 'commander';
2
2
  import ora from 'ora';
3
3
  import { IndemnClient } from '../sdk/client.js';
4
- import { AgentsSDK } from '../sdk/agents.js';
4
+ import { AgentInsightsSDK, AgentsSDK, type Lifecycle } from '../sdk/agents.js';
5
+ import { CliError, EXIT } from '../core/runtime.js';
6
+ import { confirmOrExit } from './link.js';
7
+ import { DistributionSDK } from '../sdk/distribution.js';
8
+ import { readFileSync, writeFileSync } from 'node:fs';
9
+ import chalk from 'chalk';
10
+ import { isNonInteractive } from '../core/runtime.js';
11
+ import { askLine } from './utils/output.js';
12
+ import {
13
+ createAgentFromSkeleton, exportAgent, importAgent, missingSkeletonFields, parseModel, type AgentBundle, type AgentSkeleton,
14
+ } from '../sdk/agent-bundle.js';
5
15
  import { exportAgentCard } from '../report/export.js';
6
16
  import { formatTable, printJson, printError, printSuccess, confirm } from './utils/output.js';
7
17
 
@@ -98,22 +108,108 @@ export function registerAgentCommands(program: Command): void {
98
108
 
99
109
  agents
100
110
  .command('create')
101
- .description('Create a new agent')
102
- .requiredOption('--name <name>', 'Agent name')
103
- .option('--channels <channels>', 'Comma-separated channels', 'WEB')
111
+ .description('Create an agent with its basics: name, channels, purpose, prompt and model. Asks for missing ones in a terminal.')
112
+ .option('--name <name>', 'Agent name')
113
+ .option('--channels <channels>', 'Comma-separated channels (see `indemn channels list`)', 'WEB')
114
+ .option('--description <text>', 'What the agent is for')
115
+ .option('--prompt <text>', 'System prompt')
116
+ .option('--prompt-file <path>', 'Read the system prompt from a file')
117
+ .option('--model <provider/model>', 'LLM, for example openai/gpt-4.1 (see `indemn models`)')
118
+ .option('--allow-duplicate', 'Create even if an agent with this name exists in the org')
119
+ .option('--template <id>', 'Copy a template instead (see `indemn templates list`); keeps its prompt, model, skills and knowledge')
120
+ .option('--json', 'Output as JSON')
104
121
  .action(async (options) => {
105
122
  try {
123
+ if (options.template) {
124
+ if (!options.name) throw new CliError('Name the new agent: --name "<name>".', EXIT.NEEDS_INPUT);
125
+ const client = new IndemnClient();
126
+ if (!options.allowDuplicate && (await new AgentsSDK(client).findByName(options.name)).length) {
127
+ throw new CliError(`An agent named "${options.name}" already exists in this org. Choose another name or pass --allow-duplicate.`, EXIT.NEEDS_INPUT);
128
+ }
129
+ const res = await new DistributionSDK(client).createFromTemplate(options.template, {
130
+ name: options.name, channels: String(options.channels).split(',').map((c: string) => c.trim()).filter(Boolean),
131
+ });
132
+ if (options.json) { printJson(res); return; }
133
+ printSuccess(`Agent created from template: ${res.new_agent_id} (${res.new_agent_name})`);
134
+ console.log(chalk.dim(' Not copied: phone numbers, voice settings, website distributions.'));
135
+ return;
136
+ }
137
+ const skeleton: AgentSkeleton = {
138
+ name: options.name ?? '',
139
+ channels: String(options.channels).split(',').map((c: string) => c.trim()).filter(Boolean),
140
+ description: options.description,
141
+ system_prompt: options.promptFile ? readFileSync(options.promptFile, 'utf8') : options.prompt,
142
+ model: options.model ? parseModel(options.model) : undefined,
143
+ };
144
+ const missing = missingSkeletonFields(skeleton);
145
+ if (missing.length && !isNonInteractive()) {
146
+ if (missing.includes('name')) skeleton.name = await askLine('Agent name: ');
147
+ if (missing.includes('description')) skeleton.description = await askLine('What is this agent for? (one line, Enter to skip): ');
148
+ if (missing.includes('system_prompt')) skeleton.system_prompt = await askLine('System prompt (Enter to skip; use --prompt-file for long prompts): ');
149
+ }
150
+ if (!skeleton.name.trim()) throw new CliError('An agent needs a name: --name "<name>".', EXIT.NEEDS_INPUT);
151
+
106
152
  const client = new IndemnClient();
107
- const sdk = new AgentsSDK(client);
108
153
  const spinner = ora('Creating agent...').start();
109
-
110
- const channels = options.channels.split(',').map((c: string) => c.trim());
111
- const agent = await sdk.createAgent({ name: options.name, channels });
154
+ const { agent, set } = await createAgentFromSkeleton(client, skeleton, { allowDuplicate: options.allowDuplicate });
112
155
  spinner.stop();
113
-
156
+ if (options.json) { printJson({ agent, set }); return; }
114
157
  printSuccess(`Agent created: ${agent._id}`);
115
158
  console.log(` Name: ${agent.name}`);
116
159
  console.log(` Channels: ${formatChannels(agent.channels || [])}`);
160
+ console.log(` Set: ${set.join(', ')}`);
161
+ const unset = missingSkeletonFields(skeleton);
162
+ if (unset.length) console.log(chalk.dim(` Not set yet: ${unset.join(', ')}. Use \`indemn config set ${agent._id}\`.`));
163
+ } catch (err) {
164
+ printError(err);
165
+ process.exit(1);
166
+ }
167
+ });
168
+
169
+ agents
170
+ .command('export')
171
+ .argument('<id>', 'Agent ID')
172
+ .option('-o, --output <file>', 'Write to a file instead of stdout')
173
+ .option('--include-secrets', 'Keep API keys and tokens inside skill settings (default: redacted)')
174
+ .description('Save an agent (prompt, greeting, model, skills, linked knowledge bases) to a JSON bundle')
175
+ .action(async (id: string, options) => {
176
+ try {
177
+ const bundle = await exportAgent(new IndemnClient(), id, { includeSecrets: options.includeSecrets });
178
+ const text = JSON.stringify(bundle, null, 2) + '\n';
179
+ if (options.output) {
180
+ writeFileSync(options.output, text, { mode: options.includeSecrets ? 0o600 : 0o644 });
181
+ printSuccess(`Wrote ${options.output}.`);
182
+ if (bundle.redacted.length) console.log(chalk.dim(` Redacted ${bundle.redacted.length} secret value(s); fill them in after import.`));
183
+ } else {
184
+ process.stdout.write(text);
185
+ }
186
+ } catch (err) {
187
+ printError(err);
188
+ process.exit(1);
189
+ }
190
+ });
191
+
192
+ agents
193
+ .command('import')
194
+ .argument('<file>', 'Bundle written by `indemn agents export`')
195
+ .option('--name <name>', 'Name for the new agent (default: the bundle name)')
196
+ .option('--link-kbs', 'Link the same knowledge base ids if they exist in this org')
197
+ .option('--allow-duplicate', 'Create even if an agent with this name exists in the org')
198
+ .option('--yes', 'Do not ask for confirmation')
199
+ .option('--json', 'Output as JSON')
200
+ .description('Create a new agent in the chosen org from an exported bundle')
201
+ .action(async (file: string, options) => {
202
+ try {
203
+ const bundle = JSON.parse(readFileSync(file, 'utf8')) as AgentBundle;
204
+ const client = new IndemnClient();
205
+ const org = await client.resolveOrg();
206
+ await confirmOrExit(`Create agent "${options.name ?? bundle.agent?.name}" with ${bundle.skills?.length ?? 0} skill(s) in ${org.name ?? org.id} on ${client.getEnvironment()}?`, Boolean(options.yes));
207
+ const report = await importAgent(client, bundle, { name: options.name, allowDuplicate: options.allowDuplicate, linkKnowledgeBases: options.linkKbs });
208
+ if (options.json) { printJson(report); return; }
209
+ printSuccess(`Agent created: ${report.agent._id} (${report.skills_created} skill(s) created, ${report.built_in_skills_reused.length} built-in reused)`);
210
+ for (const f of report.skills_failed) console.log(chalk.yellow(` Skill ${f.name ?? '(unnamed)'} failed: ${f.error}`));
211
+ if (report.needs_secrets.length) console.log(chalk.yellow(` Fill in ${report.needs_secrets.length} redacted secret(s) in the skills.`));
212
+ if (report.knowledge_bases_skipped.length) console.log(chalk.dim(` Knowledge bases not linked: ${report.knowledge_bases_skipped.join(', ')}`));
117
213
  } catch (err) {
118
214
  printError(err);
119
215
  process.exit(1);
@@ -156,22 +252,18 @@ export function registerAgentCommands(program: Command): void {
156
252
  agents
157
253
  .command('delete')
158
254
  .argument('<id>', 'Agent ID')
159
- .description('Delete an agent')
160
- .action(async (id: string) => {
255
+ .option('--yes', 'Do not ask for confirmation')
256
+ .description('Delete an agent (asks first; --yes for scripts)')
257
+ .action(async (id: string, options) => {
161
258
  try {
162
- const confirmed = await confirm(`Delete agent ${id}? This cannot be undone.`);
163
- if (!confirmed) {
164
- console.log('Cancelled.');
165
- return;
166
- }
167
-
168
259
  const client = new IndemnClient();
169
260
  const sdk = new AgentsSDK(client);
261
+ const agent = await sdk.getAgent(id);
262
+ const org = await client.resolveOrg();
263
+ await confirmOrExit(`Delete agent "${agent?.name ?? id}" (${id}) in org ${org.name ?? org.id} on ${client.getEnvironment()}? This cannot be undone.`, Boolean(options.yes));
170
264
  const spinner = ora('Deleting agent...').start();
171
-
172
265
  await sdk.deleteAgent(id);
173
266
  spinner.stop();
174
-
175
267
  printSuccess(`Agent ${id} deleted.`);
176
268
  } catch (err) {
177
269
  printError(err);
@@ -179,6 +271,93 @@ export function registerAgentCommands(program: Command): void {
179
271
  }
180
272
  });
181
273
 
274
+ agents
275
+ .command('lifecycle')
276
+ .argument('<id>', 'Agent ID')
277
+ .argument('<stage>', 'prototype, beta or published')
278
+ .option('--yes', 'Do not ask for confirmation')
279
+ .description('Change the lifecycle stage (only the owner or an org admin may; checked by the V2 platform)')
280
+ .action(async (id: string, stage: string, options) => {
281
+ try {
282
+ if (!['prototype', 'beta', 'published'].includes(stage)) {
283
+ throw new CliError('Stage must be prototype, beta or published.', EXIT.NEEDS_INPUT);
284
+ }
285
+ const client = new IndemnClient();
286
+ const insights = new AgentInsightsSDK(client);
287
+ const before = await insights.detail(id);
288
+ if (before.lifecycle === stage) {
289
+ console.log(`Agent ${id} is already ${stage}.`);
290
+ return;
291
+ }
292
+ await confirmOrExit(`Move "${before.name}" from ${before.lifecycle ?? 'prototype'} to ${stage}?`, Boolean(options.yes));
293
+ const after = await insights.setLifecycle(id, stage as Lifecycle);
294
+ printSuccess(`Agent ${id} is now ${after.lifecycle}.`);
295
+ } catch (err) {
296
+ printError(err);
297
+ process.exit(1);
298
+ }
299
+ });
300
+
301
+ agents
302
+ .command('owner')
303
+ .argument('<id>', 'Agent ID')
304
+ .argument('[user-id]', 'New owner (user id); omit to show the current owner')
305
+ .option('--yes', 'Do not ask for confirmation')
306
+ .option('--json', 'Output as JSON')
307
+ .description('Show or transfer the owner (transfer: owner or org admin only)')
308
+ .action(async (id: string, userId: string | undefined, options) => {
309
+ try {
310
+ const client = new IndemnClient();
311
+ const insights = new AgentInsightsSDK(client);
312
+ const detail = await insights.detail(id);
313
+ if (!userId) {
314
+ if (options.json) { printJson({ owner: detail.owner ?? null, lifecycle: detail.lifecycle }); return; }
315
+ const o = detail.owner;
316
+ console.log(o ? `${[o.firstname, o.lastname].filter(Boolean).join(' ')} <${o.email ?? ''}> (${o.id})` : 'No owner recorded.');
317
+ return;
318
+ }
319
+ await confirmOrExit(`Transfer "${detail.name}" to user ${userId}?`, Boolean(options.yes));
320
+ await insights.transferOwner(id, userId);
321
+ printSuccess(`Owner of ${id} is now ${userId}.`);
322
+ } catch (err) {
323
+ printError(err);
324
+ process.exit(1);
325
+ }
326
+ });
327
+
328
+ agents
329
+ .command('summary')
330
+ .argument('<id>', 'Agent ID')
331
+ .option('--full', 'Skills and knowledge sources instead of one paragraph')
332
+ .option('--json', 'Output as JSON')
333
+ .description('What the agent does, written for a person or an AI to read before changing it')
334
+ .action(async (id: string, options) => {
335
+ try {
336
+ const result = await new AgentInsightsSDK(new IndemnClient()).summary(id, { text: !options.full });
337
+ if (options.json || options.full) { printJson(result); return; }
338
+ console.log((result as { summary?: string }).summary ?? '');
339
+ } catch (err) {
340
+ printError(err);
341
+ process.exit(1);
342
+ }
343
+ });
344
+
345
+ agents
346
+ .command('stats')
347
+ .argument('<id>', 'Agent ID')
348
+ .option('--from <date>', 'YYYY-MM-DD (default: 30 days ago)')
349
+ .option('--to <date>', 'YYYY-MM-DD (default: today)')
350
+ .option('--channel <channel>', 'web, voice or all', 'all')
351
+ .description('Conversation metrics for one agent (test conversations excluded)')
352
+ .action(async (id: string, options) => {
353
+ try {
354
+ printJson(await new AgentInsightsSDK(new IndemnClient()).stats(id, { from: options.from, to: options.to, channel: options.channel }));
355
+ } catch (err) {
356
+ printError(err);
357
+ process.exit(1);
358
+ }
359
+ });
360
+
182
361
  agents
183
362
  .command('card')
184
363
  .argument('<id>', 'Agent ID')
@@ -203,14 +382,15 @@ export function registerAgentCommands(program: Command): void {
203
382
  agents
204
383
  .command('clone')
205
384
  .argument('<id>', 'Agent ID to clone')
206
- .description('Clone an agent')
207
- .action(async (id: string) => {
385
+ .option('--name <name>', 'Name of the copy')
386
+ .description('Copy an agent of this org into a new agent in the same org')
387
+ .action(async (id: string, options) => {
208
388
  try {
209
389
  const client = new IndemnClient();
210
390
  const sdk = new AgentsSDK(client);
211
391
  const spinner = ora('Cloning agent...').start();
212
392
 
213
- const result = await sdk.cloneAgent(id);
393
+ const result = await sdk.cloneAgent(id, { name: options.name });
214
394
  spinner.stop();
215
395
 
216
396
  // Clone returns { new_agent_id, new_agent_name } or an Agent object
@@ -0,0 +1,138 @@
1
+ import { Command } from 'commander';
2
+ import { readFileSync } from 'node:fs';
3
+ import { IndemnClient } from '../sdk/client.js';
4
+ import { AnalyticsSDK, AuditSDK, OrgSettingsSDK, type MetricKind } from '../sdk/analytics.js';
5
+ import { CliError, EXIT } from '../core/runtime.js';
6
+ import { confirmOrExit } from './link.js';
7
+ import { formatTable, printError, printJson, printSuccess } from './utils/output.js';
8
+
9
+ function run(fn: () => Promise<void>): Promise<void> {
10
+ return fn().catch((err) => {
11
+ printError(err);
12
+ process.exit(1);
13
+ });
14
+ }
15
+
16
+ function withRange(cmd: Command, agent = true): Command {
17
+ cmd.option('--from <date>', 'YYYY-MM-DD (default: 30 days ago)').option('--to <date>', 'YYYY-MM-DD (default: today)');
18
+ if (agent) cmd.option('--agent <id>', 'Only this agent').option('--channel <channel>', 'web, voice or all', 'all');
19
+ return cmd;
20
+ }
21
+
22
+ const KINDS: Record<string, MetricKind> = {
23
+ metrics: 'metrics',
24
+ performance: 'model-performance',
25
+ 'performance-by-agent': 'model-performance/bots',
26
+ volume: 'agent-volume',
27
+ handoffs: 'handoff-precursors',
28
+ feedback: 'critical-feedback',
29
+ };
30
+
31
+ export function registerAnalyticsCommands(program: Command): void {
32
+ const analytics = program.command('analytics').description('The numbers of the dashboard analytics pages (test conversations excluded)');
33
+
34
+ for (const [name, kind] of Object.entries(KINDS)) {
35
+ const perAgent = !['performance-by-agent', 'volume'].includes(name);
36
+ withRange(analytics.command(name).description(`Analytics: ${kind}`), perAgent)
37
+ .action((options) => run(async () => {
38
+ printJson(await new AnalyticsSDK(new IndemnClient()).get(kind, { from: options.from, to: options.to, channel: options.channel, agentId: options.agent }));
39
+ }));
40
+ }
41
+
42
+ withRange(analytics.command('conversations').description('The conversations behind a number, e.g. --dimension outcome --value handoff'))
43
+ .option('--dimension <name>', 'outcome, feedback, sentiment, precursor, reason, critical_feedback_theme or attribute:<key>')
44
+ .option('--value <value>', 'Value of the dimension')
45
+ .option('--search <text>', 'Text search')
46
+ .option('--page <n>', 'Page, starting at 1', '1')
47
+ .option('--page-size <n>', 'Rows per page (at most 200)', '50')
48
+ .action((options) => run(async () => {
49
+ printJson(await new AnalyticsSDK(new IndemnClient()).conversations({
50
+ from: options.from, to: options.to, channel: options.channel, agentId: options.agent, dimension: options.dimension, value: options.value,
51
+ search: options.search, page: Number(options.page), pageSize: Number(options.pageSize),
52
+ }));
53
+ }));
54
+
55
+ analytics.command('charts').description('Custom charts configured for the org').action(() => run(async () => {
56
+ printJson(await new AnalyticsSDK(new IndemnClient()).charts());
57
+ }));
58
+
59
+ withRange(analytics.command('chart').argument('<chart-id>', 'Chart ID from `analytics charts`').description('Data of one custom chart'))
60
+ .action((id: string, options) => run(async () => {
61
+ printJson(await new AnalyticsSDK(new IndemnClient()).chart(id, { from: options.from, to: options.to, channel: options.channel, agentId: options.agent }));
62
+ }));
63
+
64
+ const org = program.commands.find((c) => c.name() === 'org');
65
+ if (!org) throw new Error('org command group must be registered before analytics commands');
66
+ const config = org.command('config').description('Org settings: timezone, business hours, holidays');
67
+
68
+ config.command('get').description('All org settings (handoff settings are read-only)').action(() => run(async () => {
69
+ printJson(await new OrgSettingsSDK(new IndemnClient()).get());
70
+ }));
71
+ config.command('availability').description('Is the org inside business hours now?').action(() => run(async () => {
72
+ printJson(await new OrgSettingsSDK(new IndemnClient()).availability());
73
+ }));
74
+ config.command('timezone')
75
+ .argument('<zone>', 'For example America/New_York')
76
+ .option('--yes', 'Do not ask for confirmation')
77
+ .action((zone: string, options) => run(async () => {
78
+ await confirmOrExit(`Set the org timezone to ${zone}? Business hours are read in this zone.`, Boolean(options.yes));
79
+ await new OrgSettingsSDK(new IndemnClient()).setTimezone(zone);
80
+ printSuccess(`Timezone set to ${zone}.`);
81
+ }));
82
+ config.command('business-hours')
83
+ .requiredOption('--file <path>', 'JSON: { "businessHoursEnabled": true, "businessHours": [{ "day": 1, "slots": [...] }], "businessHoursUtterance": "..." }')
84
+ .option('--yes', 'Do not ask for confirmation')
85
+ .description('Replace the business hours (days 0 Sunday to 6; empty slots close that day)')
86
+ .action((options) => run(async () => {
87
+ const hours = JSON.parse(readFileSync(options.file, 'utf8'));
88
+ await confirmOrExit('Replace the org business hours? Agents hand off only inside them.', Boolean(options.yes));
89
+ printJson(await new OrgSettingsSDK(new IndemnClient()).setBusinessHours(hours));
90
+ }));
91
+ const holidays = config.command('holidays').description('Days the org is closed');
92
+ holidays.command('add')
93
+ .requiredOption('--date <date>', 'YYYY-MM-DD')
94
+ .requiredOption('--name <name>', 'Holiday name')
95
+ .option('--message <text>', 'What agents say that day')
96
+ .option('--yes', 'Do not ask for confirmation')
97
+ .action((options) => run(async () => {
98
+ await confirmOrExit(`Close the org on ${options.date} (${options.name})?`, Boolean(options.yes));
99
+ printJson(await new OrgSettingsSDK(new IndemnClient()).addHolidays([{ name: options.name, date: options.date, ...(options.message ? { holidayFallbackUtterance: options.message } : {}) }]));
100
+ }));
101
+ holidays.command('remove')
102
+ .argument('<dates...>', 'YYYY-MM-DD dates')
103
+ .option('--yes', 'Do not ask for confirmation')
104
+ .action((dates: string[], options) => run(async () => {
105
+ await confirmOrExit(`Remove ${dates.length} holiday(s)?`, Boolean(options.yes));
106
+ printJson(await new OrgSettingsSDK(new IndemnClient()).removeHolidays(dates));
107
+ }));
108
+ config.command('admin')
109
+ .argument('<settings...>', 'key=value pairs, e.g. enableTickets=true')
110
+ .option('--yes', 'Do not ask for confirmation')
111
+ .description('Indemn-only feature switches (the server refuses non-Indemn logins)')
112
+ .action((pairs: string[], options) => run(async () => {
113
+ const settings: Record<string, unknown> = {};
114
+ for (const pair of pairs) {
115
+ const i = pair.indexOf('=');
116
+ if (i <= 0) throw new CliError(`"${pair}" is not key=value.`, EXIT.NEEDS_INPUT);
117
+ const raw = pair.slice(i + 1);
118
+ settings[pair.slice(0, i)] = raw === 'true' ? true : raw === 'false' ? false : raw === 'null' ? null : Number.isFinite(Number(raw)) && raw !== '' ? Number(raw) : raw;
119
+ }
120
+ await confirmOrExit(`Change ${Object.keys(settings).join(', ')} for the org?`, Boolean(options.yes));
121
+ printJson(await new OrgSettingsSDK(new IndemnClient()).setAdminSettings(settings));
122
+ }));
123
+
124
+ program.command('audit').description('Who changed what in the org').command('list')
125
+ .option('--since <date>', 'Only entries from this date')
126
+ .option('--action <name>', 'Only this action or event type')
127
+ .option('--resource <type>', 'Only this resource type')
128
+ .option('--user <id>', 'Only this user')
129
+ .option('--limit <n>', 'At most this many (newest first)', '50')
130
+ .option('--json', 'Output as JSON')
131
+ .action((options) => run(async () => {
132
+ const rows = await new AuditSDK(new IndemnClient()).list({ since: options.since, action: options.action, resourceType: options.resource, user: options.user, limit: Number(options.limit) });
133
+ if (options.json) { printJson(rows); return; }
134
+ console.log(formatTable(['When', 'Action', 'Resource', 'User', 'Status'], rows.map((e) => [
135
+ String(e.createdAt ?? ''), String(e.action ?? e.event_type ?? ''), `${e.resource_type ?? ''} ${e.resource_display ?? e.resource_id ?? ''}`.trim(), String(e.id_user ?? ''), String(e.status ?? ''),
136
+ ])));
137
+ }));
138
+ }
package/src/cli/auth.ts CHANGED
@@ -3,7 +3,10 @@ import chalk from 'chalk';
3
3
  import ora from 'ora';
4
4
  import * as readline from 'readline';
5
5
  import { Writable } from 'node:stream';
6
- import { login, logout, whoami } from '../sdk/auth.js';
6
+ import { login, logout, whoami, type WhoAmI } from '../sdk/auth.js';
7
+ import { findLinkFile } from '../core/org-resolver.js';
8
+ import { getOrgFlag } from '../core/scope.js';
9
+ import { SessionExpiredError } from '../core/runtime.js';
7
10
  import type { Environment } from '../sdk/types.js';
8
11
  import { printError, printSuccess, printJson } from './utils/output.js';
9
12
 
@@ -39,6 +42,25 @@ function promptPassword(question: string): Promise<string> {
39
42
  });
40
43
  }
41
44
 
45
+ function describeCredential(info: WhoAmI): string {
46
+ if (info.credential === 'session') {
47
+ return info.source === 'env' ? 'session token from INDEMN_TOKEN (supplied by a host)' : 'login session';
48
+ }
49
+ return info.source === 'env' ? 'API key from INDEMN_API_KEY' : 'API key saved by an older login (run `indemn login` to switch to a session)';
50
+ }
51
+
52
+ /** Which org commands in this folder would use, without asking anything. */
53
+ function describeOrgRule(info: WhoAmI): string {
54
+ const flag = getOrgFlag();
55
+ if (flag) return `${flag} (from --org)`;
56
+ if (process.env.INDEMN_ORG_ID) return `${process.env.INDEMN_ORG_ID} (from INDEMN_ORG_ID)`;
57
+ const link = findLinkFile();
58
+ if (link) return `${link.org_name ?? link.org_id} (linked in ${link.path})`;
59
+ if (info.orgs.length === 1) return `${info.orgs[0].name} (your only org)`;
60
+ if (info.orgs.length > 1) return `asked for each command; you belong to ${info.orgs.length}. Use --org <id> or \`indemn link\`.`;
61
+ return 'looked up on first use';
62
+ }
63
+
42
64
  export function registerAuthCommands(program: Command): void {
43
65
  program
44
66
  .command('login')
@@ -79,10 +101,19 @@ export function registerAuthCommands(program: Command): void {
79
101
  return code;
80
102
  },
81
103
  });
82
- spinner.succeed('Authenticated successfully');
83
- console.log(chalk.dim(` Org: ${result.orgId}`));
84
- console.log(chalk.dim(` Environment: ${result.environment}`));
85
- console.log(chalk.dim(` API Key: ${result.apiKey.substring(0, 12)}...`));
104
+ spinner.succeed(`Logged in as ${result.email} on ${result.environment}`);
105
+ if (result.expiresAt) {
106
+ console.log(chalk.dim(` Session valid until ${new Date(result.expiresAt).toLocaleString()}; it renews itself.`));
107
+ }
108
+ if (result.orgs.length === 0) {
109
+ console.log(chalk.yellow(' You are not a member of any organization yet. Ask an admin to add you.'));
110
+ } else {
111
+ console.log(chalk.dim(` Organizations (${result.orgs.length}):`));
112
+ for (const o of result.orgs) console.log(chalk.dim(` ${o.id} ${o.name}`));
113
+ if (result.orgs.length > 1) {
114
+ console.log(chalk.dim(' Choose one per command with --org <id>, or pin a project folder with `indemn link --org <id>`.'));
115
+ }
116
+ }
86
117
  } catch (err) {
87
118
  spinner.fail('Authentication failed');
88
119
  if (err instanceof Error) {
@@ -100,11 +131,11 @@ export function registerAuthCommands(program: Command): void {
100
131
 
101
132
  program
102
133
  .command('logout')
103
- .description('Clear stored credentials')
104
- .action(() => {
134
+ .description('Revoke the session and clear stored credentials')
135
+ .action(async () => {
105
136
  try {
106
- logout();
107
- printSuccess('Logged out successfully. Credentials cleared.');
137
+ const { revoked } = await logout();
138
+ printSuccess(revoked ? 'Logged out. The session was revoked and removed from this machine.' : 'Logged out. Credentials removed from this machine.');
108
139
  } catch (err) {
109
140
  printError(err);
110
141
  process.exit(1);
@@ -119,23 +150,30 @@ export function registerAuthCommands(program: Command): void {
119
150
  try {
120
151
  const info = whoami();
121
152
  if (!info) {
122
- printError('Not authenticated. Run `indemn login` first.');
123
- process.exit(1);
153
+ printError(new SessionExpiredError('Not logged in. Run `indemn login` first.'));
154
+ process.exit(3);
124
155
  }
125
156
 
126
157
  if (options.json) {
127
- printJson(info);
158
+ printJson({ ...info, org_rule: describeOrgRule(info) });
128
159
  return;
129
160
  }
130
161
 
131
- console.log(chalk.bold('Current Authentication'));
132
- console.log(` API Key: ${info.apiKey}`);
133
- console.log(` Org ID: ${info.orgId}`);
134
- console.log(` User ID: ${info.userId}`);
135
- console.log(` Environment: ${info.environment}`);
162
+ console.log(chalk.bold('Current login'));
163
+ console.log(` Platform: ${info.environment}`);
164
+ if (info.email) console.log(` User: ${info.email}`);
165
+ console.log(` Credential: ${describeCredential(info)}`);
166
+ if (info.sessionExpiresAt) {
167
+ console.log(` Session: valid until ${new Date(info.sessionExpiresAt).toLocaleString()} (renews itself)`);
168
+ }
169
+ console.log(` Org here: ${describeOrgRule(info)}`);
170
+ if (info.orgs.length) {
171
+ console.log(` Orgs:`);
172
+ for (const o of info.orgs) console.log(` ${o.id} ${o.name}`);
173
+ }
136
174
  console.log(` Copilot: ${info.hosts.copilot}`);
137
175
  console.log(` Evaluations: ${info.hosts.evaluations}`);
138
- console.log(` Middleware: ${info.hosts.middleware}`);
176
+ console.log(` Middleware: ${info.hosts.middleware}`);
139
177
  } catch (err) {
140
178
  printError(err);
141
179
  process.exit(1);
@@ -0,0 +1,25 @@
1
+ import { Command } from 'commander';
2
+ import { IndemnClient } from '../sdk/client.js';
3
+ import { BillingSDK } from '../sdk/billing.js';
4
+ import { printError, printJson } from './utils/output.js';
5
+
6
+ function run(fn: () => Promise<void>): Promise<void> {
7
+ return fn().catch((err) => {
8
+ printError(err);
9
+ process.exit(1);
10
+ });
11
+ }
12
+ const sdk = () => new BillingSDK(new IndemnClient());
13
+
14
+ export function registerBillingCommands(program: Command): void {
15
+ const billing = program.command('billing').description('Plan, usage and invoices (read-only: nothing here moves money)');
16
+ billing.command('status').description('Subscription status and current period').action(() => run(async () => {
17
+ const s = await sdk().status();
18
+ printJson(s ?? { subscription: null, message: 'This org has no paid subscription.' });
19
+ }));
20
+ billing.command('invoices').option('--unpaid', 'Only open invoices, with their payment links').action((options) => run(async () => {
21
+ printJson(options.unpaid ? await sdk().unpaid() : await sdk().invoices());
22
+ }));
23
+ billing.command('usage-forecast').description('Usage so far this period and the projected cost').action(() => run(async () => printJson(await sdk().usageForecast())));
24
+ billing.command('plans').description('The org\'s plans and the public plan list').action(() => run(async () => printJson(await sdk().plans())));
25
+ }
@@ -0,0 +1,33 @@
1
+ import { Command } from 'commander';
2
+ import { IndemnClient } from '../sdk/client.js';
3
+ import { CampaignsSDK } from '../sdk/campaigns.js';
4
+ import { formatTable, printError, printJson } from './utils/output.js';
5
+
6
+ function run(fn: () => Promise<void>): Promise<void> {
7
+ return fn().catch((err) => {
8
+ printError(err);
9
+ process.exit(1);
10
+ });
11
+ }
12
+ const sdk = () => new CampaignsSDK(new IndemnClient());
13
+
14
+ export function registerCampaignCommands(program: Command): void {
15
+ const campaigns = program.command('campaigns').description('Renewal and outreach campaigns (read-only)');
16
+ campaigns.command('list')
17
+ .option('--status <status>').option('--solution <id>').option('--limit <n>', 'At most this many', '50').option('--json', 'Output as JSON')
18
+ .action((options) => run(async () => {
19
+ const res = await sdk().list({ status: options.status, solutionId: options.solution, limit: Number(options.limit) });
20
+ if (options.json) { printJson(res); return; }
21
+ const items = (res.items ?? []) as Array<Record<string, unknown>>;
22
+ console.log(formatTable(['ID', 'Name', 'Status'], items.map((c) => [String(c._id ?? c.id ?? ''), String(c.name ?? ''), String(c.status ?? '')])));
23
+ }));
24
+ campaigns.command('get').argument('<campaign-id>').action((id: string) => run(async () => printJson(await sdk().get(id))));
25
+ for (const what of ['counts', 'forecast', 'next-touchpoints', 'versions'] as const) {
26
+ campaigns.command(what).argument('<campaign-id>').option('--days <n>', 'For forecast and next-touchpoints')
27
+ .action((id: string, options) => run(async () => printJson(await sdk().detail(id, what, options.days ? Number(options.days) : undefined))));
28
+ }
29
+ const templates = campaigns.command('templates').description('Message templates of campaigns');
30
+ templates.command('list').option('--channel <channel>').option('--solution <id>').action((options) => run(async () => printJson(await sdk().templates({ channel: options.channel, solutionId: options.solution }))));
31
+ templates.command('get').argument('<template-id>').option('--versions', 'Show versions (subject and body)').action((id: string, options) => run(async () => printJson(await sdk().template(id, Boolean(options.versions)))));
32
+ templates.command('skills').option('--solution <id>').action((options) => run(async () => printJson(await sdk().templateSkills(options.solution))));
33
+ }