@duckcodeailabs/dql-cli 1.7.1 → 1.7.2

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.
@@ -4,11 +4,11 @@ import { createServer } from "node:http";
4
4
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, watch, writeFileSync, } from "node:fs";
5
5
  import { createRequire } from "node:module";
6
6
  import { homedir } from "node:os";
7
+ import { fileURLToPath } from "node:url";
7
8
  import { dirname, extname, join, normalize, relative, resolve, } from "node:path";
8
9
  import { Readable } from "node:stream";
9
10
  import { buildMixedSourceWarehouseFallbackSql, findMentionedNotebookDataset, planMixedSourceNotebookSql, planMixedSourceSql, } from "./mixed-source-sql.js";
10
- import Anthropic from "@anthropic-ai/sdk";
11
- import OpenAI from "openai";
11
+ import { resolveNpmInvocation } from './npm-runtime.js';
12
12
  import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, hasStandaloneSemanticRef, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
13
13
  import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, resolveRepoSource, getDialect, Parser, NodeKind, blockParameterDefinitions, buildLineageGraph, buildManifest, collectInputFiles, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryBusiness360, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, domainFolderSlug, previewModelingChange, applyModelingChange, previewDbtSourcePatch, applyDbtSourcePatch, loadDomainPackageRegistry, loadDbtNodeAuthoringDetail, relationshipValidationProofFingerprint, discoverDbtDomains, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, } from '@duckcodeailabs/dql-core';
14
14
  import { load as loadYaml } from 'js-yaml';
@@ -16,7 +16,7 @@ import { listBlockTemplates } from './block-templates.js';
16
16
  import { getRunner as getLLMRunner } from './llm/index.js';
17
17
  import { createDqlAgentProviderRunner } from './llm/providers/dql-agent-provider.js';
18
18
  import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
19
- import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildLocalContextPack, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureMetadataCatalogFresh, propose, proposePlan, recordCorrectionTrace, emitCorrectionEvalCase, mineJoinPatterns, reviewHint, AgentRunEngine, FileAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, normalizeAnthropicBaseUrl, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, reindexProject, invalidateAgentProjectState, resolveDomainContextEnvelope, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, probeLocalOllamaEmbeddings, upsertGeneratedDqlArtifactDraft, } from '@duckcodeailabs/dql-agent';
19
+ import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildLocalContextPack, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureMetadataCatalogFresh, propose, proposePlan, recordCorrectionTrace, emitCorrectionEvalCase, mineJoinPatterns, reviewHint, AgentRunEngine, FileAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, reindexProject, invalidateAgentProjectState, resolveDomainContextEnvelope, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, probeLocalOllamaEmbeddings, upsertGeneratedDqlArtifactDraft, } from '@duckcodeailabs/dql-agent';
20
20
  import { gatherProposeEnrichment } from './propose-enrich.js';
21
21
  import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
