@dharta/cli 0.6.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/dist/agent.d.ts +17 -0
  4. package/dist/agent.js +134 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/api-error.d.ts +3 -0
  7. package/dist/api-error.js +80 -0
  8. package/dist/api-error.js.map +1 -0
  9. package/dist/browser.d.ts +1 -0
  10. package/dist/browser.js +19 -0
  11. package/dist/browser.js.map +1 -0
  12. package/dist/cli.d.ts +5 -0
  13. package/dist/cli.js +1978 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/commands/agents.d.ts +131 -0
  16. package/dist/commands/agents.js +439 -0
  17. package/dist/commands/agents.js.map +1 -0
  18. package/dist/commands/analytics.d.ts +25 -0
  19. package/dist/commands/analytics.js +166 -0
  20. package/dist/commands/analytics.js.map +1 -0
  21. package/dist/commands/api.d.ts +19 -0
  22. package/dist/commands/api.js +278 -0
  23. package/dist/commands/api.js.map +1 -0
  24. package/dist/commands/appearance.d.ts +19 -0
  25. package/dist/commands/appearance.js +63 -0
  26. package/dist/commands/appearance.js.map +1 -0
  27. package/dist/commands/artifact.d.ts +33 -0
  28. package/dist/commands/artifact.js +235 -0
  29. package/dist/commands/artifact.js.map +1 -0
  30. package/dist/commands/audit-log.d.ts +32 -0
  31. package/dist/commands/audit-log.js +102 -0
  32. package/dist/commands/audit-log.js.map +1 -0
  33. package/dist/commands/auth.d.ts +47 -0
  34. package/dist/commands/auth.js +117 -0
  35. package/dist/commands/auth.js.map +1 -0
  36. package/dist/commands/caps.d.ts +82 -0
  37. package/dist/commands/caps.js +213 -0
  38. package/dist/commands/caps.js.map +1 -0
  39. package/dist/commands/create.d.ts +22 -0
  40. package/dist/commands/create.js +76 -0
  41. package/dist/commands/create.js.map +1 -0
  42. package/dist/commands/deploy.d.ts +45 -0
  43. package/dist/commands/deploy.js +892 -0
  44. package/dist/commands/deploy.js.map +1 -0
  45. package/dist/commands/dev.d.ts +33 -0
  46. package/dist/commands/dev.js +655 -0
  47. package/dist/commands/dev.js.map +1 -0
  48. package/dist/commands/directories.d.ts +60 -0
  49. package/dist/commands/directories.js +83 -0
  50. package/dist/commands/directories.js.map +1 -0
  51. package/dist/commands/doctor.d.ts +13 -0
  52. package/dist/commands/doctor.js +123 -0
  53. package/dist/commands/doctor.js.map +1 -0
  54. package/dist/commands/embed-keys.d.ts +61 -0
  55. package/dist/commands/embed-keys.js +165 -0
  56. package/dist/commands/embed-keys.js.map +1 -0
  57. package/dist/commands/env.d.ts +59 -0
  58. package/dist/commands/env.js +300 -0
  59. package/dist/commands/env.js.map +1 -0
  60. package/dist/commands/files.d.ts +14 -0
  61. package/dist/commands/files.js +84 -0
  62. package/dist/commands/files.js.map +1 -0
  63. package/dist/commands/git-credential.d.ts +1 -0
  64. package/dist/commands/git-credential.js +70 -0
  65. package/dist/commands/git-credential.js.map +1 -0
  66. package/dist/commands/kartas.d.ts +101 -0
  67. package/dist/commands/kartas.js +289 -0
  68. package/dist/commands/kartas.js.map +1 -0
  69. package/dist/commands/keys.d.ts +33 -0
  70. package/dist/commands/keys.js +141 -0
  71. package/dist/commands/keys.js.map +1 -0
  72. package/dist/commands/login.d.ts +22 -0
  73. package/dist/commands/login.js +78 -0
  74. package/dist/commands/login.js.map +1 -0
  75. package/dist/commands/logs.d.ts +14 -0
  76. package/dist/commands/logs.js +89 -0
  77. package/dist/commands/logs.js.map +1 -0
  78. package/dist/commands/members.d.ts +32 -0
  79. package/dist/commands/members.js +77 -0
  80. package/dist/commands/members.js.map +1 -0
  81. package/dist/commands/model-keys.d.ts +22 -0
  82. package/dist/commands/model-keys.js +222 -0
  83. package/dist/commands/model-keys.js.map +1 -0
  84. package/dist/commands/open.d.ts +29 -0
  85. package/dist/commands/open.js +119 -0
  86. package/dist/commands/open.js.map +1 -0
  87. package/dist/commands/parity.d.ts +15 -0
  88. package/dist/commands/parity.js +73 -0
  89. package/dist/commands/parity.js.map +1 -0
  90. package/dist/commands/rollback.d.ts +21 -0
  91. package/dist/commands/rollback.js +26 -0
  92. package/dist/commands/rollback.js.map +1 -0
  93. package/dist/commands/schedules.d.ts +52 -0
  94. package/dist/commands/schedules.js +231 -0
  95. package/dist/commands/schedules.js.map +1 -0
  96. package/dist/commands/scopes.d.ts +3 -0
  97. package/dist/commands/scopes.js +21 -0
  98. package/dist/commands/scopes.js.map +1 -0
  99. package/dist/commands/sessions.d.ts +53 -0
  100. package/dist/commands/sessions.js +142 -0
  101. package/dist/commands/sessions.js.map +1 -0
  102. package/dist/commands/setup.d.ts +28 -0
  103. package/dist/commands/setup.js +949 -0
  104. package/dist/commands/setup.js.map +1 -0
  105. package/dist/commands/status.d.ts +19 -0
  106. package/dist/commands/status.js +63 -0
  107. package/dist/commands/status.js.map +1 -0
  108. package/dist/commands/update.d.ts +14 -0
  109. package/dist/commands/update.js +120 -0
  110. package/dist/commands/update.js.map +1 -0
  111. package/dist/commands/usage.d.ts +3 -0
  112. package/dist/commands/usage.js +37 -0
  113. package/dist/commands/usage.js.map +1 -0
  114. package/dist/commands/webhooks.d.ts +47 -0
  115. package/dist/commands/webhooks.js +156 -0
  116. package/dist/commands/webhooks.js.map +1 -0
  117. package/dist/commands/whoami.d.ts +7 -0
  118. package/dist/commands/whoami.js +17 -0
  119. package/dist/commands/whoami.js.map +1 -0
  120. package/dist/completions.d.ts +13 -0
  121. package/dist/completions.js +120 -0
  122. package/dist/completions.js.map +1 -0
  123. package/dist/config.d.ts +20 -0
  124. package/dist/config.js +98 -0
  125. package/dist/config.js.map +1 -0
  126. package/dist/context.d.ts +14 -0
  127. package/dist/context.js +38 -0
  128. package/dist/context.js.map +1 -0
  129. package/dist/device-flow.d.ts +9 -0
  130. package/dist/device-flow.js +127 -0
  131. package/dist/device-flow.js.map +1 -0
  132. package/dist/harness-preflight.d.ts +24 -0
  133. package/dist/harness-preflight.js +252 -0
  134. package/dist/harness-preflight.js.map +1 -0
  135. package/dist/harnesses.d.ts +11 -0
  136. package/dist/harnesses.js +112 -0
  137. package/dist/harnesses.js.map +1 -0
  138. package/dist/index.d.ts +3 -0
  139. package/dist/index.js +3 -0
  140. package/dist/index.js.map +1 -0
  141. package/dist/login-offer.d.ts +9 -0
  142. package/dist/login-offer.js +36 -0
  143. package/dist/login-offer.js.map +1 -0
  144. package/dist/manifest.d.ts +15 -0
  145. package/dist/manifest.js +142 -0
  146. package/dist/manifest.js.map +1 -0
  147. package/dist/money.d.ts +1 -0
  148. package/dist/money.js +16 -0
  149. package/dist/money.js.map +1 -0
  150. package/dist/output.d.ts +2 -0
  151. package/dist/output.js +20 -0
  152. package/dist/output.js.map +1 -0
  153. package/dist/runtime.d.ts +19 -0
  154. package/dist/runtime.js +95 -0
  155. package/dist/runtime.js.map +1 -0
  156. package/dist/scaffold.d.ts +16 -0
  157. package/dist/scaffold.js +74 -0
  158. package/dist/scaffold.js.map +1 -0
  159. package/dist/setup-scan.d.ts +21 -0
  160. package/dist/setup-scan.js +127 -0
  161. package/dist/setup-scan.js.map +1 -0
  162. package/dist/templates.d.ts +14 -0
  163. package/dist/templates.js +65 -0
  164. package/dist/templates.js.map +1 -0
  165. package/dist/validators.d.ts +10 -0
  166. package/dist/validators.js +116 -0
  167. package/dist/validators.js.map +1 -0
  168. package/dist/version-check.d.ts +25 -0
  169. package/dist/version-check.js +68 -0
  170. package/dist/version-check.js.map +1 -0
  171. package/node_modules/@dharta/sdk/LICENSE +21 -0
  172. package/node_modules/@dharta/sdk/README.md +87 -0
  173. package/node_modules/@dharta/sdk/dist/errors.d.ts +20 -0
  174. package/node_modules/@dharta/sdk/dist/errors.d.ts.map +1 -0
  175. package/node_modules/@dharta/sdk/dist/errors.js +51 -0
  176. package/node_modules/@dharta/sdk/dist/errors.js.map +1 -0
  177. package/node_modules/@dharta/sdk/dist/http.d.ts +26 -0
  178. package/node_modules/@dharta/sdk/dist/http.d.ts.map +1 -0
  179. package/node_modules/@dharta/sdk/dist/http.js +136 -0
  180. package/node_modules/@dharta/sdk/dist/http.js.map +1 -0
  181. package/node_modules/@dharta/sdk/dist/index.d.ts +15 -0
  182. package/node_modules/@dharta/sdk/dist/index.d.ts.map +1 -0
  183. package/node_modules/@dharta/sdk/dist/index.js +21 -0
  184. package/node_modules/@dharta/sdk/dist/index.js.map +1 -0
  185. package/node_modules/@dharta/sdk/dist/resources.d.ts +48 -0
  186. package/node_modules/@dharta/sdk/dist/resources.d.ts.map +1 -0
  187. package/node_modules/@dharta/sdk/dist/resources.js +150 -0
  188. package/node_modules/@dharta/sdk/dist/resources.js.map +1 -0
  189. package/node_modules/@dharta/sdk/dist/types.d.ts +94 -0
  190. package/node_modules/@dharta/sdk/dist/types.d.ts.map +1 -0
  191. package/node_modules/@dharta/sdk/dist/types.js +59 -0
  192. package/node_modules/@dharta/sdk/dist/types.js.map +1 -0
  193. package/node_modules/@dharta/sdk/package.json +32 -0
  194. package/node_modules/@dharta/sdk/src/errors.ts +58 -0
  195. package/node_modules/@dharta/sdk/src/http.ts +166 -0
  196. package/node_modules/@dharta/sdk/src/index.ts +36 -0
  197. package/node_modules/@dharta/sdk/src/resources.ts +197 -0
  198. package/node_modules/@dharta/sdk/src/types.ts +179 -0
  199. package/package.json +38 -0
  200. package/src/agent.ts +147 -0
  201. package/src/api-error.ts +88 -0
  202. package/src/browser.ts +20 -0
  203. package/src/cli.ts +2339 -0
  204. package/src/commands/agents.ts +635 -0
  205. package/src/commands/analytics.ts +181 -0
  206. package/src/commands/api.ts +297 -0
  207. package/src/commands/appearance.ts +79 -0
  208. package/src/commands/artifact.ts +331 -0
  209. package/src/commands/audit-log.ts +125 -0
  210. package/src/commands/auth.ts +186 -0
  211. package/src/commands/caps.ts +302 -0
  212. package/src/commands/create.ts +114 -0
  213. package/src/commands/deploy.ts +1122 -0
  214. package/src/commands/dev.ts +792 -0
  215. package/src/commands/directories.ts +136 -0
  216. package/src/commands/doctor.ts +138 -0
  217. package/src/commands/embed-keys.ts +237 -0
  218. package/src/commands/env.ts +369 -0
  219. package/src/commands/files.ts +124 -0
  220. package/src/commands/git-credential.ts +78 -0
  221. package/src/commands/kartas.ts +439 -0
  222. package/src/commands/keys.ts +206 -0
  223. package/src/commands/login.ts +115 -0
  224. package/src/commands/logs.ts +111 -0
  225. package/src/commands/members.ts +109 -0
  226. package/src/commands/model-keys.ts +278 -0
  227. package/src/commands/open.ts +173 -0
  228. package/src/commands/parity.ts +96 -0
  229. package/src/commands/rollback.ts +45 -0
  230. package/src/commands/schedules.ts +290 -0
  231. package/src/commands/scopes.ts +22 -0
  232. package/src/commands/sessions.ts +225 -0
  233. package/src/commands/setup.ts +1176 -0
  234. package/src/commands/status.ts +94 -0
  235. package/src/commands/update.ts +157 -0
  236. package/src/commands/usage.ts +41 -0
  237. package/src/commands/webhooks.ts +200 -0
  238. package/src/commands/whoami.ts +24 -0
  239. package/src/completions.ts +168 -0
  240. package/src/config.ts +143 -0
  241. package/src/context.ts +59 -0
  242. package/src/device-flow.ts +174 -0
  243. package/src/harness-preflight.ts +293 -0
  244. package/src/harnesses.ts +117 -0
  245. package/src/index.ts +3 -0
  246. package/src/login-offer.ts +49 -0
  247. package/src/manifest.ts +153 -0
  248. package/src/money.ts +15 -0
  249. package/src/output.ts +22 -0
  250. package/src/runtime.ts +127 -0
  251. package/src/scaffold.ts +102 -0
  252. package/src/setup-scan.ts +154 -0
  253. package/src/templates.ts +85 -0
  254. package/src/validators.ts +124 -0
  255. package/src/version-check.ts +75 -0
  256. package/templates/blank/CLAUDE.md +21 -0
  257. package/templates/blank/dharta.toml +11 -0
  258. package/templates/faq-agent/CLAUDE.md +51 -0
  259. package/templates/faq-agent/dharta.toml +11 -0
