@indemn/cli 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +109 -38
  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 +105 -39
  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
@@ -0,0 +1,245 @@
1
+ import type { IndemnClient } from './client.js';
2
+ import { AgentsSDK } from './agents.js';
3
+ import { ConfigSDK } from './config.js';
4
+ import { FunctionsSDK } from './functions.js';
5
+ import { transferDestination } from './skills.js';
6
+ import { modelForAgent } from './config-change.js';
7
+ import { KBSDK } from './kb.js';
8
+ import { CliError, EXIT } from '../core/runtime.js';
9
+ import type { Agent, BotFunction, FunctionParameter } from './types.js';
10
+
11
+ export const BUNDLE_FORMAT = 'indemn-agent/1';
12
+ const REDACTED = '<redacted>';
13
+ const SECRET_KEY = /(api[-_]?key|token|secret|password|passwd|authorization|auth|bearer|credential|private[-_]?key|client[-_]?secret)/i;
14
+ /** Web addresses that are secrets themselves: webhooks (anyone holding one can post), and URLs with a key in the query. */
15
+ const SECRET_URL = /^https?:\/\/\S*(hooks\.slack\.com\/|hooks\.zapier\.com\/|discord(app)?\.com\/api\/webhooks\/|\/webhooks?\/|[?&](api[-_]?key|key|token|secret|sig|signature|access_token|password)=)/i;
16
+
17
+ /** Replace every value under a secret-looking key, and every secret web address, at any depth. */
18
+ export function redactSecrets<T>(value: T): { value: T; redacted: string[] } {
19
+ const redacted: string[] = [];
20
+ const walk = (v: unknown, path: string): unknown => {
21
+ if (Array.isArray(v)) return v.map((x, i) => walk(x, `${path}[${i}]`));
22
+ if (v && typeof v === 'object') {
23
+ const out: Record<string, unknown> = {};
24
+ for (const [k, x] of Object.entries(v as Record<string, unknown>)) {
25
+ const p = path ? `${path}.${k}` : k;
26
+ if ((SECRET_KEY.test(k) && x !== null && typeof x !== 'object' && x !== '') || (typeof x === 'string' && SECRET_URL.test(x))) {
27
+ out[k] = REDACTED;
28
+ redacted.push(p);
29
+ } else {
30
+ out[k] = walk(x, p);
31
+ }
32
+ }
33
+ return out;
34
+ }
35
+ return v;
36
+ };
37
+ return { value: walk(value, '') as T, redacted };
38
+ }
39
+
40
+ export interface AgentBundle {
41
+ format: typeof BUNDLE_FORMAT;
42
+ exported_at: string;
43
+ source: { agent_id: string; org_id: string; environment: string };
44
+ agent: { name: string; description?: string; channels: string[] };
45
+ configuration: { system_prompt?: string; first_message?: unknown; llm_config?: { provider?: string; model?: string; parameters?: unknown } };
46
+ skills: Array<{ type: string; name?: string; description?: string; configurationSchema?: Record<string, unknown>; parameters: Array<Omit<FunctionParameter, '_id'>> }>;
47
+ knowledge_base_ids: string[];
48
+ redacted: string[];
49
+ }
50
+
51
+ function channelNames(channels: unknown[] | undefined): string[] {
52
+ return (channels ?? []).map((c) => (typeof c === 'string' ? c : String((c as Record<string, unknown>).name ?? (c as Record<string, unknown>).type ?? c)));
53
+ }
54
+
55
+ /**
56
+ * Everything needed to rebuild an agent elsewhere: name, channels, prompt,
57
+ * greeting, model, skills with parameters, and the ids of linked knowledge
58
+ * bases. Secrets inside skill settings are redacted unless asked otherwise.
59
+ */
60
+ export async function exportAgent(client: IndemnClient, agentId: string, opts: { includeSecrets?: boolean } = {}): Promise<AgentBundle> {
61
+ const agents = new AgentsSDK(client);
62
+ const functions = new FunctionsSDK(client);
63
+ const agent = await agents.getAgent(agentId);
64
+ const config = await new ConfigSDK(client).getConfig(agentId);
65
+ const skills: BotFunction[] = await functions.listFunctions(agentId);
66
+ const withParams = [];
67
+ for (const f of skills) {
68
+ const params = await functions.listParameters(agentId, f._id).catch(() => [] as FunctionParameter[]);
69
+ withParams.push({
70
+ type: f.type,
71
+ name: f.name,
72
+ description: f.description,
73
+ configurationSchema: (f as unknown as { configurationSchema?: Record<string, unknown> }).configurationSchema,
74
+ parameters: params.map(({ _id, ...rest }) => rest),
75
+ });
76
+ }
77
+ const mappings = await new KBSDK(client).listMappings(agentId).catch(() => [] as Array<{ id: string }>);
78
+ const llm = (config as { llm_config?: { provider?: string; model?: string; parameters?: unknown } }).llm_config;
79
+ const bundle: AgentBundle = {
80
+ format: BUNDLE_FORMAT,
81
+ exported_at: new Date().toISOString(),
82
+ source: { agent_id: agentId, org_id: await client.currentOrgId(), environment: client.getEnvironment() },
83
+ agent: { name: agent.name, description: agent.description, channels: channelNames(agent.channels as unknown[] | undefined) },
84
+ configuration: {
85
+ system_prompt: config.ai_config?.system_prompt,
86
+ first_message: config.first_message,
87
+ llm_config: llm ? { provider: llm.provider, model: llm.model, parameters: llm.parameters } : undefined,
88
+ },
89
+ skills: withParams,
90
+ knowledge_base_ids: mappings.map((m) => String(m.id ?? '')).filter(Boolean),
91
+ redacted: [],
92
+ };
93
+ if (opts.includeSecrets) return bundle;
94
+ const { value, redacted } = redactSecrets(bundle.skills);
95
+ return { ...bundle, skills: value, redacted };
96
+ }
97
+
98
+ export interface ImportReport {
99
+ agent: Agent;
100
+ skills_created: number;
101
+ /** Built-in skills (FAQ retriever, handoff) the new agent already had; their settings were copied over. */
102
+ built_in_skills_reused: string[];
103
+ skills_failed: Array<{ name?: string; error: string }>;
104
+ knowledge_bases_linked: string[];
105
+ knowledge_bases_skipped: string[];
106
+ needs_secrets: string[];
107
+ }
108
+
109
+ /**
110
+ * Create a new agent in the current org from a bundle. Skills that carry
111
+ * redacted secrets are still created; the report lists what must be filled in.
112
+ */
113
+ export async function importAgent(
114
+ client: IndemnClient,
115
+ bundle: AgentBundle,
116
+ opts: { name?: string; allowDuplicate?: boolean; linkKnowledgeBases?: boolean } = {},
117
+ ): Promise<ImportReport> {
118
+ if (bundle.format !== BUNDLE_FORMAT) throw new CliError(`Not an agent bundle (format ${String(bundle.format)}; expected ${BUNDLE_FORMAT}).`);
119
+ const agents = new AgentsSDK(client);
120
+ const functions = new FunctionsSDK(client);
121
+ const name = opts.name ?? bundle.agent.name;
122
+ if (!opts.allowDuplicate && (await agents.findByName(name)).length) {
123
+ throw new CliError(`An agent named "${name}" already exists in this org. Pass a new name, or allow a duplicate explicitly.`, EXIT.NEEDS_INPUT);
124
+ }
125
+ const agent = await agents.createAgent({ name, channels: bundle.agent.channels.length ? bundle.agent.channels : ['WEB'] });
126
+ if (bundle.agent.description) await agents.updateAgent(agent._id, { description: bundle.agent.description });
127
+
128
+ const config: Record<string, unknown> = {};
129
+ if (bundle.configuration.system_prompt !== undefined) config.system_prompt = bundle.configuration.system_prompt;
130
+ if (bundle.configuration.first_message) config.first_message = JSON.stringify(bundle.configuration.first_message);
131
+ if (bundle.configuration.llm_config?.model) config.llm_config = bundle.configuration.llm_config;
132
+ if (Object.keys(config).length) await new ConfigSDK(client).setConfig(agent._id, config);
133
+
134
+ const report: ImportReport = {
135
+ agent, skills_created: 0, built_in_skills_reused: [], skills_failed: [], knowledge_bases_linked: [], knowledge_bases_skipped: [], needs_secrets: bundle.redacted,
136
+ };
137
+ // A new agent comes with the built-in skills; creating them again duplicates them (or fails).
138
+ const builtIns = (await functions.listFunctions(agent._id)).filter((f) => (f as { systemDeveloped?: boolean }).systemDeveloped);
139
+ for (const skill of bundle.skills) {
140
+ try {
141
+ // Call transfers are saved the way the dashboard saves them (older exports carry the destination elsewhere).
142
+ const config = skill.type === 'transfer_call' && skill.configurationSchema ? transferDestination({}, skill.configurationSchema) : skill.configurationSchema;
143
+ const builtIn = builtIns.find((f) => f.type === skill.type);
144
+ if (builtIn) {
145
+ if (config) {
146
+ await functions.updateFunction(agent._id, builtIn._id, {
147
+ type: skill.type, description: skill.description ?? builtIn.description ?? '', configurationSchema: config,
148
+ });
149
+ }
150
+ builtIns.splice(builtIns.indexOf(builtIn), 1);
151
+ report.built_in_skills_reused.push(skill.name ?? skill.type);
152
+ continue;
153
+ }
154
+ const created = await functions.createFunction(agent._id, { type: skill.type, name: skill.name, description: skill.description });
155
+ if (config) {
156
+ await functions.updateFunction(agent._id, created._id, {
157
+ type: skill.type, description: skill.description ?? '', configurationSchema: config,
158
+ });
159
+ }
160
+ for (const p of skill.parameters) {
161
+ const allowedValues = p.isEnumsEnabled ? (p.enums ?? []).map((e) => e.payload ?? e.title ?? '').filter(Boolean) : undefined;
162
+ await functions.addParameter(agent._id, created._id, { name: p.name, type: p.type, description: p.description, required: p.is_required, allowedValues });
163
+ }
164
+ report.skills_created++;
165
+ } catch (err) {
166
+ report.skills_failed.push({ name: skill.name, error: err instanceof Error ? err.message : String(err) });
167
+ }
168
+ }
169
+ if (opts.linkKnowledgeBases && bundle.knowledge_base_ids.length) {
170
+ const kb = new KBSDK(client);
171
+ for (const id of bundle.knowledge_base_ids) {
172
+ try {
173
+ await kb.linkKB(agent._id, [id]);
174
+ report.knowledge_bases_linked.push(id);
175
+ } catch {
176
+ report.knowledge_bases_skipped.push(id);
177
+ }
178
+ }
179
+ } else {
180
+ report.knowledge_bases_skipped.push(...bundle.knowledge_base_ids);
181
+ }
182
+ return report;
183
+ }
184
+
185
+ export interface AgentSkeleton {
186
+ name: string;
187
+ channels?: string[];
188
+ /** What the agent is for; stored as the agent description. */
189
+ description?: string;
190
+ system_prompt?: string;
191
+ model?: { provider: string; model: string };
192
+ }
193
+
194
+ /** Fields a new agent should have before it is created; the caller asks for the missing ones. */
195
+ export function missingSkeletonFields(s: Partial<AgentSkeleton>): Array<keyof AgentSkeleton> {
196
+ const missing: Array<keyof AgentSkeleton> = [];
197
+ if (!s.name?.trim()) missing.push('name');
198
+ if (!s.description?.trim()) missing.push('description');
199
+ if (!s.system_prompt?.trim()) missing.push('system_prompt');
200
+ return missing;
201
+ }
202
+
203
+ /**
204
+ * Create an agent with its basics in one go: refuse a duplicate name, create
205
+ * it, then set the purpose, prompt and model. Returns what was set.
206
+ */
207
+ export async function createAgentFromSkeleton(
208
+ client: IndemnClient,
209
+ s: AgentSkeleton,
210
+ opts: { allowDuplicate?: boolean } = {},
211
+ ): Promise<{ agent: Agent; set: string[] }> {
212
+ const agents = new AgentsSDK(client);
213
+ const name = s.name.trim();
214
+ if (!name) throw new CliError('An agent needs a name.', EXIT.NEEDS_INPUT);
215
+ if (!opts.allowDuplicate) {
216
+ const same = await agents.findByName(name);
217
+ if (same.length) {
218
+ throw new CliError(
219
+ `An agent named "${name}" already exists in this org (${same.map((a) => a._id).join(', ')}). Choose another name or allow a duplicate explicitly.`,
220
+ EXIT.NEEDS_INPUT,
221
+ );
222
+ }
223
+ }
224
+ const agent = await agents.createAgent({ name, channels: s.channels?.length ? s.channels : ['WEB'] });
225
+ const set = ['name', 'channels'];
226
+ if (s.description?.trim()) {
227
+ await agents.updateAgent(agent._id, { description: s.description.trim() });
228
+ set.push('description');
229
+ }
230
+ const config: Record<string, unknown> = {};
231
+ if (s.system_prompt?.trim()) config.system_prompt = s.system_prompt;
232
+ if (s.model) config.llm_config = await modelForAgent(client, s.model, s.channels ?? ['WEB']);
233
+ if (Object.keys(config).length) {
234
+ await new ConfigSDK(client).setConfig(agent._id, config);
235
+ set.push(...Object.keys(config).map((k) => (k === 'llm_config' ? 'model' : k)));
236
+ }
237
+ return { agent, set };
238
+ }
239
+
240
+ /** "openai/gpt-4.1" -> { provider: "openai", model: "gpt-4.1" }. */
241
+ export function parseModel(value: string): { provider: string; model: string } {
242
+ const i = value.indexOf('/');
243
+ if (i <= 0 || i === value.length - 1) throw new CliError(`Model must look like provider/model, for example openai/gpt-4.1 (got "${value}").`, EXIT.NEEDS_INPUT);
244
+ return { provider: value.slice(0, i), model: value.slice(i + 1) };
245
+ }
package/src/sdk/agents.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { IndemnClient } from './client.js';
2
+ import { CliError } from '../core/runtime.js';
2
3
  import type { Agent, CreateAgentInput, UpdateAgentInput } from './types.js';
