@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/chat.ts CHANGED
@@ -3,116 +3,80 @@ import * as readline from 'readline';
3
3
  import chalk from 'chalk';
4
4
  import ora from 'ora';
5
5
  import { IndemnClient } from '../sdk/client.js';
6
- import { AgentsSDK } from '../sdk/agents.js';
7
6
  import { ChatSession } from '../sdk/chat.js';
8
- import { printError } from './utils/output.js';
7
+ import { exchange, openTestChat } from '../sdk/test-chat.js';
8
+ import { printError, printJson } from './utils/output.js';
9
+
10
+ /** Print streamed agent text as it arrives (terminal mode only). */
11
+ function attachPrinter(session: ChatSession, state: { receiving: boolean }): void {
12
+ let streamed = false;
13
+ session.on('stream', (data: { text?: string; isFirstChunk?: boolean; streamFinished?: boolean }) => {
14
+ if (data.isFirstChunk) {
15
+ state.receiving = true;
16
+ streamed = true;
17
+ process.stdout.write(chalk.cyan('Agent: '));
18
+ }
19
+ if (data.text) process.stdout.write(chalk.cyan(data.text));
20
+ if (data.streamFinished) {
21
+ process.stdout.write('\n');
22
+ state.receiving = false;
23
+ }
24
+ });
25
+ session.on('message', (text: string) => {
26
+ if (streamed) {
27
+ streamed = false;
28
+ return;
29
+ }
30
+ if (!state.receiving) console.log(chalk.cyan(`Agent: ${text}`));
31
+ });
32
+ }
9
33
 