@@ -0,0 +1,181 @@
1
+ import type { CliContext } from "../context.js";
2
+ import { table } from "../output.js";
3
+ import { closestChoice, normalizeDateOnly, parsePositiveInteger } from "../validators.js";
4
+
5
+ export type AnalyticsView = "agents" | "users" | "instances" | "sessions" | "models" | "cost";
6
+
7
+ export const ENTITY_ANALYTICS_RANGES = ["7d", "30d", "90d"] as const;
8
+ export const COST_ANALYTICS_RANGES = ["mtd", "7d", "30d", "90d", "last_month", "custom"] as const;
9
+ export const COST_ANALYTICS_GROUPS = ["agent", "karta", "session", "user", "model"] as const;
10
+
11
+ export interface AnalyticsOptions {
12
+ range?: string;
13
+ groupBy?: string;
14
+ from?: string;
15
+ to?: string;
16
+ limit?: string;
17
+ }
18
+
19
+ interface AnalyticsResponse {
20
+ view: string;
21
+ range: string;
22
+ from: string;
23
+ to: string;
24
+ summary?: Record<string, unknown>;
25
+ totals?: Record<string, unknown>;
26
+ data: Array<Record<string, unknown>>;
27
+ }
28
+
29
+ export async function analyticsData(ctx: CliContext, view: AnalyticsView, opts: AnalyticsOptions = {}): Promise<AnalyticsResponse> {
30
+ const normalized = normalizeAnalyticsOptions(view, opts);
31
+ const params = new URLSearchParams();
32
+ if (normalized.range) params.set("range", normalized.range);
33
+ if (normalized.groupBy) params.set("group_by", normalized.groupBy);
34
+ if (normalized.from) params.set("from", normalized.from);
35
+ if (normalized.to) params.set("to", normalized.to);
36
+ if (normalized.limit) params.set("limit", normalized.limit);
37
+ const qs = params.toString();
38
+ return ctx.http.request<AnalyticsResponse>({ path: `/api/analytics/${view}${qs ? `?${qs}` : ""}` });
39
+ }
40
+
41
+ export async function analytics(ctx: CliContext, view: AnalyticsView, opts: AnalyticsOptions = {}): Promise<string> {
42
+ const res = await analyticsData(ctx, view, opts);
43
+ const lines = [`${title(view)} (${res.from} to ${res.to})`];
44
+ const summary = res.totals ?? res.summary;
45
+ if (summary) {
46
+ for (const [key, value] of Object.entries(summary)) {
47
+ lines.push(`${formatSummaryKey(key)}: ${formatSummaryValue(key, value)}`);
48
+ }
49
+ }
50
+ if (res.data.length === 0) return lines.join("\n");
51
+ lines.push("");
52
+ lines.push(table(res.data.map((row) => compactRow(view, row))));
53
+ return lines.join("\n");
54
+ }
55
+
56
+ export function normalizeAnalyticsOptions(view: AnalyticsView, opts: AnalyticsOptions): AnalyticsOptions {
57
+ const normalized: AnalyticsOptions = { ...opts };
58
+ if (normalized.range !== undefined) normalized.range = normalizeRequiredFilter(normalized.range, "--range");
59
+ if (normalized.groupBy !== undefined) normalized.groupBy = normalizeRequiredFilter(normalized.groupBy, "--group-by");
60
+ if (normalized.from !== undefined) normalized.from = normalizeDateOnly(normalized.from, "--from");
61
+ if (normalized.to !== undefined) normalized.to = normalizeDateOnly(normalized.to, "--to");
62
+ if (normalized.limit !== undefined) normalized.limit = normalizeRequiredFilter(normalized.limit, "--limit");
63
+ if (view === "cost" && (normalized.from !== undefined || normalized.to !== undefined)) {
64
+ if (normalized.range === undefined) {
65
+ normalized.range = "custom";
66
+ } else if (normalized.range !== "custom") {
67
+ throw new Error("--from/--to require --range custom.");
68
+ }
69
+ }
70
+
71
+ if (normalized.range) {
72
+ const allowed = view === "cost" ? COST_ANALYTICS_RANGES : ENTITY_ANALYTICS_RANGES;
73
+ if (!(allowed as readonly string[]).includes(normalized.range)) {
74
+ const suggestion = closestChoice(normalized.range, allowed);
75
+ throw new Error(
76
+ `--range must be one of: ${allowed.join(", ")}.` +
77
+ (suggestion ? ` Did you mean ${suggestion}?` : ""),
78
+ );
79
+ }
80
+ }
81
+ if (normalized.groupBy !== undefined) {
82
+ if (view !== "cost") throw new Error("--group-by is only supported for analytics cost.");
83
+ if (!(COST_ANALYTICS_GROUPS as readonly string[]).includes(normalized.groupBy)) {
84
+ const suggestion = closestChoice(normalized.groupBy, COST_ANALYTICS_GROUPS);
85
+ throw new Error(
86
+ `--group-by must be one of: ${COST_ANALYTICS_GROUPS.join(", ")}.` +
87
+ (suggestion ? ` Did you mean ${suggestion}?` : ""),
88
+ );
89
+ }
90
+ }
91
+ if (normalized.limit !== undefined) {
92
+ parsePositiveInteger(normalized.limit, "--limit");
93
+ }
94
+ return normalized;
95
+ }
96
+
97
+ function normalizeRequiredFilter(value: string, flag: string): string {
98
+ const normalized = value.trim();
99
+ if (!normalized) throw new Error(`${flag} cannot be blank.`);
100
+ return normalized;
101
+ }
102
+
103
+ function title(view: AnalyticsView): string {
104
+ if (view === "cost") return "Analytics cost";
105
+ return `Analytics ${view}`;
106
+ }
107
+
108
+ function formatSummaryKey(key: string): string {
109
+ if (key.endsWith("_micros")) return key.slice(0, -"_micros".length);
110
+ if (key.endsWith("_pct")) return key.slice(0, -"_pct".length);
111
+ return key;
112
+ }
113
+
114
+ function formatSummaryValue(key: string, value: unknown): string {
115
+ if (key.endsWith("_micros")) return formatMicros(value);
116
+ if (key.endsWith("_pct")) return formatPercent(value);
117
+ return String(value);
118
+ }
119
+
120
+ function compactRow(view: AnalyticsView, row: Record<string, unknown>): Record<string, string> {
121
+ if (view === "cost") {
122
+ return {
123
+ label: String(row.label ?? row.id ?? ""),
124
+ tokens: String(row.tokens ?? 0),
125
+ total: formatMicros(row.total_micros),
126
+ share: formatPercent(row.share_pct),
127
+ };
128
+ }
129
+ if (view === "agents") {
130
+ return {
131
+ slug: String(row.slug ?? ""),
132
+ tokens: String(row.tokens ?? 0),
133
+ sessions: String(row.sessions_count ?? 0),
134
+ spend: formatMicros(row.spend_micros),
135
+ };
136
+ }
137
+ if (view === "sessions") {
138
+ return {
139
+ id: String(row.id ?? ""),
140
+ agent: String(row.agent_slug ?? ""),
141
+ turns: String(row.turns ?? 0),
142
+ tokens: String(row.tokens ?? row.input_tokens ?? 0),
143
+ spend: formatMicros(row.spend_micros ?? row.cost_micros),
144
+ };
145
+ }
146
+ if (view === "models") {
147
+ return {
148
+ model: String(row.id ?? row.model ?? ""),
149
+ tokens: String(row.tokens ?? 0),
150
+ sessions: String(row.sessions_count ?? 0),
151
+ spend: formatMicros(row.spend_micros ?? row.cost_micros),
152
+ };
153
+ }
154
+ if (view === "instances") {
155
+ return {
156
+ id: String(row.id ?? ""),
157
+ agent: String(row.agent_slug ?? ""),
158
+ tokens: String(row.tokens ?? row.input_tokens ?? 0),
159
+ sessions: String(row.sessions_count ?? 0),
160
+ spend: formatMicros(row.spend_micros ?? row.cost_micros),
161
+ };
162
+ }
163
+ return {
164
+ id: String(row.id ?? ""),
165
+ tokens: String(row.tokens ?? row.input_tokens ?? 0),
166
+ sessions: String(row.sessions_count ?? 0),
167
+ spend: formatMicros(row.spend_micros ?? row.cost_micros),
168
+ };
169
+ }
170
+
171
+ function formatMicros(value: unknown): string {
172
+ const micros = Number(value ?? 0);
173
+ if (!Number.isFinite(micros)) return "$0.0000";
174
+ return `$${(micros / 100_000_000).toFixed(4)}`;
175
+ }
176
+
177
+ function formatPercent(value: unknown): string {
178
+ const percent = Number(value ?? 0);
179
+ if (!Number.isFinite(percent)) return "0%";
180
+ return `${percent}%`;
181
+ }
@@ -0,0 +1,297 @@
1
+ import type { CliContext } from "../context.js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { cliFormatError } from "../api-error.js";
4
+ import { closestChoice } from "../validators.js";
5
+
6
+ const METHOD_NAMES = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const;
7
+ const METHODS = new Set<string>(METHOD_NAMES);
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
+ ];
75
+
76
+ export interface ApiOptions {
77
+ field?: string[];
78
+ rawField?: string[];
79
+ header?: string[];
80
+ bodyStdin?: boolean;
81
+ body?: string;
82
+ }
83
+
84
+ export async function api(ctx: CliContext, methodOrPath: string | undefined, pathArg: string | undefined, opts: ApiOptions) {
85
+ const { method, path } = validateApiRequest(methodOrPath, pathArg, opts);
86
+
87
+ const headers = parseHeaders(opts.header ?? []);
88
+ const body = await buildBody(opts);
89
+ if (body !== undefined && !hasHeader(headers, "content-type")) {
90
+ headers["Content-Type"] = "application/json";
91
+ }
92
+ if (method !== "GET" && !hasHeader(headers, "idempotency-key")) {
93
+ headers["Idempotency-Key"] = randomUUID();
94
+ }
95
+ const res = await ctx.http.raw(path, { method, headers, body });
96
+ const text = await res.text();
97
+ const parsed = parseMaybeJson(text);
98
+
99
+ if (!res.ok) {
100
+ throw cliFormatError(res.status, parsed);
101
+ }
102
+
103
+ return {
104
+ status: res.status,
105
+ body: parsed,
106
+ text,
107
+ };
108
+ }
109
+
110
+ export function validateApiRequest(methodOrPath: string | undefined, pathArg: string | undefined, opts: ApiOptions) {
111
+ const { method, path } = normalizeTarget(methodOrPath, pathArg, hasFields(opts));
112
+ 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.");
114
+ }
115
+ assertAllowlisted(method, path);
116
+ parseHeaders(opts.header ?? []);
117
+ validateBodyFields(opts);
118
+ return { method, path };
119
+ }
120
+
121
+ export async function prepareApiOptions(opts: ApiOptions, read: () => Promise<string> = readStdin): Promise<ApiOptions> {
122
+ parseHeaders(opts.header ?? []);
123
+ validateBodyFields(opts);
124
+ if (!opts.bodyStdin) return opts;
125
+ const body = await read();
126
+ if (body.length === 0) throw new Error("--body-stdin was set, but stdin was empty.");
127
+ return { ...opts, body, bodyStdin: false };
128
+ }
129
+
130
+ export function formatApiResult(result: Awaited<ReturnType<typeof api>>): string {
131
+ if (result.status === 204 || result.text.length === 0) return "";
132
+ return typeof result.body === "string" ? result.body : JSON.stringify(result.body, null, 2);
133
+ }
134
+
135
+ function normalizeTarget(methodOrPath: string | undefined, pathArg: string | undefined, hasBodyFields: boolean) {
136
+ const first = methodOrPath?.trim();
137
+ const second = pathArg?.trim();
138
+ if (!first) {
139
+ throw new Error("Usage: dharta api [METHOD] /path");
140
+ }
141
+
142
+ let method: string;
143
+ let path: string;
144
+ const maybeMethod = first.toUpperCase();
145
+ if (METHODS.has(maybeMethod) && !second) {
146
+ throw new Error("Usage: dharta api [METHOD] /path");
147
+ }
148
+ if (METHODS.has(maybeMethod)) {
149
+ method = maybeMethod;
150
+ path = second as string;
151
+ } else if (second) {
152
+ const suggestion = closestChoice(maybeMethod, METHOD_NAMES);
153
+ throw new Error(
154
+ `Unknown API method ${maybeMethod}. Allowed methods: ${METHOD_NAMES.join(", ")}.` +
155
+ (suggestion ? ` Did you mean ${suggestion}?` : ""),
156
+ );
157
+ } else {
158
+ method = hasBodyFields ? "POST" : "GET";
159
+ path = first;
160
+ }
161
+
162
+ if (!path.startsWith("/")) {
163
+ throw new Error("API path must start with '/', e.g. /me or /api/agents/support-bot");
164
+ }
165
+ 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.");
167
+ }
168
+ return { method, path };
169
+ }
170
+
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;
174
+
175
+ 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." +
179
+ (suggestion ? ` Did you mean ${method} ${suggestion}?` : ""),
180
+ );
181
+ }
182
+
183
+ function apiRouteSuggestion(path: string): string | null {
184
+ const replacements: Array<[RegExp, string]> = [
185
+ [/^\/analytics(?=\/|[?#]|$)/, "/api/analytics"],
186
+ [/^\/billing(?=\/|[?#]|$)/, "/api/billing"],
187
+ [/^\/limits(?=\/|[?#]|$)/, "/api/limits"],
188
+ [/^\/members(?=\/|[?#]|$)/, "/api/members"],
189
+ [/^\/usage(?=\/|[?#]|$)/, "/api/usage"],
190
+ [/^\/webhook_endpoints(?=\/|[?#]|$)/, "/api/webhook_endpoints"],
191
+ ];
192
+ for (const [pattern, replacement] of replacements) {
193
+ if (pattern.test(path)) return path.replace(pattern, replacement);
194
+ }
195
+ return null;
196
+ }
197
+
198
+ function hasFields(opts: ApiOptions): boolean {
199
+ return (opts.field?.length ?? 0) > 0 || (opts.rawField?.length ?? 0) > 0 || !!opts.bodyStdin || opts.body !== undefined;
200
+ }
201
+
202
+ function hasStructuredFields(opts: ApiOptions): boolean {
203
+ return (opts.field?.length ?? 0) > 0 || (opts.rawField?.length ?? 0) > 0;
204
+ }
205
+
206
+ function validateBodyFields(opts: ApiOptions): void {
207
+ if (opts.bodyStdin && hasStructuredFields(opts)) {
208
+ throw new Error("Use either --body-stdin or --field/--raw-field, not both.");
209
+ }
210
+ for (const raw of opts.field ?? []) splitAssignment(raw, "--field");
211
+ for (const raw of opts.rawField ?? []) splitAssignment(raw, "--raw-field");
212
+ }
213
+
214
+ async function buildBody(opts: ApiOptions): Promise<string | undefined> {
215
+ if (opts.body !== undefined) return opts.body;
216
+ const fields: Record<string, unknown> = {};
217
+ for (const raw of opts.field ?? []) {
218
+ const [key, value] = splitAssignment(raw, "--field");
219
+ fields[key] = coerceField(value);
220
+ }
221
+ for (const raw of opts.rawField ?? []) {
222
+ const [key, value] = splitAssignment(raw, "--raw-field");
223
+ fields[key] = value;
224
+ }
225
+
226
+ if (Object.keys(fields).length > 0) return JSON.stringify(fields);
227
+ if (opts.bodyStdin) return readStdin();
228
+ return undefined;
229
+ }
230
+
231
+ function parseHeaders(input: string[]): Record<string, string> {
232
+ const headers: Record<string, string> = {};
233
+ for (const raw of input) {
234
+ const idx = raw.indexOf(":");
235
+ if (idx <= 0) throw new Error(`Invalid --header '${raw}'. Use 'Name: value'.`);
236
+ const key = raw.slice(0, idx).trim();
237
+ const value = raw.slice(idx + 1).trim();
238
+ if (!key || !value) throw new Error(`Invalid --header '${raw}'. Use 'Name: value'.`);
239
+ const lower = key.toLowerCase();
240
+ 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.`);
242
+ }
243
+ headers[key] = value;
244
+ }
245
+ if (Object.keys(headers).length > 0) return headers;
246
+ return {};
247
+ }
248
+
249
+ function hasHeader(headers: Record<string, string>, name: string): boolean {
250
+ const normalized = name.toLowerCase();
251
+ return Object.keys(headers).some((key) => key.toLowerCase() === normalized);
252
+ }
253
+
254
+ function splitAssignment(raw: string, flag: string): [string, string] {
255
+ const idx = raw.indexOf("=");
256
+ if (idx <= 0) throw new Error(`Invalid ${flag} '${raw}'. Use key=value.`);
257
+ const key = raw.slice(0, idx).trim();
258
+ if (!key) throw new Error(`Invalid ${flag} '${raw}'. Use key=value.`);
259
+ return [key, raw.slice(idx + 1)];
260
+ }
261
+
262
+ function coerceField(value: string): unknown {
263
+ if (value === "true") return true;
264
+ if (value === "false") return false;
265
+ if (value === "null") return null;
266
+ if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
267
+ if (/^[\[{]/.test(value)) {
268
+ try {
269
+ return JSON.parse(value);
270
+ } catch {
271
+ return value;
272
+ }
273
+ }
274
+ return value;
275
+ }
276
+
277
+ function parseMaybeJson(text: string): unknown {
278
+ if (!text) return null;
279
+ try {
280
+ return JSON.parse(text);
281
+ } catch {
282
+ return text;
283
+ }
284
+ }
285
+
286
+ async function readStdin(): Promise<string> {
287
+ if (process.stdin.isTTY) {
288
+ throw new Error(
289
+ "No piped input. Usage: echo '{\"name\":\"Example\"}' | dharta api POST /api/agents/support-bot/embed_keys --body-stdin",
290
+ );
291
+ }
292
+ const chunks: Buffer[] = [];
293
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
294
+ const text = Buffer.concat(chunks).toString("utf8");
295
+ if (text.length === 0) throw new Error("--body-stdin was set, but stdin was empty.");
296
+ return text;
297
+ }
@@ -0,0 +1,79 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { inferAgentSlug, type AgentInferOptions } from "../agent.js";
4
+ import type { CliContext } from "../context.js";
5
+ import { normalizeNonBlank } from "../validators.js";
6
+
7
+ export interface AppearanceEnvelope {
8
+ agent: {
9
+ id: string;
10
+ slug: string;
11
+ name: string;
12
+ };
13
+ appearance: Record<string, unknown>;
14
+ resolved_appearance: Record<string, unknown>;
15
+ }
16
+
17
+ export interface AppearanceOptions extends AgentInferOptions {
18
+ file?: string;
19
+ }
20
+
21
+ export async function appearanceGetData(ctx: CliContext, opts: AgentInferOptions): Promise<AppearanceEnvelope> {
22
+ const slug = await inferAgentSlug(opts);
23
+ return ctx.http.request<AppearanceEnvelope>({
24
+ path: `/api/agents/${encodeURIComponent(slug)}/appearance`,
25
+ });
26
+ }
27
+
28
+ export async function appearanceGet(ctx: CliContext, opts: AgentInferOptions): Promise<string> {
29
+ const res = await appearanceGetData(ctx, opts);
30
+ return JSON.stringify(res.appearance, null, 2);
31
+ }
32
+
33
+ export async function appearanceSetData(ctx: CliContext, opts: AppearanceOptions): Promise<AppearanceEnvelope> {
34
+ await validateAppearanceSetInput(opts);
35
+ if (!opts.file) throw new Error("Pass --file <file>. Usage: dharta appearance set --file appearance.json");
36
+ const slug = await inferAgentSlug(opts);
37
+ const appearance = await readAppearanceFile(opts.file);
38
+ return ctx.http.request<AppearanceEnvelope>({
39
+ method: "PATCH",
40
+ path: `/api/agents/${encodeURIComponent(slug)}/appearance`,
41
+ headers: { "Idempotency-Key": randomUUID() },
42
+ body: { appearance },
43
+ });
44
+ }
45
+
46
+ export async function appearanceSet(ctx: CliContext, opts: AppearanceOptions): Promise<string> {
47
+ const res = await appearanceSetData(ctx, opts);
48
+ return `Saved appearance for ${res.agent.slug}.`;
49
+ }
50
+
51
+ 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");
53
+ await readAppearanceFile(opts.file);
54
+ }
55
+
56
+ async function readAppearanceFile(file: string): Promise<Record<string, unknown>> {
57
+ const normalized = normalizeNonBlank(file, "--file");
58
+ let text: string;
59
+ try {
60
+ text = await readFile(normalized, "utf8");
61
+ } catch (error) {
62
+ const code = (error as NodeJS.ErrnoException).code;
63
+ if (code === "ENOENT") throw new Error(`Cannot read --file ${normalized}: file does not exist.`);
64
+ if (code === "EISDIR") throw new Error(`Cannot read --file ${normalized}: expected a file, got a directory.`);
65
+ throw new Error(`Cannot read --file ${normalized}: ${(error as Error).message}`);
66
+ }
67
+
68
+ let parsed: unknown;
69
+ try {
70
+ parsed = JSON.parse(text);
71
+ } catch {
72
+ throw new Error(`--file ${normalized} must contain valid JSON.`);
73
+ }
74
+
75
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
76
+ throw new Error(`--file ${normalized} must contain a JSON object.`);
77
+ }
78
+ return parsed as Record<string, unknown>;
79
+ }