3
4
 
4
5
  export class AgentsSDK {
@@ -14,14 +15,17 @@ export class AgentsSDK {
14
15
  const result = await this.client.copilotGet<Agent | Agent[]>(
15
16
  `/organization/:org_id/ai-studio/v2/bots/${id}`,
16
17
  );
17
- // v2 endpoint returns array even for single bot
18
- return Array.isArray(result) ? result[0] : result;
18
+ // v2 endpoint returns array even for single bot; an empty one means deleted or not in this org
19
+ const agent = Array.isArray(result) ? result[0] : result;
20
+ if (!agent) throw new CliError(`Agent ${id} was not found in this organization (deleted, or in another org).`);
21
+ return agent;
19
22
  }
20
23
 
21
24
  async createAgent(input: CreateAgentInput): Promise<Agent> {
22
25
  return this.client.copilotPost<Agent>(
23
26
  '/organization/:org_id/ai-studio/bots',
24
- { name: input.name, channels: input.channels || ['WEB'] },
27
+ // copilot matches channel names exactly (WEB, VOICE, EMAIL) and fails on "voice".
28
+ { name: input.name, channels: (input.channels || ['WEB']).map((c) => c.toUpperCase()) },
25
29
  );
26
30
  }
27
31
 
@@ -38,10 +42,75 @@ export class AgentsSDK {
38
42
  );
39
43
  }
