deepline 0.1.136 → 0.1.138

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 CHANGED
@@ -65,8 +65,8 @@ Do not route the SDK CLI through `.env`, `.env.local`, or `.env.worktree`.
65
65
  Config resolution order:
66
66
 
67
67
  ```text
68
- host: explicit SDK option -> DEEPLINE_HOST_URL -> nearest .env.deepline -> saved production auth -> https://code.deepline.com
69
- key: explicit SDK option -> DEEPLINE_API_KEY -> matching nearest .env.deepline -> saved auth for the resolved host
68
+ host: explicit SDK option -> DEEPLINE_HOST_URL -> nearest .env.deepline -> Cowork workspace .env.deepline -> saved production auth -> https://code.deepline.com
69
+ key: explicit SDK option -> DEEPLINE_API_KEY -> matching nearest .env.deepline -> matching Cowork workspace .env.deepline -> saved auth for the resolved host
70
70
  ```
71
71
 
72
72
  ## CLI commands
@@ -15,21 +15,31 @@
15
15
  * 1. `options.baseUrl`
16
16
  * 2. `DEEPLINE_HOST_URL`
17
17
  * 3. nearest project `.env.deepline`
18
- * 4. production host auth file
19
- * 5. production fallback: `https://code.deepline.com`
18
+ * 4. Cowork mounted workspace `.env.deepline`
19
+ * 5. production host auth file
20
+ * 6. production fallback: `https://code.deepline.com`
20
21
  *
21
22
  * API key:
22
23
  * 1. `options.apiKey`
23
24
  * 2. `DEEPLINE_API_KEY`
24
25
  * 3. nearest project `.env.deepline`
25
- * 4. host auth file for the resolved base URL
26
+ * 4. Cowork mounted workspace `.env.deepline`
27
+ * 5. host auth file for the resolved base URL
26
28
  *
27
29
  * App/runtime env files such as `.env`, `.env.local`, and `.env.worktree` do
28
30
  * not route the SDK CLI. Put CLI routing in `.env.deepline`.
29
31
  *
30
32
  * @module
31
33
  */
32
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
34
+ import {
35
+ existsSync,
36
+ mkdirSync,
37
+ readdirSync,
38
+ realpathSync,
39
+ readFileSync,
40
+ statSync,
41
+ writeFileSync,
42
+ } from 'node:fs';
33
43
  import { homedir } from 'node:os';
34
44
  import { dirname, join, resolve } from 'node:path';
35
45
  import type { DeeplineClientOptions, ResolvedConfig } from './types.js';
@@ -48,8 +58,41 @@ const DEFAULT_TIMEOUT = 60_000;
48
58
  const DEFAULT_MAX_RETRIES = 3;
49
59
 
50
60
  const PROJECT_DEEPLINE_ENV_FILE = '.env.deepline';
61
+ const COWORK_IGNORED_WORKSPACE_DIRS = new Set([
62
+ '.auto-memory',
63
+ '.claude',
64
+ '.remote-plugins',
65
+ 'outputs',
66
+ 'plugins',
67
+ 'uploads',
68
+ ]);
69
+ const COWORK_PROJECT_MARKERS = [
70
+ '.deepline',
71
+ '.env.deepline',
72
+ '.git',
73
+ 'AGENTS.md',
74
+ 'package.json',
75
+ 'pyproject.toml',
76
+ ];
51
77
 
52
78
  type EnvValues = Record<string, string>;
79
+ type ProjectEnvCandidate = {
80
+ filePath: string;
81
+ env: EnvValues;
82
+ source: 'nearest' | 'cowork';
83
+ };
84
+ export type ProjectAuthSource = ProjectEnvCandidate;
85
+ export type ProjectPinTarget =
86
+ | {
87
+ ok: true;
88
+ dir: string;
89
+ source: 'nearest' | 'cowork' | 'cwd';
90
+ }
91
+ | {
92
+ ok: false;
93
+ reason: 'ambiguous_cowork_project';
94
+ candidates: string[];
95
+ };
53
96
 
