deepline 0.1.12 → 0.1.20

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 (82) hide show
  1. package/README.md +14 -6
  2. package/dist/cli/index.js +1346 -717
  3. package/dist/cli/index.mjs +1342 -713
  4. package/dist/index.d.mts +199 -23
  5. package/dist/index.d.ts +199 -23
  6. package/dist/index.js +221 -14
  7. package/dist/index.mjs +221 -14
  8. package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +214 -77
  9. package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +85 -60
  10. package/dist/repo/apps/play-runner-workers/src/entry.ts +385 -66
  11. package/dist/repo/sdk/src/client.ts +237 -0
  12. package/dist/repo/sdk/src/config.ts +125 -8
  13. package/dist/repo/sdk/src/http.ts +29 -5
  14. package/dist/repo/sdk/src/play.ts +19 -36
  15. package/dist/repo/sdk/src/plays/bundle-play-file.ts +22 -8
  16. package/dist/repo/sdk/src/plays/local-file-discovery.ts +207 -160
  17. package/dist/repo/sdk/src/types.ts +25 -0
  18. package/dist/repo/sdk/src/version.ts +2 -2
  19. package/dist/repo/shared_libs/play-runtime/tool-result.ts +237 -145
  20. package/dist/repo/shared_libs/plays/bundling/index.ts +206 -229
  21. package/dist/repo/shared_libs/plays/dataset.ts +28 -0
  22. package/dist/repo/shared_libs/plays/row-identity.ts +59 -4
  23. package/package.json +5 -4
  24. package/dist/cli/index.js.map +0 -1
  25. package/dist/cli/index.mjs.map +0 -1
  26. package/dist/index.js.map +0 -1
  27. package/dist/index.mjs.map +0 -1
  28. package/dist/repo/apps/play-runner-workers/src/runtime/README.md +0 -21
  29. package/dist/repo/apps/play-runner-workers/src/runtime/batching.ts +0 -177
  30. package/dist/repo/apps/play-runner-workers/src/runtime/execution-plan.ts +0 -52
  31. package/dist/repo/apps/play-runner-workers/src/runtime/tool-batch.ts +0 -100
  32. package/dist/repo/sdk/src/cli/commands/auth.ts +0 -500
  33. package/dist/repo/sdk/src/cli/commands/billing.ts +0 -188
  34. package/dist/repo/sdk/src/cli/commands/csv.ts +0 -123
  35. package/dist/repo/sdk/src/cli/commands/db.ts +0 -119
  36. package/dist/repo/sdk/src/cli/commands/feedback.ts +0 -40
  37. package/dist/repo/sdk/src/cli/commands/org.ts +0 -117
  38. package/dist/repo/sdk/src/cli/commands/play.ts +0 -3441
  39. package/dist/repo/sdk/src/cli/commands/tools.ts +0 -687
  40. package/dist/repo/sdk/src/cli/dataset-stats.ts +0 -415
  41. package/dist/repo/sdk/src/cli/index.ts +0 -148
  42. package/dist/repo/sdk/src/cli/progress.ts +0 -149
  43. package/dist/repo/sdk/src/cli/skills-sync.ts +0 -141
  44. package/dist/repo/sdk/src/cli/trace.ts +0 -61
  45. package/dist/repo/sdk/src/cli/utils.ts +0 -145
  46. package/dist/repo/sdk/src/compat.ts +0 -77
  47. package/dist/repo/shared_libs/observability/node-tracing.ts +0 -129
  48. package/dist/repo/shared_libs/observability/tracing.ts +0 -98
  49. package/dist/repo/shared_libs/play-runtime/context.ts +0 -4242
  50. package/dist/repo/shared_libs/play-runtime/ctx-contract.ts +0 -250
  51. package/dist/repo/shared_libs/play-runtime/ctx-types.ts +0 -725
  52. package/dist/repo/shared_libs/play-runtime/dataset-id.ts +0 -10
  53. package/dist/repo/shared_libs/play-runtime/db-session-crypto.ts +0 -304
  54. package/dist/repo/shared_libs/play-runtime/db-session.ts +0 -462
  55. package/dist/repo/shared_libs/play-runtime/live-events.ts +0 -214
  56. package/dist/repo/shared_libs/play-runtime/live-state-contract.ts +0 -50
  57. package/dist/repo/shared_libs/play-runtime/map-execution-frame.ts +0 -114
  58. package/dist/repo/shared_libs/play-runtime/map-row-identity.ts +0 -158
  59. package/dist/repo/shared_libs/play-runtime/progress-emitter.ts +0 -172
  60. package/dist/repo/shared_libs/play-runtime/protocol.ts +0 -121
  61. package/dist/repo/shared_libs/play-runtime/public-play-contract.ts +0 -42
  62. package/dist/repo/shared_libs/play-runtime/result-normalization.ts +0 -33
  63. package/dist/repo/shared_libs/play-runtime/runtime-api.ts +0 -1873
  64. package/dist/repo/shared_libs/play-runtime/runtime-constraints.ts +0 -2
  65. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-neon-serverless.ts +0 -201
  66. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-pg.ts +0 -48
  67. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver.ts +0 -84
  68. package/dist/repo/shared_libs/play-runtime/static-pipeline-types.ts +0 -147
  69. package/dist/repo/shared_libs/play-runtime/suspension.ts +0 -68
  70. package/dist/repo/shared_libs/play-runtime/tracing.ts +0 -31
  71. package/dist/repo/shared_libs/play-runtime/waterfall-replay.ts +0 -75
  72. package/dist/repo/shared_libs/play-runtime/worker-api-types.ts +0 -140
  73. package/dist/repo/shared_libs/plays/artifact-transport.ts +0 -14
  74. package/dist/repo/shared_libs/plays/artifact-types.ts +0 -49
  75. package/dist/repo/shared_libs/plays/compiler-manifest.ts +0 -186
  76. package/dist/repo/shared_libs/plays/definition.ts +0 -264
  77. package/dist/repo/shared_libs/plays/file-refs.ts +0 -11
  78. package/dist/repo/shared_libs/plays/rate-limit-scheduler.ts +0 -206
  79. package/dist/repo/shared_libs/plays/resolve-static-pipeline.ts +0 -164
  80. package/dist/repo/shared_libs/plays/runtime-validation.ts +0 -395
  81. package/dist/repo/shared_libs/temporal/constants.ts +0 -39
  82. package/dist/repo/shared_libs/temporal/preview-config.ts +0 -153
