@dharta/cli 0.6.77

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 (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/dist/agent.d.ts +17 -0
  4. package/dist/agent.js +134 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/api-error.d.ts +3 -0
  7. package/dist/api-error.js +80 -0
  8. package/dist/api-error.js.map +1 -0
  9. package/dist/browser.d.ts +1 -0
  10. package/dist/browser.js +19 -0
  11. package/dist/browser.js.map +1 -0
  12. package/dist/cli.d.ts +5 -0
  13. package/dist/cli.js +1978 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/commands/agents.d.ts +131 -0
  16. package/dist/commands/agents.js +439 -0
  17. package/dist/commands/agents.js.map +1 -0
  18. package/dist/commands/analytics.d.ts +25 -0
  19. package/dist/commands/analytics.js +166 -0
  20. package/dist/commands/analytics.js.map +1 -0
  21. package/dist/commands/api.d.ts +19 -0
  22. package/dist/commands/api.js +278 -0
  23. package/dist/commands/api.js.map +1 -0
  24. package/dist/commands/appearance.d.ts +19 -0
  25. package/dist/commands/appearance.js +63 -0
  26. package/dist/commands/appearance.js.map +1 -0
  27. package/dist/commands/artifact.d.ts +33 -0
  28. package/dist/commands/artifact.js +235 -0
  29. package/dist/commands/artifact.js.map +1 -0
  30. package/dist/commands/audit-log.d.ts +32 -0
  31. package/dist/commands/audit-log.js +102 -0
  32. package/dist/commands/audit-log.js.map +1 -0
  33. package/dist/commands/auth.d.ts +47 -0
  34. package/dist/commands/auth.js +117 -0
  35. package/dist/commands/auth.js.map +1 -0
  36. package/dist/commands/caps.d.ts +82 -0
  37. package/dist/commands/caps.js +213 -0
  38. package/dist/commands/caps.js.map +1 -0
  39. package/dist/commands/create.d.ts +22 -0
  40. package/dist/commands/create.js +76 -0
  41. package/dist/commands/create.js.map +1 -0
  42. package/dist/commands/deploy.d.ts +45 -0
  43. package/dist/commands/deploy.js +892 -0
  44. package/dist/commands/deploy.js.map +1 -0
  45. package/dist/commands/dev.d.ts +33 -0
  46. package/dist/commands/dev.js +655 -0
  47. package/dist/commands/dev.js.map +1 -0
  48. package/dist/commands/directories.d.ts +60 -0
  49. package/dist/commands/directories.js +83 -0
  50. package/dist/commands/directories.js.map +1 -0
  51. package/dist/commands/doctor.d.ts +13 -0
  52. package/dist/commands/doctor.js +123 -0
  53. package/dist/commands/doctor.js.map +1 -0
  54. package/dist/commands/embed-keys.d.ts +61 -0
  55. package/dist/commands/embed-keys.js +165 -0
  56. package/dist/commands/embed-keys.js.map +1 -0
  57. package/dist/commands/env.d.ts +59 -0
  58. package/dist/commands/env.js +300 -0
  59. package/dist/commands/env.js.map +1 -0
  60. package/dist/commands/files.d.ts +14 -0
  61. package/dist/commands/files.js +84 -0
  62. package/dist/commands/files.js.map +1 -0
  63. package/dist/commands/git-credential.d.ts +1 -0
  64. package/dist/commands/git-credential.js +70 -0
  65. package/dist/commands/git-credential.js.map +1 -0
  66. package/dist/commands/kartas.d.ts +101 -0
  67. package/dist/commands/kartas.js +289 -0
  68. package/dist/commands/kartas.js.map +1 -0
  69. package/dist/commands/keys.d.ts +33 -0
  70. package/dist/commands/keys.js +141 -0
  71. package/dist/commands/keys.js.map +1 -0
  72. package/dist/commands/login.d.ts +22 -0
  73. package/dist/commands/login.js +78 -0
  74. package/dist/commands/login.js.map +1 -0
  75. package/dist/commands/logs.d.ts +14 -0
  76. package/dist/commands/logs.js +89 -0
  77. package/dist/commands/logs.js.map +1 -0
  78. package/dist/commands/members.d.ts +32 -0
  79. package/dist/commands/members.js +77 -0
  80. package/dist/commands/members.js.map +1 -0
  81. package/dist/commands/model-keys.d.ts +22 -0
  82. package/dist/commands/model-keys.js +222 -0
  83. package/dist/commands/model-keys.js.map +1 -0
  84. package/dist/commands/open.d.ts +29 -0
  85. package/dist/commands/open.js +119 -0
  86. package/dist/commands/open.js.map +1 -0
  87. package/dist/commands/parity.d.ts +15 -0
  88. package/dist/commands/parity.js +73 -0
  89. package/dist/commands/parity.js.map +1 -0
  90. package/dist/commands/rollback.d.ts +21 -0
  91. package/dist/commands/rollback.js +26 -0
  92. package/dist/commands/rollback.js.map +1 -0
  93. package/dist/commands/schedules.d.ts +52 -0
  94. package/dist/commands/schedules.js +231 -0
  95. package/dist/commands/schedules.js.map +1 -0
  96. package/dist/commands/scopes.d.ts +3 -0
  97. package/dist/commands/scopes.js +21 -0
  98. package/dist/commands/scopes.js.map +1 -0
  99. package/dist/commands/sessions.d.ts +53 -0
  100. package/dist/commands/sessions.js +142 -0
  101. package/dist/commands/sessions.js.map +1 -0
  102. package/dist/commands/setup.d.ts +28 -0
  103. package/dist/commands/setup.js +949 -0
  104. package/dist/commands/setup.js.map +1 -0
  105. package/dist/commands/status.d.ts +19 -0
  106. package/dist/commands/status.js +63 -0
  107. package/dist/commands/status.js.map +1 -0
  108. package/dist/commands/update.d.ts +14 -0
  109. package/dist/commands/update.js +120 -0
  110. package/dist/commands/update.js.map +1 -0
  111. package/dist/commands/usage.d.ts +3 -0
  112. package/dist/commands/usage.js +37 -0
  113. package/dist/commands/usage.js.map +1 -0
  114. package/dist/commands/webhooks.d.ts +47 -0
  115. package/dist/commands/webhooks.js +156 -0
  116. package/dist/commands/webhooks.js.map +1 -0
  117. package/dist/commands/whoami.d.ts +7 -0
  118. package/dist/commands/whoami.js +17 -0
  119. package/dist/commands/whoami.js.map +1 -0
  120. package/dist/completions.d.ts +13 -0
  121. package/dist/completions.js +120 -0
  122. package/dist/completions.js.map +1 -0
  123. package/dist/config.d.ts +20 -0
  124. package/dist/config.js +98 -0
  125. package/dist/config.js.map +1 -0
  126. package/dist/context.d.ts +14 -0
  127. package/dist/context.js +38 -0
  128. package/dist/context.js.map +1 -0
  129. package/dist/device-flow.d.ts +9 -0
  130. package/dist/device-flow.js +127 -0
  131. package/dist/device-flow.js.map +1 -0
  132. package/dist/harness-preflight.d.ts +24 -0
  133. package/dist/harness-preflight.js +252 -0
  134. package/dist/harness-preflight.js.map +1 -0
  135. package/dist/harnesses.d.ts +11 -0
  136. package/dist/harnesses.js +112 -0
  137. package/dist/harnesses.js.map +1 -0
  138. package/dist/index.d.ts +3 -0
  139. package/dist/index.js +3 -0
  140. package/dist/index.js.map +1 -0
  141. package/dist/login-offer.d.ts +9 -0
  142. package/dist/login-offer.js +36 -0
  143. package/dist/login-offer.js.map +1 -0
  144. package/dist/manifest.d.ts +15 -0
  145. package/dist/manifest.js +142 -0
  146. package/dist/manifest.js.map +1 -0
  147. package/dist/money.d.ts +1 -0
  148. package/dist/money.js +16 -0
  149. package/dist/money.js.map +1 -0
  150. package/dist/output.d.ts +2 -0
  151. package/dist/output.js +20 -0
  152. package/dist/output.js.map +1 -0
  153. package/dist/runtime.d.ts +19 -0
  154. package/dist/runtime.js +95 -0
  155. package/dist/runtime.js.map +1 -0
  156. package/dist/scaffold.d.ts +16 -0
  157. package/dist/scaffold.js +74 -0
  158. package/dist/scaffold.js.map +1 -0
  159. package/dist/setup-scan.d.ts +21 -0
  160. package/dist/setup-scan.js +127 -0
  161. package/dist/setup-scan.js.map +1 -0
  162. package/dist/templates.d.ts +14 -0
  163. package/dist/templates.js +65 -0
  164. package/dist/templates.js.map +1 -0
  165. package/dist/validators.d.ts +10 -0
  166. package/dist/validators.js +116 -0
  167. package/dist/validators.js.map +1 -0
  168. package/dist/version-check.d.ts +25 -0
  169. package/dist/version-check.js +68 -0
  170. package/dist/version-check.js.map +1 -0
  171. package/node_modules/@dharta/sdk/LICENSE +21 -0
  172. package/node_modules/@dharta/sdk/README.md +87 -0
  173. package/node_modules/@dharta/sdk/dist/errors.d.ts +20 -0
  174. package/node_modules/@dharta/sdk/dist/errors.d.ts.map +1 -0
  175. package/node_modules/@dharta/sdk/dist/errors.js +51 -0
  176. package/node_modules/@dharta/sdk/dist/errors.js.map +1 -0
  177. package/node_modules/@dharta/sdk/dist/http.d.ts +26 -0
  178. package/node_modules/@dharta/sdk/dist/http.d.ts.map +1 -0
  179. package/node_modules/@dharta/sdk/dist/http.js +136 -0
  180. package/node_modules/@dharta/sdk/dist/http.js.map +1 -0
  181. package/node_modules/@dharta/sdk/dist/index.d.ts +15 -0
  182. package/node_modules/@dharta/sdk/dist/index.d.ts.map +1 -0
  183. package/node_modules/@dharta/sdk/dist/index.js +21 -0
  184. package/node_modules/@dharta/sdk/dist/index.js.map +1 -0
  185. package/node_modules/@dharta/sdk/dist/resources.d.ts +48 -0
  186. package/node_modules/@dharta/sdk/dist/resources.d.ts.map +1 -0
  187. package/node_modules/@dharta/sdk/dist/resources.js +150 -0
  188. package/node_modules/@dharta/sdk/dist/resources.js.map +1 -0
  189. package/node_modules/@dharta/sdk/dist/types.d.ts +94 -0
  190. package/node_modules/@dharta/sdk/dist/types.d.ts.map +1 -0
  191. package/node_modules/@dharta/sdk/dist/types.js +59 -0
  192. package/node_modules/@dharta/sdk/dist/types.js.map +1 -0
  193. package/node_modules/@dharta/sdk/package.json +32 -0
  194. package/node_modules/@dharta/sdk/src/errors.ts +58 -0
  195. package/node_modules/@dharta/sdk/src/http.ts +166 -0
  196. package/node_modules/@dharta/sdk/src/index.ts +36 -0
  197. package/node_modules/@dharta/sdk/src/resources.ts +197 -0
  198. package/node_modules/@dharta/sdk/src/types.ts +179 -0
  199. package/package.json +38 -0
  200. package/src/agent.ts +147 -0
  201. package/src/api-error.ts +88 -0
  202. package/src/browser.ts +20 -0
  203. package/src/cli.ts +2339 -0
  204. package/src/commands/agents.ts +635 -0
  205. package/src/commands/analytics.ts +181 -0
  206. package/src/commands/api.ts +297 -0
  207. package/src/commands/appearance.ts +79 -0
  208. package/src/commands/artifact.ts +331 -0
  209. package/src/commands/audit-log.ts +125 -0
  210. package/src/commands/auth.ts +186 -0
  211. package/src/commands/caps.ts +302 -0
  212. package/src/commands/create.ts +114 -0
  213. package/src/commands/deploy.ts +1122 -0
  214. package/src/commands/dev.ts +792 -0
  215. package/src/commands/directories.ts +136 -0
  216. package/src/commands/doctor.ts +138 -0
  217. package/src/commands/embed-keys.ts +237 -0
  218. package/src/commands/env.ts +369 -0
  219. package/src/commands/files.ts +124 -0
  220. package/src/commands/git-credential.ts +78 -0
  221. package/src/commands/kartas.ts +439 -0
  222. package/src/commands/keys.ts +206 -0
  223. package/src/commands/login.ts +115 -0
  224. package/src/commands/logs.ts +111 -0
  225. package/src/commands/members.ts +109 -0
  226. package/src/commands/model-keys.ts +278 -0
  227. package/src/commands/open.ts +173 -0
  228. package/src/commands/parity.ts +96 -0
  229. package/src/commands/rollback.ts +45 -0
  230. package/src/commands/schedules.ts +290 -0
  231. package/src/commands/scopes.ts +22 -0
  232. package/src/commands/sessions.ts +225 -0
  233. package/src/commands/setup.ts +1176 -0
  234. package/src/commands/status.ts +94 -0
  235. package/src/commands/update.ts +157 -0
  236. package/src/commands/usage.ts +41 -0
  237. package/src/commands/webhooks.ts +200 -0
  238. package/src/commands/whoami.ts +24 -0
  239. package/src/completions.ts +168 -0
  240. package/src/config.ts +143 -0
  241. package/src/context.ts +59 -0
  242. package/src/device-flow.ts +174 -0
  243. package/src/harness-preflight.ts +293 -0
  244. package/src/harnesses.ts +117 -0
  245. package/src/index.ts +3 -0
  246. package/src/login-offer.ts +49 -0
  247. package/src/manifest.ts +153 -0
  248. package/src/money.ts +15 -0
  249. package/src/output.ts +22 -0
  250. package/src/runtime.ts +127 -0
  251. package/src/scaffold.ts +102 -0
  252. package/src/setup-scan.ts +154 -0
  253. package/src/templates.ts +85 -0
  254. package/src/validators.ts +124 -0
  255. package/src/version-check.ts +75 -0
  256. package/templates/blank/CLAUDE.md +21 -0
  257. package/templates/blank/dharta.toml +11 -0
  258. package/templates/faq-agent/CLAUDE.md +51 -0
  259. package/templates/faq-agent/dharta.toml +11 -0
@@ -0,0 +1,115 @@
1
+ import { spawn } from "node:child_process";
2
+ import { hostname } from "node:os";
3
+ import { HttpClient } from "@dharta/sdk";
4
+ import { cliFormatError, cliFormatNetworkError } from "../api-error.js";
5
+ import {
6
+ DEFAULT_BASE_URL,
7
+ DEFAULT_GIT_BASE_URL,
8
+ saveProfile,
9
+ } from "../config.js";
10
+ import { deviceFlowLogin, type DeviceFlowOptions } from "../device-flow.js";
11
+ import type { Me } from "./auth.js";
12
+ import { normalizeHttpUrl, normalizeNonBlank } from "../validators.js";
13
+
14
+ export interface LoginOptions {
15
+ baseUrl?: string;
16
+ gitBaseUrl?: string;
17
+ profile?: string;
18
+ // Direct key entry: `--with-token` pipes it via stdin (cli.ts reads the
19
+ // stream and passes it here); tests pass it straight in.
20
+ apiKey?: string;
21
+ fetchImpl?: typeof fetch;
22
+ runGit?: (args: string[]) => Promise<boolean>; // for tests
23
+ deviceFlow?: Partial<DeviceFlowOptions>; // test seams for the browser dance
24
+ log?: (line: string) => void;
25
+ }
26
+
27
+ export interface LoginResult {
28
+ path: string;
29
+ helperInstalled: boolean;
30
+ gitBaseUrl: string;
31
+ organization: { slug: string; name: string };
32
+ keyName: string;
33
+ }
34
+
35
+ // Default path is the RFC 8628 device flow (no key ever pasted); the
36
+ // `--with-token` / apiKey path is the gh-parity fallback for CI and
37
+ // air-gapped boxes.
38
+ export async function login(opts: LoginOptions = {}): Promise<LoginResult> {
39
+ const baseUrl = opts.baseUrl === undefined ? DEFAULT_BASE_URL : normalizeHttpUrl(opts.baseUrl, "--base-url");
40
+ const gitBaseUrl = opts.gitBaseUrl === undefined ? DEFAULT_GIT_BASE_URL : normalizeHttpUrl(opts.gitBaseUrl, "--git-base-url");
41
+ const profileName = opts.profile === undefined ? "default" : normalizeNonBlank(opts.profile, "--profile");
42
+
43
+ const key =
44
+ opts.apiKey ??
45
+ (await deviceFlowLogin({
46
+ baseUrl,
47
+ clientName: hostname(),
48
+ fetchImpl: opts.fetchImpl,
49
+ log: opts.log,
50
+ ...opts.deviceFlow,
51
+ }));
52
+ if (!key) {
53
+ throw new Error(
54
+ "No API key provided. Run `dharta login` for the browser flow, or pipe a key: " +
55
+ "echo $DHARTA_API_KEY | dharta login --with-token",
56
+ );
57
+ }
58
+
59
+ // Validate the credential and learn who it is in one round-trip.
60
+ const http = new HttpClient({
61
+ apiKey: key,
62
+ baseUrl,
63
+ fetch: opts.fetchImpl,
64
+ formatError: cliFormatError,
65
+ formatNetworkError: cliFormatNetworkError,
66
+ });
67
+ const me = await http.request<Me>({ path: "/me" });
68
+
69
+ // RFC 0043 D8: the data-plane host this org clones karta workspaces from (the helper
70
+ // mints against it; the clone bytes never pass through the control plane).
71
+ const workspaceGitBaseUrl = me.workspace_git?.base_url;
72
+ const path = await saveProfile(profileName, {
73
+ api_key: key,
74
+ base_url: baseUrl,
75
+ git_base_url: gitBaseUrl,
76
+ workspace_git_base_url: workspaceGitBaseUrl,
77
+ });
78
+
79
+ const runGitFn = opts.runGit ?? runGit;
80
+
81
+ // RFC 0011 GIT-5: wire the credential helper so `git push dharta`
82
+ // never prompts. Scoped to the git host only; the key itself stays
83
+ // in ~/.dharta/config.toml (the helper reads it at use time, so
84
+ // re-login/rotation needs no re-wiring). Best effort: a machine
85
+ // without git still logs in fine.
86
+ const helperInstalled = await runGitFn([
87
+ "config",
88
+ "--global",
89
+ `credential.${gitBaseUrl}.helper`,
90
+ "!dharta git-credential",
91
+ ]);
92
+
93
+ // RFC 0043 D8: the same helper for the workspace clone host, plus useHttpPath so git
94
+ // passes the karta id in the path (the helper mints a per-karta read-only token).
95
+ if (workspaceGitBaseUrl) {
96
+ await runGitFn(["config", "--global", `credential.${workspaceGitBaseUrl}.helper`, "!dharta git-credential"]);
97
+ await runGitFn(["config", "--global", `credential.${workspaceGitBaseUrl}.useHttpPath`, "true"]);
98
+ }
99
+
100
+ return {
101
+ path,
102
+ helperInstalled,
103
+ gitBaseUrl,
104
+ organization: me.organization,
105
+ keyName: me.api_key.name,
106
+ };
107
+ }
108
+
109
+ function runGit(args: string[]): Promise<boolean> {
110
+ return new Promise((resolvePromise) => {
111
+ const proc = spawn("git", args, { stdio: "ignore" });
112
+ proc.on("error", () => resolvePromise(false));
113
+ proc.on("close", (code) => resolvePromise(code === 0));
114
+ });
115
+ }
@@ -0,0 +1,111 @@
1
+ import { cliFormatRawError } from "../api-error.js";
2
+ import type { CliContext } from "../context.js";
3
+ import { validateAgentSlug } from "../agent.js";
4
+ import { normalizeIsoTimestamp, validateIsoTimestamp } from "../validators.js";
5
+
6
+ export interface LogLine {
7
+ ts: string;
8
+ level?: string;
9
+ message: string;
10
+ }
11
+
12
+ export interface LogsOptions {
13
+ tail?: boolean;
14
+ since?: string;
15
+ pollIntervalMs?: number;
16
+ // Stops polling after N iterations — tests pass a small number;
17
+ // production CLI usage leaves it undefined for "forever".
18
+ maxIterations?: number;
19
+ }
20
+
21
+ export function validateLogsOptions(opts: LogsOptions): void {
22
+ if (opts.since !== undefined) validateIsoTimestamp(opts.since, "--since");
23
+ }
24
+
25
+ // Streams or polls the agent's logs. SSE-first: if the server
26
+ // answers with `Content-Type: text/event-stream`, we parse the stream;
27
+ // otherwise we fall through to JSON polling with `since=`.
28
+ //
29
+ // Output is written line-by-line to `write` so the CLI can stream
30
+ // directly to stdout and tests can capture the sequence.
31
+ export async function logsTail(
32
+ ctx: CliContext,
33
+ slug: string,
34
+ opts: LogsOptions,
35
+ write: (line: string) => void,
36
+ ): Promise<number> {
37
+ const normalizedSlug = validateAgentSlug(slug, "agent slug");
38
+ const base = `/api/agents/${encodeURIComponent(normalizedSlug)}/logs`;
39
+ const initialSince = opts.since === undefined ? undefined : normalizeIsoTimestamp(opts.since, "--since");
40
+ let emitted = 0;
41
+ const emit = (line: LogLine): void => {
42
+ emitted += 1;
43
+ write(formatLine(line));
44
+ };
45
+
46
+ if (opts.tail) {
47
+ const res = await ctx.http.raw(`${base}?tail=1`, {
48
+ headers: { Accept: "text/event-stream" },
49
+ });
50
+ if (!res.ok) throw await cliFormatRawError(res);
51
+ const ctype = res.headers.get("content-type") ?? "";
52
+ if (ctype.includes("text/event-stream") && res.body) {
53
+ await consumeSse(res.body, emit);
54
+ return emitted;
55
+ }
56
+ // Fall through to polling if server didn't accept SSE.
57
+ }
58
+
59
+ let since = initialSince ?? new Date(Date.now() - 60_000).toISOString();
60
+ const maxIter = opts.maxIterations ?? Infinity;
61
+ for (let i = 0; i < maxIter; i++) {
62
+ const url = `${base}?since=${encodeURIComponent(since)}`;
63
+ const res = await ctx.http.request<{ data: LogLine[]; next_since?: string }>({
64
+ path: url,
65
+ });
66
+ for (const line of res.data) emit(line);
67
+ if (res.next_since) since = res.next_since;
68
+ if (!opts.tail) return emitted; // single shot for `--since` without `--tail`
69
+ await sleep(opts.pollIntervalMs ?? 2000);
70
+ }
71
+ return emitted;
72
+ }
73
+
74
+ function formatLine(line: LogLine): string {
75
+ const level = line.level ? `[${line.level}] ` : "";
76
+ return `${line.ts} ${level}${line.message}`;
77
+ }
78
+
79
+ async function consumeSse(
80
+ body: ReadableStream<Uint8Array>,
81
+ emit: (line: LogLine) => void,
82
+ ): Promise<void> {
83
+ const reader = body.getReader();
84
+ const decoder = new TextDecoder();
85
+ let buf = "";
86
+ for (;;) {
87
+ const { value, done } = await reader.read();
88
+ if (done) break;
89
+ buf += decoder.decode(value, { stream: true });
90
+ let idx: number;
91
+ while ((idx = buf.indexOf("\n\n")) >= 0) {
92
+ const block = buf.slice(0, idx);
93
+ buf = buf.slice(idx + 2);
94
+ const dataLine = block
95
+ .split("\n")
96
+ .filter((l) => l.startsWith("data:"))
97
+ .map((l) => l.slice(5).trim())
98
+ .join("\n");
99
+ if (!dataLine) continue;
100
+ try {
101
+ emit(JSON.parse(dataLine) as LogLine);
102
+ } catch {
103
+ emit({ ts: new Date().toISOString(), message: dataLine });
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ function sleep(ms: number): Promise<void> {
110
+ return new Promise((r) => setTimeout(r, ms));
111
+ }
@@ -0,0 +1,109 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { CliContext } from "../context.js";
3
+ import { table } from "../output.js";
4
+ import { closestChoice, normalizeNonBlank, parsePositiveInteger } from "../validators.js";
5
+
6
+ const MEMBER_ROLES = ["owner", "admin", "developer", "billing", "viewer"] as const;
7
+ type MemberRole = (typeof MEMBER_ROLES)[number];
8
+
9
+ export interface MemberRow {
10
+ id: number;
11
+ email: string;
12
+ role: string;
13
+ }
14
+
15
+ export interface InvitationRow {
16
+ id: number;
17
+ email: string;
18
+ role: string;
19
+ }
20
+
21
+ interface MembersEnvelope {
22
+ data: MemberRow[];
23
+ invitations: InvitationRow[];
24
+ }
25
+
26
+ export async function membersListData(ctx: CliContext): Promise<MembersEnvelope> {
27
+ return ctx.http.request<MembersEnvelope>({ path: "/api/members" });
28
+ }
29
+
30
+ export async function membersList(ctx: CliContext): Promise<string> {
31
+ const res = await membersListData(ctx);
32
+ const members = res.data.length ? table(res.data.map((m) => ({ id: String(m.id), email: m.email, role: m.role }))) : "No members.";
33
+ const invites = res.invitations.length
34
+ ? table(res.invitations.map((i) => ({ id: String(i.id), email: i.email, role: i.role })))
35
+ : "No pending invitations. Invite someone with: dharta members invite <email> --role viewer";
36
+ return `Members\n${members}\n\nInvitations\n${invites}`;
37
+ }
38
+
39
+ export async function membersInviteData(ctx: CliContext, email: string, role: string): Promise<{ invitation: InvitationRow }> {
40
+ validateMembersInviteInput(email);
41
+ const normalizedEmail = normalizeEmail(email);
42
+ const normalizedRole = normalizeRole(role);
43
+ return ctx.http.request<{ invitation: InvitationRow }>({
44
+ method: "POST",
45
+ path: "/api/members/invitations",
46
+ body: { email: normalizedEmail, role: normalizedRole },
47
+ headers: { "Idempotency-Key": randomUUID() },
48
+ });
49
+ }
50
+
51
+ export async function membersInvite(ctx: CliContext, email: string, role: string): Promise<string> {
52
+ const res = await membersInviteData(ctx, email, role);
53
+ return `Invited ${res.invitation.email} as ${res.invitation.role}.`;
54
+ }
55
+
56
+ export function validateMembersInviteInput(email: string): void {
57
+ normalizeEmail(email);
58
+ }
59
+
60
+ export async function membersSetRoleData(ctx: CliContext, id: string, role: string): Promise<{ member: MemberRow }> {
61
+ const memberId = parsePositiveInteger(id, "member id");
62
+ const normalizedRole = normalizeRole(role);
63
+ return ctx.http.request<{ member: MemberRow }>({
64
+ method: "PATCH",
65
+ path: `/api/members/${encodeURIComponent(memberId)}`,
66
+ body: { role: normalizedRole },
67
+ headers: { "Idempotency-Key": randomUUID() },
68
+ });
69
+ }
70
+
71
+ export async function membersSetRole(ctx: CliContext, id: string, role: string): Promise<string> {
72
+ const res = await membersSetRoleData(ctx, id, role);
73
+ return `${res.member.email} is now ${res.member.role}.`;
74
+ }
75
+
76
+ export async function membersRemoveData(ctx: CliContext, id: string): Promise<{ id: string; removed: boolean }> {
77
+ const memberId = parsePositiveInteger(id, "member id");
78
+ await ctx.http.request<void>({
79
+ method: "DELETE",
80
+ path: `/api/members/${encodeURIComponent(memberId)}`,
81
+ headers: { "Idempotency-Key": randomUUID() },
82
+ });
83
+ return { id: String(memberId), removed: true };
84
+ }
85
+
86
+ export async function membersRemove(ctx: CliContext, id: string): Promise<string> {
87
+ const res = await membersRemoveData(ctx, id);
88
+ return `Removed member ${res.id}.`;
89
+ }
90
+
91
+ function normalizeEmail(email: string): string {
92
+ const normalized = email.trim();
93
+ if (!normalized) {
94
+ throw new Error("Member email cannot be blank.");
95
+ }
96
+ return normalized;
97
+ }
98
+
99
+ function normalizeRole(role: string): MemberRole {
100
+ const normalized = normalizeNonBlank(role, "Member role");
101
+ if ((MEMBER_ROLES as readonly string[]).includes(normalized)) {
102
+ return normalized as MemberRole;
103
+ }
104
+ const suggestion = closestChoice(normalized, MEMBER_ROLES);
105
+ throw new Error(
106
+ `Unknown member role ${normalized}. Allowed roles: ${MEMBER_ROLES.join(", ")}.` +
107
+ (suggestion ? ` Did you mean ${suggestion}?` : ""),
108
+ );
109
+ }
@@ -0,0 +1,278 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { CliContext } from "../context.js";
3
+ import type { ModelApiFormat, ModelKey, ModelKeyProvider } from "@dharta/sdk";
4
+ import { fmtTs, table } from "../output.js";
5
+ import { closestChoice, normalizeHttpUrl, normalizeNonBlank, parsePositiveInteger } from "../validators.js";
6
+
7
+ interface ListEnvelope<T> {
8
+ data: T[];
9
+ }
10
+
11
+ interface ModelKeyEnvelope {
12
+ model_key: ModelKey;
13
+ }
14
+
15
+ const MODEL_KEY_PROVIDERS = ["anthropic", "openai", "bedrock", "vertex", "openrouter", "xai", "google"] as const;
16
+ export const MODEL_API_FORMATS = [
17
+ "anthropic_messages",
18
+ "openai_chat_completions",
19
+ "openai_responses",
20
+ "bedrock_converse",
21
+ "vertex_generate_content",
22
+ ] as const satisfies readonly ModelApiFormat[];
23
+ const MODEL_KEY_AUTH_TYPES = ["api_key", "oauth_subscription"] as const;
24
+ type ModelKeyAuthType = (typeof MODEL_KEY_AUTH_TYPES)[number];
25
+
26
+ export interface ModelKeyCreateOptions {
27
+ key?: string;
28
+ keyStdin?: boolean;
29
+ authType?: string;
30
+ endpointUrl?: string;
31
+ apiFormat?: string;
32
+ }
33
+
34
+ export async function modelKeysListData(ctx: CliContext): Promise<ModelKey[]> {
35
+ const res = await ctx.http.request<ListEnvelope<ModelKey>>({ path: "/model_keys" });
36
+ return res.data;
37
+ }
38
+
39
+ export async function modelKeysList(ctx: CliContext): Promise<string> {
40
+ const keys = await modelKeysListData(ctx);
41
+ if (keys.length === 0) return "No model keys. Store one with: dharta model-keys create <provider> <name> --key-stdin";
42
+ return table(keys.map((key) => ({
43
+ id: String(key.id),
44
+ provider: key.provider,
45
+ name: key.name,
46
+ tail: key.tail,
47
+ created: fmtTs(key.created_at),
48
+ revoked: fmtTs(key.revoked_at),
49
+ })));
50
+ }
51
+
52
+ export async function modelKeysCreateData(
53
+ ctx: CliContext,
54
+ provider: ModelKeyProvider,
55
+ name: string,
56
+ opts: ModelKeyCreateOptions = {},
57
+ ): Promise<ModelKey> {
58
+ validateModelKeysCreateOptions(provider, name, opts);
59
+ const normalizedProvider = normalizeProvider(provider);
60
+ const normalizedName = normalizeModelKeyName(name);
61
+ const plaintext = await resolvePlaintext(opts);
62
+ const normalizedAuthType = opts.authType === undefined ? undefined : normalizeAuthType(opts.authType);
63
+ const body: Record<string, string> = { provider: normalizedProvider, name: normalizedName, plaintext };
64
+ if (normalizedAuthType) body.auth_type = normalizedAuthType;
65
+ if (opts.endpointUrl) body.endpoint_url = normalizeEndpointUrl(opts.endpointUrl);
66
+ if (opts.apiFormat) body.api_format = normalizeApiFormat(opts.apiFormat);
67
+ try {
68
+ const res = await ctx.http.request<ModelKeyEnvelope>({
69
+ method: "POST",
70
+ path: "/model_keys",
71
+ body,
72
+ headers: { "Idempotency-Key": randomUUID() },
73
+ });
74
+ return res.model_key;
75
+ } catch (error) {
76
+ throw addModelKeysWriteScopeGuidance(error);
77
+ }
78
+ }
79
+
80
+ export async function modelKeysCreate(
81
+ ctx: CliContext,
82
+ provider: ModelKeyProvider,
83
+ name: string,
84
+ opts: ModelKeyCreateOptions = {},
85
+ ): Promise<string> {
86
+ const key = await modelKeysCreateData(ctx, provider, name, opts);
87
+ return `Stored ${key.provider} connector ${key.name} (${key.id}).`;
88
+ }
89
+
90
+ export function validateModelKeysCreateOptions(provider: ModelKeyProvider, name: string, opts: ModelKeyCreateOptions = {}): void {
91
+ const normalizedProvider = normalizeProvider(provider);
92
+ normalizeModelKeyName(name);
93
+ const normalizedAuthType = opts.authType === undefined ? undefined : normalizeAuthType(opts.authType);
94
+ if (normalizedAuthType === "oauth_subscription" && normalizedProvider !== "anthropic") {
95
+ throw new Error("--auth-type oauth_subscription is only supported for the anthropic provider.");
96
+ }
97
+ if (opts.key !== undefined && opts.keyStdin) {
98
+ throw new Error("Use either --key or --key-stdin, not both.");
99
+ }
100
+ if (opts.key !== undefined) {
101
+ normalizePlaintext(opts.key, "--key");
102
+ } else if (!opts.keyStdin) {
103
+ throw new Error("Model key plaintext is required. Pass --key or --key-stdin.");
104
+ } else if (process.stdin.isTTY) {
105
+ throw new Error(
106
+ "No piped input. Usage: echo $PROVIDER_API_KEY | dharta model-keys create anthropic Claude --key-stdin",
107
+ );
108
+ }
109
+ if (opts.endpointUrl !== undefined) {
110
+ normalizeEndpointUrl(opts.endpointUrl);
111
+ }
112
+ if (opts.apiFormat !== undefined) {
113
+ normalizeApiFormat(opts.apiFormat);
114
+ }
115
+ }
116
+
117
+ function normalizeProvider(provider: ModelKeyProvider): ModelKeyProvider {
118
+ const normalized = normalizeNonBlank(provider, "Model key provider");
119
+ if ((MODEL_KEY_PROVIDERS as readonly string[]).includes(normalized)) {
120
+ return normalized as ModelKeyProvider;
121
+ }
122
+ const suggestion = closestChoice(normalized, MODEL_KEY_PROVIDERS);
123
+ throw new Error(
124
+ `Unknown model key provider ${normalized}. Allowed providers: ${MODEL_KEY_PROVIDERS.join(", ")}.` +
125
+ (suggestion ? ` Did you mean ${suggestion}?` : ""),
126
+ );
127
+ }
128
+
129
+ function normalizeAuthType(authType: string): ModelKeyAuthType {
130
+ const normalized = normalizeNonBlank(authType, "--auth-type");
131
+ if ((MODEL_KEY_AUTH_TYPES as readonly string[]).includes(normalized)) {
132
+ return normalized as ModelKeyAuthType;
133
+ }
134
+ const suggestion = closestChoice(normalized, MODEL_KEY_AUTH_TYPES);
135
+ throw new Error(
136
+ `Unknown --auth-type ${normalized}. Allowed auth types: ${MODEL_KEY_AUTH_TYPES.join(", ")}.` +
137
+ (suggestion ? ` Did you mean ${suggestion}?` : ""),
138
+ );
139
+ }
140
+
141
+ function normalizeApiFormat(apiFormat: string): ModelApiFormat {
142
+ const normalized = normalizeNonBlank(apiFormat, "--api-format");
143
+ if ((MODEL_API_FORMATS as readonly string[]).includes(normalized)) {
144
+ return normalized as ModelApiFormat;
145
+ }
146
+ const suggestion = closestChoice(normalized, MODEL_API_FORMATS);
147
+ throw new Error(
148
+ `Unknown --api-format ${normalized}. Allowed API formats: ${MODEL_API_FORMATS.join(", ")}.` +
149
+ (suggestion ? ` Did you mean ${suggestion}?` : ""),
150
+ );
151
+ }
152
+
153
+ export async function prepareModelKeysCreateOptions(
154
+ provider: ModelKeyProvider,
155
+ name: string,
156
+ opts: ModelKeyCreateOptions = {},
157
+ read: () => Promise<string> = readModelKeyStdin,
158
+ ): Promise<ModelKeyCreateOptions> {
159
+ validateModelKeysCreateOptions(provider, name, opts);
160
+ if (!opts.keyStdin) return opts;
161
+ return { ...opts, key: normalizeModelKeyStdin(await read()), keyStdin: false };
162
+ }
163
+
164
+ export async function modelKeysRevokeData(ctx: CliContext, id: string): Promise<{ id: number; revoked: boolean }> {
165
+ const normalizedId = normalizeNonBlank(id, "Model key id");
166
+ const numericId = parseModelKeyId(normalizedId);
167
+ try {
168
+ await ctx.http.request<void>({
169
+ method: "DELETE",
170
+ path: `/model_keys/${encodeURIComponent(normalizedId)}`,
171
+ headers: { "Idempotency-Key": randomUUID() },
172
+ });
173
+ } catch (error) {
174
+ throw addModelKeysWriteScopeGuidance(error);
175
+ }
176
+ return { id: numericId, revoked: true };
177
+ }
178
+
179
+ export async function modelKeysRevoke(ctx: CliContext, id: string): Promise<string> {
180
+ const res = await modelKeysRevokeData(ctx, id);
181
+ return `Revoked ${res.id}.`;
182
+ }
183
+
184
+ export function validateModelKeyId(id: string): void {
185
+ normalizeModelKeyId(id);
186
+ }
187
+
188
+ async function resolvePlaintext(opts: ModelKeyCreateOptions): Promise<string> {
189
+ if (opts.key !== undefined && opts.keyStdin) {
190
+ throw new Error("Use either --key or --key-stdin, not both.");
191
+ }
192
+ if (opts.key !== undefined) return normalizePlaintext(opts.key, "--key");
193
+ if (opts.keyStdin) {
194
+ return readModelKeyStdin();
195
+ }
196
+ throw new Error("Model key plaintext is required. Pass --key or --key-stdin.");
197
+ }
198
+
199
+ async function readModelKeyStdin(): Promise<string> {
200
+ if (process.stdin.isTTY) {
201
+ throw new Error(
202
+ "No piped input. Usage: echo $PROVIDER_API_KEY | dharta model-keys create anthropic Claude --key-stdin",
203
+ );
204
+ }
205
+ const chunks: Buffer[] = [];
206
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
207
+ return normalizeModelKeyStdin(Buffer.concat(chunks).toString("utf8"));
208
+ }
209
+
210
+ function normalizeModelKeyStdin(value: string): string {
211
+ const text = value.trim();
212
+ if (text) return text;
213
+ throw new Error(
214
+ "No model key on stdin. Usage: echo $PROVIDER_API_KEY | dharta model-keys create anthropic Claude --key-stdin",
215
+ );
216
+ }
217
+
218
+ function normalizeModelKeyName(name: string): string {
219
+ const normalized = name.trim();
220
+ if (!normalized) {
221
+ throw new Error("Model key name cannot be blank.");
222
+ }
223
+ return normalized;
224
+ }
225
+
226
+ function normalizeModelKeyId(id: string): string {
227
+ const normalized = normalizeNonBlank(id, "Model key id");
228
+ parseModelKeyId(normalized);
229
+ return normalized;
230
+ }
231
+
232
+ function parseModelKeyId(id: string): number {
233
+ return parsePositiveInteger(id, "Model key id");
234
+ }
235
+
236
+ function normalizePlaintext(value: string, flag: string): string {
237
+ const normalized = value.trim();
238
+ if (!normalized) {
239
+ throw new Error(`${flag} cannot be blank.`);
240
+ }
241
+ return normalized;
242
+ }
243
+
244
+ function normalizeEndpointUrl(value: string): string {
245
+ const normalized = normalizeHttpUrl(value, "--endpoint-url");
246
+ const parsed = new URL(normalized);
247
+ if (parsed.protocol !== "https:") {
248
+ throw new Error("--endpoint-url must use https.");
249
+ }
250
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
251
+ throw new Error("--endpoint-url cannot include credentials, a query string, or a fragment.");
252
+ }
253
+ return normalized;
254
+ }
255
+
256
+ // Storing or revoking a connector needs `model_keys:write`, a tenant-admin
257
+ // scope that the `dharta login` device-flow credential deliberately never
258
+ // carries (that key is long-lived and non-admin, so it can't hold a
259
+ // forced-expiry admin scope). A CLI login therefore always 403s here. The
260
+ // generic scope hint says "manage key scopes", but the login key's scopes are
261
+ // fixed — you cannot add the scope to it — so name the real path: store the
262
+ // connector in the dashboard, or mint a bounded key and re-auth with a token.
263
+ function addModelKeysWriteScopeGuidance(error: unknown): Error {
264
+ if (!(error instanceof Error)) return new Error(String(error));
265
+ const status = (error as Error & { status?: unknown }).status;
266
+ if (
267
+ status === 403 &&
268
+ /model_keys:write/.test(error.message) &&
269
+ !error.message.includes("dharta login --with-token")
270
+ ) {
271
+ error.message +=
272
+ `${error.message.endsWith(".") ? "" : "."} ` +
273
+ "The `dharta login` credential cannot hold model_keys:write. Store the connector from the dashboard " +
274
+ "(`dharta open model-keys`), or mint a bounded key with model_keys:write there (`dharta open api-keys`) " +
275
+ "and re-authenticate with `dharta login --with-token`.";
276
+ }
277
+ return error;
278
+ }