40
44
 
41
- async cloneAgent(id: string): Promise<Agent> {
45
+ /** Agents in this org with the same name (case and surrounding spaces ignored). */
46
+ async findByName(name: string): Promise<Agent[]> {
47
+ const wanted = name.trim().toLowerCase();
48
+ return (await this.listAgents()).filter((a) => !a.trashed && (a.name ?? '').trim().toLowerCase() === wanted);
49
+ }
50
+
51
+ /**
52
+ * Copy an agent within the current org. copilot's clone route accepts a
53
+ * source from any org, so the source is first fetched through the org
54
+ * (copilot refuses an agent of another org) before cloning.
55
+ */
56
+ async cloneAgent(id: string, opts: { name?: string; channels?: string[] } = {}): Promise<Agent> {
57
+ const source = await this.getAgent(id);
58
+ if (!source) throw new Error(`Agent ${id} is not in this org.`);
42
59
  return this.client.copilotPost<Agent>(
43
60
  '/organization/:org_id/ai-studio/v2/bots/clone',
44
- { source_id_bot: id },
61
+ { source_id_bot: id, ...(opts.name ? { agent_name: opts.name } : {}), ...(opts.channels ? { channels: opts.channels.map((c) => c.toUpperCase()) } : {}) },
45
62
  );
46
63
  }
47
64
  }