54
97
  /**
55
98
  * Convert a base URL to a filesystem-safe slug for per-host config storage.
@@ -119,9 +162,161 @@ function findNearestEnvFile(
119
162
  }
120
163
  }
121
164
 
165
+ function isDirectory(path: string): boolean {
166
+ try {
167
+ return statSync(path).isDirectory();
168
+ } catch {
169
+ return false;
170
+ }
171
+ }
172
+
173
+ function canonicalPath(path: string): string {
174
+ try {
175
+ return realpathSync(path);
176
+ } catch {
177
+ return resolve(path);
178
+ }
179
+ }
180
+
181
+ function isTruthy(value: string | undefined): boolean {
182
+ return /^(1|true|yes|on)$/i.test(value?.trim() ?? '');
183
+ }
184
+
185
+ function sessionRootFromPath(path: string | undefined): string | null {
186
+ const trimmed = path?.trim();
187
+ if (!trimmed) return null;
188
+ const match = /^\/sessions\/[^/]+(?=\/|$)/.exec(trimmed);
189
+ return match?.[0] ?? null;
190
+ }
191
+
192
+ function coworkSessionRoot(): string | null {
193
+ const home = process.env.HOME?.trim();
194
+ const homeSessionRoot = sessionRootFromPath(home);
195
+ if (homeSessionRoot && isDirectory(join(homeSessionRoot, 'mnt'))) {
196
+ return homeSessionRoot;
197
+ }
198
+
199
+ const cwdSessionRoot = sessionRootFromPath(process.cwd());
200
+ if (cwdSessionRoot && isDirectory(join(cwdSessionRoot, 'mnt'))) {
201
+ return cwdSessionRoot;
202
+ }
203
+
204
+ if (isTruthy(process.env.CLAUDE_CODE_REMOTE) && home) {
205
+ const mountedRoot = join(home, 'mnt');
206
+ if (isDirectory(mountedRoot)) return resolve(home);
207
+ }
208
+
209
+ return null;
210
+ }
211
+
212
+ function isCoworkLikeSandbox(): boolean {
213
+ const home = process.env.HOME?.trim();
214
+ return (
215
+ isTruthy(process.env.CLAUDE_CODE_REMOTE) ||
216
+ sessionRootFromPath(home) !== null ||
217
+ sessionRootFromPath(process.cwd()) !== null
218
+ );
219
+ }
220
+
221
+ function coworkProjectScore(path: string): number {
222
+ let score = 0;
223
+ for (const marker of COWORK_PROJECT_MARKERS) {
224
+ if (existsSync(join(path, marker))) score += 1;
225
+ }
226
+ return score;
227
+ }
228
+
229
+ function listCoworkWorkspaceDirCandidates(): string[] {
230
+ if (!isCoworkLikeSandbox()) {
231
+ return [];
232
+ }
233
+
234
+ const explicitProjectDir = process.env.CLAUDE_PROJECT_DIR?.trim();
235
+ if (explicitProjectDir && isDirectory(explicitProjectDir)) {
236
+ return [resolve(explicitProjectDir)];
237
+ }
238
+
239
+ const sessionRoot = coworkSessionRoot();
240
+ if (!sessionRoot) return [];
241
+
242
+ const mountedRoot = join(sessionRoot, 'mnt');
243
+ if (!isDirectory(mountedRoot)) return [];
244
+
245
+ let names: string[];
246
+ try {
247
+ names = readdirSync(mountedRoot).sort();
248
+ } catch {
249
+ return [];
250
+ }
251
+
252
+ const candidates: string[] = [];
253
+ for (const name of names) {
254
+ if (name.startsWith('.') || COWORK_IGNORED_WORKSPACE_DIRS.has(name)) {
255
+ continue;
256
+ }
257
+ const candidate = join(mountedRoot, name);
258
+ if (isDirectory(candidate)) candidates.push(candidate);
259
+ }
260
+
261
+ if (candidates.length <= 1) return candidates;
262
+
263
+ const projectLike = candidates.filter(
264
+ (candidate) => coworkProjectScore(candidate) > 0,
265
+ );
266
+ return projectLike.length > 0 ? projectLike : candidates;
267
+ }
268
+
269
+ function isInIgnoredCoworkMount(path: string): boolean {
270
+ const sessionRoot = coworkSessionRoot();
271
+ if (!sessionRoot) return false;
272
+ const mountedRoot = canonicalPath(join(sessionRoot, 'mnt'));
273
+ const resolvedPath = canonicalPath(path);
274
+ const prefix = `${mountedRoot}/`;
275
+ if (!resolvedPath.startsWith(prefix)) return false;
276
+ const relativePath = resolvedPath.slice(prefix.length);
277
+ const mountName = relativePath.split('/')[0];
278
+ return (
279
+ mountName.startsWith('.') || COWORK_IGNORED_WORKSPACE_DIRS.has(mountName)
280
+ );
281
+ }
282
+
283
+ function detectCoworkWorkspaceDir(): string | null {
284
+ const candidates = listCoworkWorkspaceDirCandidates();
285
+ return candidates.length === 1 ? candidates[0] : null;
286
+ }
287
+
288
+ function loadProjectEnvCandidates(
289
+ startDir: string = process.cwd(),
290
+ ): ProjectEnvCandidate[] {
291
+ const filePaths: string[] = [];
292
+ const sources = new Map<string, ProjectEnvCandidate['source']>();
293
+ const nearestFile = findNearestEnvFile(PROJECT_DEEPLINE_ENV_FILE, startDir);
294
+ if (nearestFile && !isInIgnoredCoworkMount(nearestFile)) {
295
+ filePaths.push(nearestFile);
296
+ sources.set(resolve(nearestFile), 'nearest');
297
+ }
298
+
299
+ const coworkWorkspaceDir = detectCoworkWorkspaceDir();
300
+ if (coworkWorkspaceDir) {
301
+ const coworkFile = join(coworkWorkspaceDir, PROJECT_DEEPLINE_ENV_FILE);
302
+ if (
303
+ existsSync(coworkFile) &&
304
+ !filePaths.some((filePath) => resolve(filePath) === resolve(coworkFile))
305
+ ) {
306
+ filePaths.push(coworkFile);
307
+ sources.set(resolve(coworkFile), 'cowork');
308
+ }
309
+ }
310
+
311
+ return filePaths.map((filePath) => ({
312
+ filePath,
313
+ env: parseEnvFile(filePath),
314
+ source: sources.get(resolve(filePath)) ?? 'nearest',
315
+ }));
316
+ }
317
+
122
318
  function loadProjectDeeplineEnv(startDir = process.cwd()): EnvValues {
123
- const filePath = findNearestEnvFile(PROJECT_DEEPLINE_ENV_FILE, startDir);
124
- return filePath ? parseEnvFile(filePath) : {};
319
+ return loadProjectEnvCandidates(startDir)[0]?.env ?? {};
125
320
  }
126
321
 
127
322
  function normalizeBaseUrl(baseUrl: string): string {
@@ -204,11 +399,13 @@ function loadGlobalCliEnv(): EnvValues {
204
399
  * Auto-detect the best base URL when none is explicitly provided.
205
400
  */
