@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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +9 -4
- package/.github/workflows/ci.yml +10 -0
- package/.github/workflows/publish.yml +2 -2
- package/.mcp.json +1 -4
- package/CONTRIBUTING.md +3 -1
- package/README.md +69 -176
- package/dist/cli/agents.d.ts.map +1 -1
- package/dist/cli/agents.js +226 -18
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/analytics.d.ts +3 -0
- package/dist/cli/analytics.d.ts.map +1 -0
- package/dist/cli/analytics.js +133 -0
- package/dist/cli/analytics.js.map +1 -0
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +62 -17
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/billing.d.ts +3 -0
- package/dist/cli/billing.d.ts.map +1 -0
- package/dist/cli/billing.js +23 -0
- package/dist/cli/billing.js.map +1 -0
- package/dist/cli/campaigns.d.ts +3 -0
- package/dist/cli/campaigns.d.ts.map +1 -0
- package/dist/cli/campaigns.js +34 -0
- package/dist/cli/campaigns.js.map +1 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +69 -102
- package/dist/cli/chat.js.map +1 -1
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +71 -27
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/conversations.d.ts +3 -0
- package/dist/cli/conversations.d.ts.map +1 -0
- package/dist/cli/conversations.js +129 -0
- package/dist/cli/conversations.js.map +1 -0
- package/dist/cli/distribution.d.ts +3 -0
- package/dist/cli/distribution.d.ts.map +1 -0
- package/dist/cli/distribution.js +122 -0
- package/dist/cli/distribution.js.map +1 -0
- package/dist/cli/eval.d.ts.map +1 -1
- package/dist/cli/eval.js +315 -71
- package/dist/cli/eval.js.map +1 -1
- package/dist/cli/functions.d.ts.map +1 -1
- package/dist/cli/functions.js +109 -38
- package/dist/cli/functions.js.map +1 -1
- package/dist/cli/index.js +63 -54
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +27 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +152 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/kb.d.ts.map +1 -1
- package/dist/cli/kb.js +141 -15
- package/dist/cli/kb.js.map +1 -1
- package/dist/cli/link.d.ts +21 -0
- package/dist/cli/link.d.ts.map +1 -0
- package/dist/cli/link.js +112 -0
- package/dist/cli/link.js.map +1 -0
- package/dist/cli/org.d.ts.map +1 -1
- package/dist/cli/org.js +79 -67
- package/dist/cli/org.js.map +1 -1
- package/dist/cli/platform.d.ts +3 -0
- package/dist/cli/platform.d.ts.map +1 -0
- package/dist/cli/platform.js +133 -0
- package/dist/cli/platform.js.map +1 -0
- package/dist/cli/tickets.d.ts +3 -0
- package/dist/cli/tickets.d.ts.map +1 -0
- package/dist/cli/tickets.js +125 -0
- package/dist/cli/tickets.js.map +1 -0
- package/dist/cli/utils/output.d.ts +15 -9
- package/dist/cli/utils/output.d.ts.map +1 -1
- package/dist/cli/utils/output.js +53 -46
- package/dist/cli/utils/output.js.map +1 -1
- package/dist/cli/voice.d.ts +3 -0
- package/dist/cli/voice.d.ts.map +1 -0
- package/dist/cli/voice.js +206 -0
- package/dist/cli/voice.js.map +1 -0
- package/dist/core/diff.d.ts +16 -0
- package/dist/core/diff.d.ts.map +1 -0
- package/dist/core/diff.js +57 -0
- package/dist/core/diff.js.map +1 -0
- package/dist/core/org-resolver.d.ts +37 -0
- package/dist/core/org-resolver.d.ts.map +1 -0
- package/dist/core/org-resolver.js +73 -0
- package/dist/core/org-resolver.js.map +1 -0
- package/dist/core/runtime.d.ts +36 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +62 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/core/scope.d.ts +33 -0
- package/dist/core/scope.d.ts.map +1 -0
- package/dist/core/scope.js +37 -0
- package/dist/core/scope.js.map +1 -0
- package/dist/mcp/registry.d.ts +67 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/registry.js +195 -0
- package/dist/mcp/registry.js.map +1 -0
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +87 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-meta.d.ts +21 -0
- package/dist/mcp/tool-meta.d.ts.map +1 -0
- package/dist/mcp/tool-meta.js +203 -0
- package/dist/mcp/tool-meta.js.map +1 -0
- package/dist/mcp/tools.d.ts +2 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +1462 -215
- package/dist/mcp/tools.js.map +1 -1
- package/dist/sdk/agent-bundle.d.ts +99 -0
- package/dist/sdk/agent-bundle.d.ts.map +1 -0
- package/dist/sdk/agent-bundle.js +211 -0
- package/dist/sdk/agent-bundle.js.map +1 -0
- package/dist/sdk/agents.d.ts +54 -1
- package/dist/sdk/agents.d.ts.map +1 -1
- package/dist/sdk/agents.js +59 -5
- package/dist/sdk/agents.js.map +1 -1
- package/dist/sdk/analytics.d.ts +158 -0
- package/dist/sdk/analytics.d.ts.map +1 -0
- package/dist/sdk/analytics.js +185 -0
- package/dist/sdk/analytics.js.map +1 -0
- package/dist/sdk/auth.d.ts +34 -12
- package/dist/sdk/auth.d.ts.map +1 -1
- package/dist/sdk/auth.js +60 -77
- package/dist/sdk/auth.js.map +1 -1
- package/dist/sdk/billing.d.ts +25 -0
- package/dist/sdk/billing.d.ts.map +1 -0
- package/dist/sdk/billing.js +61 -0
- package/dist/sdk/billing.js.map +1 -0
- package/dist/sdk/campaigns.d.ts +35 -0
- package/dist/sdk/campaigns.d.ts.map +1 -0
- package/dist/sdk/campaigns.js +59 -0
- package/dist/sdk/campaigns.js.map +1 -0
- package/dist/sdk/chat.d.ts +25 -2
- package/dist/sdk/chat.d.ts.map +1 -1
- package/dist/sdk/chat.js +37 -10
- package/dist/sdk/chat.js.map +1 -1
- package/dist/sdk/client.d.ts +93 -5
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +362 -100
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/config-change.d.ts +74 -0
- package/dist/sdk/config-change.d.ts.map +1 -0
- package/dist/sdk/config-change.js +117 -0
- package/dist/sdk/config-change.js.map +1 -0
- package/dist/sdk/config.d.ts.map +1 -1
- package/dist/sdk/config.js +2 -1
- package/dist/sdk/config.js.map +1 -1
- package/dist/sdk/conversations.d.ts +101 -0
- package/dist/sdk/conversations.d.ts.map +1 -0
- package/dist/sdk/conversations.js +155 -0
- package/dist/sdk/conversations.js.map +1 -0
- package/dist/sdk/credentials.d.ts +25 -0
- package/dist/sdk/credentials.d.ts.map +1 -0
- package/dist/sdk/credentials.js +44 -0
- package/dist/sdk/credentials.js.map +1 -0
- package/dist/sdk/distribution.d.ts +94 -0
- package/dist/sdk/distribution.d.ts.map +1 -0
- package/dist/sdk/distribution.js +100 -0
- package/dist/sdk/distribution.js.map +1 -0
- package/dist/sdk/eval.d.ts +81 -0
- package/dist/sdk/eval.d.ts.map +1 -1
- package/dist/sdk/eval.js +117 -0
- package/dist/sdk/eval.js.map +1 -1
- package/dist/sdk/functions.d.ts +14 -1
- package/dist/sdk/functions.d.ts.map +1 -1
- package/dist/sdk/functions.js +48 -5
- package/dist/sdk/functions.js.map +1 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +2 -0
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/jarvis-chat.d.ts +28 -0
- package/dist/sdk/jarvis-chat.d.ts.map +1 -0
- package/dist/sdk/jarvis-chat.js +117 -0
- package/dist/sdk/jarvis-chat.js.map +1 -0
- package/dist/sdk/kb.d.ts +69 -1
- package/dist/sdk/kb.d.ts.map +1 -1
- package/dist/sdk/kb.js +111 -24
- package/dist/sdk/kb.js.map +1 -1
- package/dist/sdk/orgs.d.ts +13 -0
- package/dist/sdk/orgs.d.ts.map +1 -0
- package/dist/sdk/orgs.js +59 -0
- package/dist/sdk/orgs.js.map +1 -0
- package/dist/sdk/platform.d.ts +62 -0
- package/dist/sdk/platform.d.ts.map +1 -0
- package/dist/sdk/platform.js +95 -0
- package/dist/sdk/platform.js.map +1 -0
- package/dist/sdk/session.d.ts +25 -0
- package/dist/sdk/session.d.ts.map +1 -0
- package/dist/sdk/session.js +69 -0
- package/dist/sdk/session.js.map +1 -0
- package/dist/sdk/skills.d.ts +46 -0
- package/dist/sdk/skills.d.ts.map +1 -0
- package/dist/sdk/skills.js +203 -0
- package/dist/sdk/skills.js.map +1 -0
- package/dist/sdk/test-chat.d.ts +52 -0
- package/dist/sdk/test-chat.d.ts.map +1 -0
- package/dist/sdk/test-chat.js +134 -0
- package/dist/sdk/test-chat.js.map +1 -0
- package/dist/sdk/tickets.d.ts +98 -0
- package/dist/sdk/tickets.d.ts.map +1 -0
- package/dist/sdk/tickets.js +91 -0
- package/dist/sdk/tickets.js.map +1 -0
- package/dist/sdk/types.d.ts +91 -6
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/voice.d.ts +147 -0
- package/dist/sdk/voice.d.ts.map +1 -0
- package/dist/sdk/voice.js +236 -0
- package/dist/sdk/voice.js.map +1 -0
- package/package.json +22 -3
- package/scripts/check-skills.mjs +25 -0
- package/skills/agent-builder/SKILL.md +89 -252
- package/skills/evaluations/SKILL.md +23 -234
- package/skills/evaluations/references/analysis.md +95 -0
- package/skills/evaluations/references/cli-reference.md +28 -0
- package/skills/evaluations/references/json-formats.md +100 -0
- package/src/cli/agents.ts +202 -22
- package/src/cli/analytics.ts +138 -0
- package/src/cli/auth.ts +56 -18
- package/src/cli/billing.ts +25 -0
- package/src/cli/campaigns.ts +33 -0
- package/src/cli/chat.ts +66 -115
- package/src/cli/config.ts +65 -31
- package/src/cli/conversations.ts +133 -0
- package/src/cli/distribution.ts +116 -0
- package/src/cli/eval.ts +254 -70
- package/src/cli/functions.ts +105 -39
- package/src/cli/index.ts +58 -55
- package/src/cli/init.ts +154 -0
- package/src/cli/kb.ts +120 -17
- package/src/cli/link.ts +117 -0
- package/src/cli/org.ts +81 -75
- package/src/cli/platform.ts +128 -0
- package/src/cli/tickets.ts +130 -0
- package/src/cli/utils/output.ts +51 -42
- package/src/cli/voice.ts +204 -0
- package/src/core/diff.ts +52 -0
- package/src/core/org-resolver.ts +113 -0
- package/src/core/runtime.ts +73 -0
- package/src/core/scope.ts +64 -0
- package/src/mcp/registry.ts +236 -0
- package/src/mcp/server.ts +84 -2
- package/src/mcp/tool-meta.ts +241 -0
- package/src/mcp/tools.ts +1529 -265
- package/src/sdk/agent-bundle.ts +245 -0
- package/src/sdk/agents.ts +74 -5
- package/src/sdk/analytics.ts +220 -0
- package/src/sdk/auth.ts +87 -107
- package/src/sdk/billing.ts +59 -0
- package/src/sdk/campaigns.ts +60 -0
- package/src/sdk/chat.ts +44 -10
- package/src/sdk/client.ts +373 -102
- package/src/sdk/config-change.ts +148 -0
- package/src/sdk/config.ts +2 -1
- package/src/sdk/conversations.ts +199 -0
- package/src/sdk/credentials.ts +53 -0
- package/src/sdk/distribution.ts +132 -0
- package/src/sdk/eval.ts +140 -0
- package/src/sdk/functions.ts +52 -6
- package/src/sdk/index.ts +2 -0
- package/src/sdk/jarvis-chat.ts +112 -0
- package/src/sdk/kb.ts +116 -29
- package/src/sdk/orgs.ts +62 -0
- package/src/sdk/platform.ts +115 -0
- package/src/sdk/session.ts +90 -0
- package/src/sdk/skills.ts +188 -0
- package/src/sdk/test-chat.ts +151 -0
- package/src/sdk/tickets.ts +120 -0
- package/src/sdk/types.ts +88 -6
- package/src/sdk/voice.ts +262 -0
- package/test/cli/entry.test.ts +34 -0
- package/test/cli/init.test.ts +66 -0
- package/test/cli/link.test.ts +57 -0
- package/test/core/org-resolver.test.ts +92 -0
- package/test/core/runtime.test.ts +30 -0
- package/test/core/scope.test.ts +93 -0
- package/test/harness.test.ts +24 -0
- package/test/helpers/config-home.ts +22 -0
- package/test/helpers/fake-middleware.ts +56 -0
- package/test/helpers/fetch-recorder.ts +75 -0
- package/test/helpers/jwt.ts +9 -0
- package/test/helpers/mcp-pair.ts +38 -0
- package/test/mcp/__snapshots__/tools-snapshot.test.ts.snap +145 -0
- package/test/mcp/askers.test.ts +60 -0
- package/test/mcp/elicit-timeout.test.ts +29 -0
- package/test/mcp/registry.test.ts +169 -0
- package/test/mcp/shaping.test.ts +41 -0
- package/test/mcp/tools-snapshot.test.ts +42 -0
- package/test/sdk/agents.test.ts +232 -0
- package/test/sdk/analytics.test.ts +89 -0
- package/test/sdk/auth.test.ts +70 -0
- package/test/sdk/billing.test.ts +47 -0
- package/test/sdk/campaigns.test.ts +43 -0
- package/test/sdk/client.test.ts +80 -0
- package/test/sdk/config-change.test.ts +142 -0
- package/test/sdk/conversations.test.ts +80 -0
- package/test/sdk/credentials.test.ts +33 -0
- package/test/sdk/distribution.test.ts +55 -0
- package/test/sdk/eval.test.ts +155 -0
- package/test/sdk/kb-upload.test.ts +50 -0
- package/test/sdk/kb.test.ts +95 -0
- package/test/sdk/orgs.test.ts +48 -0
- package/test/sdk/platform.test.ts +89 -0
- package/test/sdk/renewal.test.ts +115 -0
- package/test/sdk/session.test.ts +60 -0
- package/test/sdk/skills.test.ts +235 -0
- package/test/sdk/test-chat.test.ts +112 -0
- package/test/sdk/tickets.test.ts +42 -0
- package/test/sdk/voice.test.ts +147 -0
- package/vitest.config.ts +11 -0
package/src/sdk/client.ts
CHANGED
|
@@ -1,42 +1,108 @@
|
|
|
1
1
|
import Conf from 'conf';
|
|
2
2
|
import { chmodSync } from 'node:fs';
|
|
3
3
|
import type { ApiResponse, Environment, EnvironmentHosts, IndemnConfig } from './types.js';
|
|
4
|
+
import { CliError, cliVersion, isDebug, SessionExpiredError } from '../core/runtime.js';
|
|
5
|
+
import { copilotAuthHeader, darwinAuthHeader, resolveCredential, type Credential } from './credentials.js';
|
|
6
|
+
import { exchangeIdToken, needsRenewal, refreshIdToken, toStoredSession } from './session.js';
|
|
7
|
+
import { findLinkFile, resolveOrg, type LinkFile, type OrgChoice, type ResolvedOrg } from '../core/org-resolver.js';
|
|
8
|
+
import { emitOrgResolved, getAskers, getOrgFlag, scopedOrg } from '../core/scope.js';
|
|
4
9
|
|
|
5
|
-
const DEFAULT_HOSTS: Record<Environment, EnvironmentHosts> = {
|
|
10
|
+
export const DEFAULT_HOSTS: Record<Environment, EnvironmentHosts> = {
|
|
6
11
|
dev: {
|
|
7
12
|
copilot: 'https://devcopilot.indemn.ai/api',
|
|
8
13
|
evaluations: 'https://devevaluations.indemn.ai',
|
|
9
14
|
middleware: 'https://devproxy.indemn.ai',
|
|
15
|
+
platform: 'https://devplatform.indemn.ai/api/v1',
|
|
10
16
|
},
|
|
11
17
|
production: {
|
|
12
18
|
copilot: 'https://copilot.indemn.ai/api',
|
|
13
19
|
evaluations: 'https://evaluations.indemn.ai',
|
|
14
20
|
middleware: 'https://proxy.indemn.ai',
|
|
21
|
+
platform: 'https://platform.indemn.ai/api/v1',
|
|
15
22
|
},
|
|
16
23
|
};
|
|
17
24
|
|
|
25
|
+
export type ClientKind = 'cli' | 'mcp';
|
|
26
|
+
let clientKind: ClientKind = 'cli';
|
|
27
|
+
|
|
28
|
+
type Backend = 'copilot' | 'evaluations' | 'platform';
|
|
29
|
+
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
30
|
+
|
|
31
|
+
interface SendOptions {
|
|
32
|
+
body?: unknown;
|
|
33
|
+
headers?: Record<string, string>;
|
|
34
|
+
/** Attach the credential. False only for the login exchange. */
|
|
35
|
+
auth?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const RETRY_STATUSES = new Set([502, 503, 504]);
|
|
39
|
+
const RETRY_DELAYS_MS = [300, 600];
|
|
40
|
+
|
|
41
|
+
function isEnvironment(value: unknown): value is Environment {
|
|
42
|
+
return value === 'dev' || value === 'production';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The environment belongs to the credential. A saved login knows its own
|
|
47
|
+
* environment; an API key says it in its prefix. INDEMN_ENV is only read for a
|
|
48
|
+
* host-supplied session token (INDEMN_TOKEN), which carries no environment.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveEnvironment(config: IndemnConfig | null, env: NodeJS.ProcessEnv = process.env): Environment {
|
|
51
|
+
if (env.INDEMN_TOKEN && isEnvironment(env.INDEMN_ENV)) return env.INDEMN_ENV;
|
|
52
|
+
if (env.INDEMN_API_KEY?.startsWith('ind_live_')) return 'production';
|
|
53
|
+
if (env.INDEMN_API_KEY?.startsWith('ind_dev_')) return 'dev';
|
|
54
|
+
if (config && isEnvironment(config.environment)) return config.environment;
|
|
55
|
+
return 'dev';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sleep(ms: number): Promise<void> {
|
|
59
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One renewal at a time per process, shared by every client. */
|
|
63
|
+
let renewing: Promise<void> | null = null;
|
|
64
|
+
|
|
65
|
+
export interface OrgContext extends ResolvedOrg {
|
|
66
|
+
name?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
18
69
|
export class IndemnClient {
|
|
19
|
-
private
|
|
20
|
-
private
|
|
70
|
+
private explicitCredential: Credential | null;
|
|
71
|
+
private orgOverride?: string;
|
|
21
72
|
private environment: Environment;
|
|
22
73
|
private hosts: EnvironmentHosts;
|
|
74
|
+
private resolvedOrg?: Promise<OrgContext>;
|
|
75
|
+
private linkCache?: LinkFile | null;
|
|
76
|
+
private membershipCache?: OrgChoice[];
|
|
23
77
|
|
|
24
|
-
|
|
25
|
-
// Priority: explicit opts > env vars > config file
|
|
26
|
-
const config = IndemnClient.loadConfig();
|
|
78
|
+
private ignoreLink: boolean;
|
|
27
79
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
80
|
+
constructor(opts?: { apiKey?: string; token?: string; orgId?: string; environment?: Environment; ignoreLink?: boolean }) {
|
|
81
|
+
const config = IndemnClient.loadConfig();
|
|
82
|
+
const env = opts?.environment || resolveEnvironment(config);
|
|
83
|
+
this.explicitCredential = opts?.token
|
|
84
|
+
? { kind: 'token', token: opts.token, source: 'env', environment: env }
|
|
85
|
+
: opts?.apiKey
|
|
86
|
+
? { kind: 'api_key', key: opts.apiKey, source: 'env', environment: env }
|
|
87
|
+
: null;
|
|
88
|
+
this.orgOverride = opts?.orgId;
|
|
89
|
+
this.ignoreLink = Boolean(opts?.ignoreLink);
|
|
90
|
+
this.environment = env;
|
|
31
91
|
const defaultHosts = DEFAULT_HOSTS[this.environment];
|
|
32
92
|
const configHosts = config?.hosts?.[this.environment];
|
|
33
93
|
this.hosts = {
|
|
34
94
|
copilot: process.env.INDEMN_COPILOT_URL || configHosts?.copilot || defaultHosts.copilot,
|
|
35
95
|
evaluations: process.env.INDEMN_EVALS_URL || configHosts?.evaluations || defaultHosts.evaluations,
|
|
36
96
|
middleware: process.env.INDEMN_MIDDLEWARE_URL || configHosts?.middleware || defaultHosts.middleware,
|
|
97
|
+
platform: process.env.INDEMN_PLATFORM_URL || configHosts?.platform || defaultHosts.platform,
|
|
37
98
|
};
|
|
38
99
|
}
|
|
39
100
|
|
|
101
|
+
/** Name the caller in the X-Indemn-Client header: the terminal CLI or the MCP server. */
|
|
102
|
+
static setClientKind(kind: ClientKind): void {
|
|
103
|
+
clientKind = kind;
|
|
104
|
+
}
|
|
105
|
+
|
|
40
106
|
static getConfigStore(): Conf<IndemnConfig> {
|
|
41
107
|
return new Conf<IndemnConfig>({
|
|
42
108
|
projectName: 'indemn',
|
|
@@ -45,17 +111,20 @@ export class IndemnClient {
|
|
|
45
111
|
});
|
|
46
112
|
}
|
|
47
113
|
|
|
114
|
+
/** The saved login, or null. Read fresh on every request so a new login in another terminal is picked up. */
|
|
48
115
|
static loadConfig(): IndemnConfig | null {
|
|
49
116
|
try {
|
|
50
117
|
const store = IndemnClient.getConfigStore();
|
|
118
|
+
const session = store.get('session');
|
|
51
119
|
const apiKey = store.get('api_key');
|
|
52
|
-
if (!apiKey) return null;
|
|
120
|
+
if (!session?.token && !apiKey) return null;
|
|
53
121
|
return {
|
|
54
|
-
api_key: apiKey,
|
|
55
|
-
org_id: store.get('org_id', ''),
|
|
56
|
-
org_name: store.get('org_name', '') || undefined,
|
|
57
|
-
user_id: store.get('user_id', ''),
|
|
58
122
|
environment: store.get('environment', 'dev'),
|
|
123
|
+
user_id: store.get('user_id', ''),
|
|
124
|
+
email: store.get('email'),
|
|
125
|
+
session,
|
|
126
|
+
orgs: store.get('orgs'),
|
|
127
|
+
api_key: apiKey,
|
|
59
128
|
hosts: store.get('hosts', DEFAULT_HOSTS),
|
|
60
129
|
};
|
|
61
130
|
} catch {
|
|
@@ -66,9 +135,10 @@ export class IndemnClient {
|
|
|
66
135
|
static saveConfig(config: Partial<IndemnConfig>): void {
|
|
67
136
|
const store = IndemnClient.getConfigStore();
|
|
68
137
|
for (const [key, value] of Object.entries(config)) {
|
|
69
|
-
store.
|
|
138
|
+
if (value === undefined) store.delete(key as keyof IndemnConfig);
|
|
139
|
+
else store.set(key as keyof IndemnConfig, value);
|
|
70
140
|
}
|
|
71
|
-
// Restrict config file to owner-only (contains
|
|
141
|
+
// Restrict config file to owner-only (contains credentials)
|
|
72
142
|
try {
|
|
73
143
|
chmodSync(store.path, 0o600);
|
|
74
144
|
} catch {
|
|
@@ -82,19 +152,158 @@ export class IndemnClient {
|
|
|
82
152
|
}
|
|
83
153
|
|
|
84
154
|
getEnvironment(): Environment { return this.environment; }
|
|
85
|
-
getOrgId(): string { return this.orgId; }
|
|
86
|
-
getApiKey(): string { return this.apiKey; }
|
|
87
155
|
getHosts(): EnvironmentHosts { return this.hosts; }
|
|
88
156
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
157
|
+
/** The credential for the next request. Never cached: the saved login may have been renewed or replaced. */
|
|
158
|
+
getCredential(): Credential | null {
|
|
159
|
+
return this.explicitCredential ?? resolveCredential(IndemnClient.loadConfig());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private requireCredential(): Credential {
|
|
163
|
+
const cred = this.getCredential();
|
|
164
|
+
if (!cred) throw new SessionExpiredError('Not logged in. Run `indemn login` first.');
|
|
165
|
+
return cred;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get a fresh copilot session with the saved Firebase refresh token, save
|
|
170
|
+
* it, and revoke the old one. Accounts with MFA are asked for the emailed
|
|
171
|
+
* code when a terminal or MCP host can ask; otherwise the person must log in.
|
|
172
|
+
*/
|
|
173
|
+
async renewSession(): Promise<void> {
|
|
174
|
+
if (renewing) return renewing;
|
|
175
|
+
renewing = (async () => {
|
|
176
|
+
const config = IndemnClient.loadConfig();
|
|
177
|
+
const old = config?.session;
|
|
178
|
+
if (!config || !old?.refresh_token) {
|
|
179
|
+
throw new SessionExpiredError('Session expired. Run `indemn login`, then retry.');
|
|
180
|
+
}
|
|
181
|
+
const { idToken, refreshToken } = await refreshIdToken(config.environment, old.refresh_token);
|
|
182
|
+
const { token } = await exchangeIdToken(
|
|
183
|
+
(path, body, headers) => this.copilotPostUnauth(path, body, headers),
|
|
184
|
+
idToken,
|
|
185
|
+
getAskers().mfaCode,
|
|
186
|
+
);
|
|
187
|
+
IndemnClient.saveConfig({ session: toStoredSession(token, refreshToken) });
|
|
188
|
+
try {
|
|
189
|
+
await this.send('copilot', 'POST', `${this.hosts.copilot}/auth/firebase/logout`, {
|
|
190
|
+
auth: false,
|
|
191
|
+
headers: { Authorization: `JWT ${old.token}` },
|
|
192
|
+
});
|
|
193
|
+
} catch {
|
|
194
|
+
// Best effort: the old token expires within the hour anyway.
|
|
195
|
+
}
|
|
196
|
+
})().finally(() => {
|
|
197
|
+
renewing = null;
|
|
198
|
+
});
|
|
199
|
+
return renewing;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The credential for the next request, renewed first when a saved session is about to expire. */
|
|
203
|
+
private async freshCredential(): Promise<Credential> {
|
|
204
|
+
let cred = this.requireCredential();
|
|
205
|
+
if (cred.kind === 'token' && cred.source === 'config') {
|
|
206
|
+
const session = IndemnClient.loadConfig()?.session;
|
|
207
|
+
if (session && needsRenewal(session)) {
|
|
208
|
+
await this.renewSession();
|
|
209
|
+
cred = this.requireCredential();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return cred;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private async authHeader(): Promise<string> {
|
|
216
|
+
return copilotAuthHeader(await this.freshCredential());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// --- Organizations -------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
/** The `.indemn/project.json` in effect for this working directory. */
|
|
222
|
+
getLink(): LinkFile | null {
|
|
223
|
+
if (this.ignoreLink) return null;
|
|
224
|
+
if (this.linkCache === undefined) this.linkCache = findLinkFile();
|
|
225
|
+
return this.linkCache;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Orgs the user belongs to: saved at login, else asked live from copilot. */
|
|
229
|
+
async memberships(): Promise<OrgChoice[]> {
|
|
230
|
+
if (this.membershipCache) return this.membershipCache;
|
|
231
|
+
const cred = this.requireCredential();
|
|
232
|
+
const saved = cred.source === 'config' ? IndemnClient.loadConfig()?.orgs : undefined;
|
|
233
|
+
if (saved?.length) {
|
|
234
|
+
this.membershipCache = saved;
|
|
235
|
+
} else {
|
|
236
|
+
const res = await this.copilotGet<{ userOrganizations?: Array<{ _id: string; name: string }> }>('/organization');
|
|
237
|
+
this.membershipCache = (res.userOrganizations ?? []).map((o) => ({ id: String(o._id), name: o.name }));
|
|
238
|
+
}
|
|
239
|
+
return this.membershipCache;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The org this client acts on. `explicit` is an MCP tool's org_id; the
|
|
244
|
+
* terminal passes --org through the scope module. Decided once per client.
|
|
245
|
+
*/
|
|
246
|
+
async resolveOrg(explicit?: string, opts: { cache?: boolean } = {}): Promise<OrgContext> {
|
|
247
|
+
const run = async (): Promise<OrgContext> => {
|
|
248
|
+
const resolved = await resolveOrg({
|
|
249
|
+
flag: explicit ?? this.orgOverride ?? getOrgFlag(),
|
|
250
|
+
envOrg: process.env.INDEMN_ORG_ID || undefined,
|
|
251
|
+
link: this.getLink(),
|
|
252
|
+
environment: this.environment,
|
|
253
|
+
memberships: () => this.memberships(),
|
|
254
|
+
ask: getAskers().chooseOrg,
|
|
255
|
+
});
|
|
256
|
+
const known = this.membershipCache ?? IndemnClient.loadConfig()?.orgs ?? [];
|
|
257
|
+
const name = known.find((o) => o.id === resolved.id)?.name ?? resolved.link?.org_name;
|
|
258
|
+
return { ...resolved, name };
|
|
259
|
+
};
|
|
260
|
+
if (explicit || opts.cache === false) return run();
|
|
261
|
+
if (!this.resolvedOrg) {
|
|
262
|
+
this.resolvedOrg = run().then((org) => {
|
|
263
|
+
emitOrgResolved({ id: org.id, name: org.name, source: org.source, environment: this.environment });
|
|
264
|
+
return org;
|
|
265
|
+
});
|
|
266
|
+
this.resolvedOrg.catch(() => { this.resolvedOrg = undefined; });
|
|
267
|
+
}
|
|
268
|
+
return this.resolvedOrg;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Org id for the current call: an MCP call's scope first, then this client's resolution. */
|
|
272
|
+
async currentOrgId(): Promise<string> {
|
|
273
|
+
return scopedOrg() ?? (await this.resolveOrg()).id;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** What every MCP result reports as its origin. Never triggers a question. */
|
|
277
|
+
async getResultContext(): Promise<{ org_id?: string; org_name?: string; environment: Environment }> {
|
|
278
|
+
const scoped = scopedOrg();
|
|
279
|
+
const settled = this.resolvedOrg ? await this.resolvedOrg.catch(() => undefined) : undefined;
|
|
280
|
+
const id = scoped ?? settled?.id;
|
|
281
|
+
const known = this.membershipCache ?? IndemnClient.loadConfig()?.orgs ?? [];
|
|
282
|
+
return { org_id: id, org_name: id ? known.find((o) => o.id === id)?.name : undefined, environment: this.environment };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* In a linked folder with an agent list, refuse any agent that is not on
|
|
287
|
+
* the list before a request leaves the machine.
|
|
288
|
+
*/
|
|
289
|
+
private assertAgentInScope(path: string): void {
|
|
290
|
+
const link = this.getLink();
|
|
291
|
+
if (!link || link.agents.length === 0) return;
|
|
292
|
+
const match = /\/bots\/([a-f0-9]{24})(?:\/|$|\?)/i.exec(path);
|
|
293
|
+
if (match && !link.agents.includes(match[1])) {
|
|
294
|
+
throw new CliError(
|
|
295
|
+
`Agent ${match[1]} is not linked in ${link.path}. Linked agents: ${link.agents.join(', ')}. ` +
|
|
296
|
+
'Add it with `indemn link` or work from another folder.',
|
|
297
|
+
);
|
|
92
298
|
}
|
|
93
299
|
}
|
|
94
300
|
|
|
95
|
-
private buildCopilotUrl(path: string): string {
|
|
96
|
-
|
|
97
|
-
|
|
301
|
+
private async buildCopilotUrl(path: string): Promise<string> {
|
|
302
|
+
let resolvedPath = path;
|
|
303
|
+
if (path.includes(':org_id')) {
|
|
304
|
+
resolvedPath = path.replace(':org_id', await this.currentOrgId());
|
|
305
|
+
}
|
|
306
|
+
this.assertAgentInScope(resolvedPath);
|
|
98
307
|
return `${this.hosts.copilot}${resolvedPath}`;
|
|
99
308
|
}
|
|
100
309
|
|
|
@@ -102,134 +311,196 @@ export class IndemnClient {
|
|
|
102
311
|
return `${this.hosts.evaluations}${path}`;
|
|
103
312
|
}
|
|
104
313
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
314
|
+
/**
|
|
315
|
+
* The one place every request goes through: client headers, credential,
|
|
316
|
+
* --debug tracing, and retries for reads only (a retried create could
|
|
317
|
+
* create twice).
|
|
318
|
+
*/
|
|
319
|
+
private async send(backend: Backend, method: Method, url: string, opts: SendOptions = {}): Promise<Response> {
|
|
320
|
+
const headers: Record<string, string> = {
|
|
321
|
+
'User-Agent': `indemn-cli/${cliVersion()}`,
|
|
322
|
+
'X-Indemn-Client': clientKind,
|
|
323
|
+
...opts.headers,
|
|
324
|
+
};
|
|
325
|
+
let body: BodyInit | undefined;
|
|
326
|
+
if (opts.body instanceof FormData) {
|
|
327
|
+
body = opts.body;
|
|
328
|
+
} else if (opts.body !== undefined) {
|
|
329
|
+
headers['Content-Type'] = 'application/json';
|
|
330
|
+
body = JSON.stringify(opts.body);
|
|
331
|
+
} else if (method !== 'GET') {
|
|
332
|
+
headers['Content-Type'] = 'application/json';
|
|
333
|
+
}
|
|
334
|
+
if (backend === 'copilot' && opts.auth !== false) {
|
|
335
|
+
headers['Authorization'] = await this.authHeader();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const attempts = method === 'GET' ? RETRY_DELAYS_MS.length + 1 : 1;
|
|
339
|
+
for (let attempt = 0; ; attempt++) {
|
|
340
|
+
const started = Date.now();
|
|
341
|
+
if (isDebug()) process.stderr.write(`[debug] → ${method} ${url}\n`);
|
|
342
|
+
try {
|
|
343
|
+
const res = await fetch(url, { method, headers, body });
|
|
344
|
+
if (isDebug()) process.stderr.write(`[debug] ← ${res.status} ${method} ${url} (${Date.now() - started} ms)\n`);
|
|
345
|
+
if (RETRY_STATUSES.has(res.status) && attempt + 1 < attempts) {
|
|
346
|
+
await sleep(RETRY_DELAYS_MS[attempt]);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
return res;
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (isDebug()) process.stderr.write(`[debug] ✗ ${method} ${url}: ${(err as Error).message}\n`);
|
|
352
|
+
if (attempt + 1 < attempts) {
|
|
353
|
+
await sleep(RETRY_DELAYS_MS[attempt]);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
throw err;
|
|
112
357
|
}
|
|
113
358
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private withQuery(url: string, query?: Record<string, string>): string {
|
|
362
|
+
if (!query) return url;
|
|
363
|
+
const u = new URL(url);
|
|
364
|
+
for (const [k, v] of Object.entries(query)) {
|
|
365
|
+
if (v !== undefined && v !== null) u.searchParams.set(k, v);
|
|
366
|
+
}
|
|
367
|
+
return u.toString();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* A copilot request with the session rules: renew before expiry, and on a
|
|
372
|
+
* 401 with a saved session renew once and retry once.
|
|
373
|
+
*/
|
|
374
|
+
private async copilotRequest<T>(method: Method, path: string, opts: { body?: unknown; query?: Record<string, string> } = {}): Promise<T> {
|
|
375
|
+
const url = this.withQuery(await this.buildCopilotUrl(path), opts.query);
|
|
376
|
+
let res = await this.send('copilot', method, url, { body: opts.body });
|
|
377
|
+
const cred = this.getCredential();
|
|
378
|
+
if (res.status === 401 && cred?.kind === 'token' && cred.source === 'config') {
|
|
379
|
+
await this.renewSession();
|
|
380
|
+
res = await this.send('copilot', method, url, { body: opts.body });
|
|
381
|
+
}
|
|
120
382
|
return this.handleCopilotResponse<T>(res);
|
|
121
383
|
}
|
|
122
384
|
|
|
385
|
+
/** A copilot request with any method (for PATCH and other routes without a helper). */
|
|
386
|
+
async copilotRequestRaw<T = unknown>(method: Method | 'PATCH', path: string, body?: unknown): Promise<T> {
|
|
387
|
+
return this.copilotRequest<T>(method as Method, path, { body });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** A copilot request whose answer is a file or audio, returned as bytes. */
|
|
391
|
+
async copilotBinary(method: Method, path: string, body?: unknown): Promise<{ contentType: string; data: Buffer }> {
|
|
392
|
+
const res = await this.send('copilot', method, await this.buildCopilotUrl(path), { body });
|
|
393
|
+
if (res.status === 401) throw new SessionExpiredError('Authentication failed. Run `indemn login` to re-authenticate.');
|
|
394
|
+
if (!res.ok) throw new Error(`API error (${res.status}): ${await res.text()}`);
|
|
395
|
+
return { contentType: res.headers.get('content-type') ?? 'application/octet-stream', data: Buffer.from(await res.arrayBuffer()) };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Copilot Server requests (with /api/ prefix and org_id injection)
|
|
399
|
+
async copilotGet<T = unknown>(path: string, query?: Record<string, string>): Promise<T> {
|
|
400
|
+
return this.copilotRequest<T>('GET', path, { query });
|
|
401
|
+
}
|
|
402
|
+
|
|
123
403
|
async copilotPost<T = unknown>(path: string, body?: unknown): Promise<T> {
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
},
|
|
131
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
132
|
-
});
|
|
133
|
-
return this.handleCopilotResponse<T>(res);
|
|
404
|
+
return this.copilotRequest<T>('POST', path, { body });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Multipart upload (for example a knowledge-base file). */
|
|
408
|
+
async copilotPostForm<T = unknown>(path: string, form: FormData): Promise<T> {
|
|
409
|
+
return this.copilotRequest<T>('POST', path, { body: form });
|
|
134
410
|
}
|
|
135
411
|
|
|
136
412
|
async copilotPut<T = unknown>(path: string, body?: unknown): Promise<T> {
|
|
137
|
-
this.
|
|
138
|
-
const res = await fetch(this.buildCopilotUrl(path), {
|
|
139
|
-
method: 'PUT',
|
|
140
|
-
headers: {
|
|
141
|
-
'Authorization': `Bearer ${this.apiKey}`,
|
|
142
|
-
'Content-Type': 'application/json',
|
|
143
|
-
},
|
|
144
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
145
|
-
});
|
|
146
|
-
return this.handleCopilotResponse<T>(res);
|
|
413
|
+
return this.copilotRequest<T>('PUT', path, { body });
|
|
147
414
|
}
|
|
148
415
|
|
|
149
416
|
async copilotDelete<T = unknown>(path: string): Promise<T> {
|
|
150
|
-
this.
|
|
151
|
-
const res = await fetch(this.buildCopilotUrl(path), {
|
|
152
|
-
method: 'DELETE',
|
|
153
|
-
headers: {
|
|
154
|
-
'Authorization': `Bearer ${this.apiKey}`,
|
|
155
|
-
'Content-Type': 'application/json',
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
return this.handleCopilotResponse<T>(res);
|
|
417
|
+
return this.copilotRequest<T>('DELETE', path);
|
|
159
418
|
}
|
|
160
419
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
420
|
+
/**
|
|
421
|
+
* Darwin (the V2 platform) request: the same session token as copilot, sent
|
|
422
|
+
* as Bearer, with the org in X-Organization-Id. Darwin checks membership and
|
|
423
|
+
* roles itself. A 401 on a saved session renews once and retries once.
|
|
424
|
+
*/
|
|
425
|
+
async platformRequest<T = unknown>(method: Method, path: string, opts: { body?: unknown; query?: Record<string, string>; orgHeader?: boolean } = {}): Promise<T> {
|
|
426
|
+
const base = this.hosts.platform;
|
|
427
|
+
if (!base) throw new CliError('No V2 platform host is configured for this environment.');
|
|
428
|
+
const url = this.withQuery(`${base}${path}`, opts.query);
|
|
429
|
+
this.assertAgentInScope(path);
|
|
430
|
+
const headersFor = async (): Promise<Record<string, string>> => {
|
|
431
|
+
const headers: Record<string, string> = { Authorization: darwinAuthHeader(await this.freshCredential()) };
|
|
432
|
+
if (opts.orgHeader !== false) headers['X-Organization-Id'] = await this.currentOrgId();
|
|
433
|
+
return headers;
|
|
434
|
+
};
|
|
435
|
+
let res = await this.send('platform', method, url, { body: opts.body, headers: await headersFor() });
|
|
436
|
+
const cred = this.getCredential();
|
|
437
|
+
if (res.status === 401 && cred?.kind === 'token' && cred.source === 'config') {
|
|
438
|
+
await this.renewSession();
|
|
439
|
+
res = await this.send('platform', method, url, { body: opts.body, headers: await headersFor() });
|
|
168
440
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
441
|
+
if (res.status === 401) throw new SessionExpiredError('The V2 platform rejected the session. Run `indemn login`, then retry.');
|
|
442
|
+
if (!res.ok) {
|
|
443
|
+
const text = await res.text();
|
|
444
|
+
let detail = text;
|
|
445
|
+
try { detail = (JSON.parse(text) as { detail?: unknown }).detail as string ?? text; } catch { /* plain text */ }
|
|
446
|
+
if (res.status === 403) throw new CliError(`Not allowed by the V2 platform: ${typeof detail === 'string' ? detail : JSON.stringify(detail)}`);
|
|
447
|
+
if (res.status === 404) throw new CliError('Not found on the V2 platform.');
|
|
448
|
+
throw new CliError(`V2 platform error (${res.status}): ${typeof detail === 'string' ? detail : JSON.stringify(detail)}`);
|
|
449
|
+
}
|
|
450
|
+
if (res.status === 204) return undefined as T;
|
|
451
|
+
return res.json() as Promise<T>;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Evaluations Service requests (no /api/ prefix, no org_id, no auth)
|
|
455
|
+
async evalsGet<T = unknown>(path: string, query?: Record<string, string>): Promise<T> {
|
|
456
|
+
const res = await this.send('evaluations', 'GET', this.withQuery(this.buildEvalsUrl(path), query));
|
|
172
457
|
return this.handleEvalsResponse<T>(res);
|
|
173
458
|
}
|
|
174
459
|
|
|
175
460
|
async evalsPost<T = unknown>(path: string, body?: unknown): Promise<T> {
|
|
176
|
-
const res = await
|
|
177
|
-
method: 'POST',
|
|
178
|
-
headers: { 'Content-Type': 'application/json' },
|
|
179
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
180
|
-
});
|
|
461
|
+
const res = await this.send('evaluations', 'POST', this.buildEvalsUrl(path), { body });
|
|
181
462
|
return this.handleEvalsResponse<T>(res);
|
|
182
463
|
}
|
|
183
464
|
|
|
184
465
|
async evalsPut<T = unknown>(path: string, body?: unknown): Promise<T> {
|
|
185
|
-
const res = await
|
|
186
|
-
method: 'PUT',
|
|
187
|
-
headers: { 'Content-Type': 'application/json' },
|
|
188
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
189
|
-
});
|
|
466
|
+
const res = await this.send('evaluations', 'PUT', this.buildEvalsUrl(path), { body });
|
|
190
467
|
return this.handleEvalsResponse<T>(res);
|
|
191
468
|
}
|
|
192
469
|
|
|
193
470
|
async evalsDelete<T = unknown>(path: string): Promise<T> {
|
|
194
|
-
const res = await
|
|
195
|
-
method: 'DELETE',
|
|
196
|
-
headers: { 'Content-Type': 'application/json' },
|
|
197
|
-
});
|
|
471
|
+
const res = await this.send('evaluations', 'DELETE', this.buildEvalsUrl(path));
|
|
198
472
|
return this.handleEvalsResponse<T>(res);
|
|
199
473
|
}
|
|
200
474
|
|
|
201
475
|
// Unauthenticated Copilot Server requests (for login flow)
|
|
202
476
|
async copilotPostUnauth<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T> {
|
|
203
|
-
const res = await
|
|
204
|
-
method: 'POST',
|
|
205
|
-
headers: {
|
|
206
|
-
'Content-Type': 'application/json',
|
|
207
|
-
...headers,
|
|
208
|
-
},
|
|
209
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
210
|
-
});
|
|
477
|
+
const res = await this.send('copilot', 'POST', `${this.hosts.copilot}${path}`, { body, headers, auth: false });
|
|
211
478
|
return this.handleCopilotResponse<T>(res);
|
|
212
479
|
}
|
|
213
480
|
|
|
214
481
|
private async handleCopilotResponse<T>(res: Response): Promise<T> {
|
|
215
482
|
if (res.status === 401) {
|
|
216
|
-
throw new
|
|
483
|
+
throw new SessionExpiredError('Authentication failed. Run `indemn login` to re-authenticate.');
|
|
217
484
|
}
|
|
218
485
|
if (res.status === 403) {
|
|
219
486
|
throw new Error('Access denied. You may not have permission for this operation.');
|
|
220
487
|
}
|
|
221
488
|
if (res.status === 404) {
|
|
222
|
-
|
|
489
|
+
// Keep the server's reason: voice-service answers 404 with specific messages (no phone number, ...).
|
|
490
|
+
const text = await res.text().catch(() => '');
|
|
491
|
+
let reason = '';
|
|
492
|
+
try { const b = JSON.parse(text) as { message?: unknown; error?: unknown }; reason = String(b.message ?? b.error ?? ''); } catch { reason = text.trim().startsWith('<') ? '' : text.slice(0, 200); }
|
|
493
|
+
throw new Error(reason ? `Not found: ${reason}` : 'Resource not found.');
|
|
223
494
|
}
|
|
224
495
|
if (!res.ok) {
|
|
225
496
|
const text = await res.text();
|
|
226
497
|
throw new Error(`API error (${res.status}): ${text}`);
|
|
227
498
|
}
|
|
228
|
-
const json = await res.json() as ApiResponse<T
|
|
499
|
+
const json = await res.json() as ApiResponse<T> & { message?: string };
|
|
229
500
|
// Copilot Server wraps responses in { success, data }
|
|
230
501
|
if (json.success !== undefined) {
|
|
231
502
|
if (!json.success) {
|
|
232
|
-
throw new Error(json.msg || 'API request failed');
|
|
503
|
+
throw new Error(json.msg || json.message || 'API request failed');
|
|
233
504
|
}
|
|
234
505
|
return (json.data !== undefined ? json.data : json) as T;
|
|
235
506
|
}
|