22
22
  import { getActiveProvider, getEffectiveProviderConfig, isProviderSettingsId, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
@@ -351,6 +351,21 @@ function compactConversationRecord(record) {
351
351
  function apiRequestId(scope) {
352
352
  return `${scope}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
353
353
  }
354
+ /** UI-007 / E2E-005: report the installed CLI version that owns this runtime. */
355
+ export function readDqlRuntimeVersion(runtimeUrl = import.meta.url) {
356
+ const runtimeDir = dirname(fileURLToPath(runtimeUrl));
357
+ for (const packagePath of [join(runtimeDir, 'package.json'), join(runtimeDir, '../package.json')]) {
358
+ try {
359
+ const parsed = JSON.parse(readFileSync(packagePath, 'utf-8'));
360
+ if (typeof parsed.version === 'string' && parsed.version.trim())
361
+ return parsed.version.trim();
362
+ }
363
+ catch {
364
+ // Source and published layouts place package.json at different levels.
365
+ }
366
+ }
367
+ return 'unknown';
368
+ }
354
369
  function apiErrorEnvelope(input) {
355
370
  return {
356
371
  requestId: input.requestId,
@@ -377,6 +392,7 @@ export async function startLocalServer(opts) {
377
392
  const bindHost = opts.host ?? process.env.DQL_HOST ?? '127.0.0.1';
378
393
  const loopback = bindHost === '127.0.0.1' || bindHost === 'localhost' || bindHost === '::1';
379
394
  const authToken = opts.authToken ?? process.env.DQL_SERVER_TOKEN;
395
+ const runtimeVersion = readDqlRuntimeVersion();
380
396
  const allowedOrigins = new Set((opts.allowedOrigins ?? (process.env.DQL_ALLOWED_ORIGINS ?? '').split(','))
381
397
  .map((value) => value.trim().replace(/\/$/, ''))
382
398
  .filter(Boolean));
@@ -3409,13 +3425,42 @@ export async function startLocalServer(opts) {
3409
3425
  }
3410
3426
  if (req.method === 'GET' && path === '/api/health') {
3411
3427
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
3412
- res.end(serializeJSON({ status: 'ok' }));
3428
+ res.end(serializeJSON({ status: 'ok', version: runtimeVersion }));
3429
+ return;
3430
+ }
3431
+ if (req.method === 'GET' && path === '/api/onboarding/launch') {
3432
+ const requestId = apiRequestId('onboarding-launch');
3433
+ const acknowledgedVersion = readUserPrefs(userPrefsPath).setup?.acknowledgedVersion ?? null;
3434
+ const shouldOpen = acknowledgedVersion !== runtimeVersion;
3435
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
3436
+ res.end(serializeJSON({
3437
+ requestId,
3438
+ version: runtimeVersion,
3439
+ acknowledgedVersion,
3440
+ shouldOpen,
3441
+ reason: shouldOpen ? (acknowledgedVersion ? 'version_upgrade' : 'first_install') : null,
3442
+ }));
3443
+ return;
3444
+ }
3445
+ if (req.method === 'POST' && path === '/api/onboarding/acknowledge') {
3446
+ const requestId = apiRequestId('onboarding-acknowledge');
3447
+ const prefs = readUserPrefs(userPrefsPath);
3448
+ prefs.setup = {
3449
+ acknowledgedVersion: runtimeVersion,
3450
+ acknowledgedAt: new Date().toISOString(),
3451
+ };
3452
+ writeUserPrefs(userPrefsPath, prefs);
3453
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
3454
+ res.end(serializeJSON({ requestId, version: runtimeVersion, acknowledged: true }));
3413
3455
  return;
3414
3456
  }
3415
3457
  if (req.method === 'GET' && path === '/api/onboarding/status') {
3416
3458
  const requestId = apiRequestId('onboarding-status');
3417
3459
  const dbtProjectDir = findDbtProjectPath(projectRoot, projectConfig);
3418
3460
  const manifestPath = resolve(dbtProjectDir, projectConfig.dbt?.manifestPath ?? 'target/manifest.json');
3461
+ const configuredDbtSource = projectConfig.dbt?.repoUrl ?? projectConfig.dbt?.projectDir;
3462
+ const dbtConfigured = existsSync(join(dbtProjectDir, 'dbt_project.yml'))
3463
+ || Boolean(configuredDbtSource && !/\{\{[^}]+\}\}/.test(configuredDbtSource));
3419
3464
  const configuredProfilesDir = projectConfig.dbt?.profilesDir ? resolve(projectRoot, projectConfig.dbt.profilesDir) : undefined;
3420
3465
  const profilePath = findDbtProfilePaths(projectRoot, dbtProjectDir, configuredProfilesDir)[0];
3421
3466
  const registry = loadDomainPackageRegistry(projectRoot);
@@ -3435,7 +3480,7 @@ export async function startLocalServer(opts) {
3435
3480
  requestId,
3436
3481
  snapshotId,
3437
3482
  dbt: {
3438
- configured: Boolean(projectConfig.dbt),
3483
+ configured: dbtConfigured,
3439
3484
  projectDir: projectConfig.dbt?.projectDir,
3440
3485
  manifestPath: projectConfig.dbt?.manifestPath,
3441
3486
  profilesDir: configuredProfilesDir
@@ -3765,11 +3810,64 @@ export async function startLocalServer(opts) {
3765
3810
  // provenance references and the sparse DQL overlay, never copied dbt YAML.
3766
3811
  if (req.method === 'GET' && path === '/api/modeling/dbt-first') {
3767
3812
  const requestId = apiRequestId('modeling-dbt-first');
3768
- const snapshot = projectSnapshot();
3813
+ const modelingEnabled = projectConfig.manifestVersion === 3 && projectConfig.modeling?.mode === 'dbt-first';
3814
+ if (!modelingEnabled) {
3815
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
3816
+ res.end(serializeJSON(apiErrorEnvelope({
3817
+ requestId,
3818
+ code: 'DBT_FIRST_NOT_ENABLED',
3819
+ message: 'dbt-first modeling is not enabled. Connect and apply a dbt project from Settings → Project & dbt.',
3820
+ details: {
3821
+ manifestVersion: projectConfig.manifestVersion ?? 2,
3822
+ modelingMode: projectConfig.modeling?.mode ?? null,
3823
+ },
3824
+ nextActions: ['Open Settings → Project & dbt and apply a valid dbt project.'],
3825
+ })));
3826
+ return;
3827
+ }
3828
+ const dbtProjectDir = findDbtProjectPath(projectRoot, projectConfig);
3829
+ const configuredManifestPath = projectConfig.dbt?.manifestPath ?? 'target/manifest.json';
3830
+ const expectedManifestPath = resolve(dbtProjectDir, configuredManifestPath);
3831
+ if (!existsSync(expectedManifestPath)) {
3832
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
3833
+ res.end(serializeJSON(apiErrorEnvelope({
3834
+ requestId,
3835
+ code: 'DBT_MANIFEST_NOT_FOUND',
3836
+ message: `The configured dbt manifest was not found at ${configuredManifestPath}.`,
3837
+ details: {
3838
+ projectDir: projectConfig.dbt?.projectDir ?? portableProjectPath(projectRoot, dbtProjectDir),
3839
+ manifestPath: configuredManifestPath,
3840
+ },
3841
+ nextActions: ['Run dbt parse, dbt compile, or dbt build in the configured dbt project, then refresh Domain Studio.'],
3842
+ })));
3843
+ return;
3844
+ }
3845
+ let snapshot;
3846
+ try {
3847
+ snapshot = projectSnapshot();
3848
+ }
3849
+ catch (error) {
3850
+ res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
3851
+ res.end(serializeJSON(apiErrorEnvelope({
3852
+ requestId,
3853
+ code: 'DBT_MANIFEST_COMPILE_FAILED',
3854
+ message: apiErrorMessage(error),
3855
+ details: { manifestPath: configuredManifestPath },
3856
+ nextActions: ['Fix or rebuild the dbt manifest, then refresh Domain Studio.'],
3857
+ })));
3858
+ return;
3859
+ }
3769
3860
  const manifest = snapshot.manifest;
3770
3861
  if (manifest.manifestVersion !== 3 || !manifest.modeling || !manifest.dbtProvenance) {
3771
- res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
3772
- res.end(serializeJSON(apiErrorEnvelope({ requestId, snapshotId: snapshot.snapshotId, code: 'DBT_FIRST_NOT_ENABLED', message: 'dbt-first modeling is not enabled. Set manifestVersion: 3 and modeling.mode: "dbt-first" in dql.config.json.', nextActions: ['Use Setup to connect dbt and enable dbt-first modeling.'] })));
3862
+ res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
3863
+ res.end(serializeJSON(apiErrorEnvelope({
3864
+ requestId,
3865
+ snapshotId: snapshot.snapshotId,
3866
+ code: 'DBT_MANIFEST_COMPILE_FAILED',
3867
+ message: snapshot.error ?? 'DQL could not compile Domain Studio from the configured dbt manifest.',
3868
+ details: { manifestPath: configuredManifestPath },
3869
+ nextActions: ['Review the dbt artifact and DQL compile diagnostics, rebuild the dbt manifest, then refresh Domain Studio.'],
3870
+ })));
3773
3871
  return;
3774
3872
  }
3775
3873
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
@@ -10345,7 +10443,7 @@ export function resolveDefaultLLMProvider(projectRoot) {
10345
10443
  const preferred = ['openai', 'gemini', 'anthropic', 'custom-openai', 'ollama'];
10346
10444
  for (const id of preferred) {
10347
10445
  const provider = settings.find((item) => item.id === id);
10348
- if (provider?.enabled && provider.hasApiKey)
10446
+ if (provider?.enabled && provider.configured)
10349
10447
  return id;
10350
10448
  }
10351
10449
  return null;
@@ -11779,7 +11877,8 @@ function installConnectorPackage(projectRoot, driver) {
11779
11877
  description: 'Project-local DQL connector packages',
11780
11878
  }, null, 2) + '\n', 'utf-8');
11781
11879
  }
11782
- execFileSync('npm', ['install', '--prefix', installRoot, '--no-audit', '--no-fund', definition.packageSpec], {
11880
+ const npm = resolveNpmInvocation();
11881
+ execFileSync(npm.command, [...npm.argsPrefix, 'install', '--prefix', installRoot, '--no-audit', '--no-fund', definition.packageSpec], {
11783
11882
  cwd: projectRoot,
11784
11883
  encoding: 'utf-8',
11785
11884
  stdio: 'pipe',
@@ -12986,9 +13085,18 @@ function readUserPrefs(userPrefsPath) {
12986
13085
  return { favorites: [], recentlyUsed: [] };
12987
13086
  }
12988
13087
  const raw = JSON.parse(readFileSync(userPrefsPath, 'utf-8'));
13088
+ const acknowledgedVersion = typeof raw.setup?.acknowledgedVersion === 'string'
13089
+ ? raw.setup.acknowledgedVersion.trim()
13090
+ : '';
12989
13091
  return {
12990
13092
  favorites: Array.isArray(raw.favorites) ? raw.favorites.map(String) : [],
12991
13093
  recentlyUsed: Array.isArray(raw.recentlyUsed) ? raw.recentlyUsed.map(String) : [],
13094
+ ...(acknowledgedVersion ? {
13095
+ setup: {
13096
+ acknowledgedVersion,
13097
+ ...(typeof raw.setup?.acknowledgedAt === 'string' ? { acknowledgedAt: raw.setup.acknowledgedAt } : {}),
13098
+ },
13099
+ } : {}),
12992
13100
  };
12993
13101
  }
12994
13102
  catch {
@@ -15464,7 +15572,7 @@ async function createBlockStudioAssistProvider(projectRoot, requestedProvider) {
15464
15572
  const activeProvider = getActiveProvider(projectRoot);
15465
15573
  // Subscription CLI providers (Claude Code / Codex) carry no API key — they're
15466
15574
  // usable when enabled; their real "installed + logged in" check runs in available().
15467
- const isUsable = (provider) => provider.enabled && (provider.hasApiKey || provider.authMode === 'subscription_cli');
15575
+ const isUsable = (provider) => provider.enabled && provider.configured;
15468
15576
  const selected = requestedProvider
15469
15577
  ? settings.find((provider) => provider.id === requestedProvider && isUsable(provider))
15470
15578
  : settings.find((provider) => provider.id === activeProvider && provider.enabled)
@@ -17232,7 +17340,12 @@ async function readGitStatus(cwd) {
17232
17340
  behind = Number(match[0] ?? 0);
17233
17341
  ahead = Number(match[1] ?? 0);
17234
17342
  }
17235
- const statusRes = await execGit(gitRoot, ['status', '--porcelain=v1', '--untracked-files=normal']);
17343
+ // UI-001, E2E-001: the Source Control surface is file-oriented. Git's
17344
+ // "normal" untracked mode collapses a new directory into a single `?? dir/`
17345
+ // record, which prevents the UI from showing or diffing the files inside it.
17346
+ // Enumerate every untracked path so tracked and untracked changes share the
17347
+ // same per-file review contract.
17348
+ const statusRes = await execGit(gitRoot, ['status', '--porcelain=v1', '--untracked-files=all']);
17236
17349
  const changes = [];
17237
17350
  if (statusRes.code === 0) {
17238
17351
  for (const line of statusRes.stdout.split('\n')) {
@@ -17884,7 +17997,11 @@ async function testProviderConfig(projectRoot, id, overrides) {
17884
17997
  const base = getEffectiveProviderConfig(projectRoot, id);
17885
17998
  // When the user supplies inline values (testing what they typed before saving),
17886
17999
  // merge them over the saved config and test reachability regardless of enabled.
17887
- const inline = Boolean(overrides && (overrides.apiKey || overrides.baseUrl || overrides.model));
18000
+ // The test route always passes an override object, even when every draft field
18001
+ // is blank. That still means "test this candidate before save" and must allow
18002
+ // a previously configured-but-disabled provider to be reactivated without
18003
+ // forcing the user to paste its redacted secret again.
18004
+ const inline = overrides !== undefined;
17888
18005
  const config = {
17889
18006
  ...base,
17890
18007
  ...(overrides?.apiKey ? { apiKey: overrides.apiKey } : {}),
@@ -17964,15 +18081,19 @@ async function testOpenAIProviderConfig(config, label, details) {
17964
18081
  return { ok: false, message: `${label} is not configured${details}. Add an API key in Settings or OPENAI_API_KEY.` };
17965
18082
  }
17966
18083
  try {
17967
- const client = new OpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl });
17968
- const response = await client.responses.create({
17969
- model: config.model ?? 'gpt-5.5',
17970
- input: 'Reply with exactly: OK',
17971
- max_output_tokens: 16,
18084
+ // Use the same provider adapter as governed Ask so enterprise gateways are
18085
+ // tested against the runtime path users will actually execute.
18086
+ const provider = new OpenAIProvider({
18087
+ apiKey: config.apiKey,
18088
+ baseUrl: config.baseUrl,
18089
+ model: config.model,
17972
18090
  });
18091
+ const response = await provider.generate([
18092
+ { role: 'user', content: 'Reply with exactly: OK' },
18093
+ ], { maxTokens: 16, temperature: 0 });
17973
18094
  return {
17974
18095
  ok: true,
17975
- message: `${label} SDK responded${details}: ${(response.output_text ?? 'OK').trim().slice(0, 80) || 'OK'}`,
18096
+ message: `${label} responded through the governed runtime${details}: ${response.trim().slice(0, 80) || 'OK'}`,
17976
18097
  };
17977
18098
  }
17978
18099
  catch (error) {
@@ -17987,20 +18108,17 @@ async function testAnthropicProviderConfig(config, label, details) {
17987
18108
  return { ok: false, message: `${label} is not configured${details}. Add an API key in Settings or ANTHROPIC_API_KEY.` };
17988
18109
  }
17989
18110
  try {
17990
- const client = new Anthropic({
18111
+ const provider = new ClaudeProvider({
17991
18112
  apiKey: config.apiKey,
17992
- ...(config.baseUrl ? { baseURL: normalizeAnthropicBaseUrl(config.baseUrl) } : {}),
18113
+ baseUrl: config.baseUrl,
18114
+ model: config.model,
17993
18115
  });
17994
- const response = await client.messages.create({
17995
- model: config.model ?? 'claude-opus-4-8',
17996
- max_tokens: 16,
17997
- temperature: 0,
17998
- messages: [{ role: 'user', content: 'Reply with exactly: OK' }],
17999
- });
18000
- const text = response.content?.filter((block) => block.type === 'text').map((block) => block.text ?? '').join('') ?? '';
18116
+ const response = await provider.generate([
18117
+ { role: 'user', content: 'Reply with exactly: OK' },
18118
+ ], { maxTokens: 16, temperature: 0 });
18001
18119
  return {
18002
18120
  ok: true,
18003
- message: `${label} SDK responded${details}: ${text.trim().slice(0, 80) || 'OK'}`,
18121
+ message: `${label} responded through the governed runtime${details}: ${response.trim().slice(0, 80) || 'OK'}`,
18004
18122
  };
18005
18123
  }
18006
18124
  catch (error) {