@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/sdk/eval.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { IndemnClient } from './client.js';
2
+ import { CliError, NeedsInputError, SessionExpiredError } from '../core/runtime.js';
2
3
  import type {
3
4
  EvaluationTriggerInput,
4
5
  EvaluationTriggerResponse,
@@ -31,6 +32,114 @@ export class EvalSDK {
31
32
  return this.client.evalsGet<BotContext>(`/api/v1/bots/${botId}/context`);
32
33
  }
33
34
 
35
+ /**
36
+ * The evaluations service checks nothing about orgs. Before acting on an
37
+ * agent, confirm through copilot (which refuses other orgs' agents) that it
38
+ * belongs to the org this command acts on.
39
+ */
40
+ async assertAgentInOrg(agentId: string): Promise<void> {
41
+ const { AgentsSDK } = await import('./agents.js');
42
+ const agent = await new AgentsSDK(this.client).getAgent(agentId).catch((err) => {
43
+ // "Which org?" and "log in again" are not answers about the agent.
44
+ if (err instanceof NeedsInputError || err instanceof SessionExpiredError) throw err;
45
+ return undefined;
46
+ });
47
+ if (!agent) throw new CliError(`Agent ${agentId} is not in the organization this command acts on. Nothing was run.`);
48
+ }
49
+
50
+ /** A run's agent must be in the org too, so results of other orgs are not read. */
51
+ async getRunInOrg(runId: string): Promise<EvaluationRun> {
52
+ const run = await this.getRunStatus(runId);
53
+ if (run.agent_id) await this.assertAgentInOrg(run.agent_id);
54
+ return run;
55
+ }
56
+
57
+ qualityMetrics(): Promise<Record<string, unknown>> {
58
+ return this.client.evalsGet('/api/v1/evaluations/quality-metrics');
59
+ }
60
+
61
+ trends(agentId: string, testSetId?: string, limit = 50): Promise<{ series: unknown[]; drift: unknown }> {
62
+ const q: Record<string, string> = { agent_id: agentId, limit: String(limit) };
63
+ if (testSetId) q.test_set_id = testSetId;
64
+ return this.client.evalsGet('/api/v1/trends', q);
65
+ }
66
+
67
+ compareRuns(runA: string, runB: string): Promise<unknown> {
68
+ return this.client.evalsGet('/api/v1/trends/compare', { run_a: runA, run_b: runB });
69
+ }
70
+
71
+ /** Server-rendered PDF report of a completed run; returns a link valid for 15 minutes. */
72
+ exportReport(runId: string, force = false): Promise<{ download_url: string; report_id: string }> {
73
+ return this.client.evalsPost(`/api/v1/runs/${runId}/export${force ? '?force=true' : ''}`);
74
+ }
75
+
76
+ downloadReport(reportId: string): Promise<{ download_url: string }> {
77
+ return this.client.evalsGet(`/api/v1/reports/${reportId}/download`);
78
+ }
79
+
80
+ prodCalls(botId: string, opts: { search?: string; limit?: number } = {}): Promise<unknown[]> {
81
+ const q: Record<string, string> = { bot_id: botId, limit: String(opts.limit ?? 50) };
82
+ if (opts.search) q.search = opts.search;
83
+ return this.client.evalsGet('/api/v1/prod-calls', q);
84
+ }
85
+
86
+ prodCall(sessionId: string): Promise<{ bot_id?: string; [k: string]: unknown }> {
87
+ return this.client.evalsGet(`/api/v1/prod-calls/${sessionId}`);
88
+ }
89
+
90
+ /** Turn an ingested voice call into a regression test item (test set "Regressions (review)"). */
91
+ promoteProdCall(sessionId: string): Promise<unknown> {
92
+ return this.client.evalsPost(`/api/v1/prod-calls/${sessionId}/promote`);
93
+ }
94
+
95
+ /** Grade a replayed conversation against a baseline one with the agent's stored evaluator. */
96
+ async evaluateTranscript(input: { agent_id: string; baseline_transcript: Array<{ sender: 'user' | 'bot'; text: string }>; replay_transcript: Array<{ sender: 'user' | 'bot'; text: string }>; conversation_id?: string }): Promise<unknown> {
97
+ const res = await this.client.evalsPost<{ success?: boolean; error?: string }>('/api/v1/evaluate-transcript', input);
98
+ // The service reports internal errors with HTTP 200.
99
+ if (res && res.success === false && res.error) throw new Error(`Transcript evaluation failed: ${res.error}`);
100
+ return res;
101
+ }
102
+
103
+ listDatasets(agentId?: string): Promise<unknown[]> {
104
+ return this.client.evalsGet('/api/v1/datasets', agentId ? { agent_id: agentId } : undefined);
105
+ }
106
+
107
+ getDataset(id: string): Promise<{ agent_id?: string; [k: string]: unknown }> {
108
+ return this.client.evalsGet(`/api/v1/datasets/${id}`);
109
+ }
110
+
111
+ deleteDataset(id: string): Promise<unknown> {
112
+ return this.client.evalsDelete(`/api/v1/datasets/${id}`);
113
+ }
114
+
115
+ listTestCases(datasetId: string): Promise<unknown[]> {
116
+ return this.client.evalsGet(`/api/v1/datasets/${datasetId}/test-cases`);
117
+ }
118
+
119
+ getTestCase(id: string): Promise<unknown> {
120
+ return this.client.evalsGet(`/api/v1/test-cases/${id}`);
121
+ }
122
+
123
+ deleteTestCase(id: string): Promise<unknown> {
124
+ return this.client.evalsDelete(`/api/v1/test-cases/${id}`);
125
+ }
126
+
127
+ evaluatorTypes(): Promise<unknown[]> {
128
+ return this.client.evalsGet('/api/v1/evaluators/types');
129
+ }
130
+
131
+ listEvaluatorConfigs(agentId?: string): Promise<unknown[]> {
132
+ return this.client.evalsGet('/api/v1/evaluator-configs', agentId ? { agent_id: agentId } : undefined);
133
+ }
134
+
135
+ getEvaluatorConfig(id: string): Promise<{ agent_id?: string; [k: string]: unknown }> {
136
+ return this.client.evalsGet(`/api/v1/evaluator-configs/${id}`);
137
+ }
138
+
139
+ deleteEvaluatorConfig(id: string): Promise<unknown> {
140
+ return this.client.evalsDelete(`/api/v1/evaluator-configs/${id}`);
141
+ }
142
+
34
143
  async pollRunUntilComplete(
35
144
  runId: string,
36
145
  intervalMs = 5000,
@@ -55,3 +164,34 @@ export class EvalSDK {
55
164
  }
56
165
  }
57
166
  }
