@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
@@ -9,8 +9,10 @@ import type { CliContext } from "../context.js";
9
9
  import { slugFromRemote, deriveSlug, validateAgentName, validateAgentSlug } from "../agent.js";
10
10
  import { findGitRoot, scanAgentFolders } from "../setup-scan.js";
11
11
  import { harnessPreflight } from "../harness-preflight.js";
12
+ import { genuiPreflight } from "../genui.js";
12
13
  import { cliFormatRawError } from "../api-error.js";
13
14
  import { normalizeNonBlank } from "../validators.js";
15
+ import { CliUsageError } from "../exit-codes.js";
14
16
 
15
17
  // `dharta deploy [slug]` deploys the ONE agent folder at/above --dir (or cwd).
16
18
  // The legacy slug argument may assert that folder's manifest identity, but it
@@ -110,7 +112,7 @@ export async function deploy(
110
112
  );
111
113
  }
112
114
  if (opts.reconcileSource && mode !== "git") {
113
- throw new Error(
115
+ throw new CliUsageError(
114
116
  "--reconcile-source is only valid for git deploys. " +
115
117
  "Remove it, or use `--mode git` to reconcile Dharta's hosted source branch.",
116
118
  );
@@ -119,8 +121,17 @@ export async function deploy(
119
121
  // RFC 0035 D7: advisory harness-version pre-flight for the selected agent. A
120
122
  // no-op unless [harness] declares a version; throws to abort on an unsatisfiable
121
123
  // pin or a declined local-vs-cloud gap.
122
- const preflight = (d: string) =>
123
- harnessPreflight({
124
+ // RFC 0056: the [genui] catalog is checked first - it is purely local, so a
125
+ // broken bundle fails instantly and before the harness check can prompt. The
126
+ // deploy's own file list is handed in because every genui check reads the
127
+ // WORKING tree while deploy ships a Git-filtered one (committed HEAD, or a
128
+ // gitignore-honoring tarball), so a bundle this deploy will not carry passes
129
+ // locally and arrives missing. Asking the shipping list, rather than
130
+ // re-deriving the answer from ignore rules, is right by construction for a
131
+ // force-added file and for a non-ASCII name.
132
+ const preflight = async (d: string) => {
133
+ await genuiPreflight(d, { deployFiles: () => listDeployFiles(d, git) });
134
+ await harnessPreflight({
124
135
  dir: d,
125
136
  baseUrl: ctx.baseUrl,
126
137
  log,
@@ -128,6 +139,7 @@ export async function deploy(
128
139
  detectLocalVersion: opts.detectLocalVersion,
129
140
  confirm: opts.confirm,
130
141
  });
142
+ };
131
143
 
132
144
  if (mode === "folder") {
133
145
  await preflight(target.dir);
@@ -361,7 +373,9 @@ async function resolveDeployTarget(
361
373
  ? validateAgentSlug(manifest.slug, `${manifestPath} slug`)
362
374
  : validateAgentSlug(deriveSlug(manifestName), "dharta.toml name");
363
375
  if (explicitSlug && explicitSlug !== manifestSlug) {
364
- throw new Error(
376
+ // `explicitSlug` is the positional the caller typed, and the message tells
377
+ // them to change it, so this is exit 2 like every other rejected argument.
378
+ throw new CliUsageError(
365
379
  `Agent slug '${explicitSlug}' does not match ${manifestPath} identity ` +
366
380
  `'${manifestSlug}' (name '${manifestName}'). The manifest identity is authoritative; ` +
367
381
  `set a top-level slug in dharta.toml to pin it explicitly.`,
@@ -9,7 +9,6 @@ import { promises as fs } from "node:fs";
9
9
  import * as path from "node:path";
10
10
  import * as net from "node:net";
11
11
  import * as readline from "node:readline";
12
- import { randomUUID } from "node:crypto";
13
12
  import TOML from "@iarna/toml";
14
13
  import { resolveRuntime, spawnRuntime } from "../runtime.js";
15
14
  import { buildContext } from "../context.js";
@@ -18,6 +17,15 @@ import { parseDotenvFile } from "./env.js";
18
17
  import { describeFolder } from "../setup-scan.js";
19
18
  import { hasAgentMarker } from "../harnesses.js";
20
19
  import { normalizeNonBlank } from "../validators.js";
20
+ import { mintIdempotencyKey } from "../config.js";
21
+ import { checkGenui } from "../genui.js";
22
+ import { CliUsageError } from "../exit-codes.js";
23
+ import {
24
+ formatModelAuthCapability,
25
+ resolveDevModelAuth,
26
+ type DevModelAuth,
27
+ type RunCommandStatus,
28
+ } from "../model-auth.js";
21
29
 
22
30
  export interface DevOptions {
23
31
  dir?: string;
@@ -30,6 +38,7 @@ export interface DevOptions {
30
38
  envRemote?: boolean; // --env remote: bind prod env vars instead of .env
31
39
  includeSecrets?: boolean; // with --env remote: fetch secret values (env:secrets:read)
32
40
  project?: string; // -p override for the parity flags' project inference
41
+ modelAuth?: DevModelAuth;
33
42
  // Test seams. `attach` skips the spawn and drives an existing server.
34
43
  attach?: string;
35
44
  fetchImpl?: typeof fetch;
@@ -38,6 +47,7 @@ export interface DevOptions {
38
47
  log?: (line: string) => void;
39
48
  healthTimeoutMs?: number;
40
49
  turnIdleMs?: number; // abort a turn with no output for this long (default 60s)
50
+ modelAuthCommand?: RunCommandStatus;
41
51
  }
42
52
 
43
53
  interface SseEvent {
@@ -45,6 +55,10 @@ interface SseEvent {
45
55
  data: Record<string, unknown>;
46
56
  }
47
57
 
58
+ export function devServeArgs(dir: string, port: number, modelAuth: DevModelAuth): string[] {
59
+ return ["dev-serve", dir, "--host", "127.0.0.1", "--port", String(port), "--model-auth", modelAuth];
60
+ }
61
+
48
62
  // `dharta dev [path]` in a multi-agent repo (RFC 0024 DEP-8): with no path
49
63
  // given, serve cwd if it is itself an agent; else the sole agent folder
50
64
  // directly below it; else ask which one. An explicit path always wins (the
@@ -81,12 +95,12 @@ export async function dev(opts: DevOptions = {}): Promise<void> {
81
95
  const fetchImpl = (opts.fetchImpl ?? globalThis.fetch).bind(globalThis);
82
96
 
83
97
  if (opts.port !== undefined && (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535)) {
84
- throw new Error("--port must be an integer between 1 and 65535.");
98
+ throw new CliUsageError("--port must be an integer between 1 and 65535.");
85
99
  }
86
100
  const normalizedDir = opts.dir === undefined ? undefined : normalizeNonBlank(opts.dir, "--dir");
87
101
  const normalizedUser = opts.user === undefined ? undefined : normalizeNonBlank(opts.user, "--user");
88
102
  const normalizedMessage = opts.message === undefined ? undefined : normalizeNonBlank(opts.message, "--message");
89
- if (opts.includeSecrets && !opts.envRemote) throw new Error("--include-secrets requires --env remote.");
103
+ if (opts.includeSecrets && !opts.envRemote) throw new CliUsageError("--include-secrets requires --env remote.");
90
104
 
91
105
  let dir = path.resolve(normalizedDir ?? process.cwd());
92
106
  if (normalizedDir !== undefined) {
@@ -96,6 +110,7 @@ export async function dev(opts: DevOptions = {}): Promise<void> {
96
110
  // ask) - RFC 0024 DEP-8. An explicit --dir / [path] is served as-is.
97
111
  if (normalizedDir === undefined) dir = await resolveAgentDir(dir, log);
98
112
  let childEnv: NodeJS.ProcessEnv | undefined;
113
+ let selectedModelAuth: DevModelAuth | undefined;
99
114
  if (opts.release || opts.envRemote) {
100
115
  const ctx = await buildContext({ fetch: opts.fetchImpl });
101
116
  const inferOpts = { dir: normalizedDir, project: opts.project, fetchImpl: opts.fetchImpl };
@@ -121,6 +136,17 @@ export async function dev(opts: DevOptions = {}): Promise<void> {
121
136
  }
122
137
  }
123
138
 
139
+ if (!opts.attach) {
140
+ let authEnv = childEnv ?? process.env;
141
+ if (!opts.envRemote) {
142
+ authEnv = await devModelAuthEnv(dir, authEnv);
143
+ }
144
+ const resolvedAuth = resolveDevModelAuth(opts.modelAuth, authEnv, opts.modelAuthCommand);
145
+ childEnv = resolvedAuth.env;
146
+ selectedModelAuth = resolvedAuth.selection;
147
+ log(`✓ Model auth: ${formatModelAuthCapability(resolvedAuth.capability)}`);
148
+ }
149
+
124
150
  const agentName = (await describeFolder(dir, ".")).name ?? path.basename(dir);
125
151
  log(`Dharta dev mode: ${agentName}`);
126
152
  log("");
@@ -137,7 +163,7 @@ export async function dev(opts: DevOptions = {}): Promise<void> {
137
163
  const port = opts.port ?? (await freePort());
138
164
  baseUrl = `http://127.0.0.1:${port}`;
139
165
  log(`✓ Starting agent runtime (${runtime.description})…`);
140
- child = spawnRuntime(runtime, ["dev-serve", dir, "--host", "127.0.0.1", "--port", String(port)], {
166
+ child = spawnRuntime(runtime, devServeArgs(dir, port, selectedModelAuth as DevModelAuth), {
141
167
  stdio: "pipe",
142
168
  env: childEnv,
143
169
  });
@@ -180,19 +206,6 @@ export async function dev(opts: DevOptions = {}): Promise<void> {
180
206
  log(" Preview parity is code + config, not infrastructure: no microVM, no per-user");
181
207
  log(" workspaces, and turns run on YOUR harness credentials (prod injects its own).");
182
208
  }
183
- // The agent turn spawns `claude` on YOUR credentials; with none it exits 1.
184
- // Hard-stop here rather than drop into a chat that can only fail (spawn
185
- // path only - an attached runtime carries its own credentials).
186
- if (child && !process.env.ANTHROPIC_API_KEY && !process.env.CLAUDE_CODE_OAUTH_TOKEN) {
187
- log("");
188
- log("! Error: No model credential found. Two options -");
189
- log(" export ANTHROPIC_API_KEY=<your Anthropic API key>");
190
- log(" OR");
191
- log(" export CLAUDE_CODE_OAUTH_TOKEN=<Claude Code token from `claude setup-token`>");
192
- process.exitCode = 1;
193
- return;
194
- }
195
-
196
209
  const session = await createSession(baseUrl, fetchImpl, normalizedUser);
197
210
  if (normalizedUser) log(`Simulating end user '${normalizedUser}'.`);
198
211
 
@@ -216,7 +229,12 @@ export async function dev(opts: DevOptions = {}): Promise<void> {
216
229
  // Run one turn but never hang: abort if the runtime crashes (onFatal) or
217
230
  // goes silent for idleMs, then surface the buffered runtime output. The
218
231
  // idle timer pauses while we wait on a human (approval prompt).
232
+ // Counts turns in this REPL session. One root --idempotency-key covers the
233
+ // whole process, so without the index every turn after the first would
234
+ // replay the first turn's answer instead of running.
235
+ let turnIndex = 0;
219
236
  const runTurnWatched = async (turnText: string, prefix: string): Promise<void> => {
237
+ turnIndex += 1;
220
238
  const controller = new AbortController();
221
239
  let idle: ReturnType<typeof setTimeout> | undefined;
222
240
  let produced = false; // did the agent write any content this turn?
@@ -243,7 +261,14 @@ export async function dev(opts: DevOptions = {}): Promise<void> {
243
261
  const aborted = new Promise<never>((_, reject) => {
244
262
  controller.signal.addEventListener("abort", () => reject(new Error("turn aborted")), { once: true });
245
263
  });
246
- const turn = runTurn(baseUrl as string, fetchImpl, session, turnText, { write: tracked, log, ask: pausedAsk }, controller.signal);
264
+ const turn = runTurn(
265
+ baseUrl as string,
266
+ fetchImpl,
267
+ session,
268
+ turnText,
269
+ { write: tracked, log, ask: pausedAsk },
270
+ { signal: controller.signal, operation: `turn.${turnIndex}` },
271
+ );
247
272
  try {
248
273
  // Race the turn against the abort. A real fetch unblocks its body read
249
274
  // on signal, but racing also covers a stream that wedges open without
@@ -261,7 +286,7 @@ export async function dev(opts: DevOptions = {}): Promise<void> {
261
286
  runtimeOutput?.dump();
262
287
  throw new Error(
263
288
  "No response - the agent runtime errored or went silent (see [runtime] output above). " +
264
- "A missing model credential (ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN) is the usual cause.",
289
+ "Check the selected model authentication with `dharta doctor`, then retry.",
265
290
  );
266
291
  }
267
292
  throw err;
@@ -299,6 +324,14 @@ export async function dev(opts: DevOptions = {}): Promise<void> {
299
324
  }
300
325
  }
301
326
 
327
+ export async function devModelAuthEnv(
328
+ dir: string,
329
+ baseEnv: NodeJS.ProcessEnv,
330
+ ): Promise<NodeJS.ProcessEnv> {
331
+ const localEnv = await readOptionalLocalFile(path.join(dir, ".env"), ".env");
332
+ return { ...parseDotenvFile(localEnv.content), ...baseEnv };
333
+ }
334
+
302
335
  async function validateExplicitDevDir(dir: string): Promise<void> {
303
336
  try {
304
337
  const stat = await fs.stat(dir);
@@ -321,7 +354,8 @@ async function validateExplicitDevDir(dir: string): Promise<void> {
321
354
  export async function miniDoctor(dir: string): Promise<string[]> {
322
355
  const warnings: string[] = [];
323
356
 
324
- const karta = await readOptionalLocalFile(path.join(dir, "dharta.toml"), "dharta.toml");
357
+ const manifestPath = path.join(dir, "dharta.toml");
358
+ const karta = await readOptionalLocalFile(manifestPath, "dharta.toml");
325
359
  if (karta.warning) warnings.push(karta.warning);
326
360
  const kartaToml = karta.content;
327
361
  if (kartaToml === null) {
@@ -331,6 +365,14 @@ export async function miniDoctor(dir: string): Promise<string[]> {
331
365
  } else {
332
366
  try {
333
367
  const parsed = TOML.parse(kartaToml) as Record<string, unknown>;
368
+
369
+ // RFC 0056: the same [genui] problems the deploy pre-flight refuses on,
370
+ // said as warnings here so a broken component catalog surfaces while you
371
+ // develop. The manifest this doctor already read is handed straight in
372
+ // rather than read and parsed a second time.
373
+ const genui = await checkGenui({ path: manifestPath, text: kartaToml, parsed });
374
+ warnings.push(...genui.problems);
375
+
334
376
  const env = parsed.env as Record<string, unknown> | undefined;
335
377
  if (env) {
336
378
  const localEnv = await readOptionalLocalFile(path.join(dir, ".env"), ".env");
@@ -493,7 +535,10 @@ async function createSession(
493
535
  ): Promise<string> {
494
536
  const res = await fetchAgentApi(baseUrl, fetchImpl, "/v1/sessions", "create a session", {
495
537
  method: "POST",
496
- headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
538
+ headers: {
539
+ "Content-Type": "application/json",
540
+ "Idempotency-Key": mintIdempotencyKey("session"),
541
+ },
497
542
  body: JSON.stringify({ metadata: endUserId ? { end_user_id: endUserId } : {} }),
498
543
  });
499
544
  if (!res.ok) {
@@ -523,13 +568,21 @@ interface PendingInput {
523
568
  target?: string;
524
569
  }
525
570
 
571
+ export interface RunTurnOptions {
572
+ signal?: AbortSignal;
573
+ // The idempotency operation this turn writes under. A REPL that sends many
574
+ // turns under one root key has to distinguish them, or every turn after the
575
+ // first replays the first one's answer, so `dharta dev` passes `turn.<n>`.
576
+ operation?: string;
577
+ }
578
+
526
579
  export async function runTurn(
527
580
  baseUrl: string,
528
581
  fetchImpl: typeof fetch,
529
582
  sessionId: string,
530
583
  text: string,
531
584
  io: TurnIo,
532
- signal?: AbortSignal,
585
+ { signal, operation = "turn" }: RunTurnOptions = {},
533
586
  ): Promise<void> {
534
587
  const res = await fetchAgentApi(
535
588
  baseUrl,
@@ -541,7 +594,7 @@ export async function runTurn(
541
594
  headers: {
542
595
  "Content-Type": "application/json",
543
596
  Accept: "text/event-stream",
544
- "Idempotency-Key": randomUUID(),
597
+ "Idempotency-Key": mintIdempotencyKey(operation),
545
598
  },
546
599
  body: JSON.stringify({ text, stream: true }),
547
600
  signal,
@@ -555,7 +608,7 @@ export async function runTurn(
555
608
  }
556
609
 
557
610
  const pending = await renderEventStream(parseSse(res.body), io);
558
- await resolvePendingInputs(baseUrl, fetchImpl, sessionId, pending, io);
611
+ await resolvePendingInputs(baseUrl, fetchImpl, sessionId, pending, io, operation);
559
612
  }
560
613
 
561
614
  // Approval loop: the harness pauses on tool permissions; the decision
@@ -567,10 +620,15 @@ async function resolvePendingInputs(
567
620
  sessionId: string,
568
621
  first: PendingInput | null,
569
622
  io: TurnIo,
623
+ operation: string,
570
624
  ): Promise<void> {
571
625
  let pending = first;
572
626
  let abortHintShown = false;
627
+ // Counts the decisions in THIS turn. The request id is unique too, but it is
628
+ // minted per run, so a key built from it would never match on a rerun.
629
+ let decisionIndex = 0;
573
630
  while (pending) {
631
+ decisionIndex += 1;
574
632
  const prompt = pending.message ?? `The agent wants to use ${pending.tool ?? "a tool"}`;
575
633
  const answer = await io.ask(`\n${prompt} - approve? [y=once / s=session / n=deny] `);
576
634
  if (answer === null && !abortHintShown) {
@@ -601,7 +659,10 @@ async function resolvePendingInputs(
601
659
  "deliver the decision",
602
660
  {
603
661
  method: "PUT",
604
- headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
662
+ headers: {
663
+ "Content-Type": "application/json",
664
+ "Idempotency-Key": mintIdempotencyKey(`${operation}.input.${decisionIndex}`),
665
+ },
605
666
  body: JSON.stringify({ decision }),
606
667
  },
607
668
  );
@@ -8,6 +8,30 @@ import { loadConfig } from "../config.js";
8
8
  import { resolveRuntime, whichAll, type RuntimeResolveOptions } from "../runtime.js";
9
9
  import { installChannel, packageRoot } from "./update.js";
10
10
  import { checkNpmLatestVersion, NPM_LATEST_URL } from "../version-check.js";
11
+ import {
12
+ capabilityFor,
13
+ checkClaudeAuth,
14
+ formatModelAuthCapability,
15
+ MODEL_AUTH_MISSING,
16
+ runtimeSwitchOpen,
17
+ type RunCommandStatus,
18
+ } from "../model-auth.js";
19
+
20
+ // A malformed `DHARTA_MODEL_AUTH_*_ENABLED` is exactly the class of problem
21
+ // doctor exists to explain, so it is reported as a failed line rather than
22
+ // thrown: `runtimeSwitchOpen` raises on any value outside its two lists, and an
23
+ // unguarded call took out the whole report over one typo. A capability whose
24
+ // switch cannot be read counts as closed.
25
+ function readRuntimeSwitch(
26
+ capability: Parameters<typeof runtimeSwitchOpen>[0],
27
+ env: NodeJS.ProcessEnv,
28
+ ): { open: boolean; problem?: string } {
29
+ try {
30
+ return { open: runtimeSwitchOpen(capability, env) };
31
+ } catch (err) {
32
+ return { open: false, problem: err instanceof Error ? err.message : String(err) };
33
+ }
34
+ }
11
35
 
12
36
  export interface DoctorReport {
13
37
  lines: string[];
@@ -20,6 +44,7 @@ export interface DoctorOptions extends RuntimeResolveOptions {
20
44
  currentVersion?: string;
21
45
  fetchImpl?: typeof fetch;
22
46
  latestUrl?: string;
47
+ modelAuthCommand?: RunCommandStatus;
23
48
  }
24
49
 
25
50
  export async function doctor(opts: DoctorOptions = {}): Promise<DoctorReport> {
@@ -69,6 +94,50 @@ export async function doctor(opts: DoctorOptions = {}): Promise<DoctorReport> {
69
94
  }
70
95
  }
71
96
 
97
+ const apiKeyCapability = capabilityFor("api-key");
98
+ const apiKeySwitch = readRuntimeSwitch(apiKeyCapability, env);
99
+ if (apiKeySwitch.problem) {
100
+ ok = false;
101
+ lines.push(`✗ ${apiKeySwitch.problem}`);
102
+ lines.push(" Fix: set it to true or false, or unset it.");
103
+ }
104
+ const apiKeyPresent = Boolean(env.ANTHROPIC_API_KEY?.trim());
105
+ const apiKeyReady =
106
+ apiKeyPresent && apiKeyCapability.admission === "enabled" && apiKeySwitch.open;
107
+ if (!apiKeyPresent) {
108
+ lines.push(`- ${MODEL_AUTH_MISSING}: ANTHROPIC_API_KEY is not set.`);
109
+ } else if (!apiKeyReady) {
110
+ lines.push(`- ANTHROPIC_API_KEY is set, but ${apiKeyCapability.id} is disabled (model_auth_capability_disabled).`);
111
+ lines.push(` ${formatModelAuthCapability(apiKeyCapability)}`);
112
+ } else {
113
+ lines.push(`✓ Anthropic API key auth: ${formatModelAuthCapability(apiKeyCapability)}`);
114
+ }
115
+
116
+ const claude = checkClaudeAuth(env, opts.modelAuthCommand);
117
+ const claudeCapability = capabilityFor("claude");
118
+ const claudeSwitch = readRuntimeSwitch(claudeCapability, env);
119
+ if (claudeSwitch.problem) {
120
+ ok = false;
121
+ lines.push(`✗ ${claudeSwitch.problem}`);
122
+ lines.push(" Fix: set it to true or false, or unset it.");
123
+ }
124
+ let claudeReady = false;
125
+ if (!claude.installed) {
126
+ lines.push(`- ${MODEL_AUTH_MISSING}: Claude client is not on PATH. Install it, then run \`claude auth login\` to use \`dharta dev --model-auth claude\`.`);
127
+ } else if (!claude.authenticated) {
128
+ lines.push(`- ${MODEL_AUTH_MISSING}: Claude client is not logged in. Run \`claude auth login\`, then retry \`dharta dev --model-auth claude\`.`);
129
+ } else if (claudeCapability.admission !== "enabled" || !claudeSwitch.open) {
130
+ lines.push(`- Claude client is logged in, but ${claudeCapability.id} is disabled (model_auth_capability_disabled).`);
131
+ lines.push(` ${formatModelAuthCapability(claudeCapability)}`);
132
+ } else {
133
+ claudeReady = true;
134
+ lines.push(`✓ Claude client auth: ${formatModelAuthCapability(claudeCapability)}`);
135
+ }
136
+ if (!apiKeyReady && !claudeReady) {
137
+ ok = false;
138
+ lines.push(" Fix: export ANTHROPIC_API_KEY or run `claude auth login` before `dharta dev`.");
139
+ }
140
+
72
141
  if (opts.checkLatest !== false) {
73
142
  const version = await checkLatestVersion(opts);
74
143
  if (version.status === "outdated") {
@@ -1,8 +1,8 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import { inferAgentSlug, type AgentInferOptions } from "../agent.js";
3
2
  import type { CliContext } from "../context.js";
4
3
  import { fmtTs, table } from "../output.js";
5
4
  import { normalizeHttpUrl, normalizeNonBlank } from "../validators.js";
5
+ import { CliUsageError } from "../exit-codes.js";
6
6
 
7
7
  export interface EmbedKey {
8
8
  id: string;
@@ -63,7 +63,6 @@ export async function embedKeysCreateData(
63
63
  const res = await ctx.http.request<EmbedKeyEnvelope>({
64
64
  method: "POST",
65
65
  path: `/api/agents/${encodeURIComponent(slug)}/embed_keys`,
66
- headers: idempotencyHeader(),
67
66
  body: {
68
67
  name: normalizedName,
69
68
  allowed_origins: normalizedOrigins,
@@ -92,7 +91,6 @@ export async function embedKeysRotateData(ctx: CliContext, id: string, opts: Age
92
91
  const res = await ctx.http.request<EmbedKeyEnvelope>({
93
92
  method: "POST",
94
93
  path: `/api/agents/${encodeURIComponent(slug)}/embed_keys/${encodeURIComponent(normalizedId)}/rotate`,
95
- headers: idempotencyHeader(),
96
94
  });
97
95
  return { slug, ...res };
98
96
  }
@@ -117,7 +115,6 @@ export async function embedKeysToggleData(
117
115
  const res = await ctx.http.request<EmbedKeyEnvelope>({
118
116
  method: "POST",
119
117
  path: `/api/agents/${encodeURIComponent(slug)}/embed_keys/${encodeURIComponent(normalizedId)}/${action}`,
120
- headers: idempotencyHeader(),
121
118
  });
122
119
  return { slug, ...res };
123
120
  }
@@ -142,7 +139,6 @@ export async function embedKeysSetIdentityData(
142
139
  const res = await ctx.http.request<IdentityEnvelope>({
143
140
  method: "POST",
144
141
  path: `/api/agents/${encodeURIComponent(slug)}/embed_keys/${encodeURIComponent(normalizedId)}/identity`,
145
- headers: idempotencyHeader(),
146
142
  });
147
143
  return { slug, ...res };
148
144
  }
@@ -166,7 +162,6 @@ export async function embedKeysClearIdentityData(
166
162
  const res = await ctx.http.request<EmbedKeyEnvelope>({
167
163
  method: "DELETE",
168
164
  path: `/api/agents/${encodeURIComponent(slug)}/embed_keys/${encodeURIComponent(normalizedId)}/identity`,
169
- headers: idempotencyHeader(),
170
165
  });
171
166
  return { slug, ...res };
172
167
  }
@@ -182,7 +177,6 @@ export async function embedKeysRevokeData(ctx: CliContext, id: string, opts: Age
182
177
  await ctx.http.request<void>({
183
178
  method: "DELETE",
184
179
  path: `/api/agents/${encodeURIComponent(slug)}/embed_keys/${encodeURIComponent(normalizedId)}`,
185
- headers: idempotencyHeader(),
186
180
  });
187
181
  return { slug, id: normalizedId, revoked: true };
188
182
  }
@@ -199,7 +193,7 @@ export function parseOrigins(input: string[] | undefined): string[] {
199
193
  .map((value) => value.trim())
200
194
  .filter(Boolean);
201
195
  if (origins.length === 0) {
202
- throw new Error("--origin must include at least one origin, e.g. --origin https://app.example.com.");
196
+ throw new CliUsageError("--origin must include at least one origin, e.g. --origin https://app.example.com.");
203
197
  }
204
198
  return origins.map((origin) => normalizeOrigin(origin));
205
199
  }
@@ -208,7 +202,7 @@ function normalizeOrigin(origin: string): string {
208
202
  const normalized = normalizeHttpUrl(origin, "--origin");
209
203
  const parsed = new URL(normalized);
210
204
  if (parsed.pathname !== "/" || parsed.search || parsed.hash) {
211
- throw new Error("--origin must be an origin like https://app.example.com, without a path, query, or fragment.");
205
+ throw new CliUsageError("--origin must be an origin like https://app.example.com, without a path, query, or fragment.");
212
206
  }
213
207
  return parsed.origin;
214
208
  }
@@ -224,14 +218,10 @@ function row(key: EmbedKey): Record<string, string> {
224
218
  };
225
219
  }
226
220
 
227
- function idempotencyHeader(): Record<string, string> {
228
- return { "Idempotency-Key": randomUUID() };
229
- }
230
-
231
221
  function normalizeEmbedKeyName(name: string): string {
232
222
  const normalized = name.trim();
233
223
  if (!normalized) {
234
- throw new Error("Embed key name cannot be blank.");
224
+ throw new CliUsageError("Embed key name cannot be blank.");
235
225
  }
236
226
  return normalized;
237
227
  }
@@ -4,12 +4,12 @@
4
4
 
5
5
  import { promises as fs } from "node:fs";
6
6
  import * as path from "node:path";
7
- import { randomUUID } from "node:crypto";
8
7
  import TOML from "@iarna/toml";
9
8
  import type { CliContext } from "../context.js";
10
9
  import { inferAgentSlug, validateAgentSelectorOptions, type AgentInferOptions } from "../agent.js";
11
10
  import { table } from "../output.js";
12
11
  import { normalizeNonBlank } from "../validators.js";
12
+ import { CliUsageError } from "../exit-codes.js";
13
13
 
14
14
  export interface EnvOptions extends AgentInferOptions {
15
15
  force?: boolean; // pull: overwrite an existing .env
@@ -22,7 +22,7 @@ export interface EnvOptions extends AgentInferOptions {
22
22
  // (a GitHub PAT, a webhook signing key). `list` therefore masks EVERY value by
23
23
  // default - a routine listing must never dump a live secret to the terminal or
24
24
  // CI logs. A short tail is kept so an operator can still tell two bindings
25
- // apart (matching how `whoami`/model keys show only a tail); `--reveal` prints
25
+ // apart (matching how `whoami`/model connections show only a tail); `--reveal` prints
26
26
  // the full value for the rare time it is actually needed.
27
27
  export function maskEnvValue(v: { secret: boolean; value?: string | null }, reveal: boolean): string {
28
28
  if (reveal) return v.value ?? "";
@@ -78,7 +78,6 @@ export async function envSetData(
78
78
  method: "PUT",
79
79
  path: `/api/agents/${encodeURIComponent(slug)}/env/${encodeURIComponent(name)}`,
80
80
  body,
81
- headers: { "Idempotency-Key": randomUUID() },
82
81
  });
83
82
  return { slug, name, secret: !!opts.secret };
84
83
  }
@@ -94,7 +93,7 @@ export function validateEnvName(name: string): void {
94
93
  function parseEnvAssignment(assignment: string): { name: string; value: string } {
95
94
  const eq = assignment.indexOf("=");
96
95
  if (eq <= 0) {
97
- throw new Error("Usage: dharta env set NAME=value [--secret]");
96
+ throw new CliUsageError("Usage: dharta env set NAME=value [--secret]");
98
97
  }
99
98
  const name = normalizeEnvName(assignment.slice(0, eq));
100
99
  const value = assignment.slice(eq + 1);
@@ -123,7 +122,6 @@ export async function envUnsetData(
123
122
  await ctx.http.request({
124
123
  method: "DELETE",
125
124
  path: `/api/agents/${encodeURIComponent(slug)}/env/${encodeURIComponent(normalizedName)}`,
126
- headers: { "Idempotency-Key": randomUUID() },
127
125
  });
128
126
  return { slug, name: normalizedName, removed: true };
129
127
  }
@@ -154,7 +152,6 @@ export async function envPullData(
154
152
  method: "POST",
155
153
  path: `/api/agents/${encodeURIComponent(slug)}/env/pull`,
156
154
  body: { include_secrets: !!opts.includeSecrets },
157
- headers: { "Idempotency-Key": randomUUID() },
158
155
  });
159
156
 
160
157
  const bound = body.env.filter((v) => !v.withheld);
@@ -347,7 +344,7 @@ async function validateEnvPullTarget(dir: string, target: string, force: boolean
347
344
  throw new Error(`Cannot write .env to ${target}: expected a file, got a directory.`);
348
345
  }
349
346
  if (!force) {
350
- throw new Error(
347
+ throw new CliUsageError(
351
348
  `${target} already exists. Re-run with --force to overwrite it (local-only keys are lost).`,
352
349
  );
353
350
  }
@@ -1,10 +1,10 @@
1
1
  import { HttpClient } from "@dharta/sdk";
2
- import { randomUUID } from "node:crypto";
3
2
  import { writeFile } from "node:fs/promises";
4
3
  import type { CliContext } from "../context.js";
5
4
  import { cliFormatError, cliFormatNetworkError, cliFormatRawError } from "../api-error.js";
6
5
  import { table } from "../output.js";
7
6
  import { validateAgentSlug } from "../agent.js";
7
+ import { CliUsageError } from "../exit-codes.js";
8
8
 
9
9
  // `dharta files ls|get <agent> <karta>` - the owner file-read channel (RFC 0060 D3/P3).
10
10
  //
@@ -35,7 +35,6 @@ async function mintFilesToken(ctx: CliContext, agent: string, instance: string):
35
35
  const res = await ctx.http.request<{ token?: string; files_url?: string }>({
36
36
  method: "POST",
37
37
  path: `/api/agents/${encodeURIComponent(agent)}/workspace_tokens`,
38
- headers: { "Idempotency-Key": randomUUID() },
39
38
  body: { instance, scope: "workspace_read" },
40
39
  });
41
40
  if (!res.token) {
@@ -65,7 +64,7 @@ function filesClient(ctx: CliContext, mint: FilesMint): { http: HttpClient; base
65
64
 
66
65
  function normalizeInstance(instance: string): string {
67
66
  const trimmed = instance.trim();
68
- if (!trimmed) throw new Error("A dharta id is required.");
67
+ if (!trimmed) throw new CliUsageError("A dharta id is required.");
69
68
  return trimmed;
70
69
  }
71
70
 
@@ -109,7 +108,7 @@ export async function filesGet(
109
108
  const slug = validateAgentSlug(agent, "agent slug");
110
109
  const karta = normalizeInstance(instance);
111
110
  const filePath = path.trim();
112
- if (!filePath) throw new Error("A file path is required.");
111
+ if (!filePath) throw new CliUsageError("A file path is required.");
113
112
  const mint = await mintFilesToken(ctx, slug, karta);
114
113
  const { http, basePath } = filesClient(ctx, mint);
115
114
  const res = await http.raw(`${basePath}/file?path=${encodeURIComponent(filePath)}`);