@@ -56,6 +56,8 @@ import type {
56
56
  StartPlayRunRequest,
57
57
  DeletePlayResult,
58
58
  ToolDefinition,
59
+ ToolSearchOptions,
60
+ ToolSearchResult,
59
61
  ToolMetadata,
60
62
  CustomerDbQueryResult,
61
63
  } from './types.js';
@@ -63,6 +65,11 @@ import type { PlayStagedFileRef } from './plays/local-file-discovery.js';
63
65
  import type { PlayCompilerManifest } from '../../shared_libs/plays/compiler-manifest.js';
64
66
 
65
67
  const TERMINAL_PLAY_STATUSES = new Set(['completed', 'failed', 'cancelled']);
68
+ const INCLUDE_TOOL_METADATA_HEADER = 'x-deepline-include-tool-metadata';
69
+
70
+ type ExecuteToolRawOptions = {
71
+ includeToolMetadata?: boolean;
72
+ };
66
73
 
67
74
  export type ToolExecution<TData = unknown, TMeta = Record<string, unknown>> = {
68
75
  status: string;
@@ -75,6 +82,45 @@ export type ToolExecution<TData = unknown, TMeta = Record<string, unknown>> = {
75
82
  [key: string]: unknown;
76
83
  };
77
84
 
85
+ export type RunsListOptions = {
86
+ play: string;
87
+ status?: string;
88
+ };
89
+
90
+ export type RunsTailOptions = {
91
+ cursor?: string | number;
92
+ afterLogIndex?: number;
93
+ waitMs?: number;
94
+ terminalOnly?: boolean;
95
+ compact?: boolean;
96
+ };
97
+
98
+ export type RunsLogsOptions = {
99
+ limit?: number;
100
+ };
101
+
102
+ export type RunsLogsResult = {
103
+ runId: string;
104
+ totalCount: number;
105
+ returnedCount: number;
106
+ firstSequence: number | null;
107
+ lastSequence: number | null;
108
+ truncated: boolean;
109
+ hasMore: boolean;
110
+ entries: string[];
111
+ };
112
+
113
+ export type RunsNamespace = {
114
+ get: (runId: string) => Promise<PlayStatus>;
115
+ list: (options: RunsListOptions) => Promise<PlayRunListItem[]>;
116
+ tail: (runId: string, options?: RunsTailOptions) => Promise<PlayStatus>;
117
+ logs: (runId: string, options?: RunsLogsOptions) => Promise<RunsLogsResult>;
118
+ stop: (
119
+ runId: string,
120
+ options?: { reason?: string },
121
+ ) => Promise<StopPlayRunResult>;
122
+ };
123
+
78
124
  function isRecord(value: unknown): value is Record<string, unknown> {
79
125
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
80
126
  }
@@ -140,6 +186,7 @@ function mapLegacyTemporalStatus(status: string): PlayStatus['status'] {
140
186
  export class DeeplineClient {
141
187
  private readonly http: HttpClient;
142
188
  private readonly config: ResolvedConfig;
189
+ readonly runs: RunsNamespace;
143
190
 
144
191
  /**
145
192
  * @param options - Optional overrides for API key, base URL, timeout, and retries.
@@ -148,6 +195,13 @@ export class DeeplineClient {
148
195
  constructor(options?: DeeplineClientOptions) {
149
196
  this.config = resolveConfig(options);
150
197
  this.http = new HttpClient(this.config);
198
+ this.runs = {
199
+ get: (runId) => this.getRunStatus(runId),
200
+ list: (options) => this.listRuns(options),
201
+ tail: (runId, options) => this.tailRun(runId, options),
202
+ logs: (runId, options) => this.getRunLogs(runId, options),
203
+ stop: (runId, options) => this.stopRun(runId, options),
204
+ };
151
205
  }
152
206
 
153
207
  /** The resolved base URL this client is targeting (e.g. `"http://localhost:3000"`). */
@@ -273,6 +327,34 @@ export class DeeplineClient {
273
327
  return res.tools;
274
328
  }
275
329
 
330
+ /**
331
+ * Search available tools using Deepline's ranked backend search.
332
+ *
333
+ * This is the same discovery surface used by the legacy CLI: it ranks across
334
+ * tool metadata, categories, agent guidance, and input schema fields.
335
+ */
336
+ async searchTools(
337
+ options: ToolSearchOptions = {},
338
+ ): Promise<ToolSearchResult> {
339
+ const params = new URLSearchParams();
340
+ const query = options.query?.trim() ?? '';
341
+ params.set('q', query);
342
+ params.set(
343
+ 'include_search_debug',
344
+ options.includeSearchDebug ? 'true' : 'false',
345
+ );
346
+ params.set('search_mode', options.searchMode ?? 'v2');
347
+ if (options.categories?.trim()) {
348
+ params.set('categories', options.categories.trim());
349
+ }
350
+ if (options.searchTerms?.trim()) {
351
+ params.set('search_terms', options.searchTerms.trim());
352
+ }
353
+ return this.http.get<ToolSearchResult>(
354
+ `/api/v2/integrations/list?${params.toString()}`,
355
+ );
356
+ }
357
+
276
358
  /**
277
359
  * Get detailed metadata for a single tool.
278
360
  *
@@ -312,13 +394,26 @@ export class DeeplineClient {
312
394
  async executeTool<TData = unknown, TMeta = Record<string, unknown>>(
313
395
  toolId: string,
314
396
  input: Record<string, unknown>,
397
+ options?: ExecuteToolRawOptions,
315
398
  ): Promise<ToolExecution<TData, TMeta>> {
399
+ const headers = options?.includeToolMetadata
400
+ ? { [INCLUDE_TOOL_METADATA_HEADER]: 'true' }
401
+ : undefined;
316
402
  return this.http.post<ToolExecution<TData, TMeta>>(
317
403
  `/api/v2/integrations/${encodeURIComponent(toolId)}/execute`,
318
404
  { payload: input },
405
+ headers,
319
406
  );
320
407
  }
321
408
 
409
+ async executeToolRaw<TData = unknown, TMeta = Record<string, unknown>>(
410
+ toolId: string,
411
+ input: Record<string, unknown>,
412
+ options?: ExecuteToolRawOptions,
413
+ ): Promise<ToolExecution<TData, TMeta>> {
414
+ return this.executeTool<TData, TMeta>(toolId, input, options);
415
+ }
416
+
322
417
  async queryCustomerDb(input: {
323
418
  sql: string;
324
419
  maxRows?: number;
@@ -374,6 +469,7 @@ export class DeeplineClient {
374
469
  ? { artifactStorageKey: request.artifactStorageKey }
375
470
  : {}),
376
471
  ...(request.sourceCode ? { sourceCode: request.sourceCode } : {}),
472
+ ...(request.sourceFiles ? { sourceFiles: request.sourceFiles } : {}),
377
473
  ...('staticPipeline' in request
378
474
  ? { staticPipeline: request.staticPipeline }
379
475
  : {}),
@@ -418,6 +514,7 @@ export class DeeplineClient {
418
514
  ? { artifactStorageKey: request.artifactStorageKey }
419
515
  : {}),
420
516
  ...(request.sourceCode ? { sourceCode: request.sourceCode } : {}),
517
+ ...(request.sourceFiles ? { sourceFiles: request.sourceFiles } : {}),
421
518
  ...('staticPipeline' in request
422
519
  ? { staticPipeline: request.staticPipeline }
423
520
  : {}),
@@ -466,6 +563,7 @@ export class DeeplineClient {
466
563
  async registerPlayArtifact(input: {
467
564
  name: string;
468
565
  sourceCode: string;
566
+ sourceFiles?: Record<string, string>;
469
567
  artifact: Record<string, unknown>;
470
568
  compilerManifest?: PlayCompilerManifest;
471
569
  publish?: boolean;
@@ -490,6 +588,7 @@ export class DeeplineClient {
490
588
  (await this.compilePlayManifest({
491
589
  name: input.name,
492
590
  sourceCode: input.sourceCode,
591
+ sourceFiles: input.sourceFiles,
493
592
  artifact: input.artifact,
494
593
  }));
495
594
  return this.http.post('/api/v2/plays/artifacts', {
@@ -502,6 +601,7 @@ export class DeeplineClient {
502
601
  artifacts: Array<{
503
602
  name: string;
504
603
  sourceCode: string;
604
+ sourceFiles?: Record<string, string>;
505
605
  artifact: Record<string, unknown>;
506
606
  compilerManifest?: PlayCompilerManifest;
507
607
  publish?: boolean;
@@ -533,6 +633,7 @@ export class DeeplineClient {
533
633
  (await this.compilePlayManifest({
534
634
  name: artifact.name,
535
635
  sourceCode: artifact.sourceCode,
636
+ sourceFiles: artifact.sourceFiles,
536
637
  artifact: artifact.artifact,
537
638
  })),
538
639
  })),
@@ -545,6 +646,7 @@ export class DeeplineClient {
545
646
  async compilePlayManifest(input: {
546
647
  name: string;
547
648
  sourceCode: string;
649
+ sourceFiles?: Record<string, string>;
548
650
  artifact: Record<string, unknown>;
549
651
  importedPlayDependencies?: PlayCompilerManifest[];
550
652
  }): Promise<PlayCompilerManifest> {
@@ -564,6 +666,7 @@ export class DeeplineClient {
564
666
  async checkPlayArtifact(input: {
565
667
  name?: string;
566
668
  sourceCode: string;
669
+ sourceFiles?: Record<string, string>;
567
670
  artifact: Record<string, unknown>;
568
671
  }): Promise<PlayCheckResult> {
569
672
  return this.http.post('/api/v2/plays/check', input);
@@ -572,6 +675,7 @@ export class DeeplineClient {
572
675
  async startPlayRunFromBundle(input: {
573
676
  name: string;
574
677
  sourceCode: string;
678
+ sourceFiles?: Record<string, string>;
575
679
  artifact: Record<string, unknown>;
576
680
  compilerManifest?: PlayCompilerManifest;
577
681
  input?: Record<string, unknown>;
@@ -584,11 +688,13 @@ export class DeeplineClient {
584
688
  (await this.compilePlayManifest({
585
689
  name: input.name,
586
690
  sourceCode: input.sourceCode,
691
+ sourceFiles: input.sourceFiles,
587
692
  artifact: input.artifact,
588
693
  }));
589
694
  const registeredArtifact = await this.registerPlayArtifact({
590
695
  name: input.name,
591
696
  sourceCode: input.sourceCode,
697
+ sourceFiles: input.sourceFiles,
592
698
  artifact: input.artifact,
593
699
  compilerManifest,
594
700
  publish: false,
@@ -642,6 +748,7 @@ export class DeeplineClient {
642
748
  name?: string,
643
749
  options?: {
644
750
  sourceCode?: string;
751
+ sourceFiles?: Record<string, string>;
645
752
  artifact?: Record<string, unknown>;
646
753
  compilerManifest?: PlayCompilerManifest;
647
754
  input?: Record<string, unknown>;
@@ -669,12 +776,14 @@ export class DeeplineClient {
669
776
  (await this.compilePlayManifest({
670
777
  name,
671
778
  sourceCode,
779
+ sourceFiles: options?.sourceFiles,
672
780
  artifact,
673
781
  }));
674
782
 
675
783
  const registeredArtifact = await this.registerPlayArtifact({
676
784
  name,
677
785
  sourceCode,
786
+ sourceFiles: options?.sourceFiles,
678
787
  artifact,
679
788
  compilerManifest,
680
789
  publish: false,
@@ -916,6 +1025,134 @@ export class DeeplineClient {
916
1025
  return response.runs ?? [];
917
1026
  }
918
1027
 
1028
+ /**
1029
+ * Get a run by id using the public runs resource model.
1030
+ *
1031
+ * This is the SDK equivalent of:
1032
+ *
1033
+ * ```bash
1034
+ * deepline runs get <run-id> --json
1035
+ * ```
1036
+ */
1037
+ async getRunStatus(runId: string): Promise<PlayStatus> {
1038
+ const response = await this.http.get<Record<string, unknown>>(
1039
+ `/api/v2/runs/${encodeURIComponent(runId)}`,
1040
+ );
1041
+ return normalizePlayStatus(response);
1042
+ }
1043
+
1044
+ /**
1045
+ * List play runs using the public runs resource model.
1046
+ *
1047
+ * This is the SDK equivalent of:
1048
+ *
1049
+ * ```bash
1050
+ * deepline runs list --play <play-name> --status failed --json
1051
+ * ```
1052
+ */
1053
+ async listRuns(options: RunsListOptions): Promise<PlayRunListItem[]> {
1054
+ const playName = options.play.trim();
1055
+ if (!playName) {
1056
+ throw new Error('runs.list requires options.play.');
1057
+ }
1058
+ const params = new URLSearchParams({ play: playName });
1059
+ const status = options.status?.trim();
1060
+ if (status) {
1061
+ params.set('status', status);
1062
+ }
1063
+ const response = await this.http.get<{ runs: PlayRunListItem[] }>(
1064
+ `/api/v2/runs?${params.toString()}`,
1065
+ );
1066
+ return response.runs ?? [];
1067
+ }
1068
+
1069
+ /**
1070
+ * Fetch the lightweight tail status for a run using the public runs resource model.
1071
+ *
1072
+ * This is the SDK equivalent of:
1073
+ *
1074
+ * ```bash
1075
+ * deepline runs tail <run-id> --json
1076
+ * ```
1077
+ */
1078
+ async tailRun(runId: string, options?: RunsTailOptions): Promise<PlayStatus> {
1079
+ const afterLogIndex =
1080
+ typeof options?.afterLogIndex === 'number'
1081
+ ? options.afterLogIndex
1082
+ : typeof options?.cursor === 'number'
1083
+ ? options.cursor
1084
+ : typeof options?.cursor === 'string' && options.cursor.trim()
1085
+ ? Number(options.cursor)
1086
+ : undefined;
1087
+ const params = new URLSearchParams();
1088
+ if (Number.isFinite(afterLogIndex)) {
1089
+ params.set('afterLogIndex', String(Number(afterLogIndex)));
1090
+ }
1091
+ if (typeof options?.waitMs === 'number') {
1092
+ params.set('waitMs', String(options.waitMs));
1093
+ }
1094
+ if (options?.terminalOnly) {
1095
+ params.set('terminalOnly', 'true');
1096
+ }
1097
+ const suffix = params.toString() ? `?${params.toString()}` : '';
1098
+ const response = await this.http.get<Record<string, unknown>>(
1099
+ `/api/v2/runs/${encodeURIComponent(runId)}/tail${suffix}`,
1100
+ );
1101
+ return normalizePlayStatus(response);
1102
+ }
1103
+
1104
+ /**
1105
+ * Fetch persisted logs for a run using the public runs resource model.
1106
+ *
1107
+ * This is the SDK equivalent of:
1108
+ *
1109
+ * ```bash
1110
+ * deepline runs logs <run-id> --limit 200 --json
1111
+ * ```
1112
+ */
1113
+ async getRunLogs(
1114
+ runId: string,
1115
+ options?: RunsLogsOptions,
1116
+ ): Promise<RunsLogsResult> {
1117
+ const status = await this.getRunStatus(runId);
1118
+ const logs = status.progress?.logs ?? [];
1119
+ const limit =
1120
+ typeof options?.limit === 'number' && Number.isFinite(options.limit)
1121
+ ? Math.max(0, Math.trunc(options.limit))
1122
+ : 200;
1123
+ const entries = logs.slice(Math.max(0, logs.length - limit));
1124
+ return {
1125
+ runId: status.runId,
1126
+ totalCount: logs.length,
1127
+ returnedCount: entries.length,
1128
+ firstSequence:
1129
+ logs.length === 0 ? null : logs.length - entries.length + 1,
1130
+ lastSequence: logs.length === 0 ? null : logs.length,
1131
+ truncated: logs.length > entries.length,
1132
+ hasMore: logs.length > entries.length,
1133
+ entries,
1134
+ };
1135
+ }
1136
+
1137
+ /**
1138
+ * Stop a run by id using the public runs resource model.
1139
+ *
1140
+ * This is the SDK equivalent of:
1141
+ *
1142
+ * ```bash
1143
+ * deepline runs stop <run-id> --reason "stale lock" --json
1144
+ * ```
1145
+ */
1146
+ async stopRun(
1147
+ runId: string,
1148
+ options?: { reason?: string },
1149
+ ): Promise<StopPlayRunResult> {
1150
+ return this.http.post<StopPlayRunResult>(
1151
+ `/api/v2/runs/${encodeURIComponent(runId)}/stop`,
1152
+ options?.reason ? { reason: options.reason } : {},
1153
+ );
1154
+ }
1155
+
919
1156
  async listPlays(): Promise<PlayListItem[]> {
920
1157
  const response = await this.http.get<{ plays: PlayListItem[] }>(
921
1158
  '/api/v2/plays',
@@ -10,9 +10,12 @@
10
10
  * 1. `options.baseUrl` (explicit constructor argument)
11
11
  * 2. `DEEPLINE_ORIGIN_URL` environment variable
12
12
  * 3. `DEEPLINE_API_BASE_URL` environment variable
13
- * 4. Nearest checkout-local `.env.worktree`
14
- * 5. `DEEPLINE_ORIGIN_URL` from the production host auth file
15
- * 6. Production fallback: `https://code.deepline.com`
13
+ * 4. Nearest checkout-local `.env.deepline`
14
+ * 5. Nearest checkout-local profile file (`.env.deepline.prod`, etc.)
15
+ * 6. Nearest checkout-local app env file (`.env.prod`, `.env.staging`, `.env.local`, `.env`)
16
+ * 7. Nearest checkout-local `.env.worktree`
17
+ * 8. `DEEPLINE_ORIGIN_URL` from the production host auth file
18
+ * 9. Production fallback: `https://code.deepline.com`
16
19
  *
17
20
  * ### API Key
18
21
  * 1. `options.apiKey` (explicit constructor argument)
@@ -48,6 +51,20 @@ const DEFAULT_TIMEOUT = 60_000;
48
51
  /** Default retry count for transient failures. */
49
52
  const DEFAULT_MAX_RETRIES = 3;
50
53
 
54
+ const ACTIVE_DEEPLINE_ENV_FILE = '.env.deepline';
55
+
56
+ function isProdBaseUrl(baseUrl: string): boolean {
57
+ return baseUrl.trim().replace(/\/$/, '') === PROD_URL;
58
+ }
59
+
60
+ function profileNameForBaseUrl(baseUrl: string): 'prod' | 'dev' {
61
+ return isProdBaseUrl(baseUrl) ? 'prod' : 'dev';
62
+ }
63
+
64
+ function projectEnvStartDir(): string {
65
+ return process.env.DEEPLINE_PROJECT_ENV_DIR?.trim() || process.cwd();
66
+ }
67
+
51
68
  /**
52
69
  * Convert a base URL to a filesystem-safe slug for per-host config storage.
53
70
  *
@@ -116,17 +133,70 @@ function parseEnvFile(filePath: string): Record<string, string> {
116
133
  return env;
117
134
  }
118
135
 
119
- function findNearestWorktreeEnv(startDir: string = process.cwd()): Record<string, string> {
136
+ function findNearestEnvFile(
137
+ names: string[],
138
+ startDir: string = process.cwd(),
139
+ ): string | null {
120
140
  let current = resolve(startDir);
121
141
  while (true) {
122
- const values = parseEnvFile(join(current, '.env.worktree'));
123
- if (Object.keys(values).length > 0) return values;
142
+ for (const name of names) {
143
+ const filePath = join(current, name);
144
+ if (existsSync(filePath)) return filePath;
145
+ }
124
146
  const parent = dirname(current);
125
- if (parent === current) return {};
147
+ if (parent === current) return null;
126
148
  current = parent;
127
149
  }
128
150
  }
129
151
 
152
+ function findNearestEnv(names: string[], startDir: string = process.cwd()): Record<string, string> {
153
+ const filePath = findNearestEnvFile(names, startDir);
154
+ return filePath ? parseEnvFile(filePath) : {};
155
+ }
156
+
157
+ function findNearestWorktreeEnv(startDir: string = process.cwd()): Record<string, string> {
158
+ return findNearestEnv(['.env.worktree'], startDir);
159
+ }
160
+
161
+ function resolveProfileEnvFileNames(): string[] {
162
+ const explicitProfile =
163
+ process.env.DEEPLINE_ENV_PROFILE?.trim() ||
164
+ process.env.DEEPLINE_PROFILE?.trim() ||
165
+ '';
166
+ const names: string[] = [];
167
+ if (explicitProfile) names.push(`.env.deepline.${explicitProfile}`);
168
+ const nodeEnv = process.env.NODE_ENV?.trim();
169
+ if (nodeEnv === 'production') names.push('.env.deepline.prod');
170
+ else if (nodeEnv === 'staging') names.push('.env.deepline.staging');
171
+ names.push(ACTIVE_DEEPLINE_ENV_FILE);
172
+ return names;
173
+ }
174
+
175
+ function resolveProjectAppEnvFileNames(): string[] {
176
+ const nodeEnv = process.env.NODE_ENV?.trim();
177
+ const names: string[] = [];
178
+ if (nodeEnv === 'production') names.push('.env.prod');
179
+ if (nodeEnv === 'staging') names.push('.env.staging');
180
+ names.push('.env.local', '.env');
181
+ return names;
182
+ }
183
+
184
+ function resolveBaseUrlFromEnvValues(env: Record<string, string>): string {
185
+ return (
186
+ env.DEEPLINE_ORIGIN_URL?.trim() ||
187
+ env.DEEPLINE_API_BASE_URL?.trim() ||
188
+ ''
189
+ );
190
+ }
191
+
192
+ function loadProjectDeeplineEnv(): Record<string, string> {
193
+ return findNearestEnv(resolveProfileEnvFileNames(), projectEnvStartDir());
194
+ }
195
+
196
+ function loadProjectAppEnv(): Record<string, string> {
197
+ return findNearestEnv(resolveProjectAppEnvFileNames(), projectEnvStartDir());
198
+ }
199
+
130
200
  function normalizeWorktreeBaseUrl(baseUrl: string, worktreeEnv = findNearestWorktreeEnv()): string {
131
201
  const trimmed = baseUrl.trim().replace(/\/$/, '');
132
202
  if (!trimmed) return trimmed;
@@ -219,6 +289,12 @@ function autoDetectBaseUrl(): string {
219
289
  const envBase = process.env.DEEPLINE_API_BASE_URL?.trim();
220
290
  if (envBase) return normalizeWorktreeBaseUrl(envBase);
221
291
 
292
+ const projectDeeplineBaseUrl = resolveBaseUrlFromEnvValues(loadProjectDeeplineEnv());
293
+ if (projectDeeplineBaseUrl) return normalizeWorktreeBaseUrl(projectDeeplineBaseUrl);
294
+
295
+ const projectAppBaseUrl = resolveBaseUrlFromEnvValues(loadProjectAppEnv());
296
+ if (projectAppBaseUrl) return normalizeWorktreeBaseUrl(projectAppBaseUrl);
297
+
222
298
  const worktreeBaseUrl = resolveWorktreeBaseUrl();
223
299
  if (worktreeBaseUrl) return worktreeBaseUrl;
224
300
 
@@ -260,11 +336,15 @@ export function resolveConfig(options?: DeeplineClientOptions): ResolvedConfig {
260
336
  const baseUrl = normalizeWorktreeBaseUrl(requestedBaseUrl);
261
337
 
262
338
  const cliEnv = loadCliEnv(baseUrl);
339
+ const projectDeeplineEnv = loadProjectDeeplineEnv();
340
+ const projectAppEnv = loadProjectAppEnv();
263
341
 
264
342
  // Resolve API key: option > env var > SDK CLI env
265
343
  const apiKey =
266
344
  options?.apiKey?.trim() ||
267
345
  process.env.DEEPLINE_API_KEY?.trim() ||
346
+ projectDeeplineEnv.DEEPLINE_API_KEY ||
347
+ projectAppEnv.DEEPLINE_API_KEY ||
268
348
  cliEnv.DEEPLINE_API_KEY ||
269
349
  '';
270
350
 
@@ -282,4 +362,41 @@ export function resolveConfig(options?: DeeplineClientOptions): ResolvedConfig {
282
362
  };
283
363
  }
284
364
 
285
- export { baseUrlSlug, loadCliEnv, loadGlobalCliEnv, parseEnvFile, autoDetectBaseUrl, PROD_URL };
365
+ function mergeEnvFile(filePath: string, values: Record<string, string>): void {
366
+ const existing = existsSync(filePath) ? parseEnvFile(filePath) : {};
367
+ const merged = { ...existing, ...values };
368
+ const dir = dirname(filePath);
369
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
370
+ const lines = Object.entries(merged)
371
+ .filter(([, value]) => value !== '')
372
+ .map(([key, value]) => `${key}=${value}`);
373
+ writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf-8');
374
+ }
375
+
376
+ export function saveProjectDeeplineEnvValues(
377
+ baseUrl: string,
378
+ values: Record<string, string>,
379
+ startDir: string = projectEnvStartDir(),
380
+ ): string[] {
381
+ const root = resolve(startDir);
382
+ const profile = profileNameForBaseUrl(baseUrl);
383
+ const files = [
384
+ join(root, ACTIVE_DEEPLINE_ENV_FILE),
385
+ join(root, `.env.deepline.${profile}`),
386
+ ];
387
+ if (profile === 'dev') files.push(join(root, '.env'));
388
+
389
+ for (const filePath of files) {
390
+ mergeEnvFile(filePath, values);
391
+ }
392
+ return files;
393
+ }
394
+
395
+ export {
396
+ baseUrlSlug,
397
+ loadCliEnv,
398
+ loadGlobalCliEnv,
399
+ parseEnvFile,
400
+ autoDetectBaseUrl,
401
+ PROD_URL,
402
+ };
@@ -179,10 +179,26 @@ export class HttpClient {
179
179
  }
180
180
 
181
181
  if (!response.ok) {
182
- const msg =
182
+ const errorValue =
183
183
  typeof parsed === 'object' && parsed && 'error' in parsed
184
- ? String((parsed as Record<string, unknown>).error)
185
- : `HTTP ${response.status}`;
184
+ ? (parsed as Record<string, unknown>).error
185
+ : undefined;
186
+ const msg =
187
+ typeof errorValue === 'string'
188
+ ? errorValue
189
+ : errorValue &&
190
+ typeof errorValue === 'object' &&
191
+ 'message' in errorValue &&
192
+ typeof (errorValue as Record<string, unknown>).message ===
193
+ 'string'
194
+ ? (errorValue as Record<string, string>).message
195
+ : typeof parsed === 'object' &&
196
+ parsed &&
197
+ 'message' in parsed &&
198
+ typeof (parsed as Record<string, unknown>).message ===
199
+ 'string'
200
+ ? (parsed as Record<string, string>).message
201
+ : `HTTP ${response.status}`;
186
202
  throw new DeeplineError(msg, response.status, 'API_ERROR', {
187
203
  response: parsed,
188
204
  });
@@ -289,8 +305,16 @@ export class HttpClient {
289
305
  * @param path - API path
290
306
  * @param body - Request body (will be JSON-serialized)
291
307
  */
292
- async post<T = unknown>(path: string, body: unknown): Promise<T> {
293
- return this.request<T>(path, { method: 'POST', body });
308
+ async post<T = unknown>(
309
+ path: string,
310
+ body: unknown,
311
+ headers?: Record<string, string>,
312
+ ): Promise<T> {
313
+ return this.request<T>(path, {
314
+ method: 'POST',
315
+ body,
316
+ headers,
317
+ });
294
318
  }
295
319
 
296
320
  /**