deepline 0.1.0 → 0.1.1

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 (97) hide show
  1. package/dist/cli/index.js +212 -54
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/cli/index.mjs +198 -40
  4. package/dist/cli/index.mjs.map +1 -1
  5. package/dist/index.d.mts +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.mjs +1 -1
  9. package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +3256 -0
  10. package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +710 -0
  11. package/dist/repo/apps/play-runner-workers/src/entry.ts +5070 -0
  12. package/dist/repo/apps/play-runner-workers/src/runtime/README.md +21 -0
  13. package/dist/repo/apps/play-runner-workers/src/runtime/batching.ts +177 -0
  14. package/dist/repo/apps/play-runner-workers/src/runtime/execution-plan.ts +52 -0
  15. package/dist/repo/apps/play-runner-workers/src/runtime/tool-batch.ts +100 -0
  16. package/dist/repo/apps/play-runner-workers/src/runtime/tool-result.ts +184 -0
  17. package/dist/repo/sdk/src/cli/commands/auth.ts +482 -0
  18. package/dist/repo/sdk/src/cli/commands/billing.ts +188 -0
  19. package/dist/repo/sdk/src/cli/commands/csv.ts +123 -0
  20. package/dist/repo/sdk/src/cli/commands/db.ts +119 -0
  21. package/dist/repo/sdk/src/cli/commands/feedback.ts +40 -0
  22. package/dist/repo/sdk/src/cli/commands/org.ts +117 -0
  23. package/dist/repo/sdk/src/cli/commands/play.ts +3200 -0
  24. package/dist/repo/sdk/src/cli/commands/tools.ts +687 -0
  25. package/dist/repo/sdk/src/cli/dataset-stats.ts +341 -0
  26. package/dist/repo/sdk/src/cli/index.ts +138 -0
  27. package/dist/repo/sdk/src/cli/progress.ts +135 -0
  28. package/dist/repo/sdk/src/cli/trace.ts +61 -0
  29. package/dist/repo/sdk/src/cli/utils.ts +145 -0
  30. package/dist/repo/sdk/src/client.ts +1188 -0
  31. package/dist/repo/sdk/src/compat.ts +77 -0
  32. package/dist/repo/sdk/src/config.ts +285 -0
  33. package/dist/repo/sdk/src/errors.ts +125 -0
  34. package/dist/repo/sdk/src/http.ts +391 -0
  35. package/dist/repo/sdk/src/index.ts +139 -0
  36. package/dist/repo/sdk/src/play.ts +1330 -0
  37. package/dist/repo/sdk/src/plays/bundle-play-file.ts +133 -0
  38. package/dist/repo/sdk/src/plays/harness-stub.ts +210 -0
  39. package/dist/repo/sdk/src/plays/local-file-discovery.ts +326 -0
  40. package/dist/repo/sdk/src/tool-output.ts +489 -0
  41. package/dist/repo/sdk/src/types.ts +669 -0
  42. package/dist/repo/sdk/src/version.ts +2 -0
  43. package/dist/repo/sdk/src/worker-play-entry.ts +286 -0
  44. package/dist/repo/shared_libs/observability/node-tracing.ts +129 -0
  45. package/dist/repo/shared_libs/observability/tracing.ts +98 -0
  46. package/dist/repo/shared_libs/play-runtime/backend.ts +139 -0
  47. package/dist/repo/shared_libs/play-runtime/batch-runtime.ts +182 -0
  48. package/dist/repo/shared_libs/play-runtime/batching-types.ts +91 -0
  49. package/dist/repo/shared_libs/play-runtime/context.ts +3999 -0
  50. package/dist/repo/shared_libs/play-runtime/coordinator-headers.ts +78 -0
  51. package/dist/repo/shared_libs/play-runtime/ctx-contract.ts +250 -0
  52. package/dist/repo/shared_libs/play-runtime/ctx-types.ts +713 -0
  53. package/dist/repo/shared_libs/play-runtime/dataset-id.ts +10 -0
  54. package/dist/repo/shared_libs/play-runtime/db-session-crypto.ts +304 -0
  55. package/dist/repo/shared_libs/play-runtime/db-session.ts +462 -0
  56. package/dist/repo/shared_libs/play-runtime/dedup-backend.ts +0 -0
  57. package/dist/repo/shared_libs/play-runtime/default-batch-strategies.ts +124 -0
  58. package/dist/repo/shared_libs/play-runtime/execution-plan.ts +262 -0
  59. package/dist/repo/shared_libs/play-runtime/live-events.ts +214 -0
  60. package/dist/repo/shared_libs/play-runtime/live-state-contract.ts +50 -0
  61. package/dist/repo/shared_libs/play-runtime/map-execution-frame.ts +114 -0
  62. package/dist/repo/shared_libs/play-runtime/map-row-identity.ts +158 -0
  63. package/dist/repo/shared_libs/play-runtime/profiles.ts +90 -0
  64. package/dist/repo/shared_libs/play-runtime/progress-emitter.ts +172 -0
  65. package/dist/repo/shared_libs/play-runtime/protocol.ts +121 -0
  66. package/dist/repo/shared_libs/play-runtime/public-play-contract.ts +42 -0
  67. package/dist/repo/shared_libs/play-runtime/result-normalization.ts +33 -0
  68. package/dist/repo/shared_libs/play-runtime/runtime-actions.ts +208 -0
  69. package/dist/repo/shared_libs/play-runtime/runtime-api.ts +1873 -0
  70. package/dist/repo/shared_libs/play-runtime/runtime-constraints.ts +2 -0
  71. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-neon-serverless.ts +201 -0
  72. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-pg.ts +48 -0
  73. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver.ts +84 -0
  74. package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +174 -0
  75. package/dist/repo/shared_libs/play-runtime/static-pipeline-types.ts +147 -0
  76. package/dist/repo/shared_libs/play-runtime/suspension.ts +68 -0
  77. package/dist/repo/shared_libs/play-runtime/tool-batch-executor.ts +146 -0
  78. package/dist/repo/shared_libs/play-runtime/tool-result.ts +387 -0
  79. package/dist/repo/shared_libs/play-runtime/tracing.ts +31 -0
  80. package/dist/repo/shared_libs/play-runtime/waterfall-replay.ts +75 -0
  81. package/dist/repo/shared_libs/play-runtime/worker-api-types.ts +140 -0
  82. package/dist/repo/shared_libs/plays/artifact-transport.ts +14 -0
  83. package/dist/repo/shared_libs/plays/artifact-types.ts +49 -0
  84. package/dist/repo/shared_libs/plays/bundling/index.ts +1346 -0
  85. package/dist/repo/shared_libs/plays/compiler-manifest.ts +186 -0
  86. package/dist/repo/shared_libs/plays/contracts.ts +51 -0
  87. package/dist/repo/shared_libs/plays/dataset.ts +308 -0
  88. package/dist/repo/shared_libs/plays/definition.ts +264 -0
  89. package/dist/repo/shared_libs/plays/file-refs.ts +11 -0
  90. package/dist/repo/shared_libs/plays/rate-limit-scheduler.ts +206 -0
  91. package/dist/repo/shared_libs/plays/resolve-static-pipeline.ts +164 -0
  92. package/dist/repo/shared_libs/plays/row-identity.ts +302 -0
  93. package/dist/repo/shared_libs/plays/runtime-validation.ts +415 -0
  94. package/dist/repo/shared_libs/plays/static-pipeline.ts +560 -0
  95. package/dist/repo/shared_libs/temporal/constants.ts +39 -0
  96. package/dist/repo/shared_libs/temporal/preview-config.ts +153 -0
  97. package/package.json +4 -4
