deepline 0.3.63 → 0.3.65

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.
@@ -47,6 +47,8 @@ import { ConfigError } from './errors.js';
47
47
 
48
48
  export const HOST_URL_ENV = 'DEEPLINE_HOST_URL';
49
49
  export const API_KEY_ENV = 'DEEPLINE_API_KEY';
50
+ export const ACTIVE_ORG_ID_ENV = 'DEEPLINE_ACTIVE_ORG_ID';
51
+ export const ACTIVE_ORG_NAME_ENV = 'DEEPLINE_ACTIVE_ORG_NAME';
50
52
 
51
53
  /** Production API base URL. */
52
54
  const PROD_URL = 'https://code.deepline.com';
@@ -76,6 +78,21 @@ const COWORK_PROJECT_MARKERS = [
76
78
  ];
77
79
 
78
80
  type EnvValues = Record<string, string>;
81
+ const CLI_ENV_ALLOWED_KEYS = new Set([
82
+ HOST_URL_ENV,
83
+ API_KEY_ENV,
84
+ ACTIVE_ORG_ID_ENV,
85
+ ACTIVE_ORG_NAME_ENV,
86
+ ]);
87
+
88
+ export type CliCommandContext = {
89
+ active_organization: {
90
+ org_id: string;
91
+ org_name: string | null;
92
+ } | null;
93
+ auth_scope: 'env' | 'folder' | 'global' | null;
94
+ metadata_source: 'process_env' | 'folder_env' | 'host_env' | null;
95
+ };
79
96
  type ProjectEnvCandidate = {
80
97
  filePath: string;
81
98
  env: EnvValues;
@@ -155,10 +172,16 @@ function parseEnvFile(filePath: string): EnvValues {
155
172
  if (eqIndex < 0) continue;
156
173
  const key = trimmed.slice(0, eqIndex).trim();
157
174
  let value = trimmed.slice(eqIndex + 1).trim();
158
- if (
175
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
176
+ try {
177
+ value = JSON.parse(value) as string;
178
+ } catch {
179
+ value = value.slice(1, -1);
180
+ }
181
+ } else if (
159
182
  value.length >= 2 &&
160
- ((value.startsWith('"') && value.endsWith('"')) ||
161
- (value.startsWith("'") && value.endsWith("'")))
183
+ value.startsWith("'") &&
184
+ value.endsWith("'")
162
185
  ) {
163
186
  value = value.slice(1, -1);
164
187
  }
@@ -362,6 +385,35 @@ function firstNonEmpty(...values: Array<string | undefined | null>): string {
362
385
  return '';
363
386
  }
364
387
 
388
+ function formatEnvFileValue(value: string): string {
389
+ return /^[a-zA-Z0-9_./:@%+=,-]+$/.test(value) ? value : JSON.stringify(value);
390
+ }
391
+
392
+ function mergePersistedEnvValues(
393
+ existing: EnvValues,
394
+ values: EnvValues,
395
+ ): EnvValues {
396
+ const merged = { ...existing, ...values };
397
+ const nextApiKey = values[API_KEY_ENV];
398
+ if (
399
+ nextApiKey !== undefined &&
400
+ nextApiKey !== existing[API_KEY_ENV] &&
401
+ values[ACTIVE_ORG_ID_ENV] === undefined
402
+ ) {
403
+ delete merged[ACTIVE_ORG_ID_ENV];
404
+ delete merged[ACTIVE_ORG_NAME_ENV];
405
+ }
406
+ return merged;
407
+ }
408
+
409
+ function persistedEnvLines(values: EnvValues): string[] {
410
+ return Object.entries(values)
411
+ .filter(
412
+ ([key, value]) => CLI_ENV_ALLOWED_KEYS.has(key) && value.trim() !== '',
413
+ )
414
+ .map(([key, value]) => `${key}=${formatEnvFileValue(value)}`);
415
+ }
416
+
365
417
  function sdkCliConfigDir(baseUrl: string): string {
366
418
  const home = process.env.HOME?.trim() || homedir();
367
419
  return join(home, '.local', 'deepline', baseUrlSlug(baseUrl || PROD_URL));
@@ -404,11 +456,8 @@ export function saveHostEnvValues(baseUrl: string, values: EnvValues): void {
404
456
  }
405
457
 
406
458
  const existing = parseEnvFile(filePath);
407
- const merged = { ...existing, ...values };
408
- const allowedKeys = new Set([HOST_URL_ENV, API_KEY_ENV]);
409
- const lines = Object.entries(merged)
410
- .filter(([key, value]) => allowedKeys.has(key) && value !== '')
411
- .map(([key, value]) => `${key}=${value}`);
459
+ const merged = mergePersistedEnvValues(existing, values);
460
+ const lines = persistedEnvLines(merged);
412
461
  writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf-8');
413
462
  }
414
463
 
@@ -520,14 +569,11 @@ export function resolveConfig(options?: DeeplineClientOptions): ResolvedConfig {
520
569
 
521
570
  function mergeProjectEnvFile(filePath: string, values: EnvValues): void {
522
571
  const existing = parseEnvFile(filePath);
523
- const merged = { ...existing, ...values };
572
+ const merged = mergePersistedEnvValues(existing, values);
524
573
  const dir = dirname(filePath);
525
574
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
526
575
  ensureProjectEnvIsIgnored(dir);
527
- const allowedKeys = new Set([HOST_URL_ENV, API_KEY_ENV]);
528
- const lines = Object.entries(merged)
529
- .filter(([key, value]) => allowedKeys.has(key) && value !== '')
530
- .map(([key, value]) => `${key}=${value}`);
576
+ const lines = persistedEnvLines(merged);
531
577
  writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf-8');
532
578
  }
533
579
 
@@ -766,6 +812,100 @@ export function resolveCliAuthProvenance(
766
812
  };
767
813
  }
768
814
 
815
+ function activeOrganizationFromEnv(
816
+ env: EnvValues | NodeJS.ProcessEnv,
817
+ ): CliCommandContext['active_organization'] {
818
+ const orgId = env[ACTIVE_ORG_ID_ENV]?.trim();
819
+ if (!orgId) return null;
820
+ const orgName = env[ACTIVE_ORG_NAME_ENV]?.trim() || null;
821
+ return { org_id: orgId, org_name: orgName };
822
+ }
823
+
824
+ /**
825
+ * Resolve display-only organization metadata from the exact credential source
826
+ * selected for this CLI process. The metadata never participates in routing.
827
+ */
828
+ export function resolveCliCommandContext(
829
+ inputConfig?: Pick<ResolvedConfig, 'baseUrl' | 'apiKey'>,
830
+ requestedScope?: NonNullable<CliCommandContext['auth_scope']>,
831
+ ): CliCommandContext {
832
+ let config = inputConfig;
833
+ if (!config) {
834
+ try {
835
+ config = resolveConfig();
836
+ } catch {
837
+ return {
838
+ active_organization: null,
839
+ auth_scope: null,
840
+ metadata_source: null,
841
+ };
842
+ }
843
+ }
844
+
845
+ const contextForScope = (
846
+ scope: NonNullable<CliCommandContext['auth_scope']>,
847
+ ): CliCommandContext | null => {
848
+ if (scope === 'env') {
849
+ const processApiKey = process.env[API_KEY_ENV]?.trim();
850
+ return processApiKey && processApiKey === config.apiKey
851
+ ? {
852
+ active_organization: activeOrganizationFromEnv(process.env),
853
+ auth_scope: 'env',
854
+ metadata_source: 'process_env',
855
+ }
856
+ : null;
857
+ }
858
+ if (scope === 'folder') {
859
+ const projectAuth = getResolvedProjectAuthSource(
860
+ config.baseUrl,
861
+ config.apiKey,
862
+ );
863
+ return projectAuth
864
+ ? {
865
+ active_organization: activeOrganizationFromEnv(projectAuth.env),
866
+ auth_scope: 'folder',
867
+ metadata_source: 'folder_env',
868
+ }
869
+ : null;
870
+ }
871
+ const hostEnv = loadCliEnv(config.baseUrl);
872
+ return (hostEnv[API_KEY_ENV] ?? '').trim() === config.apiKey
873
+ ? {
874
+ active_organization: activeOrganizationFromEnv(hostEnv),
875
+ auth_scope: 'global',
876
+ metadata_source: 'host_env',
877
+ }
878
+ : null;
879
+ };
880
+
881
+ // An explicit process key always wins credential resolution, even when a
882
+ // command accepts --auth-scope to select a saved fallback. Report the
883
+ // provenance of the key the command actually used.
884
+ const processContext = contextForScope('env');
885
+ if (processContext) return processContext;
886
+
887
+ if (requestedScope) {
888
+ return (
889
+ contextForScope(requestedScope) ?? {
890
+ active_organization: null,
891
+ auth_scope: requestedScope,
892
+ metadata_source: null,
893
+ }
894
+ );
895
+ }
896
+
897
+ for (const scope of ['folder', 'global'] as const) {
898
+ const context = contextForScope(scope);
899
+ if (context) return context;
900
+ }
901
+
902
+ return {
903
+ active_organization: null,
904
+ auth_scope: null,
905
+ metadata_source: null,
906
+ };
907
+ }
908
+
769
909
  export {
770
910
  baseUrlSlug,
771
911
  loadCliEnv,
@@ -199,7 +199,7 @@ export const SDK_RELEASE = {
199
199
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
- version: '0.3.63',
202
+ version: '0.3.65',
203
203
  updateSummary:
204
204
  'Automatic CLI updates are now enabled by default. To opt out, run `deepline settings autoupdate off`; use `deepline settings autoupdate on` to re-enable updates or `deepline settings autoupdate pin <version>` to hold an exact release. This release also adds raw-v2 tool responses at toolResponse.rawV2 while preserving existing toolResponse.raw and declared getters.',
205
205
  packageCapabilities: {