@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/auth.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { initializeApp, getApp } from 'firebase/app';
2
2
  import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';
3
- import { IndemnClient } from './client.js';
4
- import type { Environment, EnvironmentHosts } from './types.js';
3
+ import { DEFAULT_HOSTS, IndemnClient } from './client.js';
4
+ import { exchangeIdToken, toStoredSession } from './session.js';
5
+ import type { Credential } from './credentials.js';
6
+ import type { Environment, EnvironmentHosts, OrgSummary } from './types.js';
5
7
 
6
8
  // Firebase client configs per environment — public configuration (not secrets),
7
9
  // bundled the same way the Copilot Dashboard bundles them via envsubst
8
- const FIREBASE_CONFIGS: Record<Environment, Record<string, string>> = {
10
+ export const FIREBASE_CONFIGS: Record<Environment, Record<string, string>> = {
9
11
  dev: {
10
12
  apiKey: 'AIzaSyB8bA3w8oaMbyMxYA7lyQy_OvrPcPfW7x8',
11
13
  authDomain: 'agent-449107.firebaseapp.com',
@@ -24,127 +26,105 @@ const FIREBASE_CONFIGS: Record<Environment, Record<string, string>> = {
24
26
  },
25
27
  };
26
28
 
27
- export async function login(email: string, password: string, env: Environment = 'dev', opts?: {
28
- mfaCodePrompt?: () => Promise<string>;
29
- }): Promise<{
30
- apiKey: string;
31
- orgId: string;
32
- userId: string;
33
- environment: Environment;
34
- }> {
35
- // Step 1: Firebase client SDK auth (environment-specific config, separate app per env)
36
- const firebaseConfig = FIREBASE_CONFIGS[env];
29
+ export type FirebaseSignIn = (email: string, password: string, env: Environment) => Promise<{ idToken: string; refreshToken: string }>;
30
+
31
+ /** Email and password sign-in with the Firebase web SDK, as the dashboard does. */
32
+ const firebaseSignIn: FirebaseSignIn = async (email, password, env) => {
37
33
  const appName = `indemn-cli-${env}`;
38
34
  let app;
39
- try { app = getApp(appName); } catch { app = initializeApp(firebaseConfig, appName); }
40
- const auth = getAuth(app);
41
- const userCredential = await signInWithEmailAndPassword(auth, email, password);
42
- const idToken = await userCredential.user.getIdToken();
43
-
44
- // Step 2: Exchange Firebase ID token for Copilot Server JWT
45
- const client = new IndemnClient({ environment: env });
46
- const signinResponse = await client.copilotPostUnauth<{
47
- success: boolean;
48
- requiresMfa: boolean;
49
- mfaSessionId?: string;
50
- mfaEmail?: string;
51
- token?: string;
52
- user?: { _id: string; email: string };
53
- }>('/auth/firebase/signin', { idToken });
54
-
55
- let jwt: string;
56
- let userId: string;
35
+ try { app = getApp(appName); } catch { app = initializeApp(FIREBASE_CONFIGS[env], appName); }
36
+ const credential = await signInWithEmailAndPassword(getAuth(app), email, password);
37
+ return { idToken: await credential.user.getIdToken(), refreshToken: credential.user.refreshToken };
38
+ };
57
39
 
58
- if (signinResponse.requiresMfa) {
59
- // MFA required — prompt for code and verify
60
- if (!opts?.mfaCodePrompt) {
61
- throw new Error('MFA is required but no code prompt handler was provided.');
62
- }
63
- const code = await opts.mfaCodePrompt();
64
- const mfaResponse = await client.copilotPostUnauth<{
65
- success: boolean;
66
- token: string;
67
- user: { _id: string; email: string };
68
- }>('/auth/firebase/mfa/verify', {
69
- sessionId: signinResponse.mfaSessionId,
70
- code,
71
- });
72
- jwt = mfaResponse.token.replace(/^JWT\s+/i, '');
73
- userId = mfaResponse.user._id;
74
- } else {
75
- // No MFA — direct JWT
76
- jwt = signinResponse.token!.replace(/^JWT\s+/i, '');
77
- userId = signinResponse.user!._id;
78
- }
40
+ export interface LoginResult {
41
+ userId: string;
42
+ email: string;
43
+ environment: Environment;
44
+ orgs: OrgSummary[];
45
+ expiresAt?: string;
46
+ }
79
47
 
80
- // Step 3: Generate persistent API key using the JWT
81
- const apiKeyResponse = await client.copilotPostUnauth<{
82
- _id: string;
83
- key: string;
84
- name: string;
85
- org_id: string;
86
- }>('/auth/api-keys', { name: `indemn-cli-${Date.now()}` }, {
87
- 'Authorization': `jwt ${jwt}`,
88
- });
48
+ /**
49
+ * Log in and keep the copilot session token, the same one the dashboard uses.
50
+ * No API key is created. The Firebase refresh token is kept so the session
51
+ * can renew itself.
52
+ */
53
+ export async function login(email: string, password: string, env: Environment = 'dev', opts?: {
54
+ mfaCodePrompt?: () => Promise<string>;
55
+ firebaseSignIn?: FirebaseSignIn;
56
+ }): Promise<LoginResult> {
57
+ const { idToken, refreshToken } = await (opts?.firebaseSignIn ?? firebaseSignIn)(email, password, env);
89
58
 
90
- if (!apiKeyResponse.org_id) {
91
- throw new Error('Login succeeded but no organization found. Contact your admin to be added to an organization.');
92
- }
59
+ const unauth = new IndemnClient({ environment: env });
60
+ const session = await exchangeIdToken(
61
+ (path, body, headers) => unauth.copilotPostUnauth(path, body, headers),
62
+ idToken,
63
+ opts?.mfaCodePrompt,
64
+ );
93
65
 
94
- // Step 3.5: Fetch the org name so the banner can display it without a lookup on every command.
95
- let orgName: string | undefined;
96
- try {
97
- const orgRes = await fetch(`${client.getHosts().copilot}/organization/${apiKeyResponse.org_id}`, {
98
- headers: {
99
- 'Authorization': `Bearer ${apiKeyResponse.key}`,
100
- 'Content-Type': 'application/json',
101
- },
102
- });
103
- if (orgRes.ok) {
104
- const data = await orgRes.json() as { organization?: { name?: string } };
105
- orgName = data.organization?.name;
106
- }
107
- } catch {
108
- // Non-fatal — banner falls back to org ID if name unavailable
109
- }
66
+ // Which orgs this person belongs to, asked with the new session itself.
67
+ const authed = new IndemnClient({ environment: env, token: session.token });
68
+ const res = await authed.copilotGet<{ userOrganizations?: Array<{ _id: string; name: string }> }>('/organization');
69
+ const orgs = (res.userOrganizations ?? []).map((o) => ({ id: String(o._id), name: o.name }));
110
70
 
111
- const config = {
112
- api_key: apiKeyResponse.key,
113
- org_id: apiKeyResponse.org_id,
114
- org_name: orgName,
115
- user_id: userId,
71
+ const stored = toStoredSession(session.token, refreshToken);
72
+ IndemnClient.clearConfig();
73
+ IndemnClient.saveConfig({
116
74
  environment: env,
117
- };
118
-
119
- // Step 4: Save to config file
120
- IndemnClient.saveConfig(config);
75
+ user_id: session.userId,
76
+ email: session.email ?? email,
77
+ session: stored,
78
+ orgs,
79
+ hosts: DEFAULT_HOSTS,
80
+ });
121
81
 
122
- return {
123
- apiKey: config.api_key,
124
- orgId: config.org_id,
125
- userId: config.user_id,
126
- environment: env,
127
- };
82
+ return { userId: session.userId, email: session.email ?? email, environment: env, orgs, expiresAt: stored.expires_at };
128
83
  }
129
84
 
130
- export function logout(): void {
85
+ /** Revoke the session on the server, then forget it locally. */
86
+ export async function logout(): Promise<{ revoked: boolean }> {
87
+ const config = IndemnClient.loadConfig();
88
+ let revoked = false;
89
+ if (config?.session?.token) {
90
+ try {
91
+ const client = new IndemnClient({ environment: config.environment });
92
+ await client.copilotPostUnauth('/auth/firebase/logout', undefined, { Authorization: `JWT ${config.session.token}` });
93
+ revoked = true;
94
+ } catch {
95
+ // Offline or already expired: still forget it locally.
96
+ }
97
+ }
131
98
  IndemnClient.clearConfig();
99
+ return { revoked };
132
100
  }
133
101
 
134
- export function whoami(): {
135
- apiKey: string;
136
- orgId: string;
137
- userId: string;
102
+ export interface WhoAmI {
138
103
  environment: Environment;
104
+ email?: string;
105
+ userId?: string;
106
+ credential: 'session' | 'api_key';
107
+ source: Credential['source'];
108
+ sessionExpiresAt?: string;
109
+ orgs: OrgSummary[];
139
110
  hosts: EnvironmentHosts;
140
- } | null {
111
+ }
112
+
113
+ /** Who and where, without any secret. */
114
+ export function whoami(): WhoAmI | null {
115
+ const client = new IndemnClient();
116
+ const cred = client.getCredential();
117
+ if (!cred) return null;
141
118
  const config = IndemnClient.loadConfig();
142
- if (!config || !config.api_key) return null;
119
+ const fromConfig = cred.source === 'config';
143
120
  return {
144
- apiKey: config.api_key.substring(0, 12) + '...',
145
- orgId: config.org_id,
146
- userId: config.user_id,
147
- environment: config.environment,
148
- hosts: config.hosts[config.environment],
121
+ environment: client.getEnvironment(),
122
+ email: fromConfig ? config?.email : undefined,
123
+ userId: fromConfig ? config?.user_id : undefined,
124
+ credential: cred.kind === 'token' ? 'session' : 'api_key',
125
+ source: cred.source,
126
+ sessionExpiresAt: fromConfig && cred.kind === 'token' ? config?.session?.expires_at : undefined,
127
+ orgs: fromConfig ? config?.orgs ?? [] : [],
128
+ hosts: client.getHosts(),
149
129
  };
150
130
  }
@@ -0,0 +1,59 @@
1
+ import type { IndemnClient } from './client.js';
2
+
3
+ /**
4
+ * Billing, READ-ONLY by design: every call here is a GET. The CLI never
5
+ * changes a plan, pays, pauses, cancels, posts meters or edits prices; those
6
+ * stay in the dashboard. (A test fails if this file gains a write.)
7
+ */
8
+ export class BillingSDK {
9
+ constructor(private client: IndemnClient) {}
10
+
11
+ /** The org's subscription, or null when it has none (copilot answers 500 then) or never pays (infinite plan). */
12
+ async status(): Promise<Record<string, unknown> | null> {
13
+ try {
14
+ const res = await this.client.copilotGet<Record<string, unknown>>('/organization/:org_id/billing-subscription');
15
+ return res;
16
+ } catch (err) {
17
+ if (err instanceof Error && /\(500\)|infinite subscription|Subscription does not exist/i.test(err.message)) return null;
18
+ throw err;
19
+ }
20
+ }
21
+
22
+ private async subscriptionId(): Promise<string | null> {
23
+ const sub = await this.status();
24
+ return sub && sub._id ? String(sub._id) : null;
25
+ }
26
+
27
+ /** Invoices, newest first (empty when there are none; copilot answers 500 then). */
28
+ async invoices(): Promise<unknown[]> {
29
+ const sid = await this.subscriptionId();
30
+ if (!sid) return [];
31
+ try {
32
+ const res = await this.client.copilotGet<{ invoices?: unknown[] }>(`/invoice/organization/:org_id/subscription/${sid}`);
33
+ return res.invoices ?? [];
34
+ } catch (err) {
35
+ if (err instanceof Error && /\(500\)|Could not fetch invoices/i.test(err.message)) return [];
36
+ throw err;
37
+ }
38
+ }
39
+
40
+ /** Open invoices with their payment links. */
41
+ async unpaid(): Promise<unknown[]> {
42
+ const sid = await this.subscriptionId();
43
+ if (!sid) return [];
44
+ const res = await this.client.copilotGet<{ unpaidInvoices?: unknown[] }>(`/invoice/organization/:org_id/subscription/${sid}/unpaid`);
45
+ return res.unpaidInvoices ?? [];
46
+ }
47
+
48
+ /** Usage so far this period and the projected cost per meter. */
49
+ usageForecast(): Promise<unknown> {
50
+ return this.client.copilotGet('/invoice/organization/:org_id/usage-forecast');
51
+ }
52
+
53
+ /** The org's own plans and the public plan list. */
54
+ async plans(): Promise<{ org_plans: unknown[]; public_plans: unknown[] }> {
55
+ const org = await this.client.copilotGet<{ plans?: unknown[] }>('/organization/:org_id/customer-plans').catch(() => ({ plans: [] }));
56
+ const pub = await this.client.copilotGet<unknown[]>('/subscription-plans').catch(() => []);
57
+ return { org_plans: org.plans ?? [], public_plans: Array.isArray(pub) ? pub : [] };
58
+ }
59
+ }
@@ -0,0 +1,60 @@
1
+ import type { IndemnClient } from './client.js';
2
+ import { CliError, EXIT } from '../core/runtime.js';
3
+
4
+ /** Paths of the campaigns service the CLI may read. Everything else, and every write, is refused. */
5
+ const READABLE = [
6
+ /^\/campaigns$/,
7
+ /^\/campaigns\/[\w-]+$/,
8
+ /^\/campaigns\/[\w-]+\/(versions|counts|forecast|next-touchpoints)$/,
9
+ /^\/templates$/,
10
+ /^\/templates\/[\w-]+$/,
11
+ /^\/templates\/[\w-]+\/versions$/,
12
+ /^\/template-skills$/,
13
+ /^\/template-skills\/[\w-]+$/,
14
+ ];
15
+
16
+ /**
17
+ * Campaigns, READ-ONLY by design, through copilot's proxy to the campaigns
18
+ * service. The CLI never publishes a campaign, sends a communication or
19
+ * triggers a book sync; those act on real customers.
20
+ */
21
+ export class CampaignsSDK {
22
+ constructor(private client: IndemnClient) {}
23
+
24
+ private async read<T>(path: string, query?: Record<string, string>): Promise<T> {
25
+ if (!READABLE.some((r) => r.test(path))) throw new CliError(`Campaigns path ${path} is not readable from the CLI.`, EXIT.NEEDS_INPUT);
26
+ return this.client.copilotGet<T>(`/organization/:org_id/campaigns-service${path}`, query);
27
+ }
28
+
29
+ list(filter: { status?: string; solutionId?: string; limit?: number; skip?: number } = {}): Promise<{ total?: number; items?: unknown[] }> {
30
+ const q: Record<string, string> = { limit: String(Math.min(filter.limit ?? 50, 500)), skip: String(filter.skip ?? 0) };
31
+ if (filter.status) q.status = filter.status;
32
+ if (filter.solutionId) q.solution_id = filter.solutionId;
33
+ return this.read('/campaigns', q);
34
+ }
35
+
36
+ get(id: string): Promise<unknown> {
37
+ return this.read(`/campaigns/${id}`);
38
+ }
39
+
40
+ /** Enrollment counts, forecast of the next days, or upcoming touchpoints of a campaign. */
41
+ detail(id: string, what: 'counts' | 'forecast' | 'next-touchpoints' | 'versions', days?: number): Promise<unknown> {
42
+ return this.read(`/campaigns/${id}/${what}`, days ? { days: String(days) } : undefined);
43
+ }
44
+
45
+ templates(filter: { channel?: string; solutionId?: string; limit?: number } = {}): Promise<{ total?: number; items?: unknown[] }> {
46
+ const q: Record<string, string> = { limit: String(Math.min(filter.limit ?? 50, 500)) };
47
+ if (filter.channel) q.channel = filter.channel;
48
+ if (filter.solutionId) q.solution_id = filter.solutionId;
49
+ return this.read('/templates', q);
50
+ }
51
+
52
+ /** A template; with versions: true, its versions (where subject and body live). */
53
+ template(id: string, versions = false): Promise<unknown> {
54
+ return this.read(versions ? `/templates/${id}/versions` : `/templates/${id}`);
55
+ }
56
+
57
+ templateSkills(solutionId?: string): Promise<unknown> {
58
+ return this.read('/template-skills', solutionId ? { solution_id: solutionId } : undefined);
59
+ }
60
+ }
package/src/sdk/chat.ts CHANGED
@@ -11,19 +11,42 @@ export class ChatSession extends EventEmitter {
11
11
  private connected = false;
12
12
  private streamBuffers: Map<string, string> = new Map();
13
13
 
14
- constructor(botType: string, middlewareUrl: string, sessionId?: string) {
14
+ private customData: Record<string, unknown>;
15
+
16
+ /**
17
+ * Every CLI conversation is a test conversation: `isTestMode` is always
18
+ * true and cannot be overridden. `tester` and `preview` let the dashboard
19
+ * show who tested, as it does for its own "Talk to agent" window.
20
+ */
21
+ constructor(
22
+ botType: string,
23
+ middlewareUrl: string,
24
+ sessionId?: string,
25
+ opts: { tester?: string | null; preview?: string | null; customData?: Record<string, unknown> } = {},
26
+ ) {
15
27
  super();
16
28
  this.botType = botType;
17
29
  this.middlewareUrl = middlewareUrl;
18
30
  this.userId = `cli-user-${Date.now()}`;
19
31
  this.sessionId = sessionId || null;
32
+ this.customData = {
33
+ ...opts.customData,
34
+ ...(opts.tester ? { conversation_tester: opts.tester } : {}),
35
+ ...(opts.preview ? { preview: opts.preview } : {}),
36
+ isTestMode: true,
37
+ };
38
+ }
39
+
40
+ /** The custom data sent with session_request (for tests and diagnostics). */
41
+ getCustomData(): Record<string, unknown> {
42
+ return { ...this.customData };
20
43
  }
21
44
 
22
45
  async connect(): Promise<string> {
23
46
  return new Promise<string>((resolve, reject) => {
24
47
  const timeout = setTimeout(() => {
25
48
  reject(new Error('Connection timed out after 10s'));
26
- this.disconnect();
49
+ void this.disconnect({ end: false });
27
50
  }, 10000);
28
51
 
29
52
  this.socket = io(this.middlewareUrl, {
@@ -42,7 +65,7 @@ export class ChatSession extends EventEmitter {
42
65
  session_id: this.sessionId,
43
66
  bot_type: this.botType,
44
67
  user_id: this.userId,
45
- customData: {},
68
+ customData: this.customData,
46
69
  };
47
70
  this.socket!.emit('session_request', request);
48
71
  });
@@ -115,15 +138,26 @@ export class ChatSession extends EventEmitter {
115
138
  this.socket.emit('user_uttered', payload);
116
139
  }
117
140
 
118
- disconnect(): void {
119
- if (this.socket) {
120
- if (this.sessionId) {
121
- this.socket.emit('session_closed', { session_id: this.sessionId });
141
+ /**
142
+ * Leave the conversation. By default it is also ended on the platform
143
+ * (session_closed). `{ end: false }` only drops the socket, so the same
144
+ * session id can continue the conversation later.
145
+ *
146
+ * The close message is given time to arrive before the socket drops: an
147
+ * event that reaches Socket.IO right before the disconnect is discarded.
148
+ */
149
+ async disconnect(opts: { end?: boolean; flushMs?: number } = {}): Promise<void> {
150
+ const socket = this.socket;
151
+ this.socket = null;
152
+ this.connected = false;
153
+ if (socket) {
154
+ if (this.sessionId && opts.end !== false && socket.connected) {
155
+ // The middleware accepts only allow-listed reasons; this one records a deliberate close.
156
+ socket.emit('session_closed', { session_id: this.sessionId, reason: 'client_close_conversation' });
157
+ await new Promise((resolve) => setTimeout(resolve, opts.flushMs ?? 300));
122
158
  }
123
- this.socket.disconnect();
124
- this.socket = null;
159
+ socket.disconnect();
125
160
  }
126
- this.connected = false;
127
161
  this.sessionId = null;
128
162
  }
129
163