167
+
168
+ export interface GateOptions {
169
+ /** Fail when passed/total is below this (0..1). */
170
+ minPassRate?: number;
171
+ /** Fail when the run's p95 latency is above this. */
172
+ maxP95Ms?: number;
173
+ /** block: a failed run fails the gate (default). warn: judge on pass rate and latency only. */
174
+ errorPolicy?: 'block' | 'warn';
175
+ /** Also fail when any case had an infrastructure error. */
176
+ failOnError?: boolean;
177
+ }
178
+
179
+ /**
180
+ * The same verdict as the evaluations repo's scripts/eval_gate.py, plus an
181
+ * optional fail-on-error. pass rate = passed / total, so errored and
182
+ * unevaluated cases lower it.
183
+ */
184
+ export function gateVerdict(run: EvaluationRun, opts: GateOptions): { pass: boolean; passRate: number; reasons: string[] } {
185
+ const reasons: string[] = [];
186
+ const total = run.total ?? 0;
187
+ const passRate = total ? (run.passed ?? 0) / total : 0;
188
+ if ((opts.errorPolicy ?? 'block') === 'block' && run.status === 'failed') reasons.push('run status=failed');
189
+ if (total === 0) reasons.push('no cases evaluated');
190
+ if (opts.minPassRate !== undefined && total > 0 && passRate < opts.minPassRate) {
191
+ reasons.push(`pass rate ${passRate.toFixed(3)} < ${opts.minPassRate}`);
192
+ }
193
+ const p95 = run.latency?.p95_ms;
194
+ if (opts.maxP95Ms !== undefined && typeof p95 === 'number' && p95 > opts.maxP95Ms) reasons.push(`p95 latency ${p95} > ${opts.maxP95Ms}`);
195
+ if (opts.failOnError && (run.errors ?? 0) > 0) reasons.push(`${run.errors} case(s) errored`);
196
+ return { pass: reasons.length === 0, passRate, reasons };
197
+ }
@@ -1,6 +1,13 @@
1
1
  import type { IndemnClient } from './client.js';
2
+ import { CliError } from '../core/runtime.js';
3
+ import { assertSkillNameAllowed } from './skills.js';
2
4
  import type { BotFunction, CreateFunctionInput, UpdateFunctionInput, FunctionParameter } from './types.js';
3
5
 