@@ -0,0 +1,482 @@
1
+ import { writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { hostname } from 'node:os';
3
+ import { dirname } from 'node:path';
4
+ import { Command } from 'commander';
5
+ import { hostEnvFilePath, loadCliEnv, autoDetectBaseUrl, parseEnvFile } from '../../config.js';
6
+ import { argsWantJson, openInBrowser } from '../utils.js';
7
+
8
+ const EXIT_OK = 0;
9
+ const EXIT_AUTH = 1;
10
+ const EXIT_SERVER = 2;
11
+
12
+ function envFilePath(baseUrl: string): string {
13
+ return hostEnvFilePath(baseUrl);
14
+ }
15
+
16
+ function saveEnvValues(values: Record<string, string>, baseUrl: string): void {
17
+ const filePath = envFilePath(baseUrl);
18
+ const dir = dirname(filePath);
19
+ if (!existsSync(dir)) {
20
+ mkdirSync(dir, { recursive: true });
21
+ }
22
+
23
+ // Merge with existing values
24
+ const existing = existsSync(filePath) ? parseEnvFile(filePath) : {};
25
+ const merged = { ...existing, ...values };
26
+
27
+ const lines = Object.entries(merged)
28
+ .filter(([, v]) => v !== '')
29
+ .map(([k, v]) => `${k}=${v}`);
30
+ writeFileSync(filePath, lines.join('\n') + '\n', 'utf-8');
31
+ }
32
+
33
+ async function httpJson(
34
+ method: string,
35
+ url: string,
36
+ apiKey: string | null,
37
+ body?: unknown,
38
+ ): Promise<{ status: number; data: Record<string, unknown> }> {
39
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
40
+ if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
41
+
42
+ let response: Response | null = null;
43
+ let lastError: unknown = null;
44
+ for (const candidateUrl of buildCandidateUrls(url)) {
45
+ try {
46
+ response = await fetch(candidateUrl, {
47
+ method,
48
+ headers,
49
+ body: body !== undefined ? JSON.stringify(body) : undefined,
50
+ });
51
+ break;
52
+ } catch (error) {
53
+ lastError = error;
54
+ }
55
+ }
56
+
57
+ if (!response) {
58
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
59
+ }
60
+
61
+ let data: Record<string, unknown>;
62
+ try {
63
+ data = (await response.json()) as Record<string, unknown>;
64
+ } catch {
65
+ data = {};
66
+ }
67
+ return { status: response.status, data };
68
+ }
69
+
70
+ function buildCandidateUrls(url: string): string[] {
71
+ try {
72
+ const parsed = new URL(url);
73
+ const candidates = [url];
74
+ const configuredInternalOrigin =
75
+ process.env.DEEPLINE_INTERNAL_APP_URL ||
76
+ process.env.WORKTREE_INTERNAL_APP_URL ||
77
+ (process.env.PORT ? `http://127.0.0.1:${process.env.PORT}` : '');
78
+ if (parsed.hostname === 'localhost') {
79
+ const loopback = new URL(url);
80
+ loopback.hostname = '127.0.0.1';
81
+ candidates.push(loopback.toString());
82
+ }
83
+ if (parsed.hostname.endsWith('.localhost') && configuredInternalOrigin) {
84
+ const internal = new URL(url);
85
+ const origin = new URL(configuredInternalOrigin);
86
+ internal.protocol = origin.protocol;
87
+ internal.hostname = origin.hostname;
88
+ internal.port = origin.port;
89
+ candidates.push(internal.toString());
90
+ }
91
+ return [...new Set(candidates)];
92
+ } catch {
93
+ return [url];
94
+ }
95
+ }
96
+
97
+ function sleep(ms: number): Promise<void> {
98
+ return new Promise((resolve) => setTimeout(resolve, ms));
99
+ }
100
+
101
+ export async function handleRegister(args: string[]): Promise<number> {
102
+ const baseUrl = autoDetectBaseUrl().replace(/\/$/, '');
103
+
104
+ let orgName = '';
105
+ let agentName = '';
106
+ let noWait = false;
107
+
108
+ for (let i = 0; i < args.length; i++) {
109
+ if (args[i] === '--org-name' && args[i + 1]) orgName = args[++i]!;
110
+ else if (args[i] === '--agent-name' && args[i + 1]) agentName = args[++i]!;
111
+ else if (args[i] === '--no-wait') noWait = true;
112
+ }
113
+
114
+ if (!agentName) {
115
+ try {
116
+ agentName = hostname() || 'Deepline CLI (TS)';
117
+ } catch {
118
+ agentName = 'Deepline CLI (TS)';
119
+ }
120
+ }
121
+
122
+ const payload: Record<string, string> = {};
123
+ if (orgName) payload.org_name = orgName;
124
+ if (agentName) payload.agent_name = agentName;
125
+
126
+ const { status, data } = await httpJson('POST', `${baseUrl}/api/v2/auth/cli/register`, null, payload);
127
+
128
+ if (status >= 400) {
129
+ console.error(`Auth register failed (status ${status}).`);
130
+ if (data.error) console.error(String(data.error));
131
+ return EXIT_SERVER;
132
+ }
133
+
134
+ const claimUrl = String(data.claim_url || '');
135
+ const claimToken = String(data.claim_token || '');
136
+
137
+ if (claimToken) {
138
+ saveEnvValues({
139
+ DEEPLINE_ORIGIN_URL: baseUrl,
140
+ DEEPLINE_CLAIM_TOKEN: claimToken,
141
+ }, baseUrl);
142
+ }
143
+
144
+ if (claimUrl) {
145
+ console.log(' Opening approval page in your browser.');
146
+ console.log(` If it didn't open, cmd+click: ${claimUrl}`);
147
+ openInBrowser(claimUrl);
148
+ }
149
+
150
+ if (data.cli_message) {
151
+ console.log(String(data.cli_message));
152
+ }
153
+
154
+ if (noWait) return EXIT_OK;
155
+
156
+ if (!claimToken) {
157
+ console.error('Missing claim token from register response.');
158
+ return EXIT_SERVER;
159
+ }
160
+
161
+ // Poll for claim approval
162
+ while (true) {
163
+ const { status: s, data: statusData } = await httpJson(
164
+ 'POST',
165
+ `${baseUrl}/api/v2/auth/cli/status`,
166
+ null,
167
+ { claim_token: claimToken, reveal: true },
168
+ );
169
+
170
+ if (s === 401 || s === 403) {
171
+ console.log('Status: unauthorized');
172
+ return EXIT_AUTH;
173
+ }
174
+ if (s >= 500 || s === 0 || s === 400) {
175
+ await sleep(2000);
176
+ continue;
177
+ }
178
+ if (s >= 400) {
179
+ console.error(`Auth status error (status ${s}).`);
180
+ return EXIT_SERVER;
181
+ }
182
+
183
+ const state = String(statusData.status || '').toLowerCase();
184
+ if (state === 'claimed') {
185
+ const apiKey = String(statusData.api_key || '');
186
+ if (apiKey) {
187
+ saveEnvValues({
188
+ DEEPLINE_ORIGIN_URL: baseUrl,
189
+ DEEPLINE_API_KEY: apiKey,
190
+ DEEPLINE_CLAIM_TOKEN: '',
191
+ }, baseUrl);
192
+ console.log('');
193
+ console.log('DEEPLINE');
194
+ console.log('All set! Your CLI is connected.');
195
+ if (statusData.org_name) {
196
+ console.log(` Signed in with organization: ${statusData.org_name}`);
197
+ }
198
+ return EXIT_OK;
199
+ }
200
+ }
201
+ if (state === 'expired') {
202
+ console.log('That approval link expired. Please run: deepline auth register');
203
+ return EXIT_AUTH;
204
+ }
205
+ await sleep(2000);
206
+ }
207
+ }
208
+
209
+ export async function handleWait(args: string[]): Promise<number> {
210
+ const baseUrl = autoDetectBaseUrl().replace(/\/$/, '');
211
+ let timeoutSeconds = 300;
212
+
213
+ for (let i = 0; i < args.length; i++) {
214
+ if (args[i] === '--timeout' && args[i + 1]) {
215
+ const parsed = Number.parseInt(args[++i]!, 10);
216
+ if (Number.isFinite(parsed) && parsed > 0) {
217
+ timeoutSeconds = parsed;
218
+ }
219
+ }
220
+ }
221
+
222
+ const env = loadCliEnv(baseUrl);
223
+ if (env.DEEPLINE_API_KEY?.trim()) {
224
+ console.log('Already connected.');
225
+ return EXIT_OK;
226
+ }
227
+
228
+ const claimToken = env.DEEPLINE_CLAIM_TOKEN?.trim() || '';
229
+ if (!claimToken) {
230
+ console.error('No pending approval. Run: deepline auth register --no-wait');
231
+ return EXIT_AUTH;
232
+ }
233
+
234
+ const deadline = Date.now() + timeoutSeconds * 1000;
235
+ while (Date.now() <= deadline) {
236
+ const { status, data } = await httpJson(
237
+ 'POST',
238
+ `${baseUrl}/api/v2/auth/cli/status`,
239
+ null,
240
+ { claim_token: claimToken, reveal: true },
241
+ );
242
+
243
+ if (status === 401 || status === 403) {
244
+ console.error('Claim is invalid. Run: deepline auth register');
245
+ return EXIT_AUTH;
246
+ }
247
+ if (status >= 500 || status === 0 || status === 400) {
248
+ await sleep(2000);
249
+ continue;
250
+ }
251
+ if (status >= 400) {
252
+ console.error(`Auth status error (status ${status}).`);
253
+ return EXIT_SERVER;
254
+ }
255
+
256
+ const state = String(data.status || '').toLowerCase();
257
+ if (state === 'claimed') {
258
+ const apiKey = String(data.api_key || '');
259
+ if (apiKey) {
260
+ saveEnvValues({
261
+ DEEPLINE_ORIGIN_URL: baseUrl,
262
+ DEEPLINE_API_KEY: apiKey,
263
+ DEEPLINE_CLAIM_TOKEN: '',
264
+ }, baseUrl);
265
+ console.log('All set! Your CLI is connected.');
266
+ return EXIT_OK;
267
+ }
268
+ }
269
+ if (state === 'expired') {
270
+ console.error('That approval link expired. Run: deepline auth register');
271
+ return EXIT_AUTH;
272
+ }
273
+
274
+ await sleep(2000);
275
+ }
276
+
277
+ console.error('Still pending. Approve the browser link, then run: deepline auth wait');
278
+ return EXIT_AUTH;
279
+ }
280
+
281
+ export async function handleStatus(args: string[]): Promise<number> {
282
+ const baseUrl = autoDetectBaseUrl().replace(/\/$/, '');
283
+ const reveal = args.includes('--reveal');
284
+ const jsonOutput = argsWantJson(args);
285
+ let hostStatusPayload: Record<string, unknown> | null = null;
286
+
287
+ // Health check
288
+ try {
289
+ const { status: hStatus, data: hData } = await httpJson('GET', `${baseUrl}/api/v2/health`, null);
290
+ if (hStatus === 200) {
291
+ hostStatusPayload = {
292
+ host: baseUrl,
293
+ hostStatus: hData.status || 'ok',
294
+ hostVersion: hData.version || '(unknown)',
295
+ };
296
+ if (!jsonOutput) {
297
+ console.log(`Host: ${baseUrl}`);
298
+ console.log(`Host status: ${hData.status || 'ok'}`);
299
+ console.log(`Host version: ${hData.version || '(unknown)'}`);
300
+ }
301
+ }
302
+ } catch {
303
+ hostStatusPayload = {
304
+ host: baseUrl,
305
+ hostStatus: 'unreachable',
306
+ hostVersion: null,
307
+ };
308
+ if (!jsonOutput) {
309
+ console.log(`Host: ${baseUrl} (unreachable)`);
310
+ }
311
+ }
312
+
313
+ const env = loadCliEnv(baseUrl);
314
+ const apiKey =
315
+ process.env.DEEPLINE_API_KEY?.trim() ||
316
+ env.DEEPLINE_API_KEY ||
317
+ '';
318
+
319
+ if (!apiKey) {
320
+ if (env.DEEPLINE_CLAIM_TOKEN?.trim()) {
321
+ if (jsonOutput) {
322
+ process.stdout.write(`${JSON.stringify({
323
+ ...(hostStatusPayload ?? { host: baseUrl }),
324
+ status: 'pending',
325
+ connected: false,
326
+ next: 'deepline auth wait',
327
+ })}\n`);
328
+ return EXIT_OK;
329
+ }
330
+ console.log('Status: pending');
331
+ console.log('Run: deepline auth wait');
332
+ return EXIT_OK;
333
+ }
334
+ if (jsonOutput) {
335
+ process.stdout.write(`${JSON.stringify({
336
+ ...(hostStatusPayload ?? { host: baseUrl }),
337
+ status: 'not connected',
338
+ connected: false,
339
+ next: 'deepline auth register',
340
+ })}\n`);
341
+ return EXIT_OK;
342
+ }
343
+ console.log('Status: not connected');
344
+ console.log('Run: deepline auth register');
345
+ return EXIT_OK;
346
+ }
347
+
348
+ const { status, data } = await httpJson('POST', `${baseUrl}/api/v2/auth/cli/status`, apiKey, {
349
+ api_key: apiKey,
350
+ reveal,
351
+ });
352
+
353
+ if (status === 401 || status === 403) {
354
+ if (jsonOutput) {
355
+ process.stdout.write(`${JSON.stringify({
356
+ ...(hostStatusPayload ?? { host: baseUrl }),
357
+ status: 'unauthorized',
358
+ connected: false,
359
+ next: 'deepline auth register',
360
+ })}\n`);
361
+ return EXIT_AUTH;
362
+ }
363
+ console.log('Status: unauthorized');
364
+ console.log('Run: deepline auth register');
365
+ return EXIT_AUTH;
366
+ }
367
+ if (status >= 400) {
368
+ console.error(`Auth status error (status ${status}).`);
369
+ return EXIT_SERVER;
370
+ }
371
+
372
+ const payload = {
373
+ ...(hostStatusPayload ?? { host: baseUrl }),
374
+ status: data.status || '(unknown)',
375
+ connected: true,
376
+ rateLimitTier: data.rate_limit_tier || '(unknown)',
377
+ workspace: {
378
+ id: data.org_id ?? null,
379
+ name: data.org_name ?? null,
380
+ slug: data.org_slug ?? null,
381
+ },
382
+ user: {
383
+ id: data.user_id ?? null,
384
+ },
385
+ examples: Array.isArray(data.examples) ? data.examples : [],
386
+ };
387
+
388
+ if (jsonOutput) {
389
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
390
+ } else {
391
+ console.log(`Status: ${payload.status}`);
392
+ console.log(`Rate limit tier: ${payload.rateLimitTier}`);
393
+ if (payload.workspace.name) console.log(`Workspace: ${payload.workspace.name}`);
394
+ if (payload.workspace.slug) console.log(`Workspace slug: ${payload.workspace.slug}`);
395
+ if (payload.workspace.id != null) console.log(`Org ID: ${payload.workspace.id}`);
396
+ if (payload.user.id != null) console.log(`User ID: ${payload.user.id}`);
397
+ if (payload.examples.length > 0) {
398
+ console.log('Examples:');
399
+ for (const example of payload.examples.slice(0, 3)) {
400
+ console.log(` ${String(example)}`);
401
+ }
402
+ }
403
+ }
404
+
405
+ if (reveal) {
406
+ const apiKeyResp = String(data.api_key || apiKey);
407
+ if (apiKeyResp) {
408
+ saveEnvValues({
409
+ DEEPLINE_ORIGIN_URL: baseUrl,
410
+ DEEPLINE_API_KEY: apiKeyResp,
411
+ DEEPLINE_CLAIM_TOKEN: '',
412
+ }, baseUrl);
413
+ console.log(`Saved API key to ${envFilePath(baseUrl)}`);
414
+ }
415
+ }
416
+
417
+ return EXIT_OK;
418
+ }
419
+
420
+ export function registerAuthCommands(program: Command): void {
421
+ const auth = program
422
+ .command('auth')
423
+ .description('Register this device and show CLI auth status.')
424
+ .addHelpText(
425
+ 'after',
426
+ `
427
+ Common commands:
428
+ deepline auth register
429
+ deepline auth status
430
+ deepline auth status --json
431
+ `,
432
+ );
433
+
434
+ auth
435
+ .command('register')
436
+ .description('Register this device and open the approval page in your browser.')
437
+ .option('--org-name <name>', 'Workspace name to prefill')
438
+ .option('--agent-name <name>', 'Agent name to register')
439
+ .option('--no-wait', 'Return immediately after opening the approval page')
440
+ .action(async (options) => {
441
+ process.exitCode = await handleRegister([
442
+ ...(options.orgName ? ['--org-name', options.orgName] : []),
443
+ ...(options.agentName ? ['--agent-name', options.agentName] : []),
444
+ ...(options.noWait || options.wait === false ? ['--no-wait'] : []),
445
+ ]);
446
+ });
447
+
448
+ auth
449
+ .command('wait')
450
+ .description('Wait for a pending browser approval and save the API key.')
451
+ .option('--timeout <seconds>', 'Maximum seconds to wait', '300')
452
+ .action(async (options) => {
453
+ process.exitCode = await handleWait([
454
+ ...(options.timeout ? ['--timeout', options.timeout] : []),
455
+ ]);
456
+ });
457
+
458
+ auth
459
+ .command('status')
460
+ .description('Show the current CLI auth and workspace status.')
461
+ .addHelpText(
462
+ 'after',
463
+ `
464
+ Notes:
465
+ Shows the host, server health, connection state, active workspace, and user id.
466
+ Use --reveal only when you intentionally want the server to return and persist
467
+ the API key into this host's CLI auth file.
468
+
469
+ Examples:
470
+ deepline auth status
471
+ deepline auth status --json
472
+ `,
473
+ )
474
+ .option('--reveal', 'Persist the revealed API key back to the host auth file')
475
+ .option('--json', 'Emit JSON output. Also automatic when stdout is piped')
476
+ .action(async (options) => {
477
+ process.exitCode = await handleStatus([
478
+ ...(options.reveal ? ['--reveal'] : []),
479
+ ...(options.json ? ['--json'] : []),
480
+ ]);
481
+ });
482
+ }
@@ -0,0 +1,188 @@
1
+ import { Command } from 'commander';
2
+ import { getAuthedHttpClient, openInBrowser, printJson, shouldEmitJson, writeCsvRowsFile } from '../utils.js';
3
+
4
+ type UsageEntry = {
5
+ operation?: string | null;
6
+ provider?: string | null;
7
+ credits?: number | string | null;
8
+ billing_mode?: string | null;
9
+ status?: string | null;
10
+ created_at?: string | null;
11
+ batch_count?: number | null;
12
+ request_id?: string | null;
13
+ };
14
+
15
+ function humanize(value: string | null | undefined): string {
16
+ return String(value || '')
17
+ .split('_')
18
+ .filter(Boolean)
19
+ .map((token) => token[0]?.toUpperCase() + token.slice(1))
20
+ .join(' ') || 'Unknown';
21
+ }
22
+
23
+ function printRecentUsage(entries: UsageEntry[]): void {
24
+ if (entries.length === 0) {
25
+ process.stdout.write('Recent activity: none yet\n');
26
+ return;
27
+ }
28
+ process.stdout.write('Recent activity:\n');
29
+ for (const entry of entries) {
30
+ const op = `${humanize(entry.provider)} ${humanize(entry.operation)}`.trim();
31
+ const charge = entry.billing_mode === 'no_bill' ? 'free' : `${entry.credits ?? 0} cr`;
32
+ const status = entry.status || 'completed';
33
+ process.stdout.write(` ${op} | ${charge} | ${status} | ${entry.created_at || 'unknown'}\n`);
34
+ }
35
+ }
36
+
37
+ async function handleBalance(options: { json?: boolean }): Promise<void> {
38
+ const { http } = getAuthedHttpClient();
39
+ const payload = await http.get<Record<string, unknown>>('/api/v2/billing/balance');
40
+ if (shouldEmitJson(options.json)) return printJson(payload);
41
+ const status = String(payload.balance_status || '');
42
+ if (status === 'no_billing') {
43
+ process.stdout.write('Balance: 0 credits\n');
44
+ process.stdout.write("Billing: No billing account or payment method set up for this workspace.\n");
45
+ return;
46
+ }
47
+ process.stdout.write(`Balance: ${payload.balance ?? '(unknown)'} credits\n`);
48
+ }
49
+
50
+ async function handleUsage(options: { limit?: string; offset?: string; json?: boolean }): Promise<void> {
51
+ const { http } = getAuthedHttpClient();
52
+ const params = new URLSearchParams();
53
+ if (options.limit) params.set('recent_limit', options.limit);
54
+ if (options.offset) params.set('recent_offset', options.offset);
55
+ const suffix = params.size > 0 ? `?${params.toString()}` : '';
56
+ const payload = await http.get<Record<string, unknown>>(`/api/v2/billing/usage${suffix}`);
57
+ if (shouldEmitJson(options.json)) return printJson(payload);
58
+ const usage = (payload.usage ?? {}) as Record<string, unknown>;
59
+ const quota = (payload.quota ?? {}) as Record<string, unknown>;
60
+ const recent = (payload.recent ?? {}) as Record<string, unknown>;
61
+ process.stdout.write(`Balance: ${payload.balance ?? '(unknown)'}\n`);
62
+ process.stdout.write(`Last 30 days spent: ${usage.month_spent_credits ?? '(unknown)'}\n`);
63
+ process.stdout.write(
64
+ `Monthly limit: ${quota.enabled ? quota.monthly_credits_limit ?? '(unknown)' : 'off'}\n`,
65
+ );
66
+ printRecentUsage(Array.isArray(recent.entries) ? (recent.entries as UsageEntry[]) : []);
67
+ }
68
+
69
+ async function handleLimit(options: { json?: boolean }): Promise<void> {
70
+ const { http } = getAuthedHttpClient();
71
+ const payload = await http.get<Record<string, unknown>>('/api/v2/billing/limit');
72
+ if (shouldEmitJson(options.json)) return printJson(payload);
73
+ if (payload.enabled) {
74
+ process.stdout.write(`Monthly limit: ${payload.monthly_credits_limit ?? '(unknown)'}\n`);
75
+ process.stdout.write(`Remaining before cap: ${payload.remaining_credits ?? '(unknown)'}\n`);
76
+ return;
77
+ }
78
+ process.stdout.write('Monthly limit: off\n');
79
+ }
80
+
81
+ async function handleSetLimit(credits: string, options: { json?: boolean }): Promise<void> {
82
+ const { http } = getAuthedHttpClient();
83
+ const payload = await http.request('/api/v2/billing/limit', {
84
+ method: 'PUT',
85
+ body: { monthly_credits_limit: Number.parseInt(credits, 10) },
86
+ });
87
+ if (shouldEmitJson(options.json)) return printJson(payload);
88
+ process.stdout.write(`Monthly billing limit set to ${credits} credits.\n`);
89
+ }
90
+
91
+ async function handleLimitOff(options: { json?: boolean }): Promise<void> {
92
+ const { http } = getAuthedHttpClient();
93
+ const payload = await http.request('/api/v2/billing/limit', { method: 'DELETE' });
94
+ if (shouldEmitJson(options.json)) return printJson(payload);
95
+ process.stdout.write('Monthly billing limit is now off.\n');
96
+ }
97
+
98
+ async function handleHistory(
99
+ options: { time: '1d' | '1w' | '1m' | '1y'; json?: boolean },
100
+ ): Promise<void> {
101
+ const { http } = getAuthedHttpClient();
102
+ const windows: Record<string, number> = { '1d': 86400, '1w': 604800, '1m': 2592000, '1y': 31536000 };
103
+ const sinceAt = Math.max(0, Math.floor(Date.now() / 1000) - windows[options.time]) * 1000;
104
+ const payload = await http.get<Record<string, unknown>>(`/api/v2/billing/ledger?since_at=${sinceAt}&limit=5000`);
105
+ const entries = Array.isArray(payload.entries) ? payload.entries as Array<Record<string, unknown>> : [];
106
+ const rows = entries.map((entry) => {
107
+ const metadata = (entry.metadata ?? {}) as Record<string, unknown>;
108
+ return {
109
+ created_at: entry.created_at ?? '',
110
+ delta_credits: entry.delta ?? '',
111
+ reason: entry.reason ?? '',
112
+ provider: metadata.provider ?? '',
113
+ operation: metadata.operation ?? '',
114
+ };
115
+ });
116
+ const outputPath = await writeCsvRowsFile(`billing-history-${options.time}`, rows);
117
+ if (shouldEmitJson(options.json)) {
118
+ return printJson({ output_path: outputPath, row_count: rows.length, time_window: options.time });
119
+ }
120
+ process.stdout.write(`Billing history written to ${outputPath}\n`);
121
+ process.stdout.write(`${rows.length} row(s) exported.\n`);
122
+ }
123
+
124
+ async function handleCheckout(options: {
125
+ tier?: string;
126
+ credits?: string;
127
+ discountCode?: string;
128
+ noOpen?: boolean;
129
+ json?: boolean;
130
+ }): Promise<void> {
131
+ const { http } = getAuthedHttpClient();
132
+ const payload = await http.post<Record<string, unknown>>('/api/v2/billing/checkout', {
133
+ ...(options.tier ? { tierId: options.tier } : {}),
134
+ ...(options.credits ? { credits: Number.parseInt(options.credits, 10) } : {}),
135
+ ...(options.discountCode ? { discountCode: options.discountCode } : {}),
136
+ });
137
+ if (shouldEmitJson(options.json)) return printJson(payload);
138
+ const url = String(payload.url || payload.checkout_url || '');
139
+ if (!options.noOpen && url) openInBrowser(url);
140
+ process.stdout.write(`${url || 'Checkout session created.'}\n`);
141
+ }
142
+
143
+ async function handleRedeemCode(code: string, options: { noOpen?: boolean; json?: boolean }): Promise<void> {
144
+ const { http } = getAuthedHttpClient();
145
+ const payload = await http.post<Record<string, unknown>>('/api/v2/billing/checkout/verify', { code });
146
+ if (shouldEmitJson(options.json)) return printJson(payload);
147
+ const url = String(payload.url || '');
148
+ if (!options.noOpen && url) openInBrowser(url);
149
+ process.stdout.write(`${url || 'Code redeemed.'}\n`);
150
+ }
151
+
152
+ export function registerBillingCommands(program: Command): void {
153
+ const billing = program.command('billing').description('Inspect balance, usage, limits, and checkout flows.');
154
+
155
+ billing.command('balance').description('Show current billing balance.').option('--json', 'Emit JSON output').action(handleBalance);
156
+ billing
157
+ .command('usage')
158
+ .description('Show current usage plus recent calls.')
159
+ .option('--limit <n>', 'Recent-call page size')
160
+ .option('--offset <n>', 'Recent-call offset')
161
+ .option('--json', 'Emit JSON output')
162
+ .action(handleUsage);
163
+ billing.command('limit').description('Show configured monthly limit state.').option('--json', 'Emit JSON output').action(handleLimit);
164
+ billing.command('set-limit').description('Set monthly credit cap.').argument('<credits>', 'Monthly credits limit').option('--json', 'Emit JSON output').action(handleSetLimit);
165
+ billing.command('off').description('Disable monthly credit cap.').option('--json', 'Emit JSON output').action(handleLimitOff);
166
+ billing
167
+ .command('history')
168
+ .description('Export billing ledger history to CSV.')
169
+ .requiredOption('--time <window>', 'Rolling time window: 1d, 1w, 1m, or 1y')
170
+ .option('--json', 'Emit JSON output')
171
+ .action(handleHistory);
172
+ billing
173
+ .command('checkout')
174
+ .description('Create a checkout session and optionally open it in your browser.')
175
+ .option('--tier <tierId>', 'Named pricing tier')
176
+ .option('--credits <credits>', 'Custom credit amount')
177
+ .option('--discount-code <code>', 'Apply a discount code')
178
+ .option('--no-open', 'Print the checkout URL without opening a browser')
179
+ .option('--json', 'Emit JSON output')
180
+ .action(handleCheckout);
181
+ billing
182
+ .command('redeem-code')
183
+ .description('Redeem a billing code.')
184
+ .requiredOption('--code <code>', 'Code to redeem')
185
+ .option('--no-open', 'Do not open a browser')
186
+ .option('--json', 'Emit JSON output')
187
+ .action(({ code, ...options }) => handleRedeemCode(code, options));
188
+ }