@dharta/cli 0.6.77 → 0.6.78

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 (293) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +215 -11
  3. package/dist/agent.js +3 -2
  4. package/dist/agent.js.map +1 -1
  5. package/dist/api-error.js +6 -6
  6. package/dist/api-error.js.map +1 -1
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.js +860 -149
  9. package/dist/cli.js.map +1 -1
  10. package/dist/command-tree.d.ts +3 -0
  11. package/dist/command-tree.js +10 -0
  12. package/dist/command-tree.js.map +1 -0
  13. package/dist/commands/agents.d.ts +54 -17
  14. package/dist/commands/agents.js +246 -136
  15. package/dist/commands/agents.js.map +1 -1
  16. package/dist/commands/analytics.js +6 -5
  17. package/dist/commands/analytics.js.map +1 -1
  18. package/dist/commands/api.js +71 -86
  19. package/dist/commands/api.js.map +1 -1
  20. package/dist/commands/appearance.js +5 -6
  21. package/dist/commands/appearance.js.map +1 -1
  22. package/dist/commands/artifact.js +7 -6
  23. package/dist/commands/artifact.js.map +1 -1
  24. package/dist/commands/audit-log.js +2 -1
  25. package/dist/commands/audit-log.js.map +1 -1
  26. package/dist/commands/auth.js +26 -6
  27. package/dist/commands/auth.js.map +1 -1
  28. package/dist/commands/caps.js +11 -15
  29. package/dist/commands/caps.js.map +1 -1
  30. package/dist/commands/deploy.js +25 -10
  31. package/dist/commands/deploy.js.map +1 -1
  32. package/dist/commands/dev.d.ts +10 -1
  33. package/dist/commands/dev.js +56 -25
  34. package/dist/commands/dev.js.map +1 -1
  35. package/dist/commands/doctor.d.ts +2 -0
  36. package/dist/commands/doctor.js +60 -0
  37. package/dist/commands/doctor.js.map +1 -1
  38. package/dist/commands/embed-keys.js +4 -13
  39. package/dist/commands/embed-keys.js.map +1 -1
  40. package/dist/commands/env.js +4 -7
  41. package/dist/commands/env.js.map +1 -1
  42. package/dist/commands/files.js +3 -4
  43. package/dist/commands/files.js.map +1 -1
  44. package/dist/commands/fleet.d.ts +129 -0
  45. package/dist/commands/fleet.js +408 -0
  46. package/dist/commands/fleet.js.map +1 -0
  47. package/dist/commands/genui.d.ts +2 -0
  48. package/dist/commands/genui.js +53 -0
  49. package/dist/commands/genui.js.map +1 -0
  50. package/dist/commands/git-credential.js +2 -3
  51. package/dist/commands/git-credential.js.map +1 -1
  52. package/dist/commands/kartas.d.ts +9 -0
  53. package/dist/commands/kartas.js +33 -11
  54. package/dist/commands/kartas.js.map +1 -1
  55. package/dist/commands/keys.d.ts +17 -18
  56. package/dist/commands/keys.js +108 -19
  57. package/dist/commands/keys.js.map +1 -1
  58. package/dist/commands/login.js +2 -1
  59. package/dist/commands/login.js.map +1 -1
  60. package/dist/commands/members.d.ts +10 -0
  61. package/dist/commands/members.js +20 -6
  62. package/dist/commands/members.js.map +1 -1
  63. package/dist/commands/model-connections.d.ts +44 -0
  64. package/dist/commands/model-connections.js +435 -0
  65. package/dist/commands/model-connections.js.map +1 -0
  66. package/dist/commands/open.d.ts +2 -2
  67. package/dist/commands/open.js +4 -3
  68. package/dist/commands/open.js.map +1 -1
  69. package/dist/commands/org.d.ts +9 -0
  70. package/dist/commands/org.js +42 -0
  71. package/dist/commands/org.js.map +1 -0
  72. package/dist/commands/organization.d.ts +16 -0
  73. package/dist/commands/organization.js +118 -0
  74. package/dist/commands/organization.js.map +1 -0
  75. package/dist/commands/parity.js +1 -3
  76. package/dist/commands/parity.js.map +1 -1
  77. package/dist/commands/repos.d.ts +4 -0
  78. package/dist/commands/repos.js +17 -0
  79. package/dist/commands/repos.js.map +1 -0
  80. package/dist/commands/rollback.js +2 -3
  81. package/dist/commands/rollback.js.map +1 -1
  82. package/dist/commands/schedules.js +26 -21
  83. package/dist/commands/schedules.js.map +1 -1
  84. package/dist/commands/scopes.js +8 -1
  85. package/dist/commands/scopes.js.map +1 -1
  86. package/dist/commands/sessions.js +9 -3
  87. package/dist/commands/sessions.js.map +1 -1
  88. package/dist/commands/setup.js +5 -4
  89. package/dist/commands/setup.js.map +1 -1
  90. package/dist/commands/update.d.ts +1 -0
  91. package/dist/commands/update.js +24 -11
  92. package/dist/commands/update.js.map +1 -1
  93. package/dist/commands/webhooks.d.ts +4 -1
  94. package/dist/commands/webhooks.js +21 -16
  95. package/dist/commands/webhooks.js.map +1 -1
  96. package/dist/completions.js +24 -13
  97. package/dist/completions.js.map +1 -1
  98. package/dist/config.d.ts +12 -0
  99. package/dist/config.js +121 -7
  100. package/dist/config.js.map +1 -1
  101. package/dist/context.d.ts +2 -0
  102. package/dist/context.js +23 -7
  103. package/dist/context.js.map +1 -1
  104. package/dist/docs/cli-reference.d.ts +2 -0
  105. package/dist/docs/cli-reference.js +191 -0
  106. package/dist/docs/cli-reference.js.map +1 -0
  107. package/dist/exit-codes.d.ts +25 -0
  108. package/dist/exit-codes.js +107 -0
  109. package/dist/exit-codes.js.map +1 -0
  110. package/dist/genui.d.ts +178 -0
  111. package/dist/genui.js +1130 -0
  112. package/dist/genui.js.map +1 -0
  113. package/dist/harnesses.js +2 -1
  114. package/dist/harnesses.js.map +1 -1
  115. package/dist/jq.d.ts +4 -0
  116. package/dist/jq.js +91 -0
  117. package/dist/jq.js.map +1 -0
  118. package/dist/login-offer.js +6 -2
  119. package/dist/login-offer.js.map +1 -1
  120. package/dist/manifest.d.ts +12 -0
  121. package/dist/manifest.js +126 -0
  122. package/dist/manifest.js.map +1 -1
  123. package/dist/model-auth.d.ts +29 -0
  124. package/dist/model-auth.js +117 -0
  125. package/dist/model-auth.js.map +1 -0
  126. package/dist/model-settings.d.ts +23 -0
  127. package/dist/model-settings.js +186 -0
  128. package/dist/model-settings.js.map +1 -0
  129. package/dist/money.js +2 -1
  130. package/dist/money.js.map +1 -1
  131. package/dist/output.d.ts +6 -0
  132. package/dist/output.js +54 -0
  133. package/dist/output.js.map +1 -1
  134. package/dist/runtime.d.ts +1 -1
  135. package/dist/runtime.js +1 -1
  136. package/dist/scaffold.js +2 -1
  137. package/dist/scaffold.js.map +1 -1
  138. package/dist/validators.js +17 -11
  139. package/dist/validators.js.map +1 -1
  140. package/dist/version-check.d.ts +1 -1
  141. package/dist/version-check.js +1 -1
  142. package/dist/version-check.js.map +1 -1
  143. package/node_modules/@dharta/contracts/LICENSE +21 -0
  144. package/node_modules/@dharta/contracts/README.md +45 -0
  145. package/node_modules/@dharta/contracts/artifact-runtime.json +750 -0
  146. package/node_modules/@dharta/contracts/dist/activity.d.ts +62 -0
  147. package/node_modules/@dharta/contracts/dist/activity.d.ts.map +1 -0
  148. package/node_modules/@dharta/contracts/dist/activity.js +178 -0
  149. package/node_modules/@dharta/contracts/dist/activity.js.map +1 -0
  150. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.d.ts +20 -0
  151. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.d.ts.map +1 -0
  152. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.js +53 -0
  153. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.js.map +1 -0
  154. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.d.ts +37 -0
  155. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.d.ts.map +1 -0
  156. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.js +5107 -0
  157. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.js.map +1 -0
  158. package/node_modules/@dharta/contracts/dist/artifact-runtime.d.ts +154 -0
  159. package/node_modules/@dharta/contracts/dist/artifact-runtime.d.ts.map +1 -0
  160. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.d.ts +1670 -0
  161. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.d.ts.map +1 -0
  162. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.js +1903 -0
  163. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.js.map +1 -0
  164. package/node_modules/@dharta/contracts/dist/artifact-runtime.js +377 -0
  165. package/node_modules/@dharta/contracts/dist/artifact-runtime.js.map +1 -0
  166. package/node_modules/@dharta/contracts/dist/index.d.ts +6 -0
  167. package/node_modules/@dharta/contracts/dist/index.d.ts.map +1 -0
  168. package/node_modules/@dharta/contracts/dist/index.js +6 -0
  169. package/node_modules/@dharta/contracts/dist/index.js.map +1 -0
  170. package/node_modules/@dharta/contracts/dist/model-auth.d.ts +31 -0
  171. package/node_modules/@dharta/contracts/dist/model-auth.d.ts.map +1 -0
  172. package/node_modules/@dharta/contracts/dist/model-auth.js +45 -0
  173. package/node_modules/@dharta/contracts/dist/model-auth.js.map +1 -0
  174. package/node_modules/@dharta/contracts/dist/tasks.d.ts +44 -0
  175. package/node_modules/@dharta/contracts/dist/tasks.d.ts.map +1 -0
  176. package/node_modules/@dharta/contracts/dist/tasks.js +127 -0
  177. package/node_modules/@dharta/contracts/dist/tasks.js.map +1 -0
  178. package/node_modules/@dharta/contracts/dist/wire-events.d.ts +71 -0
  179. package/node_modules/@dharta/contracts/dist/wire-events.d.ts.map +1 -0
  180. package/node_modules/@dharta/contracts/dist/wire-events.js +48 -0
  181. package/node_modules/@dharta/contracts/dist/wire-events.js.map +1 -0
  182. package/node_modules/@dharta/contracts/model-auth-capabilities-v1.json +168 -0
  183. package/node_modules/@dharta/contracts/model-auth-reasons-v1.json +38 -0
  184. package/node_modules/@dharta/contracts/package.json +44 -0
  185. package/node_modules/@dharta/contracts/src/activity.ts +215 -0
  186. package/node_modules/@dharta/contracts/src/artifact-runtime-formats.ts +54 -0
  187. package/node_modules/@dharta/contracts/src/artifact-runtime-validators.generated.ts +29 -0
  188. package/node_modules/@dharta/contracts/src/artifact-runtime.generated.ts +2093 -0
  189. package/node_modules/@dharta/contracts/src/artifact-runtime.ts +520 -0
  190. package/node_modules/@dharta/contracts/src/index.ts +5 -0
  191. package/node_modules/@dharta/contracts/src/model-auth.ts +82 -0
  192. package/node_modules/@dharta/contracts/src/tasks.ts +142 -0
  193. package/node_modules/@dharta/contracts/src/wire-events.ts +150 -0
  194. package/node_modules/@dharta/contracts/wire-events.json +29 -0
  195. package/node_modules/@dharta/sdk/LICENSE +1 -1
  196. package/node_modules/@dharta/sdk/README.md +22 -8
  197. package/node_modules/@dharta/sdk/dist/errors.d.ts +9 -7
  198. package/node_modules/@dharta/sdk/dist/errors.d.ts.map +1 -1
  199. package/node_modules/@dharta/sdk/dist/errors.js +36 -18
  200. package/node_modules/@dharta/sdk/dist/errors.js.map +1 -1
  201. package/node_modules/@dharta/sdk/dist/fleet.d.ts +69 -0
  202. package/node_modules/@dharta/sdk/dist/fleet.d.ts.map +1 -0
  203. package/node_modules/@dharta/sdk/dist/fleet.js +240 -0
  204. package/node_modules/@dharta/sdk/dist/fleet.js.map +1 -0
  205. package/node_modules/@dharta/sdk/dist/http.d.ts +6 -2
  206. package/node_modules/@dharta/sdk/dist/http.d.ts.map +1 -1
  207. package/node_modules/@dharta/sdk/dist/http.js +71 -12
  208. package/node_modules/@dharta/sdk/dist/http.js.map +1 -1
  209. package/node_modules/@dharta/sdk/dist/idempotency.d.ts +12 -0
  210. package/node_modules/@dharta/sdk/dist/idempotency.d.ts.map +1 -0
  211. package/node_modules/@dharta/sdk/dist/idempotency.js +72 -0
  212. package/node_modules/@dharta/sdk/dist/idempotency.js.map +1 -0
  213. package/node_modules/@dharta/sdk/dist/index.d.ts +20 -6
  214. package/node_modules/@dharta/sdk/dist/index.d.ts.map +1 -1
  215. package/node_modules/@dharta/sdk/dist/index.js +27 -4
  216. package/node_modules/@dharta/sdk/dist/index.js.map +1 -1
  217. package/node_modules/@dharta/sdk/dist/resources.d.ts +108 -13
  218. package/node_modules/@dharta/sdk/dist/resources.d.ts.map +1 -1
  219. package/node_modules/@dharta/sdk/dist/resources.js +403 -57
  220. package/node_modules/@dharta/sdk/dist/resources.js.map +1 -1
  221. package/node_modules/@dharta/sdk/dist/types.d.ts +436 -16
  222. package/node_modules/@dharta/sdk/dist/types.d.ts.map +1 -1
  223. package/node_modules/@dharta/sdk/dist/types.js +31 -2
  224. package/node_modules/@dharta/sdk/dist/types.js.map +1 -1
  225. package/node_modules/@dharta/sdk/package.json +9 -3
  226. package/node_modules/@dharta/sdk/src/errors.ts +41 -18
  227. package/node_modules/@dharta/sdk/src/fleet.ts +339 -0
  228. package/node_modules/@dharta/sdk/src/http.ts +110 -17
  229. package/node_modules/@dharta/sdk/src/idempotency.ts +101 -0
  230. package/node_modules/@dharta/sdk/src/index.ts +50 -11
  231. package/node_modules/@dharta/sdk/src/resources.ts +549 -68
  232. package/node_modules/@dharta/sdk/src/types.ts +629 -20
  233. package/package.json +16 -6
  234. package/src/agent.ts +3 -2
  235. package/src/api-error.ts +6 -5
  236. package/src/cli.ts +978 -163
  237. package/src/command-tree.ts +13 -0
  238. package/src/commands/agents.ts +365 -167
  239. package/src/commands/analytics.ts +6 -5
  240. package/src/commands/api.ts +73 -92
  241. package/src/commands/appearance.ts +5 -6
  242. package/src/commands/artifact.ts +7 -6
  243. package/src/commands/audit-log.ts +2 -1
  244. package/src/commands/auth.ts +25 -5
  245. package/src/commands/caps.ts +11 -15
  246. package/src/commands/deploy.ts +18 -4
  247. package/src/commands/dev.ts +86 -25
  248. package/src/commands/doctor.ts +69 -0
  249. package/src/commands/embed-keys.ts +4 -14
  250. package/src/commands/env.ts +4 -7
  251. package/src/commands/files.ts +3 -4
  252. package/src/commands/fleet.ts +672 -0
  253. package/src/commands/genui.ts +59 -0
  254. package/src/commands/git-credential.ts +2 -3
  255. package/src/commands/kartas.ts +45 -11
  256. package/src/commands/keys.ts +152 -35
  257. package/src/commands/login.ts +2 -1
  258. package/src/commands/members.ts +31 -6
  259. package/src/commands/model-connections.ts +547 -0
  260. package/src/commands/open.ts +5 -4
  261. package/src/commands/org.ts +57 -0
  262. package/src/commands/organization.ts +153 -0
  263. package/src/commands/parity.ts +1 -3
  264. package/src/commands/repos.ts +23 -0
  265. package/src/commands/rollback.ts +2 -3
  266. package/src/commands/schedules.ts +33 -22
  267. package/src/commands/scopes.ts +8 -1
  268. package/src/commands/sessions.ts +9 -3
  269. package/src/commands/setup.ts +5 -4
  270. package/src/commands/update.ts +28 -12
  271. package/src/commands/webhooks.ts +30 -19
  272. package/src/completions.ts +25 -13
  273. package/src/config.ts +141 -9
  274. package/src/context.ts +34 -7
  275. package/src/docs/cli-reference.ts +206 -0
  276. package/src/exit-codes.ts +122 -0
  277. package/src/genui.ts +1398 -0
  278. package/src/harnesses.ts +2 -1
  279. package/src/jq.ts +109 -0
  280. package/src/login-offer.ts +6 -2
  281. package/src/manifest.ts +134 -0
  282. package/src/model-auth.ts +173 -0
  283. package/src/model-settings.ts +222 -0
  284. package/src/money.ts +5 -1
  285. package/src/output.ts +60 -0
  286. package/src/runtime.ts +1 -1
  287. package/src/scaffold.ts +2 -1
  288. package/src/validators.ts +18 -11
  289. package/src/version-check.ts +1 -1
  290. package/dist/commands/model-keys.d.ts +0 -22
  291. package/dist/commands/model-keys.js +0 -222
  292. package/dist/commands/model-keys.js.map +0 -1
  293. package/src/commands/model-keys.ts +0 -278