6
+ /** Allowed values as copilot stores them (quick-reply buttons); its parameter route parses them from a JSON string. */
7
+ function choices(values: string[]): string {
8
+ return JSON.stringify(values.map((v) => ({ title: v, payload: v, type: 'postback' })));
9
+ }
10
+
4
11
  export class FunctionsSDK {
5
12
  constructor(private client: IndemnClient) {}
6
13
 
@@ -11,6 +18,7 @@ export class FunctionsSDK {
11
18
  }
12
19
 
13
20
  async createFunction(agentId: string, input: CreateFunctionInput): Promise<BotFunction> {
21
+ assertSkillNameAllowed(input.name, input.type);
14
22
  return this.client.copilotPost<BotFunction>(
15
23
  `/organization/:org_id/ai-studio/bots/${agentId}/functions`,
16
24
  input,
@@ -18,6 +26,7 @@ export class FunctionsSDK {
18
26
  }
19
27
 
20
28
  async updateFunction(agentId: string, funcId: string, input: UpdateFunctionInput): Promise<BotFunction> {
29
+ assertSkillNameAllowed(input.name, input.type);
21
30
  return this.client.copilotPut<BotFunction>(
22
31
  `/organization/:org_id/ai-studio/bots/${agentId}/functions/${funcId}`,
23
32
  input,
@@ -56,13 +65,14 @@ export class FunctionsSDK {
56
65
  );
57
66
  }
58
67
 
59
- async addParameter(agentId: string, funcId: string, param: { name: string; type: string; description?: string; required?: boolean }): Promise<FunctionParameter> {
68
+ async addParameter(agentId: string, funcId: string, param: { name: string; type: string; description?: string; required?: boolean; allowedValues?: string[] }): Promise<FunctionParameter> {
60
69
  // Server expects an array of parameters
61
70
  const payload = [{
62
71
  name: param.name,
63
72
  type: param.type,
64
73
  description: param.description,
65
74
  is_required: param.required ?? false,
75
+ ...(param.allowedValues?.length ? { enums: choices(param.allowedValues), isEnumsEnabled: true } : {}),
66
76
  }];
67
77
  return this.client.copilotPost<FunctionParameter>(
68
78
  `/organization/:org_id/ai-studio/bots/${agentId}/functions/${funcId}/parameters`,
@@ -70,12 +80,21 @@ export class FunctionsSDK {
70
80
  );
71
81
  }
72
82
 
73
- async updateParameter(agentId: string, funcId: string, paramId: string, param: { name?: string; type?: string; description?: string; required?: boolean }): Promise<FunctionParameter> {
74
- // Server uses POST upsert (no PUT /:id endpoint exists). Include the param id for upsert matching.
83
+ async updateParameter(agentId: string, funcId: string, paramId: string, param: { name?: string; type?: string; description?: string; required?: boolean; allowedValues?: string[] }): Promise<FunctionParameter> {
84
+ // Server uses POST upsert (no PUT /:id endpoint exists) and writes every field it is sent, clearing
85
+ // allowed values left out: start from the saved parameter so only the named fields change.
86
+ const current = (await this.listParameters(agentId, funcId)).find((p) => p._id === paramId);
87
+ if (!current) throw new CliError(`Parameter ${paramId} is not on this skill.`);
75
88
  const payload = [{
76
89
  id: paramId,
77
- ...param,
78
- is_required: param.required ?? false,
90
+ name: param.name ?? current.name,
91
+ type: param.type ?? current.type,
92
+ description: param.description ?? current.description,
93
+ ...(param.description === undefined ? { descriptionOps: current.descriptionOps } : {}),
94
+ question_title: current.question_title,
95
+ is_required: param.required ?? current.is_required ?? false,
96
+ enums: param.allowedValues ? choices(param.allowedValues) : JSON.stringify(current.enums ?? []),
97
+ isEnumsEnabled: param.allowedValues ? param.allowedValues.length > 0 : (current.isEnumsEnabled ?? false),
79
98
  }];
80
99
  return this.client.copilotPost<FunctionParameter>(
81
100
  `/organization/:org_id/ai-studio/bots/${agentId}/functions/${funcId}/parameters`,
@@ -89,12 +108,39 @@ export class FunctionsSDK {
89
108
  );
90
109
  }
91
110
 
92
- async listMasterFunctions(): Promise<unknown[]> {
111
+ /** Skill templates; copilot filters by channel and assumes web when none is given (so voice ones are hidden). */
112
+ async listMasterFunctions(channels?: string[]): Promise<unknown[]> {
93
113
  return this.client.copilotGet<unknown[]>(
94
114
  '/organization/:org_id/ai-studio/master-functions',
115
+ channels?.length ? { channel: channels.join(',') } : undefined,
95
116
  );
96
117
  }
97
118
 
119
+ /**
120
+ * Ask copilot to propose skills from a document (file) or pasted text.
121
+ * Nothing is created; pass the proposal to bulkCreate after review.
122
+ */
123
+ async generateFromDoc(agentId: string, source: { filePath?: string; text?: string }): Promise<unknown> {
124
+ const path = `/organization/:org_id/ai-studio/bots/${agentId}/functions/generate-from-doc`;
125
+ if (source.filePath) {
126
+ const { readFileSync } = await import('node:fs');
127
+ const { basename } = await import('node:path');
128
+ const form = new FormData();
129
+ form.append('file', new Blob([readFileSync(source.filePath)]), basename(source.filePath));
130
+ return this.client.copilotPostForm(path, form);
131
+ }
132
+ return this.client.copilotPost(path, { text: source.text });
133
+ }
134
+
135
+ /** Create several skills at once. Names are checked first; copilot creates them without a transaction. */
136
+ async bulkCreate(agentId: string, functions: Array<Record<string, unknown>>): Promise<BotFunction[]> {
137
+ for (const f of functions) {
138
+ const def = (f.function_definition ?? f) as { name?: string; type?: string };
139
+ assertSkillNameAllowed(def.name, def.type);
140
+ }
141
+ return this.client.copilotPost<BotFunction[]>(`/organization/:org_id/ai-studio/bots/${agentId}/functions/bulk-create`, { functions });
142
+ }
143
+
98
144
  async listLLMModels(): Promise<unknown[]> {
99
145
  return this.client.copilotGet<unknown[]>(
100
146
  '/organization/:org_id/ai-studio/llm-models',
package/src/sdk/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { IndemnClient } from './client.js';
2
+ export { OrgsSDK } from './orgs.js';
3
+ export { resolveCredential, copilotAuthHeader, darwinAuthHeader, type Credential } from './credentials.js';
2
4
  export { login, logout, whoami } from './auth.js';
3
5
  export { AgentsSDK } from './agents.js';
4
6
  export { ConfigSDK } from './config.js';
@@ -0,0 +1,112 @@
1
+ import type { IndemnClient } from './client.js';
2
+ import { darwinAuthHeader } from './credentials.js';
3
+ import { CliError } from '../core/runtime.js';
4
+
5
+ export interface JarvisToolEvent {
6
+ name: string;
7
+ status: 'running' | 'success' | 'error';
8
+ error?: string;
9
+ }
10
+
11
+ export interface JarvisReply {
12
+ conversation_id: string;
13
+ reply: string;
14
+ tools: JarvisToolEvent[];
15
+ }
16
+
17
+ /** wss://<platform host>/ws, derived from the REST base (…/api/v1), or INDEMN_PLATFORM_WS_URL. */
18
+ export function jarvisSocketUrl(client: IndemnClient): string {
19
+ if (process.env.INDEMN_PLATFORM_WS_URL) return process.env.INDEMN_PLATFORM_WS_URL;
20
+ const base = client.getHosts().platform;
21
+ if (!base) throw new CliError('No V2 platform host is configured for this environment.');
22
+ const u = new URL(base);
23
+ u.protocol = u.protocol === 'http:' ? 'ws:' : 'wss:';
24
+ u.pathname = '/ws';
25
+ u.search = '';
26
+ return u.toString();
27
+ }
28
+
29
+ function userIdFromToken(token: string): string {
30
+ try {
31
+ const claims = JSON.parse(Buffer.from(token.split('.')[1] ?? '', 'base64url').toString('utf8')) as { _id?: string };
32
+ if (claims._id) return claims._id;
33
+ } catch {
34
+ // fall through
35
+ }
36
+ throw new CliError('The session token has no user id; log in again with `indemn login`.');
37
+ }
38
+
39
+ /**
40
+ * One Jarvis turn over Darwin's WebSocket, as the dashboard does it: a
41
+ * conversation from POST /conversations gives the thread; the reply streams
42
+ * as jarvis_chunk messages and ends at the first one with done: true (or at an
43
+ * error message). The session token is sent too, so the chat keeps working
44
+ * once Darwin starts checking it.
45
+ */
46
+ export async function jarvisChat(
47
+ client: IndemnClient,
48
+ message: string,
49
+ opts: { conversationId?: string; context?: Record<string, unknown>; timeoutMs?: number; onText?: (text: string) => void; onTool?: (t: JarvisToolEvent) => void } = {},
50
+ ): Promise<JarvisReply> {
51
+ const cred = client.getCredential();
52
+ if (!cred) throw new CliError('Not logged in. Run `indemn login` first.');
53
+ const authorization = darwinAuthHeader(cred);
54
+ const token = authorization.slice('Bearer '.length);
55
+ const orgId = await client.currentOrgId();
56
+ const conversationId = opts.conversationId ?? (await client.platformRequest<{ conversation_id: string }>('POST', '/conversations', {
57
+ body: { title: message.slice(0, 60) },
58
+ })).conversation_id;
59
+
60
+ const url = new URL(jarvisSocketUrl(client));
61
+ url.searchParams.set('client_id', `indemn-cli-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`);
62
+ url.searchParams.set('user_id', userIdFromToken(token));
63
+ url.searchParams.set('organization_id', orgId);
64
+
65
+ return new Promise<JarvisReply>((resolve, reject) => {
66
+ const WS = globalThis.WebSocket as unknown as new (u: string, init?: unknown) => WebSocket;
67
+ // Node's WebSocket (undici) accepts headers in its init object; browsers would ignore it.
68
+ const socket = new WS(url.toString(), { headers: { Authorization: authorization } });
69
+ let reply = '';
70
+ const tools = new Map<string, JarvisToolEvent>();
71
+ let settled = false;
72
+ const finish = (err?: Error) => {
73
+ if (settled) return;
74
+ settled = true;
75
+ clearTimeout(timer);
76
+ clearInterval(ping);
77
+ try { socket.close(); } catch { /* already closed */ }
78
+ if (err) reject(err);
79
+ else resolve({ conversation_id: conversationId, reply, tools: [...tools.values()] });
80
+ };
81
+ const timer = setTimeout(() => finish(new CliError(`Jarvis did not finish within ${Math.round((opts.timeoutMs ?? 180_000) / 1000)} s.`)), opts.timeoutMs ?? 180_000);
82
+ const ping = setInterval(() => {
83
+ try { socket.send(JSON.stringify({ type: 'ping', payload: {} })); } catch { /* not open yet */ }
84
+ }, 15_000);
85
+
86
+ socket.addEventListener('open', () => {
87
+ socket.send(JSON.stringify({ type: 'jarvis', payload: { content: message, thread_id: conversationId, ...(opts.context ? { context: opts.context } : {}) } }));
88
+ });
89
+ socket.addEventListener('message', (ev: MessageEvent) => {
90
+ let msg: { type?: string; payload?: Record<string, any> };
91
+ try { msg = JSON.parse(String(ev.data)); } catch { return; }
92
+ const p = msg.payload ?? {};
93
+ if (msg.type === 'jarvis_chunk') {
94
+ if (p.content) { reply += p.content; opts.onText?.(p.content); }
95
+ if (p.done) finish();
96
+ } else if (msg.type === 'jarvis_tool' && p.tool_call) {
97
+ // Darwin sends "start" twice per tool with different ids; key by name while running.
98
+ const name = String(p.tool_call.name ?? 'tool');
99
+ const status = p.tool_call.status === 'error' ? 'error' : p.tool_call.status === 'success' ? 'success' : 'running';
100
+ const evt: JarvisToolEvent = { name, status, ...(p.tool_call.error ? { error: String(p.tool_call.error) } : {}) };
101
+ const key = status === 'running' ? `${name}#running` : `${name}#${p.tool_call.id}`;
102
+ if (status !== 'running') tools.delete(`${name}#running`);
103
+ if (!tools.has(key)) opts.onTool?.(evt);
104
+ tools.set(key, evt);
105
+ } else if (msg.type === 'error') {
106
+ finish(new CliError(`Jarvis: ${p.message ?? 'error'}`));
107
+ }
108
+ });
109
+ socket.addEventListener('error', () => finish(new CliError('Could not reach Jarvis on the V2 platform.')));
110
+ socket.addEventListener('close', () => finish(new CliError('The Jarvis connection closed before the reply finished; any action it was taking may or may not have completed.')));
111
+ });
112
+ }
package/src/sdk/kb.ts CHANGED
@@ -26,9 +26,12 @@ export class KBSDK {
26
26
  }
27
27
 
28
28
  async createKB(input: CreateKBInput): Promise<KnowledgeBase> {
29
+ // copilot validates the type case-insensitively but stores it raw, and only
30
+ // the uppercase values (QNA, URL, FILE) are valid afterwards.
31
+ const type = (input as { type?: string }).type;
29
32
  return this.client.copilotPost<KnowledgeBase>(
30
33
  '/organization/:org_id/knowledge-bases',
31
- input,
34
+ { ...input, ...(type ? { type: type.toUpperCase() } : {}) },
32
35
  );
33
36
  }
34
37
 
@@ -74,52 +77,65 @@ export class KBSDK {
74
77
  );
75
78
  }
76
79
 
80
+ /**
81
+ * Import a website the way the dashboard does: "selected" imports the given page (the source url
82
+ * is its site), "full" crawls the whole site from url. copilot fails without the full source shape.
83
+ */
77
84
  async scrapeUrl(kbId: string, url: string, crawlMode: string = 'full'): Promise<unknown> {
85
+ const selected = crawlMode === 'selected';
86
+ let site = url;
87
+ if (selected) {
88
+ try { site = new URL(url).origin; } catch { site = url; }
89
+ }
78
90
  return this.client.copilotPost(
79
91
  `/organization/:org_id/knowledge-bases/${kbId}/import`,
80
92
  {
81
93
  type: 'url',
82
- sources: [{ url, crawl_mode: crawlMode }],
94
+ manual: false,
95
+ sources: [{ url: site, crawl_mode: selected ? 'selected' : 'full', selected_urls: selected ? [url] : [], exclude_urls: [], js_script: '' }],
83
96
  },
84
97
  );
85
98
  }
86
99
 
87
- async uploadFile(kbId: string, filePath: string): Promise<unknown> {
100
+ /**
101
+ * Add a document to a knowledge base the way the dashboard does: upload it
102
+ * to the org's file store for a signed link, then ask the knowledge base to
103
+ * import from that link. (The upload-urls route is only for a CSV of website
104
+ * addresses.) Pass qna: true for a file of question/answer rows.
105
+ */
106
+ async uploadFile(kbId: string, filePath: string, opts: { qna?: boolean } = {}): Promise<unknown> {
88
107
  const fs = await import('fs');
89
108
  const path = await import('path');
90
109
 
91
110
  if (!fs.existsSync(filePath)) {
92
111
  throw new Error(`File not found: ${filePath}`);
93
112
  }
94
-
95
- const fileBuffer = fs.readFileSync(filePath);
96
113
  const fileName = path.basename(filePath);
97
- const blob = new Blob([fileBuffer]);
114
+ const ext = path.extname(fileName).slice(1).toLowerCase();
115
+ const allowed = ['pdf', 'docx', 'doc', 'txt', 'csv', 'xls', 'xlsx', 'json'];
116
+ if (!allowed.includes(ext)) {
117
+ throw new Error(`Unsupported file type ".${ext}". Knowledge bases accept: ${allowed.join(', ')}.`);
118
+ }
119
+ const size = fs.statSync(filePath).size;
120
+ if (size > 10 * 1024 * 1024) {
121
+ throw new Error(`${fileName} is ${(size / 1024 / 1024).toFixed(1)} MB; the limit is 10 MB.`);
122
+ }
98
123
 
99
124
  const formData = new FormData();
100
- formData.append('file', blob, fileName);
101
-
102
- // Need a raw fetch since copilotPost sets Content-Type to JSON
103
- const hosts = this.client.getHosts();
104
- const orgId = this.client.getOrgId();
105
- const apiKey = this.client.getApiKey();
106
- const url_str = `${hosts.copilot}/organization/${orgId}/knowledge-bases/${kbId}/upload-urls`;
107
-
108
- const res = await fetch(url_str, {
109
- method: 'POST',
110
- headers: {
111
- 'Authorization': `Bearer ${apiKey}`,
112
- },
113
- body: formData,
114
- });
115
-
116
- if (!res.ok) {
117
- const text = await res.text();
118
- throw new Error(`Upload failed (${res.status}): ${text}`);
119
- }
125
+ formData.append('file', new Blob([fs.readFileSync(filePath)]), fileName);
126
+ const stored = await this.client.copilotPostForm<{ url?: string; signedUrl?: string; data?: { url?: string; signedUrl?: string } }>(
127
+ '/organization/:org_id/files/upload-org?generateSignedUrl=true',
128
+ formData,
129
+ );
130
+ // The import service must fetch the file, so it needs the signed link.
131
+ const url = stored?.signedUrl ?? stored?.data?.signedUrl ?? stored?.url ?? stored?.data?.url;
132
+ if (!url) throw new Error(`Uploading ${fileName} returned no file link; nothing was imported.`);
120
133
 
121
- const json = await res.json() as { data?: unknown };
122
- return json.data || json;
134
+ return this.importKB(kbId, {
135
+ type: 'file',
136
+ sources: [{ name: fileName, file_type: ext, url }],
137
+ ...(opts.qna ? { is_qna_import: true } : {}),
138
+ });
123
139
  }
124
140
 
125
141
  async updateDataSource(
@@ -139,6 +155,11 @@ export class KBSDK {
139
155
  );
140
156
  }
141
157
 
158
+ /** Knowledge bases linked to an agent: [{ id (KB id), name, type, connectedBots }]. */
159
+ async listMappings(agentId: string): Promise<Array<{ id: string; name?: string; type?: string; connectedBots?: Array<{ id: string; id_mapping: string }> }>> {
160
+ return this.client.copilotGet(`/organization/:org_id/ai-studio/bots/${agentId}/mappings`);
161
+ }
162
+
142
163
  async linkKB(agentId: string, kbIds: string[]): Promise<unknown> {
143
164
  return this.client.copilotPost(
144
165
  `/organization/:org_id/ai-studio/bots/${agentId}/mappings`,
@@ -175,9 +196,75 @@ export class KBSDK {
175
196
  }
176
197
 
177
198
  async importKB(kbId: string, data: unknown): Promise<unknown> {
178
- return this.client.copilotPost(
199
+ const result = await this.client.copilotPost<{ status?: string; message?: string }>(
179
200
  `/organization/:org_id/knowledge-bases/${kbId}/import`,
180
201
  data,
181
202
  );
203
+ assertJobStarted(result, 'Import');
204
+ return result;
205
+ }
206
+
207
+ /** The text chunks the agent actually retrieves from one data source. */
208
+ async chunks(kbId: string, sourceId: string): Promise<{ id: string; type?: string; source?: string; status?: string; stats?: unknown; chunks: string[] }> {
209
+ return this.client.copilotGet(`/organization/:org_id/knowledge-bases/${kbId}/data-source/${sourceId}/chunks`);
210
+ }
211
+
212
+ /** Crawl a URL source again (asynchronous). URL sources only. */
213
+ async reimport(kbId: string, sourceId: string, jsScript?: string): Promise<unknown> {
214
+ const result = await this.client.copilotPut<{ status?: string; message?: string }>(
215
+ `/organization/:org_id/knowledge-bases/${kbId}/data-source/${sourceId}/re-import`,
216
+ jsScript ? { js_script: jsScript } : {},
217
+ );
218
+ assertJobStarted(result, 'Re-import');
219
+ return result;
220
+ }
221
+
222
+ /**
223
+ * Crawl and file-import status. copilot has no status route, so this reads
224
+ * the KB's non-manual sources and their scrape jobs.
225
+ */
226
+ async scrapeStatus(kbId: string): Promise<Array<{ id?: string; source?: string; status?: string; job_status?: string; error?: string; completed_at?: string }>> {
227
+ const kb = await this.getKB(kbId) as unknown as { fileWebSources?: Array<Record<string, any>> };
228
+ return (kb.fileWebSources ?? []).map((s) => ({
229
+ id: s._id ? String(s._id) : undefined,
230
+ source: s.source,
231
+ status: s.status,
232
+ job_status: s.source_details?.id_scrape_request?.status,
233
+ error: s.error_message ?? s.source_details?.id_scrape_request?.message,
234
+ completed_at: s.source_details?.id_scrape_request?.completed_at ?? undefined,
235
+ }));
236
+ }
237
+
238
+ /** Questions the agents could not answer (0-based pages). */
239
+ async gaps(opts: { agentId?: string; kbId?: string; page?: number; limit?: number; includeTest?: boolean; dismissed?: boolean; resolved?: boolean } = {}): Promise<unknown> {
240
+ const query: Record<string, string> = {
241
+ page: String(opts.page ?? 0),
242
+ limit: String(Math.min(opts.limit ?? 20, 100)),
243
+ isDismissed: String(Boolean(opts.dismissed)),
244
+ isTestMode: String(Boolean(opts.includeTest)),
245
+ };
246
+ if (opts.agentId) query.id_bot = opts.agentId;
247
+ if (opts.kbId) query.id_kb = opts.kbId;
248
+ if (opts.resolved) query.view = 'resolved';
249
+ return this.client.copilotGet('/organization/:org_id/knowledge-bases/unanswered/embeddings', query);
250
+ }
251
+
252
+ /** Unanswered questions grouped by theme, most frequent first. */
253
+ async gapClusters(opts: { status?: 'open' | 'resolved' | 'dismissed'; days?: number; page?: number; limit?: number } = {}): Promise<unknown> {
254
+ const query: Record<string, string> = { status: opts.status ?? 'open', page: String(opts.page ?? 0), limit: String(Math.min(opts.limit ?? 50, 100)) };
255
+ if (opts.days) query.days = String(opts.days);
256
+ return this.client.copilotGet('/organization/:org_id/knowledge-bases/unanswered/clusters', query);
257
+ }
258
+
259
+ /** Could the org's knowledge answer this question? Returns the best matching chunks. */
260
+ async check(question: string): Promise<{ answerable?: boolean; top_score?: number; sources?: unknown[] }> {
261
+ return this.client.copilotGet('/organization/:org_id/knowledge-bases/unanswered/kb-check', { query: question });
262
+ }
263
+ }
264
+
265
+ /** copilot answers import and re-import with success:true even when they fail; the job status tells the truth. */
266
+ function assertJobStarted(result: { status?: string; message?: string } | undefined, what: string): void {
267
+ if (result && result.status === 'error') {
268
+ throw new Error(`${what} failed: ${result.message ?? 'no reason given'}`);
182
269
  }
183
270
  }
@@ -0,0 +1,62 @@
1
+ import { IndemnClient } from './client.js';
2
+ import type { Channel, OrgMember, OrgSummary } from './types.js';
3
+
4
+ interface RawProjectUser {
5
+ role?: string;
6
+ status?: string;
7
+ id_user?: { _id?: string; email?: string; firstname?: string; lastname?: string } | string | null;
8
+ id_project?: { name?: string } | string | null;
9
+ }
10
+
11
+ export class OrgsSDK {
12
+ constructor(private client: IndemnClient) {}
13
+
14
+ /** Orgs the logged-in person belongs to or created. Also refreshes the list saved at login. */
15
+ async listOrganizations(): Promise<OrgSummary[]> {
16
+ const res = await this.client.copilotGet<{ userOrganizations?: Array<{ _id: string; name: string }> }>('/organization');
17
+ const orgs = (res.userOrganizations ?? []).map((o) => ({ id: String(o._id), name: o.name }));
18
+ if (this.client.getCredential()?.source === 'config') IndemnClient.saveConfig({ orgs });
19
+ return orgs;
20
+ }
21
+
22
+ /** People with owner, admin or agent roles in the org, one row per person. */
23
+ async listMembers(): Promise<OrgMember[]> {
24
+ let rows: RawProjectUser[];
25
+ try {
26
+ const res = await this.client.copilotGet<{ projectUsers?: RawProjectUser[] }>('/organization/:org_id/projects-users');
27
+ rows = res.projectUsers ?? [];
28
+ } catch (err) {
29
+ // copilot answers "not found" with success:false when the org has no such members.
30
+ if (err instanceof Error && /not found for the given organization/i.test(err.message)) return [];
31
+ throw err;
32
+ }
33
+ const byUser = new Map<string, OrgMember>();
34
+ for (const row of rows) {
35
+ const user = typeof row.id_user === 'object' && row.id_user ? row.id_user : { _id: String(row.id_user ?? '') };
36
+ const id = String(user._id ?? '');
37
+ if (!id) continue;
38
+ const project = typeof row.id_project === 'object' && row.id_project ? row.id_project.name : undefined;
39
+ const existing = byUser.get(id);
40
+ if (existing) {
41
+ if (project && !existing.projects.includes(project)) existing.projects.push(project);
42
+ continue;
43
+ }
44
+ const name = [('firstname' in user ? user.firstname : ''), ('lastname' in user ? user.lastname : '')].filter(Boolean).join(' ');
45
+ byUser.set(id, {
46
+ user_id: id,
47
+ email: 'email' in user ? user.email : undefined,
48
+ name: name || undefined,
49
+ role: row.role,
50
+ status: row.status,
51
+ projects: project ? [project] : [],
52
+ });
53
+ }
54
+ return [...byUser.values()];
55
+ }
56
+
57
+ /** Channels an agent can be published on (WEB, VOICE, ...). */
58
+ async listChannels(): Promise<Channel[]> {
59
+ const channels = await this.client.copilotGet<Channel[]>('/organization/:org_id/ai-studio/channels');
60
+ return channels.map((c) => ({ _id: String(c._id), name: c.name, type: c.type, description: c.description }));
61
+ }
62
+ }