10
34
  export function registerChatCommand(program: Command): void {
11
35
  program
12
36
  .command('chat')
13
37
  .argument('<agent-id>', 'Agent ID to chat with')
14
- .description('Interactive chat with an agent (or send a single message with --message)')
15
- .option('--session <session-id>', 'Resume existing session')
38
+ .description('Chat with an agent. Always a test conversation: never billed, never in analytics, marked with your name.')
39
+ .option('--session <session-id>', 'Resume an existing CLI test conversation')
16
40
  .option('--message <text>', 'Send a single message and exit (non-interactive)')
41
+ .option('--json', 'With --message: print { session_id, url, greeting, response } as JSON')
17
42
  .action(async (agentId: string, options) => {
18
43
  try {
19
44
  const client = new IndemnClient();
20
- const agentsSDK = new AgentsSDK(client);
21
-
22
- const fetchSpinner = ora('Connecting to agent...').start();
23
-
24
- const agent = await agentsSDK.getAgent(agentId);
25
- const hosts = client.getHosts();
26
-
27
- const session = new ChatSession(agent._id, hosts.middleware, options.session);
28
-
29
- // Track whether we're currently receiving a streamed response
30
- let receiving = false;
31
- let streamedResponse = false;
32
-
33
- // Handle stream chunks — write directly to stdout
34
- session.on('stream', (data: { text?: string; isFirstChunk?: boolean; streamFinished?: boolean }) => {
35
- if (data.isFirstChunk) {
36
- receiving = true;
37
- streamedResponse = true;
38
- process.stdout.write(chalk.cyan('Agent: '));
39
- }
40
- if (data.text) {
41
- process.stdout.write(chalk.cyan(data.text));
42
- }
43
- if (data.streamFinished) {
44
- process.stdout.write('\n');
45
- receiving = false;
46
- }
45
+ const state = { receiving: false };
46
+ const quiet = Boolean(options.message && options.json);
47
+ const spinner = quiet ? null : ora('Opening a test conversation...').start();
48
+
49
+ const chat = await openTestChat(client, agentId, {
50
+ sessionId: options.session,
51
+ onSession: quiet ? undefined : (session) => {
52
+ session.on('stream', () => spinner?.stop());
53
+ attachPrinter(session, state);
54
+ },
47
55
  });
56
+ spinner?.stop();
48
57
 
49
- // Handle non-streamed bot messages (skip if already printed via streaming)
50
- session.on('message', (text: string) => {
51
- if (streamedResponse) {
52
- streamedResponse = false;
53
- return;
54
- }
55
- if (!receiving) {
56
- console.log(chalk.cyan(`Agent: ${text}`));
57
- }
58
- });
59
-
60
- session.on('disconnected', (reason: string) => {
61
- console.log(chalk.dim(`\nDisconnected: ${reason}`));
58
+ chat.session.on('disconnected', (reason: string) => {
59
+ // Our own disconnect is followed by an explicit exit; this handles the server dropping us.
60
+ if (reason === 'io client disconnect') return;
61
+ if (!quiet) console.log(chalk.dim(`\nDisconnected: ${reason}`));
62
62
  process.exit(0);
63
63
  });
64
64
 
65
- // Connect and get session ID
66
- const sessionId = await session.connect();
67
- fetchSpinner.stop();
68
-
69
- console.log(chalk.dim(`Connected to ${agent.name}`));
70
- console.log(chalk.dim(`Session: ${sessionId}`));
65
+ if (!quiet) {
66
+ console.log(chalk.dim(`Test conversation with ${chat.agent.name} · tester ${chat.tester ?? 'unknown'} · session ${chat.sessionId}`));
67
+ console.log(chalk.dim(`Open in the dashboard: ${chat.url}`));
68
+ }
71
69
 
72
- // Non-interactive mode: send one message, wait for response, exit
73
70
  if (options.message) {
74
- // Wait for greeting to finish streaming
75
- await new Promise<void>((resolve) => {
76
- const check = setInterval(() => {
77
- if (!receiving) {
78
- clearInterval(check);
79
- resolve();
80
- }
81
- }, 100);
82
- // Timeout after 8s even if no greeting
83
- setTimeout(() => { clearInterval(check); resolve(); }, 8000);
84
- });
85
-
86
- console.log(chalk.green(`You: ${options.message}`));
87
- session.send(options.message);
88
-
89
- // Wait for response to finish streaming
90
- await new Promise<void>((resolve) => {
91
- let started = false;
92
- const check = setInterval(() => {
93
- if (receiving) started = true;
94
- if (started && !receiving) {
95
- clearInterval(check);
96
- resolve();
97
- }
98
- }, 100);
99
- // Timeout after 30s
100
- setTimeout(() => { clearInterval(check); resolve(); }, 30000);
101
- });
102
-
103
- session.disconnect();
71
+ if (!quiet) console.log(chalk.green(`You: ${options.message}`));
72
+ const { greeting, response } = await exchange(chat, options.message, { timeoutMs: 30_000 });
73
+ await chat.session.disconnect();
74
+ if (quiet) printJson({ session_id: chat.sessionId, url: chat.url, greeting, response });
104
75
  process.exit(0);
105
76
  }
106
77
 
107
- // Interactive mode
108
78
  console.log(chalk.dim('Type your message and press Enter. Ctrl+C to exit.\n'));
109
-
110
- const rl = readline.createInterface({
111
- input: process.stdin,
112
- output: process.stdout,
113
- prompt: chalk.green('You: '),
114
- });
115
-
79
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: chalk.green('You: ') });
116
80
  rl.prompt();
117
81
 
118
82
  rl.on('line', (line) => {
@@ -121,38 +85,25 @@ export function registerChatCommand(program: Command): void {
121
85
  rl.prompt();
122
86
  return;
123
87
  }
124
-
125
- session.send(message);
126
-
127
- // Wait briefly before showing prompt again to let response start streaming
88
+ chat.session.send(message);
89
+ // Re-prompt once the reply has finished streaming.
128
90
  setTimeout(() => {
129
- if (!receiving) {
130
- rl.prompt();
131
- } else {
132
- // Re-prompt after stream finishes
133
- const checkDone = setInterval(() => {
134
- if (!receiving) {
135
- clearInterval(checkDone);
136
- rl.prompt();
137
- }
138
- }, 100);
139
- }
91
+ const check = setInterval(() => {
92
+ if (!state.receiving) {
93
+ clearInterval(check);
94
+ rl.prompt();
95
+ }
96
+ }, 100);
140
97
  }, 200);
141
98
  });
142
99
 
143
- rl.on('close', () => {
100
+ const close = async () => {
144
101
  console.log(chalk.dim('\nClosing session...'));
145
- session.disconnect();
102
+ await chat.session.disconnect();
146
103
  process.exit(0);
147
- });
148
-
149
- // Handle Ctrl+C
150
- process.on('SIGINT', () => {
151
- console.log(chalk.dim('\nClosing session...'));
152
- session.disconnect();
153
- rl.close();
154
- process.exit(0);
155
- });
104
+ };
105
+ rl.on('close', close);
106
+ process.on('SIGINT', close);
156
107
  } catch (err) {
157
108
  printError(err);
158
109
  process.exit(1);
package/src/cli/config.ts CHANGED
@@ -3,7 +3,25 @@ import * as fs from 'fs';
3
3
  import ora from 'ora';
4
4
  import { IndemnClient } from '../sdk/client.js';
5
5
  import { ConfigSDK } from '../sdk/config.js';
6
- import { printJson, printError, printSuccess } from './utils/output.js';
6
+ import chalk from 'chalk';
7
+ import { confirm, printJson, printError, printSuccess } from './utils/output.js';
8
+ import { confirmOrExit } from './link.js';
9
+ import { CliError, EXIT, isNonInteractive } from '../core/runtime.js';
10
+ import { diffStats, formatDiff } from '../core/diff.js';
11
+ import { applyConfigChange, mayBeLive, modelArg, planConfigChange, type ConfigChange, type ConfigPlan } from '../sdk/config-change.js';
12
+
13
+ function printPlan(plan: ConfigPlan): void {
14
+ console.log(chalk.bold(`Agent ${plan.agentId}`) + chalk.dim(` · lifecycle ${plan.lifecycle}`));
15
+ if (plan.changes.includes('system_prompt')) {
16
+ const { added, removed } = diffStats(plan.promptDiff);
17
+ console.log(chalk.bold(`Prompt: +${added} / -${removed} lines`));
18
+ for (const line of formatDiff(plan.promptDiff).split('\n')) {
19
+ console.log(line.startsWith('+') ? chalk.green(line) : line.startsWith('-') ? chalk.red(line) : chalk.dim(line));
20
+ }
21
+ }
22
+ if (plan.changes.includes('greeting')) console.log(`Greeting: ${chalk.red(plan.current.greeting || '(none)')} -> ${chalk.green(plan.next.greeting)}`);
23
+ if (plan.changes.includes('model')) console.log(`Model: ${chalk.red(plan.current.model || '(none)')} -> ${chalk.green(plan.next.model)}`);
24
+ }
7
25
 
8
26
  export function registerConfigCommands(program: Command): void {
9
27
  const config = program.command('config').description('Manage agent configuration');
@@ -32,7 +50,7 @@ export function registerConfigCommands(program: Command): void {
32
50
  value = cfg.ai_config?.system_prompt;
33
51
  break;
34
52
  case 'greeting':
35
- value = cfg.first_message;
53
+ value = cfg.first_message?.text;
36
54
  break;
37
55
  case 'model': {
38
56
  const llm = cfg.llm_config as Record<string, unknown> | undefined;
@@ -89,46 +107,62 @@ export function registerConfigCommands(program: Command): void {
89
107
  config
90
108
  .command('set')
91
109
  .argument('<agent-id>', 'Agent ID')
92
- .description('Update agent configuration')
93
- .option('--prompt <prompt>', 'Set system prompt text')
94
- .option('--prompt-file <path>', 'Set system prompt from file')
95
- .option('--model <model>', 'Set model name')
110
+ .description('Change the prompt, greeting or model. Shows the change and asks first')
111
+ .option('--prompt <prompt>', 'New system prompt text')
112
+ .option('--prompt-file <path>', 'New system prompt from a file')
113
+ .option('--greeting <text>', 'New greeting (first message)')
114
+ .option('--model <model>', 'provider/model, e.g. openai/gpt-4.1 (a bare name guesses the provider)')
115
+ .option('--dry-run', 'Show the change and stop')
116
+ .option('--yes', 'Do not ask for confirmation (not enough for a published agent)')
117
+ .option('--confirm-published', 'Also required to change a published agent without a prompt')
96
118
  .action(async (agentId: string, options) => {
97
119
  try {
98
- const input: Record<string, unknown> = {};
99
-
120
+ const change: ConfigChange = {};
100
121
  if (options.promptFile) {
101
122
  const filePath = options.promptFile as string;
102
- if (!fs.existsSync(filePath)) {
103
- printError(`File not found: ${filePath}`);
104
- process.exit(1);
105
- }
106
- input.system_prompt = fs.readFileSync(filePath, 'utf-8');
107
- } else if (options.prompt) {
108
- input.system_prompt = options.prompt;
123
+ if (!fs.existsSync(filePath)) throw new CliError(`File not found: ${filePath}`);
124
+ change.system_prompt = fs.readFileSync(filePath, 'utf-8');
125
+ } else if (options.prompt !== undefined) {
126
+ change.system_prompt = options.prompt;
109
127
  }
110
-
111
- if (options.model) {
112
- const model = options.model as string;
113
- const provider = model.startsWith('claude') ? 'anthropic'
114
- : model.startsWith('gemini') ? 'google'
115
- : 'openai';
116
- input.llm_config = { model, provider };
128
+ if (options.greeting !== undefined) change.greeting = options.greeting;
129
+ if (options.model) change.model = modelArg(options.model);
130
+ if (Object.keys(change).length === 0) {
131
+ throw new CliError('Nothing to change. Use --prompt, --prompt-file, --greeting or --model.', EXIT.NEEDS_INPUT);
117
132
  }
118
133
 
119
- if (Object.keys(input).length === 0) {
120
- printError('No configuration options provided. Use --prompt, --prompt-file, or --model.');
121
- process.exit(1);
134
+ const client = new IndemnClient();
135
+ const plan = await planConfigChange(client, agentId, change);
136
+ if (plan.changes.length === 0) {
137
+ console.log('No change: the agent already has these values.');
138
+ return;
139
+ }
140
+ printPlan(plan);
141
+ if (options.dryRun) return;
142
+
143
+ if (mayBeLive(plan.lifecycle)) {
144
+ console.log(chalk.red.bold(plan.lifecycle === 'published'
145
+ ? 'This agent is PUBLISHED: customers are talking to it now.'
146
+ : 'Could not read whether this agent is live, so it is treated as published.'));
147
+ if (isNonInteractive()) {
148
+ if (!(options.yes && options.confirmPublished)) {
149
+ throw new CliError('Changing a published agent without a person at the terminal needs both --yes and --confirm-published.', EXIT.NEEDS_INPUT);
150
+ }
151
+ } else if (!(await confirm('Change the published agent?'))) {
152
+ console.log('Cancelled.');
153
+ return;
154
+ }
155
+ } else {
156
+ await confirmOrExit(`Apply ${plan.changes.length} change(s) to agent ${agentId}?`, Boolean(options.yes));
122
157
  }
123
158
 
124
- const client = new IndemnClient();
125
- const sdk = new ConfigSDK(client);
126
159
  const spinner = ora('Updating configuration...').start();
127
-
128
- await sdk.setConfig(agentId, input);
160
+ const result = await applyConfigChange(client, plan, change);
129
161
  spinner.stop();
130
-
131
- printSuccess(`Configuration updated for agent ${agentId}.`);
162
+ if (result.mismatched.length) {
163
+ throw new CliError(`Saved: ${result.applied.join(', ') || 'nothing'}. NOT saved (read back differs): ${result.mismatched.join(', ')}.`);
164
+ }
165
+ printSuccess(`Updated ${result.applied.join(', ')} for agent ${agentId} (read back and confirmed).`);
132
166
  } catch (err) {
133
167
  printError(err);
134
168
  process.exit(1);
@@ -0,0 +1,133 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { writeFileSync } from 'node:fs';
4
+ import { IndemnClient } from '../sdk/client.js';
5
+ import {
6
+ ConversationsSDK, conversationToTestSet, conversationUrl, isCliConversation, isTestConversation, projectOfAgent,
7
+ } from '../sdk/conversations.js';
8
+ import { confirmOrExit } from './link.js';
9
+ import { formatTable, printError, printJson, printSuccess } from './utils/output.js';
10
+
11
+ export function registerConversationCommands(program: Command): void {
12
+ const conv = program.command('conversations').alias('conv').description('Read conversations of an agent, export them, or mark them as test');
13
+
14
+ conv
15
+ .command('list')
16
+ .requiredOption('--agent <id>', 'Agent whose conversations to list')
17
+ .option('--limit <n>', 'Rows per page (at most 100)', '50')
18
+ .option('--page <n>', 'Page, starting at 0', '0')
19
+ .option('--live-only', 'Hide test conversations')
20
+ .option('--cli-only', 'Only conversations opened by the CLI')
21
+ .option('--from <date>', 'Created on or after (ISO date)')
22
+ .option('--to <date>', 'Created before (ISO date)')
23
+ .option('--json', 'Output as JSON')
24
+ .description('List conversations of an agent, newest first')
25
+ .action(async (options) => {
26
+ try {
27
+ const client = new IndemnClient();
28
+ const { conversations } = await new ConversationsSDK(client).list(options.agent, {
29
+ limit: Number(options.limit), page: Number(options.page), includeTest: !options.liveOnly,
30
+ cliOnly: options.cliOnly, from: options.from, to: options.to,
31
+ });
32
+ if (options.json) {
33
+ printJson(conversations);
34
+ return;
35
+ }
36
+ if (conversations.length === 0) {
37
+ console.log('No conversations on this page.');
38
+ return;
39
+ }
40
+ console.log(formatTable(
41
+ ['Conversation', 'Created', 'Test', 'CLI', 'First message'],
42
+ conversations.map((c) => [
43
+ c.request_id,
44
+ new Date(String(c.created_at ?? c.createdAt ?? '')).toLocaleString(),
45
+ isTestConversation(c) ? 'test' : chalk.yellow('LIVE'),
46
+ isCliConversation(c) ? 'yes' : '',
47
+ String(c.first_text ?? '').slice(0, 50),
48
+ ]),
49
+ ));
50
+ } catch (err) {
51
+ printError(err);
52
+ process.exit(1);
53
+ }
54
+ });
55
+
56
+ conv
57
+ .command('get')
58
+ .argument('<conversation-id>', 'Conversation (request) id')
59
+ .requiredOption('--agent <id>', 'Agent the conversation belongs to')
60
+ .option('--json', 'Output as JSON')
61
+ .description('Show a conversation and its messages')
62
+ .action(async (id: string, options) => {
63
+ try {
64
+ const client = new IndemnClient();
65
+ const sdk = new ConversationsSDK(client);
66
+ const projectId = await projectOfAgent(client, options.agent);
67
+ const [conversation, messages] = await Promise.all([sdk.get(projectId, id), sdk.messages(projectId, id, options.agent)]);
68
+ if (options.json) {
69
+ printJson({ conversation, messages });
70
+ return;
71
+ }
72
+ console.log(chalk.bold(`Conversation ${id}`) + (conversation.isTestMode ? chalk.dim(' test') : chalk.yellow(' LIVE')));
73
+ console.log(chalk.dim(conversationUrl(client, projectId, id)));
74
+ for (const m of messages) {
75
+ const who = m.role === 'agent' ? chalk.cyan('Agent') : m.role === 'user' ? chalk.green('User ') : chalk.dim('System');
76
+ console.log(`${who}: ${m.text ?? ''}`);
77
+ }
78
+ } catch (err) {
79
+ printError(err);
80
+ process.exit(1);
81
+ }
82
+ });
83
+
84
+ conv
85
+ .command('export')
86
+ .argument('<conversation-id>', 'Conversation (request) id')
87
+ .requiredOption('--agent <id>', 'Agent the conversation belongs to')
88
+ .option('--as-testset', 'Write an evaluation test set (one scenario item) instead of the raw conversation')
89
+ .option('-o, --output <file>', 'Write to a file instead of stdout')
90
+ .description('Export a conversation as JSON, or as a test set to replay it in evaluations')
91
+ .action(async (id: string, options) => {
92
+ try {
93
+ const client = new IndemnClient();
94
+ const sdk = new ConversationsSDK(client);
95
+ const projectId = await projectOfAgent(client, options.agent);
96
+ const [conversation, messages] = await Promise.all([sdk.get(projectId, id), sdk.messages(projectId, id, options.agent)]);
97
+ const out = options.asTestset ? conversationToTestSet(conversation, messages, options.agent) : { conversation, messages };
98
+ const text = JSON.stringify(out, null, 2) + '\n';
99
+ if (options.output) {
100
+ writeFileSync(options.output, text);
101
+ printSuccess(`Wrote ${options.output}.`);
102
+ if (options.asTestset) console.log(chalk.dim('Add success criteria to the item, then: indemn testset create --file ' + options.output));
103
+ } else {
104
+ process.stdout.write(text);
105
+ }
106
+ } catch (err) {
107
+ printError(err);
108
+ process.exit(1);
109
+ }
110
+ });
111
+
112
+ conv
113
+ .command('mark-test')
114
+ .argument('<conversation-ids...>', 'Conversation (request) ids')
115
+ .requiredOption('--agent <id>', 'Agent the conversations belong to')
116
+ .option('--yes', 'Do not ask for confirmation')
117
+ .description('Mark conversations as test (removes them from analytics and reverses usage billing, as the dashboard does)')
118
+ .action(async (ids: string[], options) => {
119
+ try {
120
+ const client = new IndemnClient();
121
+ const sdk = new ConversationsSDK(client);
122
+ const projectId = await projectOfAgent(client, options.agent);
123
+ await confirmOrExit(`Mark ${ids.length} conversation(s) of agent ${options.agent} as test?`, Boolean(options.yes));
124
+ for (const id of ids) {
125
+ await sdk.markTest(projectId, id);
126
+ console.log(` ${id} marked as test`);
127
+ }
128
+ } catch (err) {
129
+ printError(err);
130
+ process.exit(1);
131
+ }
132
+ });
133
+ }
@@ -0,0 +1,116 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { IndemnClient } from '../sdk/client.js';
4
+ import { DistributionSDK } from '../sdk/distribution.js';
5
+ import { idOf } from '../sdk/conversations.js';
6
+ import { CliError, EXIT } from '../core/runtime.js';
7
+ import { confirmOrExit } from './link.js';
8
+ import { formatTable, printError, printJson, printSuccess } from './utils/output.js';
9
+
10
+ function run(fn: () => Promise<void>): Promise<void> {
11
+ return fn().catch((err) => {
12
+ printError(err);
13
+ process.exit(1);
14
+ });
15
+ }
16
+
17
+ export function registerDistributionCommands(program: Command): void {
18
+ const dist = program.command('distributions').description('Which website paths open which agent (web widget)');
19
+
20
+ dist.command('list')
21
+ .option('--json', 'Output as JSON')
22
+ .action((options) => run(async () => {
23
+ const d = await new DistributionSDK(new IndemnClient()).list();
24
+ if (options.json) { printJson(d); return; }
25
+ console.log(formatTable(['ID', 'Host', 'Path', 'Agent'], (d.paths ?? []).map((p) => [String(p.id), String(p.host_name ?? ''), String(p.path_expression ?? p.path_name ?? ''), String(p.bot_name ?? idOf(p.id_bot))])));
26
+ if (d.scriptTag) console.log(chalk.dim(`Embed code for every path: ${d.scriptTag}`));
27
+ }));
28
+
29
+ dist.command('create')
30
+ .requiredOption('--agent <id>', 'Agent to open')
31
+ .requiredOption('--host <host>', 'Website host, for example www.example.com')
32
+ .requiredOption('--path <path>', 'Page path, for example /quote')
33
+ .option('--path-expression <expr>', 'Pattern of pages (default: the path)')
34
+ .option('--greeting <text>', 'Greeting for this path')
35
+ .option('--allow-upload', 'Let visitors attach files')
36
+ .option('--yes', 'Do not ask for confirmation')
37
+ .description('Open an agent on a website path (live for visitors once the embed code is on the site)')
38
+ .action((options) => run(async () => {
39
+ await confirmOrExit(`Open agent ${options.agent} on ${options.host}${options.pathExpression ?? options.path}? Visitors of that page see it.`, Boolean(options.yes));
40
+ printJson(await new DistributionSDK(new IndemnClient()).create({
41
+ agentId: options.agent, host: options.host, path: options.path, pathExpression: options.pathExpression, greeting: options.greeting, allowUpload: options.allowUpload,
42
+ }));
43
+ }));
44
+
45
+ dist.command('update')
46
+ .argument('<distribution-id>', 'Distribution ID (from `distributions list`)')
47
+ .option('--agent <id>', 'Open another agent')
48
+ .option('--greeting <text>', 'New greeting')
49
+ .option('--path-expression <expr>', 'New page pattern')
50
+ .option('--yes', 'Do not ask for confirmation')
51
+ .action((id: string, options) => run(async () => {
52
+ const changes: Record<string, unknown> = {};
53
+ if (options.agent) changes.id_bot = options.agent;
54
+ if (options.greeting) changes.greetingMessage = options.greeting;
55
+ if (options.pathExpression) changes.pathExpression = options.pathExpression;
56
+ if (!Object.keys(changes).length) throw new CliError('Nothing to change. Use --agent, --greeting or --path-expression.', EXIT.NEEDS_INPUT);
57
+ await confirmOrExit(`Change distribution ${id} (${Object.keys(changes).join(', ')})? Visitors see it immediately.`, Boolean(options.yes));
58
+ printJson(await new DistributionSDK(new IndemnClient()).update(id, changes));
59
+ }));
60
+
61
+ const templates = program.command('templates').description('Agent templates (shared by every org on the platform)');
62
+ templates.command('list')
63
+ .option('--json', 'Output as JSON')
64
+ .action((options) => run(async () => {
65
+ const list = await new DistributionSDK(new IndemnClient()).listTemplates();
66
+ if (options.json) { printJson(list); return; }
67
+ console.log(formatTable(['ID', 'Name', 'Category', 'Source agent'], list.map((t) => [String(t._id), t.name, (t.category ?? []).join(', '), typeof t.agent_id === 'object' && t.agent_id ? String(t.agent_id.name ?? idOf(t.agent_id._id)) : String(t.agent_id ?? '')])));
68
+ }));
69
+ templates.command('create')
70
+ .requiredOption('--agent <id>', 'Agent of this org to publish as a template')
71
+ .requiredOption('--name <name>', 'Template name')
72
+ .option('--description <text>', 'Description')
73
+ .option('--category <list>', 'Comma-separated categories')
74
+ .option('--tags <list>', 'Comma-separated tags')
75
+ .option('--yes', 'Do not ask for confirmation')
76
+ .description('Publish an agent as a template; every org on the platform can copy it')
77
+ .action((options) => run(async () => {
78
+ await confirmOrExit(`Publish agent ${options.agent} as template "${options.name}"? Every org on the platform can see and copy it, including its prompt and skills.`, Boolean(options.yes));
79
+ printJson(await new DistributionSDK(new IndemnClient()).createTemplate({
80
+ agentId: options.agent, name: options.name, description: options.description,
81
+ category: options.category?.split(',').map((s: string) => s.trim()), tags: options.tags?.split(',').map((s: string) => s.trim()),
82
+ }));
83
+ }));
84
+ templates.command('update')
85
+ .argument('<template-id>')
86
+ .option('--name <name>')
87
+ .option('--description <text>')
88
+ .option('--yes', 'Do not ask for confirmation')
89
+ .action((id: string, options) => run(async () => {
90
+ await confirmOrExit(`Change template ${id}? Every org sees the change.`, Boolean(options.yes));
91
+ printJson(await new DistributionSDK(new IndemnClient()).updateTemplate(id, { name: options.name, description: options.description }));
92
+ }));
93
+ templates.command('delete')
94
+ .argument('<template-id>')
95
+ .option('--yes', 'Do not ask for confirmation')
96
+ .action((id: string, options) => run(async () => {
97
+ await confirmOrExit(`Remove template ${id} for every org?`, Boolean(options.yes));
98
+ await new DistributionSDK(new IndemnClient()).deleteTemplate(id);
99
+ printSuccess(`Template ${id} removed.`);
100
+ }));
101
+
102
+ program.command('solutions').description('Solutions of the org').command('list')
103
+ .action(() => run(async () => printJson(await new DistributionSDK(new IndemnClient()).listSolutions())));
104
+
105
+ const agents = program.commands.find((c) => c.name() === 'agents');
106
+ if (!agents) throw new Error('agents command group must be registered before distribution commands');
107
+ agents.command('embed')
108
+ .argument('<agent-id>', 'Agent ID')
109
+ .description('The embed code and the website paths that open this agent')
110
+ .action((agentId: string) => run(async () => {
111
+ const res = await new DistributionSDK(new IndemnClient()).embed(agentId);
112
+ console.log(res.scriptTag ?? '(no embed code)');
113
+ if (!res.paths.length) console.log(chalk.dim('No website path opens this agent yet: indemn distributions create --agent ' + agentId + ' --host <host> --path <path>'));
114
+ for (const p of res.paths) console.log(chalk.dim(` ${p.host_name}${p.path_expression ?? p.path_name}`));
115
+ }));
116
+ }