@@ -1,6 +1,7 @@
1
1
  import type { CliContext } from "../context.js";
2
2
  import { table } from "../output.js";
3
3
  import { closestChoice, normalizeDateOnly, parsePositiveInteger } from "../validators.js";
4
+ import { CliUsageError } from "../exit-codes.js";
4
5
 
5
6
  export type AnalyticsView = "agents" | "users" | "instances" | "sessions" | "models" | "cost";
6
7
 
@@ -64,7 +65,7 @@ export function normalizeAnalyticsOptions(view: AnalyticsView, opts: AnalyticsOp
64
65
  if (normalized.range === undefined) {
65
66
  normalized.range = "custom";
66
67
  } else if (normalized.range !== "custom") {
67
- throw new Error("--from/--to require --range custom.");
68
+ throw new CliUsageError("--from/--to require --range custom.");
68
69
  }
69
70
  }
70
71
 
@@ -72,17 +73,17 @@ export function normalizeAnalyticsOptions(view: AnalyticsView, opts: AnalyticsOp
72
73
  const allowed = view === "cost" ? COST_ANALYTICS_RANGES : ENTITY_ANALYTICS_RANGES;
73
74
  if (!(allowed as readonly string[]).includes(normalized.range)) {
74
75
  const suggestion = closestChoice(normalized.range, allowed);
75
- throw new Error(
76
+ throw new CliUsageError(
76
77
  `--range must be one of: ${allowed.join(", ")}.` +
77
78
  (suggestion ? ` Did you mean ${suggestion}?` : ""),
78
79
  );
79
80
  }
80
81
  }
81
82
  if (normalized.groupBy !== undefined) {
82
- if (view !== "cost") throw new Error("--group-by is only supported for analytics cost.");
83
+ if (view !== "cost") throw new CliUsageError("--group-by is only supported for analytics cost.");
83
84
  if (!(COST_ANALYTICS_GROUPS as readonly string[]).includes(normalized.groupBy)) {
84
85
  const suggestion = closestChoice(normalized.groupBy, COST_ANALYTICS_GROUPS);
85
- throw new Error(
86
+ throw new CliUsageError(
86
87
  `--group-by must be one of: ${COST_ANALYTICS_GROUPS.join(", ")}.` +
87
88
  (suggestion ? ` Did you mean ${suggestion}?` : ""),
88
89
  );
@@ -96,7 +97,7 @@ export function normalizeAnalyticsOptions(view: AnalyticsView, opts: AnalyticsOp
96
97
 
97
98
  function normalizeRequiredFilter(value: string, flag: string): string {
98
99
  const normalized = value.trim();
99
- if (!normalized) throw new Error(`${flag} cannot be blank.`);
100
+ if (!normalized) throw new CliUsageError(`${flag} cannot be blank.`);
100
101
  return normalized;
101
102
  }
102
103
 
@@ -1,77 +1,21 @@
1
1
  import type { CliContext } from "../context.js";
2
- import { randomUUID } from "node:crypto";
3
2
  import { cliFormatError } from "../api-error.js";
3
+ import { CliUsageError } from "../exit-codes.js";
4
4
  import { closestChoice } from "../validators.js";
5
5
 
6
6
  const METHOD_NAMES = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const;
7
7
  const METHODS = new Set<string>(METHOD_NAMES);
8
8
 
9
- const READ_ALLOWLIST: Array<RegExp> = [
10
- /^\/me(?:[?#]|$)/,
11
- /^\/version(?:[?#]|$)/,
12
- /^\/agent_templates(?:[?#]|$)/,
13
- /^\/api\/billing(?:[?#]|$)/,
14
- /^\/api\/usage(?:[?#]|$)/,
15
- /^\/api\/analytics\/(?:agents|users|instances|sessions|models|cost)(?:[?#]|$)/,
16
- /^\/api\/audit_events(?:\.csv)?(?:[?#]|$)/,
17
- /^\/api\/members(?:[?#]|$)/,
18
- /^\/api\/kartas(?:[?#]|$)/,
19
- /^\/api\/kartas\/[^/?#]+(?:[?#]|$)/,
20
- /^\/api\/agents(?:[?#]|$)/,
21
- /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}(?:[?#]|$)/,
22
- /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/invocations(?:[?#]|$)/,
23
- /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/logs(?:[?#]|$)/,
24
- /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/env(?:[?#]|$)/,
25
- /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/releases(?:[?#]|$)/,
26
- /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/appearance(?:[?#]|$)/,
27
- /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/schedules(?:[?#]|$)/,
28
- /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/directories\/(?:instances|end_users|sessions)(?:[?#]|$)/,
29
- /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/embed_keys(?:[?#]|$)/,
30
- /^\/api\/limits\/budget(?:[?#]|$)/,
31
- /^\/api\/limits\/instances\/[^/?#]+\/(?:cap|seat_cap)(?:[?#]|$)/,
32
- /^\/api\/limits\/end_users\/[^/?#]+\/cap(?:[?#]|$)/,
33
- /^\/api\/webhook_endpoints(?:[?#]|$)/,
34
- /^\/api_keys(?:[?#]|$)/,
35
- /^\/model_keys(?:[?#]|$)/,
36
- ];
37
-
38
- interface WriteAllowlistEntry {
39
- methods: readonly string[];
40
- pattern: RegExp;
41
- }
42
-
43
- const WRITE_ALLOWLIST: WriteAllowlistEntry[] = [
44
- { methods: ["PATCH"], pattern: /^\/api\/kartas\/[^/?#]+(?:[?#]|$)/ },
45
- { methods: ["POST"], pattern: /^\/api\/kartas\/[^/?#]+\/members(?:[?#]|$)/ },
46
- { methods: ["PATCH", "DELETE"], pattern: /^\/api\/kartas\/[^/?#]+\/members\/[^/?#]+(?:[?#]|$)/ },
47
- { methods: ["POST"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/embed_keys(?:[?#]|$)/ },
48
- { methods: ["PATCH", "DELETE"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/embed_keys\/[^/?#]+(?:[?#]|$)/ },
49
- { methods: ["POST"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/embed_keys\/[^/?#]+\/(?:rotate|disable|enable)(?:[?#]|$)/ },
50
- { methods: ["PATCH"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/appearance(?:[?#]|$)/ },
51
- { methods: ["PUT", "DELETE"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/env\/[^/?#]+(?:[?#]|$)/ },
52
- { methods: ["POST"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/env\/pull(?:[?#]|$)/ },
53
- { methods: ["PATCH"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/(?:serving|dashboard_edits|model)(?:[?#]|$)/ },
54
- { methods: ["PATCH"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/workspace_inspectable(?:[?#]|$)/ },
55
- { methods: ["DELETE"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}(?:[?#]|$)/ },
56
- { methods: ["POST"], pattern: /^\/api\/members\/invitations(?:[?#]|$)/ },
57
- { methods: ["PATCH", "DELETE"], pattern: /^\/api\/members\/[^/?#]+(?:[?#]|$)/ },
58
- { methods: ["POST"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/schedules(?:[?#]|$)/ },
59
- { methods: ["DELETE"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/schedules\/[^/?#]+(?:[?#]|$)/ },
60
- { methods: ["POST"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/schedules\/[^/?#]+\/(?:pause|resume|run)(?:[?#]|$)/ },
61
- { methods: ["POST"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/releases\/v?\d+\/activate(?:[?#]|$)/ },
62
- { methods: ["POST"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/rollback(?:[?#]|$)/ },
63
- { methods: ["POST"], pattern: /^\/api\/agents\/[a-z0-9][a-z0-9-]{0,62}\/workspace_tokens(?:[?#]|$)/ },
64
- { methods: ["PATCH"], pattern: /^\/api\/limits\/budget(?:[?#]|$)/ },
65
- { methods: ["PATCH"], pattern: /^\/api\/limits\/instances\/[^/?#]+\/(?:cap|seat_cap)(?:[?#]|$)/ },
66
- { methods: ["PATCH"], pattern: /^\/api\/limits\/end_users\/[^/?#]+\/cap(?:[?#]|$)/ },
67
- { methods: ["POST"], pattern: /^\/api\/webhook_endpoints(?:[?#]|$)/ },
68
- { methods: ["PATCH", "DELETE"], pattern: /^\/api\/webhook_endpoints\/[^/?#]+(?:[?#]|$)/ },
69
- { methods: ["POST"], pattern: /^\/api\/webhook_endpoints\/[^/?#]+\/enable(?:[?#]|$)/ },
70
- { methods: ["POST"], pattern: /^\/api_keys(?:[?#]|$)/ },
71
- { methods: ["PATCH", "DELETE"], pattern: /^\/api_keys\/[^/?#]+(?:[?#]|$)/ },
72
- { methods: ["POST"], pattern: /^\/model_keys(?:[?#]|$)/ },
73
- { methods: ["DELETE"], pattern: /^\/model_keys\/[^/?#]+(?:[?#]|$)/ },
74
- ];
9
+ // Prefix-only reachability (RFC 0048 D1, founder decision 2026-08-31). The CLI
10
+ // used to carry a hand-maintained read/write allowlist, which meant every new
11
+ // public endpoint was unreachable until someone remembered to add a regex here
12
+ // - a second, always-stale copy of the server's route table. The server's
13
+ // scope model is the authorization boundary; this list only keeps the
14
+ // passthrough pointed at the public JSON API instead of, say, a Devise HTML
15
+ // form. Matching is by exact first path segment, so `/apix` and `/me2` are
16
+ // rejected while `/me?fields=x` is allowed.
17
+ const ALLOWED_PREFIXES = ["/api/", "/me", "/version", "/api_keys", "/model_connections", "/agent_templates"] as const;
18
+ const ALLOWED_FIRST_SEGMENTS = new Set(ALLOWED_PREFIXES.map((prefix) => prefix.replace(/^\/|\/$/g, "")));
75
19
 
76
20
  export interface ApiOptions {
77
21
  field?: string[];
@@ -86,12 +30,17 @@ export async function api(ctx: CliContext, methodOrPath: string | undefined, pat
86
30
 
87
31
  const headers = parseHeaders(opts.header ?? []);
88
32
  const body = await buildBody(opts);
33
+ // `HttpClient.raw` sends no Accept of its own, so fetch defaults to `*/*`.
34
+ // The /api_keys and /model_connections routes are mounted behind a JSON
35
+ // constraint and fall through to the console HTML controller without this,
36
+ // which reads as a mystifying redirect to the sign-in page. A caller who
37
+ // names their own Accept keeps it.
38
+ if (!hasHeader(headers, "accept")) {
39
+ headers.Accept = "application/json";
40
+ }
89
41
  if (body !== undefined && !hasHeader(headers, "content-type")) {
90
42
  headers["Content-Type"] = "application/json";
91
43
  }
92
- if (method !== "GET" && !hasHeader(headers, "idempotency-key")) {
93
- headers["Idempotency-Key"] = randomUUID();
94
- }
95
44
  const res = await ctx.http.raw(path, { method, headers, body });
96
45
  const text = await res.text();
97
46
  const parsed = parseMaybeJson(text);
@@ -110,9 +59,9 @@ export async function api(ctx: CliContext, methodOrPath: string | undefined, pat
110
59
  export function validateApiRequest(methodOrPath: string | undefined, pathArg: string | undefined, opts: ApiOptions) {
111
60
  const { method, path } = normalizeTarget(methodOrPath, pathArg, hasFields(opts));
112
61
  if (method === "GET" && hasFields(opts)) {
113
- throw new Error("GET requests cannot include --field, --raw-field, or --body-stdin. Use POST, PUT, or PATCH for request bodies.");
62
+ throw new CliUsageError("GET requests cannot include --field, --raw-field, or --body-stdin. Use POST, PUT, or PATCH for request bodies.");
114
63
  }
115
- assertAllowlisted(method, path);
64
+ assertAllowedPrefix(method, path);
116
65
  parseHeaders(opts.header ?? []);
117
66
  validateBodyFields(opts);
118
67
  return { method, path };
@@ -123,7 +72,7 @@ export async function prepareApiOptions(opts: ApiOptions, read: () => Promise<st
123
72
  validateBodyFields(opts);
124
73
  if (!opts.bodyStdin) return opts;
125
74
  const body = await read();
126
- if (body.length === 0) throw new Error("--body-stdin was set, but stdin was empty.");
75
+ if (body.length === 0) throw new CliUsageError("--body-stdin was set, but stdin was empty.");
127
76
  return { ...opts, body, bodyStdin: false };
128
77
  }
129
78
 
@@ -136,21 +85,21 @@ function normalizeTarget(methodOrPath: string | undefined, pathArg: string | und
136
85
  const first = methodOrPath?.trim();
137
86
  const second = pathArg?.trim();
138
87
  if (!first) {
139
- throw new Error("Usage: dharta api [METHOD] /path");
88
+ throw new CliUsageError("Usage: dharta api [METHOD] /path");
140
89
  }
141
90
 
142
91
  let method: string;
143
92
  let path: string;
144
93
  const maybeMethod = first.toUpperCase();
145
94
  if (METHODS.has(maybeMethod) && !second) {
146
- throw new Error("Usage: dharta api [METHOD] /path");
95
+ throw new CliUsageError("Usage: dharta api [METHOD] /path");
147
96
  }
148
97
  if (METHODS.has(maybeMethod)) {
149
98
  method = maybeMethod;
150
99
  path = second as string;
151
100
  } else if (second) {
152
101
  const suggestion = closestChoice(maybeMethod, METHOD_NAMES);
153
- throw new Error(
102
+ throw new CliUsageError(
154
103
  `Unknown API method ${maybeMethod}. Allowed methods: ${METHOD_NAMES.join(", ")}.` +
155
104
  (suggestion ? ` Did you mean ${suggestion}?` : ""),
156
105
  );
@@ -160,26 +109,58 @@ function normalizeTarget(methodOrPath: string | undefined, pathArg: string | und
160
109
  }
161
110
 
162
111
  if (!path.startsWith("/")) {
163
- throw new Error("API path must start with '/', e.g. /me or /api/agents/support-bot");
112
+ throw new CliUsageError("API path must start with '/', e.g. /me or /api/agents/support-bot");
164
113
  }
165
114
  if (/^\/\//.test(path) || /^[a-z][a-z0-9+.-]*:\/\//i.test(path)) {
166
- throw new Error("API path must be relative to the Dharta API host.");
115
+ throw new CliUsageError("API path must be relative to the Dharta API host.");
167
116
  }
168
117
  return { method, path };
169
118
  }
170
119
 
171
- function assertAllowlisted(method: string, path: string): void {
172
- if (method === "GET" && READ_ALLOWLIST.some((pattern) => pattern.test(path))) return;
173
- if (method !== "GET" && WRITE_ALLOWLIST.some((entry) => entry.methods.includes(method) && entry.pattern.test(path))) return;
120
+ function assertAllowedPrefix(method: string, path: string): void {
121
+ // Check the path the SERVER will see, not the one that was typed.
122
+ // HttpClient.raw concatenates baseUrl + path and fetch resolves the result
123
+ // with the WHATWG URL parser, which collapses dot segments and treats a
124
+ // backslash as a separator: `/api/../users/sign_in`, `/api/%2e%2e/users`,
125
+ // and `/api\..\users` all arrive as paths the prefix list does not allow.
126
+ const normalized = normalizedPathname(path);
127
+ const typed = path.split(/[?#]/)[0];
128
+ if (normalized !== typed) {
129
+ throw new CliUsageError(
130
+ `API path '${typed}' is not the path that would be requested ('${normalized}'). ` +
131
+ "Write the literal path; '..', '.', and '\\' are not part of any Dharta API route.",
132
+ );
133
+ }
134
+ if (hasAllowedPrefix(normalized)) return;
174
135
 
175
136
  const suggestion = apiRouteSuggestion(path);
176
- throw new Error(
177
- "`dharta api` only allows documented public read endpoints and curated scoped writes. " +
178
- "Use the matching `karta <command>` for unsupported writes." +
137
+ throw new CliUsageError(
138
+ `\`dharta api\` reaches the public JSON API only. Allowed path prefixes: ${ALLOWED_PREFIXES.join(", ")}.` +
179
139
  (suggestion ? ` Did you mean ${method} ${suggestion}?` : ""),
180
140
  );
181
141
  }
182
142
 
143
+ // The pathname fetch would resolve this to. The base is a placeholder: only
144
+ // the path matters, and the caller has already rejected absolute and
145
+ // protocol-relative inputs.
146
+ function normalizedPathname(path: string): string {
147
+ try {
148
+ return new URL(path, "http://placeholder").pathname;
149
+ } catch {
150
+ throw new CliUsageError(`API path '${path}' is not a valid URL path.`);
151
+ }
152
+ }
153
+
154
+ function hasAllowedPrefix(path: string): boolean {
155
+ const segments = path.split("/");
156
+ const first = segments[1] ?? "";
157
+ if (!ALLOWED_FIRST_SEGMENTS.has(first)) return false;
158
+ // `/api` is a namespace, never a route of its own, so it needs a second
159
+ // segment. The other prefixes name real endpoints.
160
+ if (first === "api") return (segments[2] ?? "") !== "";
161
+ return true;
162
+ }
163
+
183
164
  function apiRouteSuggestion(path: string): string | null {
184
165
  const replacements: Array<[RegExp, string]> = [
185
166
  [/^\/analytics(?=\/|[?#]|$)/, "/api/analytics"],
@@ -205,7 +186,7 @@ function hasStructuredFields(opts: ApiOptions): boolean {
205
186
 
206
187
  function validateBodyFields(opts: ApiOptions): void {
207
188
  if (opts.bodyStdin && hasStructuredFields(opts)) {
208
- throw new Error("Use either --body-stdin or --field/--raw-field, not both.");
189
+ throw new CliUsageError("Use either --body-stdin or --field/--raw-field, not both.");
209
190
  }
210
191
  for (const raw of opts.field ?? []) splitAssignment(raw, "--field");
211
192
  for (const raw of opts.rawField ?? []) splitAssignment(raw, "--raw-field");
@@ -232,13 +213,13 @@ function parseHeaders(input: string[]): Record<string, string> {
232
213
  const headers: Record<string, string> = {};
233
214
  for (const raw of input) {
234
215
  const idx = raw.indexOf(":");
235
- if (idx <= 0) throw new Error(`Invalid --header '${raw}'. Use 'Name: value'.`);
216
+ if (idx <= 0) throw new CliUsageError(`Invalid --header '${raw}'. Use 'Name: value'.`);
236
217
  const key = raw.slice(0, idx).trim();
237
218
  const value = raw.slice(idx + 1).trim();
238
- if (!key || !value) throw new Error(`Invalid --header '${raw}'. Use 'Name: value'.`);
219
+ if (!key || !value) throw new CliUsageError(`Invalid --header '${raw}'. Use 'Name: value'.`);
239
220
  const lower = key.toLowerCase();
240
221
  if (lower === "authorization" || lower === "host" || lower.startsWith("x-forwarded-")) {
241
- throw new Error(`Unsafe --header '${key}' cannot be overridden. The CLI manages auth and connection headers.`);
222
+ throw new CliUsageError(`Unsafe --header '${key}' cannot be overridden. The CLI manages auth and connection headers.`);
242
223
  }
243
224
  headers[key] = value;
244
225
  }
@@ -253,9 +234,9 @@ function hasHeader(headers: Record<string, string>, name: string): boolean {
253
234
 
254
235
  function splitAssignment(raw: string, flag: string): [string, string] {
255
236
  const idx = raw.indexOf("=");
256
- if (idx <= 0) throw new Error(`Invalid ${flag} '${raw}'. Use key=value.`);
237
+ if (idx <= 0) throw new CliUsageError(`Invalid ${flag} '${raw}'. Use key=value.`);
257
238
  const key = raw.slice(0, idx).trim();
258
- if (!key) throw new Error(`Invalid ${flag} '${raw}'. Use key=value.`);
239
+ if (!key) throw new CliUsageError(`Invalid ${flag} '${raw}'. Use key=value.`);
259
240
  return [key, raw.slice(idx + 1)];
260
241
  }
261
242
 
@@ -285,13 +266,13 @@ function parseMaybeJson(text: string): unknown {
285
266
 
286
267
  async function readStdin(): Promise<string> {
287
268
  if (process.stdin.isTTY) {
288
- throw new Error(
269
+ throw new CliUsageError(
289
270
  "No piped input. Usage: echo '{\"name\":\"Example\"}' | dharta api POST /api/agents/support-bot/embed_keys --body-stdin",
290
271
  );
291
272
  }
292
273
  const chunks: Buffer[] = [];
293
274
  for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
294
275
  const text = Buffer.concat(chunks).toString("utf8");
295
- if (text.length === 0) throw new Error("--body-stdin was set, but stdin was empty.");
276
+ if (text.length === 0) throw new CliUsageError("--body-stdin was set, but stdin was empty.");
296
277
  return text;
297
278
  }
@@ -1,8 +1,8 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import { readFile } from "node:fs/promises";
3
2
  import { inferAgentSlug, type AgentInferOptions } from "../agent.js";
4
3
  import type { CliContext } from "../context.js";
5
4
  import { normalizeNonBlank } from "../validators.js";
5
+ import { CliUsageError } from "../exit-codes.js";
6
6
 
7
7
  export interface AppearanceEnvelope {
8
8
  agent: {
@@ -32,13 +32,12 @@ export async function appearanceGet(ctx: CliContext, opts: AgentInferOptions): P
32
32
 
33
33
  export async function appearanceSetData(ctx: CliContext, opts: AppearanceOptions): Promise<AppearanceEnvelope> {
34
34
  await validateAppearanceSetInput(opts);
35
- if (!opts.file) throw new Error("Pass --file <file>. Usage: dharta appearance set --file appearance.json");
35
+ if (!opts.file) throw new CliUsageError("Pass --file <file>. Usage: dharta appearance set --file appearance.json");
36
36
  const slug = await inferAgentSlug(opts);
37
37
  const appearance = await readAppearanceFile(opts.file);
38
38
  return ctx.http.request<AppearanceEnvelope>({
39
39
  method: "PATCH",
40
40
  path: `/api/agents/${encodeURIComponent(slug)}/appearance`,
41
- headers: { "Idempotency-Key": randomUUID() },
42
41
  body: { appearance },
43
42
  });
44
43
  }
@@ -49,7 +48,7 @@ export async function appearanceSet(ctx: CliContext, opts: AppearanceOptions): P
49
48
  }
50
49
 
51
50
  export async function validateAppearanceSetInput(opts: AppearanceOptions): Promise<void> {
52
- if (!opts.file) throw new Error("Pass --file <file>. Usage: dharta appearance set --file appearance.json");
51
+ if (!opts.file) throw new CliUsageError("Pass --file <file>. Usage: dharta appearance set --file appearance.json");
53
52
  await readAppearanceFile(opts.file);
54
53
  }
55
54
 
@@ -69,11 +68,11 @@ async function readAppearanceFile(file: string): Promise<Record<string, unknown>
69
68
  try {
70
69
  parsed = JSON.parse(text);
71
70
  } catch {
72
- throw new Error(`--file ${normalized} must contain valid JSON.`);
71
+ throw new CliUsageError(`--file ${normalized} must contain valid JSON.`);
73
72
  }
74
73
 
75
74
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
76
- throw new Error(`--file ${normalized} must contain a JSON object.`);
75
+ throw new CliUsageError(`--file ${normalized} must contain a JSON object.`);
77
76
  }
78
77
  return parsed as Record<string, unknown>;
79
78
  }
@@ -2,6 +2,7 @@ import { resolveRuntime, spawnRuntime } from "../runtime.js";
2
2
  import { promises as fs } from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import type { Command } from "commander";
5
+ import { CliUsageError } from "../exit-codes.js";
5
6
 
6
7
  export async function artifact(args: string[]): Promise<void> {
7
8
  if (args[0] !== "run") {
@@ -154,7 +155,7 @@ export function registerArtifactCommands(program: Command): void {
154
155
  .option("--id <id>", "Stable schema-2 Artifact ID")
155
156
  .action(async (opts: { key?: string; id?: string }) => {
156
157
  if ((!opts.key && !opts.id) || (opts.key && opts.id)) {
157
- throw new Error(
158
+ throw new CliUsageError(
158
159
  "Exactly one of --key or --id is required. See `dharta artifact remove --help`.",
159
160
  );
160
161
  }
@@ -234,7 +235,7 @@ type ArtifactPutCliOptions = {
234
235
 
235
236
  async function runPut(opts: ArtifactPutCliOptions): Promise<void> {
236
237
  if (!opts.title) {
237
- throw new Error("--title is required. See `dharta artifact put --help`.");
238
+ throw new CliUsageError("--title is required. See `dharta artifact put --help`.");
238
239
  }
239
240
  const requestedAgentPolicy = Boolean(
240
241
  opts.agentAuthority.length
@@ -253,13 +254,13 @@ async function runPut(opts: ArtifactPutCliOptions): Promise<void> {
253
254
  || opts.provenance || opts.continuation.length
254
255
  );
255
256
  if (v2 && legacy) {
256
- throw new Error(
257
+ throw new CliUsageError(
257
258
  "Legacy and schema-2 Artifact options cannot be mixed. See `dharta artifact put --help`.",
258
259
  );
259
260
  }
260
261
  if (v2) {
261
262
  if (!opts.id || !opts.root || Boolean(opts.entry) === Boolean(opts.run)) {
262
- throw new Error(
263
+ throw new CliUsageError(
263
264
  "Schema-2 Artifacts require --id, --root, and exactly one of --entry or --run. "
264
265
  + "See `dharta artifact put --help`.",
265
266
  );
@@ -277,7 +278,7 @@ async function runPut(opts: ArtifactPutCliOptions): Promise<void> {
277
278
  .map(([name]) => name)
278
279
  : [];
279
280
  if (missingAgentPolicy.length > 0) {
280
- throw new Error(
281
+ throw new CliUsageError(
281
282
  `Agent activation requires a complete policy; missing ${missingAgentPolicy.join(", ")}. `
282
283
  + "See `dharta artifact put --help`.",
283
284
  );
@@ -305,7 +306,7 @@ async function runPut(opts: ArtifactPutCliOptions): Promise<void> {
305
306
  }));
306
307
  }
307
308
  if (!opts.key || !opts.kind || !opts.entrypoint) {
308
- throw new Error(
309
+ throw new CliUsageError(
309
310
  "Artifacts require --key, --kind, and --entrypoint. "
310
311
  + "See `dharta artifact put --help`.",
311
312
  );
@@ -2,6 +2,7 @@ import type { CliContext } from "../context.js";
2
2
  import { cliFormatRawError } from "../api-error.js";
3
3
  import { fmtTs, table } from "../output.js";
4
4
  import { parsePositiveInteger, validateDateOnly } from "../validators.js";
5
+ import { CliUsageError } from "../exit-codes.js";
5
6
 
6
7
  interface AuditLogResponse {
7
8
  data: AuditLogEvent[];
@@ -106,7 +107,7 @@ function normalizeAuditLogOptions(opts: AuditLogOptions): AuditLogOptions {
106
107
  function normalizeOptionalFilter(value: string | undefined, flag: string): string | undefined {
107
108
  if (value === undefined) return undefined;
108
109
  const normalized = value.trim();
109
- if (normalized.length === 0) throw new Error(`${flag} cannot be blank.`);
110
+ if (normalized.length === 0) throw new CliUsageError(`${flag} cannot be blank.`);
110
111
  return normalized;
111
112
  }
112
113
 
@@ -4,12 +4,16 @@ import { HttpClient } from "@dharta/sdk";
4
4
  import { cliFormatError, cliFormatNetworkError } from "../api-error.js";
5
5
  import {
6
6
  configPath,
7
+ explicitProfileName,
7
8
  loadConfig,
9
+ profileNotFoundError,
8
10
  removeProfile,
11
+ requestedProfileName,
9
12
  DEFAULT_BASE_URL,
10
13
  } from "../config.js";
11
14
  import { normalizeNonBlank } from "../validators.js";
12
15
  import { formatApiKeyExpiry, formatScopes } from "./scopes.js";
16
+ import { CliUsageError } from "../exit-codes.js";
13
17
 
14
18
  // GET /me — the control plane's key-introspection endpoint.
15
19
  export interface Me {
@@ -142,14 +146,26 @@ function formatCause(error: unknown): string {
142
146
  return error instanceof Error && error.message ? error.message : String(error);
143
147
  }
144
148
 
145
- // Print the active token for piping (gh auth token parity). Environment
146
- // override wins, matching every other command.
149
+ // Print the active token for piping (gh auth token parity). An ambient
150
+ // DHARTA_API_KEY wins, matching every other command, unless --profile was
151
+ // passed.
147
152
  export async function authToken(): Promise<string> {
153
+ // Same precedence as buildContext: an explicit --profile is a deliberate act
154
+ // and outranks an ambient key, so `dharta --profile ci auth token` cannot
155
+ // print a credential from another organization.
148
156
  const envKey = process.env.DHARTA_API_KEY;
149
- if (envKey) return envKey;
157
+ if (envKey && explicitProfileName() === undefined) return envKey;
150
158
  const cfg = await loadConfig();
151
- const profile = cfg?.profiles[cfg.default_profile];
159
+ // The global --profile picks which stored credential this prints, so
160
+ // `dharta --profile ci auth token` pipes the ci key, not the default one.
161
+ const requested = requestedProfileName();
162
+ const name = requested ?? cfg?.default_profile;
163
+ const profile = name === undefined ? undefined : cfg?.profiles[name];
152
164
  if (!profile) {
165
+ // A profile the user named and that is not stored is a usage error, the
166
+ // same as everywhere else. Only "nothing is stored at all" is a
167
+ // not-authenticated case.
168
+ if (requested !== undefined) throw profileNotFoundError(requested);
153
169
  throw new Error("Not logged in. Run `dharta login` to sign in via your browser.");
154
170
  }
155
171
  return profile.api_key;
@@ -163,6 +179,7 @@ export async function authLogoutData(
163
179
  const requestedName = profileName === undefined ? undefined : normalizeNonBlank(profileName, "--profile");
164
180
  const cfg = await loadConfig();
165
181
  if (!cfg) {
182
+ if (requestedName !== undefined) throw profileNotFoundError(requestedName);
166
183
  throw new Error(
167
184
  "Not logged in - nothing to remove. `dharta auth status` shows what is stored.",
168
185
  );
@@ -170,7 +187,10 @@ export async function authLogoutData(
170
187
  const name = requestedName ?? cfg.default_profile;
171
188
  const removed = await removeProfile(name);
172
189
  if (!removed) {
173
- throw new Error(
190
+ // Naming a profile that is not there is a usage error, and it reads the
191
+ // same as it does on every other command.
192
+ if (requestedName !== undefined) throw profileNotFoundError(name);
193
+ throw new CliUsageError(
174
194
  `No profile named '${name}' in ${configPath()}. Run \`dharta auth status\` to list profiles.`,
175
195
  );
176
196
  }
@@ -1,6 +1,6 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import type { CliContext } from "../context.js";
3
2
  import { parseDollarAmountToCents } from "../money.js";
3
+ import { CliUsageError } from "../exit-codes.js";
4
4
 
5
5
  export interface Budget {
6
6
  period: string;
@@ -108,7 +108,6 @@ export async function capsBudgetSetData(ctx: CliContext, opts: BudgetSetOptions)
108
108
  return ctx.http.request<BudgetEnvelope>({
109
109
  method: "PATCH",
110
110
  path: "/api/limits/budget",
111
- headers: { "Idempotency-Key": randomUUID() },
112
111
  body,
113
112
  });
114
113
  }
@@ -134,7 +133,6 @@ export async function capsInstanceSetData(ctx: CliContext, instance: string, opt
134
133
  return ctx.http.request<InstanceCapEnvelope>({
135
134
  method: "PATCH",
136
135
  path: `/api/limits/instances/${encodeURIComponent(normalizedInstance)}/cap`,
137
- headers: { "Idempotency-Key": randomUUID() },
138
136
  body: capBody(opts),
139
137
  });
140
138
  }
@@ -161,7 +159,6 @@ export async function capsEndUserSetData(ctx: CliContext, endUserId: string, opt
161
159
  return ctx.http.request<EndUserCapEnvelope>({
162
160
  method: "PATCH",
163
161
  path: `/api/limits/end_users/${encodeURIComponent(normalizedEndUserId)}/cap`,
164
- headers: { "Idempotency-Key": randomUUID() },
165
162
  body: capBody(opts),
166
163
  });
167
164
  }
@@ -193,7 +190,6 @@ export async function capsSeatSetData(ctx: CliContext, instance: string, endUser
193
190
  return ctx.http.request<SeatCapEnvelope>({
194
191
  method: "PATCH",
195
192
  path: `/api/limits/instances/${encodeURIComponent(normalizedInstance)}/seat_cap`,
196
- headers: { "Idempotency-Key": randomUUID() },
197
193
  body: { ...capBody(opts), end_user_id: normalizedEndUserId },
198
194
  });
199
195
  }
@@ -207,7 +203,7 @@ export async function capsSeatSet(ctx: CliContext, instance: string, endUserId:
207
203
 
208
204
  function dollarsToCents(value: string | undefined): number {
209
205
  if (value === undefined) {
210
- throw new Error("Pass --amount <dollars>. Usage: dharta caps budget set --amount 250");
206
+ throw new CliUsageError("Pass --amount <dollars>. Usage: dharta caps budget set --amount 250");
211
207
  }
212
208
  return parseDollarAmountToCents(value, "--amount");
213
209
  }
@@ -229,34 +225,34 @@ export function validateCapSetOptions(opts: CapSetOptions): void {
229
225
  }
230
226
 
231
227
  function capBody(opts: CapSetOptions): Record<string, unknown> {
232
- if (opts.clear && opts.amount) throw new Error("Use either --amount or --clear, not both.");
228
+ if (opts.clear && opts.amount) throw new CliUsageError("Use either --amount or --clear, not both.");
233
229
  if (opts.clear) return { clear: true };
234
- if (!opts.amount) throw new Error("Pass --amount <dollars> or --clear.");
230
+ if (!opts.amount) throw new CliUsageError("Pass --amount <dollars> or --clear.");
235
231
  return { cap_cents: dollarsToCents(opts.amount) };
236
232
  }
237
233
 
238
234
  function parseWholeNumberInRange(value: string, flag: string, min: number, max: number): number {
239
235
  const normalized = value.trim();
240
- if (!/^\d+$/.test(normalized)) throw new Error(`${flag} must be a whole number.`);
236
+ if (!/^\d+$/.test(normalized)) throw new CliUsageError(`${flag} must be a whole number.`);
241
237
  const parsed = Number(normalized);
242
238
  if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) {
243
- throw new Error(`${flag} must be a whole number from ${min} to ${max}.`);
239
+ throw new CliUsageError(`${flag} must be a whole number from ${min} to ${max}.`);
244
240
  }
245
241
  return parsed;
246
242
  }
247
243
 
248
244
  function parsePositiveWholeNumber(value: string, flag: string): number {
249
245
  const normalized = value.trim();
250
- if (!/^\d+$/.test(normalized)) throw new Error(`${flag} must be a whole number.`);
246
+ if (!/^\d+$/.test(normalized)) throw new CliUsageError(`${flag} must be a whole number.`);
251
247
  const parsed = Number(normalized);
252
248
  if (!Number.isSafeInteger(parsed) || parsed <= 0) {
253
- throw new Error(`${flag} must be a positive whole number.`);
249
+ throw new CliUsageError(`${flag} must be a positive whole number.`);
254
250
  }
255
251
  return parsed;
256
252
  }
257
253
 
258
254
  function stopUsageOption(opts: BudgetSetOptions): boolean | undefined {
259
- if (opts.hardStop && opts.soft) throw new Error("Use either --hard-stop or --soft, not both.");
255
+ if (opts.hardStop && opts.soft) throw new CliUsageError("Use either --hard-stop or --soft, not both.");
260
256
  if (opts.hardStop) return true;
261
257
  if (opts.soft) return false;
262
258
  return undefined;
@@ -264,7 +260,7 @@ function stopUsageOption(opts: BudgetSetOptions): boolean | undefined {
264
260
 
265
261
  function alertsOption(opts: BudgetSetOptions): boolean | undefined {
266
262
  if (opts.enableAlerts && opts.disableAlerts) {
267
- throw new Error("Use either --enable-alerts or --disable-alerts, not both.");
263
+ throw new CliUsageError("Use either --enable-alerts or --disable-alerts, not both.");
268
264
  }
269
265
  if (opts.enableAlerts) return true;
270
266
  if (opts.disableAlerts) return false;
@@ -296,7 +292,7 @@ function capSetMessage(label: string, cap: EntityCap | null, changed: boolean |
296
292
  function normalizeRequired(value: string, label: string): string {
297
293
  const normalized = value.trim();
298
294
  if (!normalized) {
299
- throw new Error(`${label} cannot be blank.`);
295
+ throw new CliUsageError(`${label} cannot be blank.`);
300
296
  }
301
297
  return normalized;
302
298
  }