65
+
66
+ export type Lifecycle = 'prototype' | 'beta' | 'published';
67
+
68
+ /** An agent as the V2 platform (Darwin) describes it: lifecycle, owner, tools and knowledge. */
69
+ export interface AgentDetail {
70
+ id: string;
71
+ name: string;
72
+ lifecycle?: Lifecycle;
73
+ owner?: { id: string; firstname?: string; lastname?: string; email?: string } | null;
74
+ owner_id?: string;
75
+ lifecycle_history?: Array<{ from_stage?: string; to_stage?: string; changed_by?: string; changed_at?: string }>;
76
+ [key: string]: unknown;
77
+ }
78
+
79
+ function isoDay(d: Date): string {
80
+ return d.toISOString().slice(0, 10);
81
+ }
82
+
83
+ /**
84
+ * Agent operations that live on the V2 platform or in analytics. Lifecycle
85
+ * and ownership go through Darwin because it checks the caller is the owner
86
+ * or an org admin; copilot only checks membership.
87
+ */
88
+ export class AgentInsightsSDK {
89
+ constructor(private client: IndemnClient) {}
90
+
91
+ detail(id: string): Promise<AgentDetail> {
92
+ return this.client.platformRequest<AgentDetail>('GET', `/v1-agents/${id}`);
93
+ }
94
+
95
+ setLifecycle(id: string, lifecycle: Lifecycle): Promise<AgentDetail> {
96
+ return this.client.platformRequest<AgentDetail>('PATCH', `/v1-agents/${id}/lifecycle`, { body: { lifecycle } });
97
+ }
98
+
99
+ transferOwner(id: string, ownerId: string): Promise<AgentDetail> {
100
+ return this.client.platformRequest<AgentDetail>('PATCH', `/v1-agents/${id}/owner`, { body: { owner_id: ownerId } });
101
+ }
102
+
103
+ /** A compact description an AI can read before acting: `text` is one paragraph, otherwise skills and knowledge are listed. */
104
+ summary(id: string, opts: { text?: boolean } = {}): Promise<unknown> {
105
+ return this.client.platformRequest('GET', `/v1-agents/${id}/summary${opts.text ? '/text' : ''}`);
106
+ }
107
+
108
+ /** Conversation metrics for one agent (org analytics filtered by agent). Default: the last 30 days. */
109
+ stats(id: string, opts: { from?: string; to?: string; channel?: 'web' | 'voice' | 'all' } = {}): Promise<unknown> {
110
+ const to = opts.to ?? isoDay(new Date());
111
+ const from = opts.from ?? isoDay(new Date(Date.now() - 30 * 86_400_000));
112
+ return this.client.copilotGet('/organization/:org_id/analytics/metrics', {
113
+ agent_id: id, date_from: from, date_to: to, channel: opts.channel ?? 'all',
114
+ });
115
+ }
116
+ }
@@ -0,0 +1,220 @@
1
+ import type { IndemnClient } from './client.js';
2
+ import { CliError, EXIT } from '../core/runtime.js';
3
+
4
+ export interface AnalyticsRange {
5
+ from?: string;
6
+ to?: string;
7
+ channel?: 'web' | 'voice' | 'all';
8
+ agentId?: string;
9
+ }
10
+
11
+ function day(d: Date): string {
12
+ return d.toISOString().slice(0, 10);
13
+ }
14
+
15
+ /** YYYY-MM-DD range, default the last 30 days; copilot refuses spans over 365 days. */
16
+ export function rangeQuery(r: AnalyticsRange, opts: { withChannel?: boolean; withAgent?: boolean } = { withChannel: true, withAgent: true }): Record<string, string> {
17
+ const to = r.to ?? day(new Date());
18
+ const from = r.from ?? day(new Date(Date.parse(to) - 30 * 86_400_000));
19
+ const span = (Date.parse(to) - Date.parse(from)) / 86_400_000;
20
+ if (Number.isNaN(span) || span < 0) throw new CliError('Dates must be YYYY-MM-DD with from before to.', EXIT.NEEDS_INPUT);
21
+ if (span > 365) throw new CliError('Analytics cover at most 365 days.', EXIT.NEEDS_INPUT);
22
+ const q: Record<string, string> = { date_from: from, date_to: to };
23
+ if (opts.withChannel !== false) q.channel = r.channel ?? 'all';
24
+ if (opts.withAgent !== false && r.agentId) q.agent_id = r.agentId;
25
+ return q;
26
+ }
27
+
28
+ export type MetricKind = 'metrics' | 'model-performance' | 'model-performance/bots' | 'agent-volume' | 'handoff-precursors' | 'critical-feedback';
29
+
30
+ /** The numbers the dashboard's analytics pages show. Test conversations are excluded by copilot. */
31
+ export class AnalyticsSDK {
32
+ constructor(private client: IndemnClient) {}
33
+
34
+ get(kind: MetricKind, range: AnalyticsRange = {}): Promise<unknown> {
35
+ const opts = {
36
+ withChannel: !['model-performance/bots', 'agent-volume'].includes(kind),
37
+ withAgent: !['model-performance/bots', 'agent-volume'].includes(kind),
38
+ };
39
+ return this.client.copilotGet(`/organization/:org_id/analytics/${kind}`, rangeQuery(range, opts));
40
+ }
41
+
42
+ /** Conversations behind a number, for example all with a given outcome (1-based pages, at most 200 rows). */
43
+ conversations(range: AnalyticsRange & { dimension?: string; value?: string; search?: string; page?: number; pageSize?: number }): Promise<unknown> {
44
+ const q = rangeQuery(range);
45
+ if (range.dimension) q.dimension = range.dimension;
46
+ if (range.value) q.value = range.value;
47
+ if (range.search) q.search = range.search;
48
+ q.page = String(range.page ?? 1);
49
+ q.page_size = String(Math.min(range.pageSize ?? 50, 200));
50
+ return this.client.copilotGet('/organization/:org_id/analytics/conversations/list', q);
51
+ }
52
+
53
+ charts(): Promise<{ charts?: unknown[] }> {
54
+ return this.client.copilotGet('/organization/:org_id/custom-charts');
55
+ }
56
+
57
+ chart(id: string, range: AnalyticsRange = {}): Promise<unknown> {
58
+ return this.client.copilotGet(`/organization/:org_id/custom-charts/${id}/data`, rangeQuery(range));
59
+ }
60
+ }
61
+
62
+ export interface BusinessHours {
63
+ businessHoursEnabled: boolean;
64
+ businessHours?: Array<{ day: number; slots: unknown[] }>;
65
+ businessHoursUtterance?: string;
66
+ }
67
+
68
+ /**
69
+ * Org settings the dashboard lets members change: timezone, business hours,
70
+ * holidays. Handoff settings are read-only over the API. The route that
71
+ * deletes the whole configuration is deliberately never called.
72
+ */
73
+ export class OrgSettingsSDK {
74
+ constructor(private client: IndemnClient) {}
75
+
76
+ get(): Promise<Record<string, unknown>> {
77
+ return this.client.copilotGet('/organization/:org_id/configurations');
78
+ }
79
+
80
+ /** Whether the org is inside business hours now. */
81
+ availability(): Promise<unknown> {
82
+ return this.client.copilotGet('/organization/:org_id/configurations/availability');
83
+ }
84
+
85
+ async setTimezone(timezone: string): Promise<unknown> {
86
+ try {
87
+ new Intl.DateTimeFormat('en', { timeZone: timezone });
88
+ } catch {
89
+ throw new CliError(`"${timezone}" is not a time zone name, for example America/New_York.`, EXIT.NEEDS_INPUT);
90
+ }
91
+ return this.client.copilotRequestRaw('PATCH', '/organization/:org_id/configurations', { timezone });
92
+ }
93
+
94
+ async setBusinessHours(hours: BusinessHours): Promise<unknown> {
95
+ for (const d of hours.businessHours ?? []) {
96
+ if (!Number.isInteger(d.day) || d.day < 0 || d.day > 6) throw new CliError('Each business-hours day must be 0 (Sunday) to 6.', EXIT.NEEDS_INPUT);
97
+ }
98
+ return this.client.copilotRequestRaw('PATCH', '/organization/:org_id/configurations/business-hours', hours);
99
+ }
100
+
101
+ async addHolidays(holidays: Array<{ name: string; date: string; type?: string; holidayFallbackUtterance?: string }>): Promise<unknown> {
102
+ if (!holidays.length) throw new CliError('Give at least one holiday.', EXIT.NEEDS_INPUT);
103
+ return this.client.copilotRequestRaw('PATCH', '/organization/:org_id/configurations/holidays', { holidays });
104
+ }
105
+
106
+ removeHolidays(dates: string[]): Promise<unknown> {
107
+ return this.client.copilotRequestRaw('DELETE', '/organization/:org_id/configurations/holidays', { holidays: dates.map((date) => ({ date })) });
108
+ }
109
+
110
+ /** Indemn-only feature switches; copilot refuses anyone without an @indemn.ai login. */
111
+ setAdminSettings(settings: Record<string, unknown>): Promise<unknown> {
112
+ return this.client.copilotRequestRaw('PATCH', '/organization/:org_id/configurations/admin-settings', settings);
113
+ }
114
+
115
+ /** An Admin-tab change checked as the page checks it; reads the saved disclosure only when the change touches it. */
116
+ async planAdmin(input: Record<string, unknown>): Promise<AdminPlan> {
117
+ const touchesDisclosure = input.web_ai_disclosure !== undefined || input.web_ai_disclosure_text !== undefined;
118
+ return planAdminSettings(input, touchesDisclosure ? await this.get() : {});
119
+ }
120
+ }
121
+
122
+ /** The switches on the dashboard's Settings → Admin tab (org-settings.tsx), by the name the MCP tool takes. */
123
+ export const ADMIN_SWITCHES = {
124
+ conversation_summaries: { key: 'summaryEnabled', label: 'AI Conversation Summaries' },
125
+ canned_responses: { key: 'cannedResponsesEnabled', label: 'Canned Responses' },
126
+ indemn_branding: { key: 'displayBranding', label: 'Show Indemn Branding' },
127
+ underwriting_portal: { key: 'enableUnderwritingPortal', label: 'Underwriting Portal (Intake Manager)' },
128
+ smart_inbox: { key: 'enableSmartInbox', label: 'Smart Inbox' },
129
+ solutions: { key: 'enableSolutions', label: 'Solutions' },
130
+ tickets: { key: 'enableTickets', label: 'Tickets' },
131
+ agent_testing: { key: 'enableAgentTesting', label: 'Agent Testing Mode' },
132
+ composer_dictation: { key: 'enableComposerDictation', label: 'Composer Dictation' },
133
+ web_ai_disclosure: { key: 'showAiDisclosureWeb', label: 'Show Web AI Disclosure' },
134
+ } as const;
135
+
136
+ /** The conversation timers, in minutes here; copilot stores days and refuses less than these minimums (orgConfig.js). */
137
+ export const ADMIN_TIMERS = {
138
+ ongoing_to_ended_minutes: { key: 'unassignedInactiveDays', label: 'Ongoing → Ended', min: 1 },
139
+ ended_to_resolved_minutes: { key: 'endedToResolvedDays', label: 'Ended → Resolved', min: 10 },
140
+ resolved_to_archived_minutes: { key: 'resolvedToArchivedDays', label: 'Resolved → Archived', min: 1 },
141
+ } as const;
142
+
143
+ export interface AdminPlan {
144
+ /** The admin-settings body: real booleans, days for the timers, null for "the default". */
145
+ body: Record<string, unknown>;
146
+ /** The changes in the page's words, for the approval. */
147
+ changes: string[];
148
+ }
149
+
150
+ function duration(minutes: number): string {
151
+ const [n, unit] = minutes % 1440 === 0 ? [minutes / 1440, 'day'] : minutes % 60 === 0 ? [minutes / 60, 'hour'] : [minutes, 'minute'];
152
+ return `${n} ${unit}${n === 1 ? '' : 's'}`;
153
+ }
154
+
155
+ /** Refuses what the Admin tab refuses: nothing to change, a timer under its minimum, a disclosure shown without text. */
156
+ export function planAdminSettings(input: Record<string, unknown>, saved: Record<string, unknown> = {}): AdminPlan {
157
+ const body: Record<string, unknown> = {};
158
+ const changes: string[] = [];
159
+ for (const [name, s] of Object.entries(ADMIN_SWITCHES)) {
160
+ if (typeof input[name] !== 'boolean') continue;
161
+ body[s.key] = input[name];
162
+ changes.push(`turn ${input[name] ? 'on' : 'off'} ${s.label}`);
163
+ }
164
+ // Blank text counts as not given: models fill optional text with "", which must not wipe a saved name or text.
165
+ const name = typeof input.underwriting_portal_name === 'string' ? input.underwriting_portal_name.trim() : '';
166
+ if (name) {
167
+ body.underwritingPortalName = name;
168
+ changes.push(`name the Intake Manager item "${name}"`);
169
+ }
170
+ const disclosure = typeof input.web_ai_disclosure_text === 'string' ? input.web_ai_disclosure_text.trim() : '';
171
+ if (disclosure) {
172
+ body.aiDisclosureWeb = disclosure;
173
+ changes.push(`set the web AI disclosure text to "${disclosure}"`);
174
+ }
175
+ for (const [name, t] of Object.entries(ADMIN_TIMERS)) {
176
+ const minutes = input[name];
177
+ if (typeof minutes !== 'number') continue;
178
+ if (minutes !== 0 && (!Number.isFinite(minutes) || minutes < t.min)) {
179
+ throw new CliError(`${t.label} must be at least ${t.min} minute${t.min === 1 ? '' : 's'}, or 0 for the default.`, EXIT.NEEDS_INPUT);
180
+ }
181
+ body[t.key] = minutes === 0 ? null : minutes / 1440;
182
+ changes.push(minutes === 0 ? `set ${t.label} back to the default` : `set ${t.label} to ${duration(minutes)}`);
183
+ }
184
+ if (!changes.length) throw new CliError('Give at least one Admin setting to change.', EXIT.NEEDS_INPUT);
185
+ const shown = (body.showAiDisclosureWeb ?? saved.showAiDisclosureWeb) === true;
186
+ const text = body.aiDisclosureWeb ?? saved.aiDisclosureWeb;
187
+ if (shown && !(typeof text === 'string' && text.trim())) {
188
+ throw new CliError('The web AI disclosure needs its text: give web_ai_disclosure_text.', EXIT.NEEDS_INPUT);
189
+ }
190
+ return { body, changes };
191
+ }
192
+
193
+ export interface AuditEntry {
194
+ _id?: string;
195
+ id_user?: string;
196
+ event_type?: string;
197
+ action?: string;
198
+ resource_type?: string;
199
+ resource_id?: string;
200
+ resource_display?: string;
201
+ status?: string;
202
+ createdAt?: string;
203
+ [key: string]: unknown;
204
+ }
205
+
206
+ /** Who changed what in the org. copilot returns the full history; filtering happens here. */
207
+ export class AuditSDK {
208
+ constructor(private client: IndemnClient) {}
209
+
210
+ async list(filter: { since?: string; action?: string; resourceType?: string; user?: string; limit?: number } = {}): Promise<AuditEntry[]> {
211
+ const all = await this.client.copilotGet<AuditEntry[]>('/organization/:org_id/audit-logs');
212
+ const since = filter.since ? Date.parse(filter.since) : undefined;
213
+ return (all ?? [])
214
+ .filter((e) => (since === undefined || Date.parse(String(e.createdAt)) >= since))
215
+ .filter((e) => !filter.action || e.action === filter.action || e.event_type === filter.action)
216
+ .filter((e) => !filter.resourceType || e.resource_type === filter.resourceType)
217
+ .filter((e) => !filter.user || String(e.id_user) === filter.user)
218
+ .slice(0, filter.limit ?? 50);
219
+ }
220
+ }