206
401
  function autoDetectBaseUrl(): string {
207
- const projectEnv = loadProjectDeeplineEnv();
402
+ const projectEnvs = loadProjectEnvCandidates();
208
403
  const globalEnv = loadGlobalCliEnv();
209
404
  return (
210
405
  normalizeBaseUrl(process.env[HOST_URL_ENV] ?? '') ||
211
- normalizeBaseUrl(projectEnv[HOST_URL_ENV] ?? '') ||
406
+ firstNonEmpty(
407
+ ...projectEnvs.map(({ env }) => normalizeBaseUrl(env[HOST_URL_ENV])),
408
+ ) ||
212
409
  normalizeBaseUrl(globalEnv[HOST_URL_ENV] ?? '') ||
213
410
  PROD_URL
214
411
  );
@@ -219,18 +416,38 @@ export function resolveApiKeyForBaseUrl(
219
416
  explicitApiKey?: string | null,
220
417
  ): string {
221
418
  const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
222
- const projectEnv = loadProjectDeeplineEnv();
419
+ const projectEnvs = loadProjectEnvCandidates();
223
420
  const cliEnv = loadCliEnv(normalizedBaseUrl || baseUrl);
224
- const projectBaseUrl = normalizeBaseUrl(projectEnv[HOST_URL_ENV] ?? '');
225
- const projectKeyApplies = projectBaseUrl === normalizedBaseUrl;
226
421
  return firstNonEmpty(
227
422
  explicitApiKey,
228
423
  process.env[API_KEY_ENV],
229
- projectKeyApplies ? projectEnv[API_KEY_ENV] : '',
424
+ ...projectEnvs.map(({ env }) => {
425
+ const projectBaseUrl = normalizeBaseUrl(env[HOST_URL_ENV] ?? '');
426
+ return projectBaseUrl === normalizedBaseUrl ? env[API_KEY_ENV] : '';
427
+ }),
230
428
  cliEnv[API_KEY_ENV],
231
429
  );
232
430
  }
233
431
 
432
+ function getResolvedProjectAuthSource(
433
+ baseUrl: string,
434
+ apiKey: string,
435
+ startDir: string = process.cwd(),
436
+ ): ProjectAuthSource | null {
437
+ const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
438
+ const normalizedApiKey = apiKey.trim();
439
+ if (!normalizedBaseUrl || !normalizedApiKey) return null;
440
+ return (
441
+ loadProjectEnvCandidates(startDir).find(({ env }) => {
442
+ const projectBaseUrl = normalizeBaseUrl(env[HOST_URL_ENV] ?? '');
443
+ return (
444
+ projectBaseUrl === normalizedBaseUrl &&
445
+ (env[API_KEY_ENV] ?? '').trim() === normalizedApiKey
446
+ );
447
+ }) ?? null
448
+ );
449
+ }
450
+
234
451
  /**
235
452
  * Resolve SDK configuration from the public SDK CLI env contract.
236
453
  */
@@ -265,6 +482,7 @@ function mergeProjectEnvFile(filePath: string, values: EnvValues): void {
265
482
  const merged = { ...existing, ...values };
266
483
  const dir = dirname(filePath);
267
484
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
485
+ ensureProjectEnvIsIgnored(dir);
268
486
  const allowedKeys = new Set([HOST_URL_ENV, API_KEY_ENV]);
269
487
  const lines = Object.entries(merged)
270
488
  .filter(([key, value]) => allowedKeys.has(key) && value !== '')
@@ -272,20 +490,99 @@ function mergeProjectEnvFile(filePath: string, values: EnvValues): void {
272
490
  writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf-8');
273
491
  }
274
492
 
493
+ function ensureProjectEnvIsIgnored(dir: string): void {
494
+ const gitignorePath = join(dir, '.gitignore');
495
+ const entry = PROJECT_DEEPLINE_ENV_FILE;
496
+ const existing = existsSync(gitignorePath)
497
+ ? readFileSync(gitignorePath, 'utf-8')
498
+ : '';
499
+ const alreadyIgnored = existing
500
+ .split(/\r?\n/)
501
+ .map((line) => line.trim())
502
+ .some((line) => line === entry || line === `/${entry}`);
503
+ if (alreadyIgnored) return;
504
+ const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
505
+ writeFileSync(gitignorePath, `${existing}${prefix}${entry}\n`, 'utf-8');
506
+ }
507
+
275
508
  export function saveProjectDeeplineEnvValues(
276
509
  values: EnvValues,
277
510
  startDir: string = process.cwd(),
278
511
  ): string[] {
279
- const filePath = join(resolve(startDir), PROJECT_DEEPLINE_ENV_FILE);
512
+ const target = resolveProjectPinTarget(startDir);
513
+ if (!target.ok) {
514
+ throw new ConfigError(
515
+ `Cowork project folder is ambiguous. Candidate folders: ${target.candidates.join(
516
+ ', ',
517
+ )}. Set CLAUDE_PROJECT_DIR or cd into the intended project folder before running this command.`,
518
+ );
519
+ }
520
+ const filePath = join(target.dir, PROJECT_DEEPLINE_ENV_FILE);
280
521
  mergeProjectEnvFile(filePath, values);
281
522
  return [filePath];
282
523
  }
283
524
 
525
+ export function resolveProjectPinDir(startDir: string = process.cwd()): string {
526
+ const target = resolveProjectPinTarget(startDir);
527
+ if (!target.ok) {
528
+ throw new ConfigError(
529
+ `Cowork project folder is ambiguous. Candidate folders: ${target.candidates.join(
530
+ ', ',
531
+ )}. Set CLAUDE_PROJECT_DIR or cd into the intended project folder before running this command.`,
532
+ );
533
+ }
534
+ return target.dir;
535
+ }
536
+
537
+ export function resolveProjectPinTarget(
538
+ startDir: string = process.cwd(),
539
+ ): ProjectPinTarget {
540
+ const nearestFile = findNearestEnvFile(PROJECT_DEEPLINE_ENV_FILE, startDir);
541
+ if (nearestFile && !isInIgnoredCoworkMount(nearestFile)) {
542
+ return { ok: true, dir: dirname(nearestFile), source: 'nearest' };
543
+ }
544
+
545
+ const coworkCandidates = listCoworkWorkspaceDirCandidates();
546
+ if (coworkCandidates.length === 1) {
547
+ return { ok: true, dir: coworkCandidates[0], source: 'cowork' };
548
+ }
549
+ if (coworkCandidates.length > 1) {
550
+ const resolvedStartDir = canonicalPath(startDir);
551
+ const cwdCandidate = coworkCandidates.find((candidate) => {
552
+ const resolvedCandidate = canonicalPath(candidate);
553
+ return (
554
+ resolvedStartDir === resolvedCandidate ||
555
+ resolvedStartDir.startsWith(`${resolvedCandidate}/`)
556
+ );
557
+ });
558
+ if (cwdCandidate) {
559
+ return { ok: true, dir: cwdCandidate, source: 'cowork' };
560
+ }
561
+ return {
562
+ ok: false,
563
+ reason: 'ambiguous_cowork_project',
564
+ candidates: coworkCandidates,
565
+ };
566
+ }
567
+
568
+ return { ok: true, dir: resolve(startDir), source: 'cwd' };
569
+ }
570
+
571
+ export function getActiveProjectAuthSource(
572
+ startDir: string = process.cwd(),
573
+ ): ProjectAuthSource | null {
574
+ return loadProjectEnvCandidates(startDir)[0] ?? null;
575
+ }
576
+
284
577
  export {
285
578
  baseUrlSlug,
286
579
  loadCliEnv,
287
580
  loadGlobalCliEnv,
581
+ loadProjectDeeplineEnv,
582
+ getResolvedProjectAuthSource,
583
+ listCoworkWorkspaceDirCandidates,
288
584
  parseEnvFile,
585
+ detectCoworkWorkspaceDir,
289
586
  autoDetectBaseUrl,
290
587
  PROD_URL,
291
588
  };
@@ -101,10 +101,10 @@ export const SDK_RELEASE = {
101
101
  // 0.1.108 ships explicit dataset column/tool recompute policy and removes
102
102
  // the SDK enrich generator's one-second stale policy.
103
103
  // 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
104
- version: '0.1.136',
104
+ version: '0.1.138',
105
105
  apiContract: '2026-06-dataset-column-cell-stale-hard-cutover',
106
106
  supportPolicy: {
107
- latest: '0.1.136',
107
+ latest: '0.1.138',
108
108
  minimumSupported: '0.1.53',
109
109
  deprecatedBelow: '0.1.53',
110
110
  commandMinimumSupported: [