deepline 0.1.32 → 0.1.35
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.
- package/README.md +33 -20
- package/dist/cli/index.js +1117 -381
- package/dist/cli/index.mjs +1058 -322
- package/dist/index.d.mts +131 -85
- package/dist/index.d.ts +131 -85
- package/dist/index.js +569 -124
- package/dist/index.mjs +570 -125
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +212 -1
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +129 -2
- package/dist/repo/apps/play-runner-workers/src/entry.ts +147 -64
- package/dist/repo/apps/play-runner-workers/src/workflow-retry.ts +50 -0
- package/dist/repo/sdk/src/client.ts +46 -33
- package/dist/repo/sdk/src/config.ts +109 -233
- package/dist/repo/sdk/src/http.ts +44 -4
- package/dist/repo/sdk/src/index.ts +8 -5
- package/dist/repo/sdk/src/play.ts +124 -45
- package/dist/repo/sdk/src/plays/harness-stub.ts +2 -2
- package/dist/repo/sdk/src/tool-output.ts +26 -7
- package/dist/repo/sdk/src/types.ts +48 -12
- package/dist/repo/sdk/src/version.ts +2 -2
- package/dist/repo/shared_libs/play-runtime/run-failure.ts +49 -0
- package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +1 -1
- package/dist/repo/shared_libs/play-runtime/tool-result.ts +138 -35
- package/package.json +1 -1
|
@@ -1,47 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Configuration resolution for the Deepline SDK.
|
|
2
|
+
* Configuration resolution for the Deepline SDK and SDK CLI.
|
|
3
3
|
*
|
|
4
|
-
* The SDK
|
|
5
|
-
* making it zero-config for common setups while allowing full override control.
|
|
4
|
+
* The public SDK CLI env contract is deliberately small:
|
|
6
5
|
*
|
|
7
|
-
*
|
|
6
|
+
* - `DEEPLINE_HOST_URL`: Deepline API/app host, for example `https://code.deepline.com`
|
|
7
|
+
* - `DEEPLINE_API_KEY`: API key for that host and workspace
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* 2. `DEEPLINE_ORIGIN_URL` environment variable
|
|
12
|
-
* 3. `DEEPLINE_API_BASE_URL` environment variable
|
|
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`
|
|
9
|
+
* The CLI also stores the same two keys in per-host files under
|
|
10
|
+
* `~/.local/deepline/<host-slug>/.env`, created by `deepline auth register`.
|
|
19
11
|
*
|
|
20
|
-
*
|
|
21
|
-
* 1. `options.apiKey` (explicit constructor argument)
|
|
22
|
-
* 2. `DEEPLINE_API_KEY` environment variable
|
|
23
|
-
* 3. `DEEPLINE_API_KEY` from `~/.local/deepline/<host-slug>/.env` (SDK CLI config)
|
|
12
|
+
* Resolution order:
|
|
24
13
|
*
|
|
25
|
-
*
|
|
14
|
+
* Base URL:
|
|
15
|
+
* 1. `options.baseUrl`
|
|
16
|
+
* 2. `DEEPLINE_HOST_URL`
|
|
17
|
+
* 3. nearest project `.env.deepline`
|
|
18
|
+
* 4. production host auth file
|
|
19
|
+
* 5. production fallback: `https://code.deepline.com`
|
|
26
20
|
*
|
|
27
|
-
*
|
|
21
|
+
* API key:
|
|
22
|
+
* 1. `options.apiKey`
|
|
23
|
+
* 2. `DEEPLINE_API_KEY`
|
|
24
|
+
* 3. nearest project `.env.deepline`
|
|
25
|
+
* 4. host auth file for the resolved base URL
|
|
28
26
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* ```
|
|
33
|
-
* ~/.local/deepline/code-deepline-com/.env # production
|
|
34
|
-
* ~/.local/deepline/localhost-3000/.env # local dev
|
|
35
|
-
* ```
|
|
27
|
+
* App/runtime env files such as `.env`, `.env.local`, and `.env.worktree` do
|
|
28
|
+
* not route the SDK CLI. Put CLI routing in `.env.deepline`.
|
|
36
29
|
*
|
|
37
30
|
* @module
|
|
38
31
|
*/
|
|
39
|
-
import {
|
|
32
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
40
33
|
import { homedir } from 'node:os';
|
|
41
34
|
import { dirname, join, resolve } from 'node:path';
|
|
42
35
|
import type { DeeplineClientOptions, ResolvedConfig } from './types.js';
|
|
43
36
|
import { ConfigError } from './errors.js';
|
|
44
37
|
|
|
38
|
+
export const HOST_URL_ENV = 'DEEPLINE_HOST_URL';
|
|
39
|
+
export const API_KEY_ENV = 'DEEPLINE_API_KEY';
|
|
40
|
+
|
|
45
41
|
/** Production API base URL. */
|
|
46
42
|
const PROD_URL = 'https://code.deepline.com';
|
|
47
43
|
|
|
@@ -51,26 +47,13 @@ const DEFAULT_TIMEOUT = 60_000;
|
|
|
51
47
|
/** Default retry count for transient failures. */
|
|
52
48
|
const DEFAULT_MAX_RETRIES = 3;
|
|
53
49
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
function isProdBaseUrl(baseUrl: string): boolean {
|
|
57
|
-
return baseUrl.trim().replace(/\/$/, '') === PROD_URL;
|
|
58
|
-
}
|
|
50
|
+
const PROJECT_DEEPLINE_ENV_FILE = '.env.deepline';
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
return isProdBaseUrl(baseUrl) ? 'prod' : 'dev';
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function projectEnvStartDir(): string {
|
|
65
|
-
return process.env.DEEPLINE_PROJECT_ENV_DIR?.trim() || process.cwd();
|
|
66
|
-
}
|
|
52
|
+
type EnvValues = Record<string, string>;
|
|
67
53
|
|
|
68
54
|
/**
|
|
69
55
|
* Convert a base URL to a filesystem-safe slug for per-host config storage.
|
|
70
56
|
*
|
|
71
|
-
* @param baseUrl - Full URL (e.g. `"http://localhost:3000"`)
|
|
72
|
-
* @returns Slug like `"localhost-3000"` or `"code-deepline-com"`
|
|
73
|
-
*
|
|
74
57
|
* @example
|
|
75
58
|
* ```typescript
|
|
76
59
|
* baseUrlSlug('http://localhost:3000') // "localhost-3000"
|
|
@@ -86,7 +69,7 @@ function baseUrlSlug(baseUrl: string): string {
|
|
|
86
69
|
return 'unknown';
|
|
87
70
|
}
|
|
88
71
|
const host = url.hostname || 'unknown';
|
|
89
|
-
const port = url.port ? parseInt(url.port, 10) : null;
|
|
72
|
+
const port = url.port ? Number.parseInt(url.port, 10) : null;
|
|
90
73
|
let slug = host.replace(/[^a-zA-Z0-9]/g, '-');
|
|
91
74
|
if (port && port !== 80 && port !== 443) {
|
|
92
75
|
slug = `${slug}-${port}`;
|
|
@@ -96,20 +79,10 @@ function baseUrlSlug(baseUrl: string): string {
|
|
|
96
79
|
|
|
97
80
|
/**
|
|
98
81
|
* Parse a simple `KEY=VALUE` env file. Handles `#` comments and quoted values.
|
|
99
|
-
*
|
|
100
|
-
* @param filePath - Absolute path to the env file
|
|
101
|
-
* @returns Key-value pairs; empty object if file doesn't exist
|
|
102
|
-
*
|
|
103
|
-
* @example
|
|
104
|
-
* ```typescript
|
|
105
|
-
* // File: DEEPLINE_API_KEY="dl_test_abc123"
|
|
106
|
-
* const env = parseEnvFile('/path/to/.env');
|
|
107
|
-
* console.log(env.DEEPLINE_API_KEY); // "dl_test_abc123"
|
|
108
|
-
* ```
|
|
109
82
|
*/
|
|
110
|
-
function parseEnvFile(filePath: string):
|
|
83
|
+
function parseEnvFile(filePath: string): EnvValues {
|
|
111
84
|
if (!existsSync(filePath)) return {};
|
|
112
|
-
const env:
|
|
85
|
+
const env: EnvValues = {};
|
|
113
86
|
const content = readFileSync(filePath, 'utf-8');
|
|
114
87
|
for (const line of content.split(/\r?\n/)) {
|
|
115
88
|
const trimmed = line.trim();
|
|
@@ -118,7 +91,6 @@ function parseEnvFile(filePath: string): Record<string, string> {
|
|
|
118
91
|
if (eqIndex < 0) continue;
|
|
119
92
|
const key = trimmed.slice(0, eqIndex).trim();
|
|
120
93
|
let value = trimmed.slice(eqIndex + 1).trim();
|
|
121
|
-
// Strip surrounding quotes
|
|
122
94
|
if (
|
|
123
95
|
value.length >= 2 &&
|
|
124
96
|
((value.startsWith('"') && value.endsWith('"')) ||
|
|
@@ -134,223 +106,136 @@ function parseEnvFile(filePath: string): Record<string, string> {
|
|
|
134
106
|
}
|
|
135
107
|
|
|
136
108
|
function findNearestEnvFile(
|
|
137
|
-
|
|
109
|
+
name: string,
|
|
138
110
|
startDir: string = process.cwd(),
|
|
139
111
|
): string | null {
|
|
140
112
|
let current = resolve(startDir);
|
|
141
113
|
while (true) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (existsSync(filePath)) return filePath;
|
|
145
|
-
}
|
|
114
|
+
const filePath = join(current, name);
|
|
115
|
+
if (existsSync(filePath)) return filePath;
|
|
146
116
|
const parent = dirname(current);
|
|
147
117
|
if (parent === current) return null;
|
|
148
118
|
current = parent;
|
|
149
119
|
}
|
|
150
120
|
}
|
|
151
121
|
|
|
152
|
-
function
|
|
153
|
-
const filePath = findNearestEnvFile(
|
|
122
|
+
function loadProjectDeeplineEnv(startDir = process.cwd()): EnvValues {
|
|
123
|
+
const filePath = findNearestEnvFile(PROJECT_DEEPLINE_ENV_FILE, startDir);
|
|
154
124
|
return filePath ? parseEnvFile(filePath) : {};
|
|
155
125
|
}
|
|
156
126
|
|
|
157
|
-
function
|
|
158
|
-
|
|
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
|
-
|
|
200
|
-
function normalizeWorktreeBaseUrl(baseUrl: string, worktreeEnv = findNearestWorktreeEnv()): string {
|
|
201
|
-
const trimmed = baseUrl.trim().replace(/\/$/, '');
|
|
202
|
-
if (!trimmed) return trimmed;
|
|
127
|
+
function normalizeBaseUrl(baseUrl: string): string {
|
|
128
|
+
const trimmed = baseUrl.trim().replace(/\/+$/, '');
|
|
129
|
+
if (!trimmed) return '';
|
|
203
130
|
try {
|
|
204
131
|
const parsed = new URL(trimmed);
|
|
205
|
-
if (parsed.
|
|
206
|
-
|
|
207
|
-
if (port) return `${parsed.protocol}//localhost:${port}`;
|
|
132
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
133
|
+
return '';
|
|
208
134
|
}
|
|
209
|
-
|
|
210
|
-
|
|
135
|
+
return parsed.toString().replace(/\/+$/, '');
|
|
136
|
+
} catch {
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
211
139
|
}
|
|
212
140
|
|
|
213
|
-
function
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
'';
|
|
220
|
-
if (declared) return normalizeWorktreeBaseUrl(declared, worktreeEnv);
|
|
221
|
-
const port = worktreeEnv.WORKTREE_APP_PORT || worktreeEnv.PORT || '';
|
|
222
|
-
return port ? `http://localhost:${port}` : '';
|
|
141
|
+
function firstNonEmpty(...values: Array<string | undefined | null>): string {
|
|
142
|
+
for (const value of values) {
|
|
143
|
+
const trimmed = value?.trim();
|
|
144
|
+
if (trimmed) return trimmed;
|
|
145
|
+
}
|
|
146
|
+
return '';
|
|
223
147
|
}
|
|
224
148
|
|
|
225
|
-
|
|
226
|
-
* Load the SDK CLI env file for a specific Deepline host.
|
|
227
|
-
*
|
|
228
|
-
* @returns Key-value pairs from `~/.local/deepline/<host-slug>/.env`
|
|
229
|
-
*/
|
|
230
|
-
function sdkCliEnvFilePath(baseUrl: string): string {
|
|
149
|
+
function sdkCliConfigDir(baseUrl: string): string {
|
|
231
150
|
const home = process.env.HOME?.trim() || homedir();
|
|
232
|
-
return join(home, '.local', 'deepline', baseUrlSlug(baseUrl || PROD_URL)
|
|
151
|
+
return join(home, '.local', 'deepline', baseUrlSlug(baseUrl || PROD_URL));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sdkCliEnvFilePath(baseUrl: string): string {
|
|
155
|
+
return join(sdkCliConfigDir(baseUrl), '.env');
|
|
233
156
|
}
|
|
234
157
|
|
|
235
|
-
function loadCliEnv(baseUrl = PROD_URL):
|
|
236
|
-
|
|
237
|
-
|
|
158
|
+
function loadCliEnv(baseUrl = PROD_URL): EnvValues {
|
|
159
|
+
return parseEnvFile(sdkCliEnvFilePath(baseUrl));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function hostConfigDirPath(baseUrl: string): string {
|
|
163
|
+
return sdkCliConfigDir(baseUrl);
|
|
238
164
|
}
|
|
239
165
|
|
|
240
166
|
export function hostEnvFilePath(baseUrl: string): string {
|
|
241
167
|
return sdkCliEnvFilePath(baseUrl);
|
|
242
168
|
}
|
|
243
169
|
|
|
244
|
-
export function saveHostEnvValues(baseUrl: string, values:
|
|
170
|
+
export function saveHostEnvValues(baseUrl: string, values: EnvValues): void {
|
|
245
171
|
const filePath = sdkCliEnvFilePath(baseUrl);
|
|
246
172
|
const dir = dirname(filePath);
|
|
247
173
|
if (!existsSync(dir)) {
|
|
248
174
|
mkdirSync(dir, { recursive: true });
|
|
249
175
|
}
|
|
250
176
|
|
|
251
|
-
const existing =
|
|
177
|
+
const existing = parseEnvFile(filePath);
|
|
252
178
|
const merged = { ...existing, ...values };
|
|
179
|
+
const allowedKeys = new Set([HOST_URL_ENV, API_KEY_ENV]);
|
|
253
180
|
const lines = Object.entries(merged)
|
|
254
|
-
.filter(([, value]) => value !== '')
|
|
181
|
+
.filter(([key, value]) => allowedKeys.has(key) && value !== '')
|
|
255
182
|
.map(([key, value]) => `${key}=${value}`);
|
|
256
183
|
writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf-8');
|
|
257
184
|
}
|
|
258
185
|
|
|
259
|
-
|
|
260
|
-
* Load the production SDK CLI env file.
|
|
261
|
-
*
|
|
262
|
-
* This gives `deepline` a stable production default without sharing credentials
|
|
263
|
-
* with local/worktree hosts.
|
|
264
|
-
*/
|
|
265
|
-
function loadGlobalCliEnv(): Record<string, string> {
|
|
186
|
+
function loadGlobalCliEnv(): EnvValues {
|
|
266
187
|
return loadCliEnv(PROD_URL);
|
|
267
188
|
}
|
|
268
189
|
|
|
269
|
-
/**
|
|
270
|
-
* Check if a URL points to a local development server.
|
|
271
|
-
*
|
|
272
|
-
* @example
|
|
273
|
-
* ```typescript
|
|
274
|
-
* isLocalhost('http://localhost:3000') // true
|
|
275
|
-
* isLocalhost('http://127.0.0.1:3000') // true
|
|
276
|
-
* isLocalhost('https://code.deepline.com') // false
|
|
277
|
-
* ```
|
|
278
|
-
*/
|
|
279
190
|
/**
|
|
280
191
|
* Auto-detect the best base URL when none is explicitly provided.
|
|
281
|
-
*
|
|
282
|
-
* Checks environment variables first, then checkout-local worktree config,
|
|
283
|
-
* then SDK CLI config, before falling back to production.
|
|
284
192
|
*/
|
|
285
193
|
function autoDetectBaseUrl(): string {
|
|
286
|
-
const
|
|
287
|
-
if (envOrigin) return normalizeWorktreeBaseUrl(envOrigin);
|
|
288
|
-
|
|
289
|
-
const envBase = process.env.DEEPLINE_API_BASE_URL?.trim();
|
|
290
|
-
if (envBase) return normalizeWorktreeBaseUrl(envBase);
|
|
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
|
-
|
|
298
|
-
const worktreeBaseUrl = resolveWorktreeBaseUrl();
|
|
299
|
-
if (worktreeBaseUrl) return worktreeBaseUrl;
|
|
300
|
-
|
|
194
|
+
const projectEnv = loadProjectDeeplineEnv();
|
|
301
195
|
const globalEnv = loadGlobalCliEnv();
|
|
302
|
-
|
|
303
|
-
|
|
196
|
+
return (
|
|
197
|
+
normalizeBaseUrl(process.env[HOST_URL_ENV] ?? '') ||
|
|
198
|
+
normalizeBaseUrl(projectEnv[HOST_URL_ENV] ?? '') ||
|
|
199
|
+
normalizeBaseUrl(globalEnv[HOST_URL_ENV] ?? '') ||
|
|
200
|
+
PROD_URL
|
|
201
|
+
);
|
|
202
|
+
}
|
|
304
203
|
|
|
305
|
-
|
|
204
|
+
export function resolveApiKeyForBaseUrl(
|
|
205
|
+
baseUrl: string,
|
|
206
|
+
explicitApiKey?: string | null,
|
|
207
|
+
): string {
|
|
208
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
209
|
+
const projectEnv = loadProjectDeeplineEnv();
|
|
210
|
+
const cliEnv = loadCliEnv(normalizedBaseUrl || baseUrl);
|
|
211
|
+
const projectBaseUrl = normalizeBaseUrl(projectEnv[HOST_URL_ENV] ?? '');
|
|
212
|
+
const projectKeyApplies = projectBaseUrl === normalizedBaseUrl;
|
|
213
|
+
return firstNonEmpty(
|
|
214
|
+
explicitApiKey,
|
|
215
|
+
process.env[API_KEY_ENV],
|
|
216
|
+
projectKeyApplies ? projectEnv[API_KEY_ENV] : '',
|
|
217
|
+
cliEnv[API_KEY_ENV],
|
|
218
|
+
);
|
|
306
219
|
}
|
|
307
220
|
|
|
308
221
|
/**
|
|
309
|
-
* Resolve SDK configuration from
|
|
310
|
-
*
|
|
311
|
-
* Merges explicit options, environment variables, and CLI-managed config files
|
|
312
|
-
* into a fully validated {@link ResolvedConfig}. See the module-level docs for
|
|
313
|
-
* the complete resolution order.
|
|
314
|
-
*
|
|
315
|
-
* @param options - Optional overrides (highest priority)
|
|
316
|
-
* @returns Fully resolved configuration with all fields populated
|
|
317
|
-
* @throws {@link ConfigError} if no API key can be found from any source
|
|
318
|
-
*
|
|
319
|
-
* @example
|
|
320
|
-
* ```typescript
|
|
321
|
-
* import { resolveConfig } from 'deepline';
|
|
322
|
-
*
|
|
323
|
-
* // Auto-resolve everything:
|
|
324
|
-
* const config = resolveConfig();
|
|
325
|
-
* console.log(config.baseUrl); // "http://localhost:3000" or "https://code.deepline.com"
|
|
326
|
-
*
|
|
327
|
-
* // Override specific values:
|
|
328
|
-
* const config2 = resolveConfig({ baseUrl: 'http://localhost:4000', timeout: 10_000 });
|
|
329
|
-
* ```
|
|
222
|
+
* Resolve SDK configuration from the public SDK CLI env contract.
|
|
330
223
|
*/
|
|
331
224
|
export function resolveConfig(options?: DeeplineClientOptions): ResolvedConfig {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const projectAppEnv = loadProjectAppEnv();
|
|
225
|
+
const baseUrl = normalizeBaseUrl(
|
|
226
|
+
options?.baseUrl?.trim() || autoDetectBaseUrl(),
|
|
227
|
+
);
|
|
228
|
+
if (!baseUrl) {
|
|
229
|
+
throw new ConfigError(
|
|
230
|
+
`Invalid ${HOST_URL_ENV}. Expected an http(s) URL such as https://code.deepline.com.`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
341
233
|
|
|
342
|
-
|
|
343
|
-
const apiKey =
|
|
344
|
-
options?.apiKey?.trim() ||
|
|
345
|
-
process.env.DEEPLINE_API_KEY?.trim() ||
|
|
346
|
-
projectDeeplineEnv.DEEPLINE_API_KEY ||
|
|
347
|
-
projectAppEnv.DEEPLINE_API_KEY ||
|
|
348
|
-
cliEnv.DEEPLINE_API_KEY ||
|
|
349
|
-
'';
|
|
234
|
+
const apiKey = resolveApiKeyForBaseUrl(baseUrl, options?.apiKey);
|
|
350
235
|
|
|
351
236
|
if (!apiKey) {
|
|
352
237
|
throw new ConfigError(
|
|
353
|
-
`No API key found. Set
|
|
238
|
+
`No API key found. Set ${API_KEY_ENV}, add it to .env.deepline, or run: deepline auth register`,
|
|
354
239
|
);
|
|
355
240
|
}
|
|
356
241
|
|
|
@@ -362,34 +247,25 @@ export function resolveConfig(options?: DeeplineClientOptions): ResolvedConfig {
|
|
|
362
247
|
};
|
|
363
248
|
}
|
|
364
249
|
|
|
365
|
-
function
|
|
366
|
-
const existing =
|
|
250
|
+
function mergeProjectEnvFile(filePath: string, values: EnvValues): void {
|
|
251
|
+
const existing = parseEnvFile(filePath);
|
|
367
252
|
const merged = { ...existing, ...values };
|
|
368
253
|
const dir = dirname(filePath);
|
|
369
254
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
255
|
+
const allowedKeys = new Set([HOST_URL_ENV, API_KEY_ENV]);
|
|
370
256
|
const lines = Object.entries(merged)
|
|
371
|
-
.filter(([, value]) => value !== '')
|
|
257
|
+
.filter(([key, value]) => allowedKeys.has(key) && value !== '')
|
|
372
258
|
.map(([key, value]) => `${key}=${value}`);
|
|
373
259
|
writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf-8');
|
|
374
260
|
}
|
|
375
261
|
|
|
376
262
|
export function saveProjectDeeplineEnvValues(
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
startDir: string = projectEnvStartDir(),
|
|
263
|
+
values: EnvValues,
|
|
264
|
+
startDir: string = process.cwd(),
|
|
380
265
|
): string[] {
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
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;
|
|
266
|
+
const filePath = join(resolve(startDir), PROJECT_DEEPLINE_ENV_FILE);
|
|
267
|
+
mergeProjectEnvFile(filePath, values);
|
|
268
|
+
return [filePath];
|
|
393
269
|
}
|
|
394
270
|
|
|
395
271
|
export {
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
interface RequestOptions {
|
|
32
32
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
33
33
|
body?: unknown;
|
|
34
|
-
formData?: FormData;
|
|
34
|
+
formData?: FormData | (() => FormData);
|
|
35
35
|
headers?: Record<string, string>;
|
|
36
36
|
/** Per-request timeout override in milliseconds. */
|
|
37
37
|
timeout?: number;
|
|
@@ -153,7 +153,9 @@ export class HttpClient {
|
|
|
153
153
|
headers,
|
|
154
154
|
body:
|
|
155
155
|
options?.formData !== undefined
|
|
156
|
-
? options.formData
|
|
156
|
+
? typeof options.formData === 'function'
|
|
157
|
+
? options.formData()
|
|
158
|
+
: options.formData
|
|
157
159
|
: options?.body !== undefined
|
|
158
160
|
? JSON.stringify(options.body)
|
|
159
161
|
: undefined,
|
|
@@ -277,10 +279,13 @@ export class HttpClient {
|
|
|
277
279
|
throw new AuthError();
|
|
278
280
|
}
|
|
279
281
|
if (!response.ok) {
|
|
282
|
+
const body = await response.text();
|
|
283
|
+
const parsed = parseResponseBody(body);
|
|
280
284
|
throw new DeeplineError(
|
|
281
|
-
|
|
285
|
+
apiErrorMessage(parsed, response.status),
|
|
282
286
|
response.status,
|
|
283
287
|
'API_ERROR',
|
|
288
|
+
{ response: parsed },
|
|
284
289
|
);
|
|
285
290
|
}
|
|
286
291
|
if (!response.body) {
|
|
@@ -325,7 +330,7 @@ export class HttpClient {
|
|
|
325
330
|
|
|
326
331
|
async postFormData<T = unknown>(
|
|
327
332
|
path: string,
|
|
328
|
-
formData: FormData,
|
|
333
|
+
formData: FormData | (() => FormData),
|
|
329
334
|
headers?: Record<string, string>,
|
|
330
335
|
): Promise<T> {
|
|
331
336
|
return this.request<T>(path, {
|
|
@@ -346,6 +351,41 @@ export class HttpClient {
|
|
|
346
351
|
}
|
|
347
352
|
}
|
|
348
353
|
|
|
354
|
+
function parseResponseBody(body: string): unknown {
|
|
355
|
+
try {
|
|
356
|
+
return JSON.parse(body);
|
|
357
|
+
} catch {
|
|
358
|
+
return body;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function apiErrorMessage(parsed: unknown, status: number): string {
|
|
363
|
+
const errorValue =
|
|
364
|
+
typeof parsed === 'object' && parsed && 'error' in parsed
|
|
365
|
+
? (parsed as Record<string, unknown>).error
|
|
366
|
+
: undefined;
|
|
367
|
+
if (typeof errorValue === 'string') {
|
|
368
|
+
return errorValue;
|
|
369
|
+
}
|
|
370
|
+
if (
|
|
371
|
+
errorValue &&
|
|
372
|
+
typeof errorValue === 'object' &&
|
|
373
|
+
'message' in errorValue &&
|
|
374
|
+
typeof (errorValue as Record<string, unknown>).message === 'string'
|
|
375
|
+
) {
|
|
376
|
+
return (errorValue as Record<string, string>).message;
|
|
377
|
+
}
|
|
378
|
+
if (
|
|
379
|
+
typeof parsed === 'object' &&
|
|
380
|
+
parsed &&
|
|
381
|
+
'message' in parsed &&
|
|
382
|
+
typeof (parsed as Record<string, unknown>).message === 'string'
|
|
383
|
+
) {
|
|
384
|
+
return (parsed as Record<string, string>).message;
|
|
385
|
+
}
|
|
386
|
+
return `HTTP ${status}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
349
389
|
/** Parse the `Retry-After` header as milliseconds. Falls back to 5000ms. */
|
|
350
390
|
function parseRetryAfter(response: Response): number {
|
|
351
391
|
const header = response.headers.get('retry-after');
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
* import { Deepline, definePlay } from 'deepline';
|
|
8
8
|
*
|
|
9
9
|
* // Connect (auto-resolves API key from env / CLI config)
|
|
10
|
-
* const
|
|
10
|
+
* const deepline = await Deepline.connect();
|
|
11
11
|
*
|
|
12
12
|
* // Execute a tool
|
|
13
|
-
* const result = await
|
|
14
|
-
* const company = result.
|
|
13
|
+
* const result = await deepline.tools.execute('test_company_search', { domain: 'stripe.com' });
|
|
14
|
+
* const company = result.toolOutput.raw;
|
|
15
15
|
*
|
|
16
16
|
* // Run a named play
|
|
17
|
-
* const job = await
|
|
17
|
+
* const job = await deepline.play('email-waterfall').run({ domain: 'stripe.com' });
|
|
18
18
|
* const result = await job.get();
|
|
19
19
|
* ```
|
|
20
20
|
*
|
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
*
|
|
26
26
|
* export default definePlay('my-play', async (ctx, input: { domain: string }) => {
|
|
27
27
|
* ctx.log(`Looking up ${input.domain}`);
|
|
28
|
-
* const company = await ctx.tools.execute(
|
|
28
|
+
* const company = await ctx.tools.execute({
|
|
29
|
+
* id: 'company_search',
|
|
30
|
+
* tool: 'test_company_search',
|
|
31
|
+
* input: { domain: input.domain },
|
|
29
32
|
* description: 'Look up company details by domain.',
|
|
30
33
|
* });
|
|
31
34
|
* return company;
|