@duckcodeailabs/dql-cli 1.6.1 → 1.6.3

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 (121) hide show
  1. package/dist/apps-api.d.ts +20 -0
  2. package/dist/apps-api.d.ts.map +1 -1
  3. package/dist/apps-api.js +71 -0
  4. package/dist/apps-api.js.map +1 -1
  5. package/dist/args.d.ts +2 -0
  6. package/dist/args.d.ts.map +1 -1
  7. package/dist/args.js +3 -0
  8. package/dist/args.js.map +1 -1
  9. package/dist/assets/dql-notebook/assets/index-CIMLd3Cb.js +3289 -0
  10. package/dist/assets/dql-notebook/assets/index-RaDW1A5g.css +1 -0
  11. package/dist/assets/dql-notebook/index.html +3 -3
  12. package/dist/commands/app.d.ts +4 -3
  13. package/dist/commands/app.d.ts.map +1 -1
  14. package/dist/commands/app.js +161 -75
  15. package/dist/commands/app.js.map +1 -1
  16. package/dist/commands/build.d.ts.map +1 -1
  17. package/dist/commands/build.js +7 -1
  18. package/dist/commands/build.js.map +1 -1
  19. package/dist/commands/compile.d.ts +1 -1
  20. package/dist/commands/compile.d.ts.map +1 -1
  21. package/dist/commands/compile.js +40 -4
  22. package/dist/commands/compile.js.map +1 -1
  23. package/dist/commands/init.d.ts.map +1 -1
  24. package/dist/commands/init.js +3 -1
  25. package/dist/commands/init.js.map +1 -1
  26. package/dist/commands/lineage.d.ts +3 -0
  27. package/dist/commands/lineage.d.ts.map +1 -1
  28. package/dist/commands/lineage.js +230 -2
  29. package/dist/commands/lineage.js.map +1 -1
  30. package/dist/commands/new.js +61 -2
  31. package/dist/commands/new.js.map +1 -1
  32. package/dist/commands/parse.d.ts.map +1 -1
  33. package/dist/commands/parse.js +13 -3
  34. package/dist/commands/parse.js.map +1 -1
  35. package/dist/commands/preview.d.ts.map +1 -1
  36. package/dist/commands/preview.js +7 -1
  37. package/dist/commands/preview.js.map +1 -1
  38. package/dist/commands/validate.d.ts.map +1 -1
  39. package/dist/commands/validate.js +49 -3
  40. package/dist/commands/validate.js.map +1 -1
  41. package/dist/commands/verify.d.ts.map +1 -1
  42. package/dist/commands/verify.js +6 -2
  43. package/dist/commands/verify.js.map +1 -1
  44. package/dist/index.js +79 -68
  45. package/dist/index.js.map +1 -1
  46. package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
  47. package/dist/llm/providers/dql-agent-provider.js +95 -19
  48. package/dist/llm/providers/dql-agent-provider.js.map +1 -1
  49. package/dist/llm/tools.d.ts.map +1 -1
  50. package/dist/llm/tools.js +29 -1
  51. package/dist/llm/tools.js.map +1 -1
  52. package/dist/llm/types.d.ts +3 -1
  53. package/dist/llm/types.d.ts.map +1 -1
  54. package/dist/local-runtime.d.ts +12 -0
  55. package/dist/local-runtime.d.ts.map +1 -1
  56. package/dist/local-runtime.js +724 -46
  57. package/dist/local-runtime.js.map +1 -1
  58. package/dist/package.json +44 -0
  59. package/package.json +12 -12
  60. package/dist/apps-api.test.d.ts +0 -2
  61. package/dist/apps-api.test.d.ts.map +0 -1
  62. package/dist/apps-api.test.js +0 -154
  63. package/dist/apps-api.test.js.map +0 -1
  64. package/dist/args.test.d.ts +0 -2
  65. package/dist/args.test.d.ts.map +0 -1
  66. package/dist/args.test.js +0 -41
  67. package/dist/args.test.js.map +0 -1
  68. package/dist/assets/dql-notebook/assets/index-C-s-OCLW.css +0 -1
  69. package/dist/assets/dql-notebook/assets/index-DZ_5zsCw.js +0 -869
  70. package/dist/block-studio-import.test.d.ts +0 -2
  71. package/dist/block-studio-import.test.d.ts.map +0 -1
  72. package/dist/block-studio-import.test.js +0 -168
  73. package/dist/block-studio-import.test.js.map +0 -1
  74. package/dist/commands/build.test.d.ts +0 -2
  75. package/dist/commands/build.test.d.ts.map +0 -1
  76. package/dist/commands/build.test.js +0 -44
  77. package/dist/commands/build.test.js.map +0 -1
  78. package/dist/commands/compile.test.d.ts +0 -2
  79. package/dist/commands/compile.test.d.ts.map +0 -1
  80. package/dist/commands/compile.test.js +0 -115
  81. package/dist/commands/compile.test.js.map +0 -1
  82. package/dist/commands/doctor.test.d.ts +0 -2
  83. package/dist/commands/doctor.test.d.ts.map +0 -1
  84. package/dist/commands/doctor.test.js +0 -44
  85. package/dist/commands/doctor.test.js.map +0 -1
  86. package/dist/commands/init.test.d.ts +0 -2
  87. package/dist/commands/init.test.d.ts.map +0 -1
  88. package/dist/commands/init.test.js +0 -178
  89. package/dist/commands/init.test.js.map +0 -1
  90. package/dist/commands/new.test.d.ts +0 -2
  91. package/dist/commands/new.test.d.ts.map +0 -1
  92. package/dist/commands/new.test.js +0 -191
  93. package/dist/commands/new.test.js.map +0 -1
  94. package/dist/commands/sync.test.d.ts +0 -2
  95. package/dist/commands/sync.test.d.ts.map +0 -1
  96. package/dist/commands/sync.test.js +0 -147
  97. package/dist/commands/sync.test.js.map +0 -1
  98. package/dist/commands/validate.test.d.ts +0 -2
  99. package/dist/commands/validate.test.d.ts.map +0 -1
  100. package/dist/commands/validate.test.js +0 -55
  101. package/dist/commands/validate.test.js.map +0 -1
  102. package/dist/local-runtime.test.d.ts +0 -2
  103. package/dist/local-runtime.test.d.ts.map +0 -1
  104. package/dist/local-runtime.test.js +0 -300
  105. package/dist/local-runtime.test.js.map +0 -1
  106. package/dist/metricflow.test.d.ts +0 -2
  107. package/dist/metricflow.test.d.ts.map +0 -1
  108. package/dist/metricflow.test.js +0 -54
  109. package/dist/metricflow.test.js.map +0 -1
  110. package/dist/promote-from-draft.test.d.ts +0 -2
  111. package/dist/promote-from-draft.test.d.ts.map +0 -1
  112. package/dist/promote-from-draft.test.js +0 -149
  113. package/dist/promote-from-draft.test.js.map +0 -1
  114. package/dist/semantic-import.test.d.ts +0 -2
  115. package/dist/semantic-import.test.d.ts.map +0 -1
  116. package/dist/semantic-import.test.js +0 -95
  117. package/dist/semantic-import.test.js.map +0 -1
  118. package/dist/template-adoption.test.d.ts +0 -2
  119. package/dist/template-adoption.test.d.ts.map +0 -1
  120. package/dist/template-adoption.test.js +0 -102
  121. package/dist/template-adoption.test.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { createServer } from 'node:http';
3
3
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, watch, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
4
5
  import { dirname, extname, join, normalize, relative, resolve } from 'node:path';
5
6
  import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
6
7
  import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, Parser, buildLineageGraph, buildManifest, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, } from '@duckcodeailabs/dql-core';
@@ -213,6 +214,37 @@ export async function startLocalServer(opts) {
213
214
  blockPath: block.filePath,
214
215
  };
215
216
  };
217
+ const executeGeneratedSqlForAgent = async (sql) => {
218
+ const boundedSql = buildAgentPreviewSql(sql);
219
+ const semantic = prepareSemanticSql(boundedSql, semanticLayer);
220
+ if (semantic.unresolvedRefs.length > 0) {
221
+ throw new Error(`Unknown semantic reference${semantic.unresolvedRefs.length > 1 ? 's' : ''}: ${semantic.unresolvedRefs.join(', ')}`);
222
+ }
223
+ const prepared = prepareLocalExecution(semantic.sql, connection, projectRoot, projectConfig);
224
+ const app = loadRuntimeApp(projectRoot, activePersonaAppId());
225
+ assertAppAccess({ app, domain: app?.domain, level: 'execute' });
226
+ const rawResult = await executor.executeQuery(prepared.sql, [], runtimeVariables({}), prepared.connection);
227
+ const normalized = normalizeQueryResult(rawResult, semantic.semanticRefs);
228
+ return {
229
+ columns: normalized.columns,
230
+ rows: normalized.rows,
231
+ rowCount: normalized.rowCount,
232
+ executionTime: normalized.executionTime,
233
+ sql: prepared.sql,
234
+ };
235
+ };
236
+ const getSchemaContextForAgent = async (question) => {
237
+ try {
238
+ const result = await executor.executeQuery(`SELECT table_schema, table_name, column_name, data_type
239
+ FROM information_schema.columns
240
+ WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
241
+ ORDER BY table_schema, table_name, ordinal_position`, [], runtimeVariables({}), connection);
242
+ return buildAgentSchemaContext(question, result.rows);
243
+ }
244
+ catch {
245
+ return [];
246
+ }
247
+ };
216
248
  // SSE clients for /api/watch hot-reload
217
249
  const sseClients = new Set();
218
250
  // Watch notebooks/, workbooks/, semantic-layer/, and data/ dirs for changes
@@ -1777,8 +1809,9 @@ export async function startLocalServer(opts) {
1777
1809
  const defaultKey = raw.defaultConnection
1778
1810
  ? 'default'
1779
1811
  : Object.keys(connections)[0] ?? 'default';
1812
+ const dbtProfiles = discoverDbtProfileConnections(projectRoot, cfg);
1780
1813
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1781
- res.end(serializeJSON({ default: defaultKey, connections }));
1814
+ res.end(serializeJSON({ default: defaultKey, connections, dbtProfiles }));
1782
1815
  return;
1783
1816
  }
1784
1817
  // Save/update connections
@@ -2495,7 +2528,15 @@ export async function startLocalServer(opts) {
2495
2528
  req.on('close', () => controller.abort());
2496
2529
  const emit = (turn) => { res.write(`data: ${JSON.stringify(turn)}\n\n`); };
2497
2530
  try {
2498
- await runner.run({ provider: resolvedProvider, messages, upstream, projectRoot, executeCertifiedBlock: executeCertifiedBlockForAgent }, emit, controller.signal);
2531
+ await runner.run({
2532
+ provider: resolvedProvider,
2533
+ messages,
2534
+ upstream,
2535
+ projectRoot,
2536
+ executeCertifiedBlock: executeCertifiedBlockForAgent,
2537
+ executeGeneratedSql: executeGeneratedSqlForAgent,
2538
+ getSchemaContext: getSchemaContextForAgent,
2539
+ }, emit, controller.signal);
2499
2540
  }
2500
2541
  catch (err) {
2501
2542
  emit({ kind: 'error', message: err instanceof Error ? err.message : String(err) });
@@ -3328,6 +3369,7 @@ export function loadProjectConfig(projectRoot) {
3328
3369
  // Support both `filepath` (correct) and `path` (legacy/init compat)
3329
3370
  const filepath = (defaultConn.filepath ?? defaultConn.path);
3330
3371
  config.defaultConnection = {
3372
+ ...defaultConn,
3331
3373
  driver: defaultConn.driver,
3332
3374
  ...(filepath ? { filepath } : {}),
3333
3375
  };
@@ -3344,6 +3386,100 @@ export function prepareLocalExecution(sql, connection, projectRoot, projectConfi
3344
3386
  connection: normalizedConnection,
3345
3387
  };
3346
3388
  }
3389
+ const AGENT_PREVIEW_FORBIDDEN_SQL = [
3390
+ 'alter',
3391
+ 'analyze',
3392
+ 'attach',
3393
+ 'call',
3394
+ 'copy',
3395
+ 'create',
3396
+ 'delete',
3397
+ 'detach',
3398
+ 'drop',
3399
+ 'export',
3400
+ 'grant',
3401
+ 'import',
3402
+ 'insert',
3403
+ 'install',
3404
+ 'load',
3405
+ 'merge',
3406
+ 'pragma',
3407
+ 'reset',
3408
+ 'revoke',
3409
+ 'set',
3410
+ 'truncate',
3411
+ 'update',
3412
+ 'vacuum',
3413
+ ];
3414
+ export function buildAgentPreviewSql(sql) {
3415
+ const trimmed = sql.trim();
3416
+ if (!trimmed)
3417
+ throw new Error('Generated SQL preview is empty.');
3418
+ const withoutTrailingSemicolon = trimmed.replace(/;\s*$/, '').trim();
3419
+ const scanSql = stripSqlStringsAndComments(withoutTrailingSemicolon).trim();
3420
+ if (!/^(select|with)\b/i.test(scanSql)) {
3421
+ throw new Error('Generated SQL preview only supports read-only SELECT or WITH queries.');
3422
+ }
3423
+ if (scanSql.includes(';')) {
3424
+ throw new Error('Generated SQL preview only supports one statement.');
3425
+ }
3426
+ const forbiddenPattern = new RegExp(`\\b(${AGENT_PREVIEW_FORBIDDEN_SQL.join('|')})\\b`, 'i');
3427
+ const forbidden = scanSql.match(forbiddenPattern)?.[1];
3428
+ if (forbidden) {
3429
+ throw new Error(`Generated SQL preview rejected unsupported statement keyword: ${forbidden.toUpperCase()}.`);
3430
+ }
3431
+ return `SELECT * FROM (\n${withoutTrailingSemicolon}\n) AS dql_agent_preview LIMIT 200`;
3432
+ }
3433
+ function stripSqlStringsAndComments(sql) {
3434
+ let output = '';
3435
+ for (let index = 0; index < sql.length; index += 1) {
3436
+ const current = sql[index];
3437
+ const next = sql[index + 1];
3438
+ if (current === '-' && next === '-') {
3439
+ output += ' ';
3440
+ index += 2;
3441
+ while (index < sql.length && sql[index] !== '\n') {
3442
+ output += ' ';
3443
+ index += 1;
3444
+ }
3445
+ if (index < sql.length)
3446
+ output += '\n';
3447
+ continue;
3448
+ }
3449
+ if (current === '/' && next === '*') {
3450
+ output += ' ';
3451
+ index += 2;
3452
+ while (index < sql.length && !(sql[index] === '*' && sql[index + 1] === '/')) {
3453
+ output += sql[index] === '\n' ? '\n' : ' ';
3454
+ index += 1;
3455
+ }
3456
+ if (index < sql.length) {
3457
+ output += ' ';
3458
+ index += 1;
3459
+ }
3460
+ continue;
3461
+ }
3462
+ if (current === "'" || current === '"') {
3463
+ const quote = current;
3464
+ output += ' ';
3465
+ while (index + 1 < sql.length) {
3466
+ index += 1;
3467
+ output += sql[index] === '\n' ? '\n' : ' ';
3468
+ if (sql[index] === quote) {
3469
+ if (sql[index + 1] === quote) {
3470
+ index += 1;
3471
+ output += ' ';
3472
+ continue;
3473
+ }
3474
+ break;
3475
+ }
3476
+ }
3477
+ continue;
3478
+ }
3479
+ output += current;
3480
+ }
3481
+ return output;
3482
+ }
3347
3483
  /**
3348
3484
  * Shared resolver for `@metric(name)` / `@dim(name)` refs in raw SQL.
3349
3485
  * Used by notebook SQL execution and Block Studio validation so both paths
@@ -3364,7 +3500,7 @@ export function prepareSemanticSql(sql, semanticLayer) {
3364
3500
  };
3365
3501
  }
3366
3502
  export function normalizeProjectConnection(connection, projectRoot) {
3367
- const normalized = { ...connection };
3503
+ const normalized = expandConnectionEnvPlaceholders({ ...connection });
3368
3504
  if ((normalized.driver === 'file' || normalized.driver === 'duckdb') && normalized.filepath && normalized.filepath !== ':memory:' && !isAbsoluteLikePath(normalized.filepath)) {
3369
3505
  normalized.filepath = resolve(projectRoot, normalized.filepath);
3370
3506
  }
@@ -3373,6 +3509,15 @@ export function normalizeProjectConnection(connection, projectRoot) {
3373
3509
  }
3374
3510
  return normalized;
3375
3511
  }
3512
+ function expandConnectionEnvPlaceholders(connection) {
3513
+ const expanded = {};
3514
+ for (const [key, value] of Object.entries(connection)) {
3515
+ expanded[key] = typeof value === 'string'
3516
+ ? value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, envKey) => process.env[envKey] ?? match)
3517
+ : value;
3518
+ }
3519
+ return expanded;
3520
+ }
3376
3521
  export function resolveProjectRelativeSqlPaths(sql, projectRoot, dataDir) {
3377
3522
  const resolvedRoot = resolve(projectRoot);
3378
3523
  const normalizedDataDir = typeof dataDir === 'string' && dataDir.trim().length > 0
@@ -3515,6 +3660,8 @@ function scanNotebookFiles(projectRoot) {
3515
3660
  workbooks: 'workbook',
3516
3661
  blocks: 'block',
3517
3662
  dashboards: 'dashboard',
3663
+ terms: 'term',
3664
+ 'business-views': 'business_view',
3518
3665
  };
3519
3666
  for (const [folder, type] of Object.entries(folderMap)) {
3520
3667
  const dir = join(projectRoot, folder);
@@ -3536,8 +3683,9 @@ function scanNotebookFiles(projectRoot) {
3536
3683
  continue;
3537
3684
  if (!entry.name.endsWith('.dql') && !entry.name.endsWith('.dqlnb'))
3538
3685
  continue;
3686
+ const fallbackName = entry.name.replace(/\.(dql|dqlnb)$/, '');
3539
3687
  result.push({
3540
- name: entry.name.replace(/\.(dql|dqlnb)$/, ''),
3688
+ name: inferDqlArtifactName(fullPath, type, fallbackName),
3541
3689
  path: relativePath,
3542
3690
  type,
3543
3691
  folder: relativeDir.split('/')[0] ?? relativeDir,
@@ -3547,6 +3695,27 @@ function scanNotebookFiles(projectRoot) {
3547
3695
  catch { /* skip unreadable dirs */ }
3548
3696
  }
3549
3697
  }
3698
+ function inferDqlArtifactName(fullPath, type, fallbackName) {
3699
+ if (!fullPath.endsWith('.dql'))
3700
+ return fallbackName;
3701
+ const expectedKind = {
3702
+ block: 'BlockDecl',
3703
+ dashboard: 'Dashboard',
3704
+ term: 'TermDecl',
3705
+ business_view: 'BusinessViewDecl',
3706
+ };
3707
+ const kind = expectedKind[type];
3708
+ if (!kind)
3709
+ return fallbackName;
3710
+ try {
3711
+ const ast = new Parser(readFileSync(fullPath, 'utf-8')).parse();
3712
+ const statement = ast.statements.find((item) => item.kind === kind && typeof item.name === 'string');
3713
+ return statement?.name ?? fallbackName;
3714
+ }
3715
+ catch {
3716
+ return fallbackName;
3717
+ }
3718
+ }
3550
3719
  function scanDataFiles(projectRoot) {
3551
3720
  const dataDir = join(projectRoot, 'data');
3552
3721
  if (!existsSync(dataDir))
@@ -4984,7 +5153,51 @@ function resolveDbtManifestPath(projectRoot) {
4984
5153
  const candidate = join(projectRoot, 'target', 'manifest.json');
4985
5154
  return existsSync(candidate) ? candidate : undefined;
4986
5155
  }
4987
- export function buildDbtStatus(projectRoot, projectConfig, lastSyncTime) {
5156
+ export function discoverDbtProfileConnections(projectRoot, projectConfig) {
5157
+ const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
5158
+ const projectProfileName = readDbtProjectProfileName(dbtProjectPath);
5159
+ const profilePaths = findDbtProfilePaths(projectRoot, dbtProjectPath);
5160
+ const candidates = [];
5161
+ for (const profilePath of profilePaths) {
5162
+ const profiles = readYamlFile(profilePath);
5163
+ if (!profiles)
5164
+ continue;
5165
+ for (const [profileName, rawProfile] of Object.entries(profiles)) {
5166
+ if (!rawProfile || typeof rawProfile !== 'object')
5167
+ continue;
5168
+ if (projectProfileName && profileName !== projectProfileName)
5169
+ continue;
5170
+ const profile = rawProfile;
5171
+ const outputs = profile.outputs && typeof profile.outputs === 'object'
5172
+ ? profile.outputs
5173
+ : {};
5174
+ const defaultTarget = typeof profile.target === 'string' ? profile.target : 'default';
5175
+ for (const [targetName, output] of Object.entries(outputs)) {
5176
+ if (!output || typeof output !== 'object')
5177
+ continue;
5178
+ const mapped = mapDbtProfileOutput(output);
5179
+ if (!mapped)
5180
+ continue;
5181
+ const warnings = [...mapped.warnings];
5182
+ if (targetName !== defaultTarget) {
5183
+ warnings.push(`Not the default dbt target "${defaultTarget}".`);
5184
+ }
5185
+ candidates.push({
5186
+ id: `${profilePath}:${profileName}:${targetName}`,
5187
+ profileName,
5188
+ targetName,
5189
+ adapter: mapped.adapter,
5190
+ path: profilePath,
5191
+ connection: mapped.connection,
5192
+ missingFields: requiredConnectionFields(mapped.connection, mapped.envRefs),
5193
+ warnings,
5194
+ });
5195
+ }
5196
+ }
5197
+ }
5198
+ return candidates.slice(0, 20);
5199
+ }
5200
+ function findDbtProjectPath(projectRoot, projectConfig) {
4988
5201
  const configuredDbtDir = projectConfig.dbt?.projectDir
4989
5202
  ? resolve(projectRoot, projectConfig.dbt.projectDir)
4990
5203
  : undefined;
@@ -4999,7 +5212,271 @@ export function buildDbtStatus(projectRoot, projectConfig, lastSyncTime) {
4999
5212
  resolve(projectRoot, '../dbt'),
5000
5213
  resolve(projectRoot, '../../dbt'),
5001
5214
  ].filter((value) => Boolean(value));
5002
- const dbtProjectPath = candidateDirs.find((dir, index, list) => list.indexOf(dir) === index && existsSync(join(dir, 'dbt_project.yml'))) ?? configuredDbtDir ?? semanticDbtDir ?? projectRoot;
5215
+ return candidateDirs.find((dir, index, list) => list.indexOf(dir) === index && existsSync(join(dir, 'dbt_project.yml')))
5216
+ ?? configuredDbtDir
5217
+ ?? semanticDbtDir
5218
+ ?? projectRoot;
5219
+ }
5220
+ function findDbtProfilePaths(projectRoot, dbtProjectPath) {
5221
+ const dirs = [
5222
+ process.env.DBT_PROFILES_DIR,
5223
+ dbtProjectPath,
5224
+ projectRoot,
5225
+ join(homedir(), '.dbt'),
5226
+ ].filter((value) => Boolean(value));
5227
+ const paths = [];
5228
+ for (const dir of dirs) {
5229
+ for (const filename of ['profiles.yml', 'profiles.yaml']) {
5230
+ const profilePath = resolve(dir, filename);
5231
+ if (existsSync(profilePath) && !paths.includes(profilePath)) {
5232
+ paths.push(profilePath);
5233
+ }
5234
+ }
5235
+ }
5236
+ return paths;
5237
+ }
5238
+ function readDbtProjectProfileName(dbtProjectPath) {
5239
+ const projectFile = join(dbtProjectPath, 'dbt_project.yml');
5240
+ const projectYaml = readYamlFile(projectFile);
5241
+ return typeof projectYaml?.profile === 'string' ? projectYaml.profile : null;
5242
+ }
5243
+ function readYamlFile(path) {
5244
+ if (!existsSync(path))
5245
+ return null;
5246
+ try {
5247
+ const parsed = loadYaml(readFileSync(path, 'utf-8'));
5248
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
5249
+ ? parsed
5250
+ : null;
5251
+ }
5252
+ catch {
5253
+ return null;
5254
+ }
5255
+ }
5256
+ function mapDbtProfileOutput(output) {
5257
+ const adapter = text(output, 'type').value?.toLowerCase();
5258
+ const envRefs = new Set();
5259
+ const warnings = [];
5260
+ const read = (...keys) => {
5261
+ const result = text(output, ...keys);
5262
+ result.envRefs.forEach((ref) => envRefs.add(ref));
5263
+ return result.value;
5264
+ };
5265
+ const port = numberValue(output, 'port');
5266
+ const sslRaw = read('ssl', 'sslmode');
5267
+ const ssl = sslRaw === undefined
5268
+ ? undefined
5269
+ : !['false', '0', 'disable', 'disabled', 'off'].includes(sslRaw.toLowerCase());
5270
+ switch (adapter) {
5271
+ case 'postgres':
5272
+ case 'postgresql':
5273
+ return {
5274
+ adapter,
5275
+ connection: compactConnection({
5276
+ driver: 'postgresql',
5277
+ host: read('host'),
5278
+ port,
5279
+ database: read('dbname', 'database'),
5280
+ schema: read('schema'),
5281
+ username: read('user', 'username'),
5282
+ password: read('password', 'pass'),
5283
+ ssl,
5284
+ }),
5285
+ envRefs: [...envRefs],
5286
+ warnings,
5287
+ };
5288
+ case 'redshift':
5289
+ return {
5290
+ adapter,
5291
+ connection: compactConnection({
5292
+ driver: 'redshift',
5293
+ host: read('host'),
5294
+ port: port ?? 5439,
5295
+ database: read('dbname', 'database'),
5296
+ schema: read('schema'),
5297
+ username: read('user', 'username'),
5298
+ password: read('password', 'pass'),
5299
+ ssl,
5300
+ }),
5301
+ envRefs: [...envRefs],
5302
+ warnings,
5303
+ };
5304
+ case 'snowflake': {
5305
+ const privateKeyPath = read('private_key_path', 'privateKeyPath');
5306
+ const privateKey = read('private_key', 'privateKey');
5307
+ const authenticator = read('authenticator');
5308
+ const normalizedAuthenticator = authenticator?.toLowerCase().replace(/[\s_-]/g, '');
5309
+ const authMethod = privateKeyPath || privateKey || normalizedAuthenticator === 'snowflakejwt'
5310
+ ? 'key_pair'
5311
+ : normalizedAuthenticator === 'externalbrowser'
5312
+ ? 'external_browser'
5313
+ : normalizedAuthenticator === 'oauth' || normalizedAuthenticator === 'programmaticaccesstoken'
5314
+ ? 'oauth'
5315
+ : 'password';
5316
+ return {
5317
+ adapter,
5318
+ connection: compactConnection({
5319
+ driver: 'snowflake',
5320
+ account: read('account'),
5321
+ warehouse: read('warehouse'),
5322
+ database: read('database'),
5323
+ schema: read('schema'),
5324
+ username: read('user', 'username'),
5325
+ password: read('password'),
5326
+ role: read('role'),
5327
+ privateKeyPath,
5328
+ privateKey,
5329
+ privateKeyPassphrase: read('private_key_passphrase', 'privateKeyPassphrase'),
5330
+ authenticator,
5331
+ authMethod,
5332
+ }),
5333
+ envRefs: [...envRefs],
5334
+ warnings,
5335
+ };
5336
+ }
5337
+ case 'bigquery': {
5338
+ const keyFilename = read('keyfile', 'keyFilename');
5339
+ return {
5340
+ adapter,
5341
+ connection: compactConnection({
5342
+ driver: 'bigquery',
5343
+ projectId: read('project', 'projectId'),
5344
+ schema: read('dataset', 'schema'),
5345
+ location: read('location'),
5346
+ keyFilename,
5347
+ authMethod: keyFilename ? 'service_account_key_file' : 'application_default',
5348
+ }),
5349
+ envRefs: [...envRefs],
5350
+ warnings,
5351
+ };
5352
+ }
5353
+ case 'duckdb':
5354
+ return {
5355
+ adapter,
5356
+ connection: compactConnection({
5357
+ driver: 'duckdb',
5358
+ filepath: read('path', 'database') ?? ':memory:',
5359
+ }),
5360
+ envRefs: [...envRefs],
5361
+ warnings,
5362
+ };
5363
+ case 'databricks':
5364
+ return {
5365
+ adapter,
5366
+ connection: compactConnection({
5367
+ driver: 'databricks',
5368
+ host: read('host', 'server_hostname'),
5369
+ httpPath: read('http_path', 'httpPath'),
5370
+ warehouse: read('warehouse', 'warehouse_id'),
5371
+ catalog: read('catalog'),
5372
+ database: read('catalog', 'database'),
5373
+ schema: read('schema'),
5374
+ token: read('token'),
5375
+ authMethod: 'token',
5376
+ }),
5377
+ envRefs: [...envRefs],
5378
+ warnings,
5379
+ };
5380
+ default:
5381
+ return null;
5382
+ }
5383
+ }
5384
+ function text(source, ...keys) {
5385
+ for (const key of keys) {
5386
+ const raw = source[key];
5387
+ if (raw === undefined || raw === null)
5388
+ continue;
5389
+ const value = String(raw).trim();
5390
+ if (!value)
5391
+ continue;
5392
+ return resolveDbtEnvVars(value);
5393
+ }
5394
+ return { envRefs: [] };
5395
+ }
5396
+ function resolveDbtEnvVars(value) {
5397
+ const envRefs = [];
5398
+ const replaced = value.replace(/\{\{\s*env_var\(\s*['"]([^'"]+)['"]\s*(?:,\s*(['"])(.*?)\2)?\s*\)\s*\}\}/g, (_match, envKey, _quote, fallback) => {
5399
+ const envValue = process.env[envKey];
5400
+ if (envValue !== undefined)
5401
+ return envValue;
5402
+ if (fallback !== undefined)
5403
+ return fallback;
5404
+ envRefs.push(envKey);
5405
+ return `\${${envKey}}`;
5406
+ });
5407
+ return { value: replaced, envRefs };
5408
+ }
5409
+ function numberValue(source, key) {
5410
+ const raw = source[key];
5411
+ if (raw === undefined || raw === null || raw === '')
5412
+ return undefined;
5413
+ const value = Number(raw);
5414
+ return Number.isFinite(value) ? value : undefined;
5415
+ }
5416
+ function compactConnection(connection) {
5417
+ const compact = {};
5418
+ for (const [key, value] of Object.entries(connection)) {
5419
+ if (value === undefined || value === null || value === '')
5420
+ continue;
5421
+ compact[key] = value;
5422
+ }
5423
+ return compact;
5424
+ }
5425
+ function requiredConnectionFields(connection, envRefs) {
5426
+ const missing = new Set();
5427
+ const needs = (field) => {
5428
+ const value = connection[field];
5429
+ if (value === undefined || value === null || value === '')
5430
+ missing.add(String(field));
5431
+ };
5432
+ switch (connection.driver) {
5433
+ case 'postgresql':
5434
+ case 'redshift':
5435
+ needs('host');
5436
+ needs('database');
5437
+ needs('username');
5438
+ break;
5439
+ case 'snowflake':
5440
+ needs('account');
5441
+ needs('warehouse');
5442
+ needs('database');
5443
+ needs('schema');
5444
+ needs('username');
5445
+ if (connection.authMethod === 'key_pair') {
5446
+ if (!connection.privateKeyPath && !connection.privateKey) {
5447
+ missing.add('privateKeyPath');
5448
+ }
5449
+ }
5450
+ else if (connection.authMethod === 'oauth') {
5451
+ if (!connection.token && !connection.password) {
5452
+ missing.add('token');
5453
+ }
5454
+ }
5455
+ else if (connection.authMethod !== 'external_browser') {
5456
+ needs('password');
5457
+ }
5458
+ break;
5459
+ case 'bigquery':
5460
+ needs('projectId');
5461
+ break;
5462
+ case 'duckdb':
5463
+ needs('filepath');
5464
+ break;
5465
+ case 'databricks':
5466
+ needs('host');
5467
+ if (!connection.httpPath && !connection.warehouse)
5468
+ missing.add('httpPath');
5469
+ needs('token');
5470
+ break;
5471
+ }
5472
+ for (const envKey of envRefs) {
5473
+ if (!process.env[envKey])
5474
+ missing.add(`env:${envKey}`);
5475
+ }
5476
+ return [...missing];
5477
+ }
5478
+ export function buildDbtStatus(projectRoot, projectConfig, lastSyncTime) {
5479
+ const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
5003
5480
  const configuredManifest = projectConfig.dbt?.manifestPath ?? 'target/manifest.json';
5004
5481
  const manifestPath = resolve(dbtProjectPath, configuredManifest);
5005
5482
  const catalogPath = resolve(dbtProjectPath, 'target/catalog.json');
@@ -5029,7 +5506,9 @@ export function buildDbtStatus(projectRoot, projectConfig, lastSyncTime) {
5029
5506
  const savedQueryCount = Array.isArray(semanticManifest?.saved_queries)
5030
5507
  ? semanticManifest.saved_queries.length
5031
5508
  : 0;
5032
- const configured = existsSync(join(dbtProjectPath, 'dbt_project.yml')) || Boolean(configuredDbtDir || semanticDbtDir);
5509
+ const configured = existsSync(join(dbtProjectPath, 'dbt_project.yml'))
5510
+ || Boolean(projectConfig.dbt?.projectDir)
5511
+ || Boolean(projectConfig.semanticLayer?.provider === 'dbt' && projectConfig.semanticLayer.projectPath);
5033
5512
  const manifestExists = existsSync(manifestPath);
5034
5513
  const semanticExists = existsSync(semanticManifestPath);
5035
5514
  const setupHint = !configured
@@ -5146,14 +5625,21 @@ async function execGit(cwd, args) {
5146
5625
  });
5147
5626
  });
5148
5627
  }
5149
- async function readGitStatus(cwd) {
5628
+ async function resolveGitRoot(cwd) {
5150
5629
  const isRepo = await execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
5151
- if (isRepo.code !== 0 || isRepo.stdout.trim() !== 'true') {
5630
+ if (isRepo.code !== 0 || isRepo.stdout.trim() !== 'true')
5631
+ return null;
5632
+ const root = await execGit(cwd, ['rev-parse', '--show-toplevel']);
5633
+ return root.code === 0 && root.stdout.trim() ? root.stdout.trim() : cwd;
5634
+ }
5635
+ async function readGitStatus(cwd) {
5636
+ const gitRoot = await resolveGitRoot(cwd);
5637
+ if (!gitRoot) {
5152
5638
  return { inRepo: false, branch: null, ahead: 0, behind: 0, changes: [] };
5153
5639
  }
5154
- const branchRes = await execGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
5640
+ const branchRes = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
5155
5641
  const branch = branchRes.code === 0 ? branchRes.stdout.trim() : null;
5156
- const trackRes = await execGit(cwd, ['rev-list', '--left-right', '--count', '@{u}...HEAD']);
5642
+ const trackRes = await execGit(gitRoot, ['rev-list', '--left-right', '--count', '@{u}...HEAD']);
5157
5643
  let ahead = 0;
5158
5644
  let behind = 0;
5159
5645
  if (trackRes.code === 0) {
@@ -5161,7 +5647,7 @@ async function readGitStatus(cwd) {
5161
5647
  behind = Number(match[0] ?? 0);
5162
5648
  ahead = Number(match[1] ?? 0);
5163
5649
  }
5164
- const statusRes = await execGit(cwd, ['status', '--porcelain=v1', '--untracked-files=normal']);
5650
+ const statusRes = await execGit(gitRoot, ['status', '--porcelain=v1', '--untracked-files=normal']);
5165
5651
  const changes = [];
5166
5652
  if (statusRes.code === 0) {
5167
5653
  for (const line of statusRes.stdout.split('\n')) {
@@ -5175,13 +5661,13 @@ async function readGitStatus(cwd) {
5175
5661
  return { inRepo: true, branch, ahead, behind, changes };
5176
5662
  }
5177
5663
  async function readGitLog(cwd, limit) {
5178
- const isRepo = await execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
5179
- if (isRepo.code !== 0)
5664
+ const gitRoot = await resolveGitRoot(cwd);
5665
+ if (!gitRoot)
5180
5666
  return { inRepo: false, commits: [] };
5181
5667
  const sep = '\x1f';
5182
5668
  const end = '\x1e';
5183
5669
  const fmt = ['%H', '%an', '%ad', '%s'].join(sep) + end;
5184
- const res = await execGit(cwd, ['log', `-${limit}`, `--pretty=format:${fmt}`, '--date=short']);
5670
+ const res = await execGit(gitRoot, ['log', `-${limit}`, `--pretty=format:${fmt}`, '--date=short']);
5185
5671
  if (res.code !== 0)
5186
5672
  return { inRepo: true, commits: [] };
5187
5673
  const commits = [];
@@ -5244,23 +5730,97 @@ function ensureGitignoreEntry(projectRoot, pattern) {
5244
5730
  }
5245
5731
  }
5246
5732
  async function readGitDiff(cwd, filePath, staged = false) {
5247
- const isRepo = await execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
5248
- if (isRepo.code !== 0) {
5733
+ const gitRoot = await resolveGitRoot(cwd);
5734
+ if (!gitRoot) {
5249
5735
  return { inRepo: false, diff: '', before: null, after: null, diffReport: null };
5250
5736
  }
5251
5737
  const baseArgs = staged ? ['diff', '--cached', '--no-color'] : ['diff', '--no-color'];
5252
5738
  if (!filePath) {
5253
- const res = await execGit(cwd, baseArgs);
5739
+ const res = await execGit(gitRoot, baseArgs);
5254
5740
  return { inRepo: true, diff: res.stdout, before: null, after: null, diffReport: null };
5255
5741
  }
5256
5742
  const isSemantic = filePath.endsWith('.dql') || filePath.endsWith('.dqlnb');
5257
5743
  const [diffRes, before, after] = await Promise.all([
5258
- execGit(cwd, [...baseArgs, '--', filePath]),
5259
- isSemantic ? readHeadBlob(cwd, filePath) : Promise.resolve(null),
5260
- isSemantic ? readWorkingCopy(join(cwd, filePath)) : Promise.resolve(null),
5744
+ execGit(gitRoot, [...baseArgs, '--', filePath]),
5745
+ isSemantic ? readHeadBlob(gitRoot, filePath) : Promise.resolve(null),
5746
+ isSemantic ? readWorkingCopy(join(gitRoot, filePath)) : Promise.resolve(null),
5261
5747
  ]);
5748
+ const diffText = !staged && !diffRes.stdout.trim()
5749
+ ? (await readUntrackedTextDiff(gitRoot, filePath)) || diffRes.stdout
5750
+ : diffRes.stdout;
5262
5751
  const diffReport = isSemantic ? computeSemanticDiff(filePath, before, after) : null;
5263
- return { inRepo: true, diff: diffRes.stdout, before, after, diffReport };
5752
+ return { inRepo: true, diff: diffText, before, after, diffReport };
5753
+ }
5754
+ const MAX_UNTRACKED_DIFF_FILES = 20;
5755
+ const MAX_UNTRACKED_DIFF_BYTES = 512 * 1024;
5756
+ async function readUntrackedTextDiff(cwd, filePath) {
5757
+ const status = await execGit(cwd, ['status', '--porcelain=v1', '--untracked-files=normal', '--', filePath]);
5758
+ if (status.code !== 0 || !status.stdout.split('\n').some((line) => line.startsWith('?? '))) {
5759
+ return '';
5760
+ }
5761
+ const listed = await execGit(cwd, ['ls-files', '--others', '--exclude-standard', '--', filePath]);
5762
+ if (listed.code !== 0)
5763
+ return '';
5764
+ const chunks = [];
5765
+ let totalBytes = 0;
5766
+ for (const rawPath of listed.stdout.split('\n').map((p) => p.trim()).filter(Boolean)) {
5767
+ if (chunks.length >= MAX_UNTRACKED_DIFF_FILES || totalBytes >= MAX_UNTRACKED_DIFF_BYTES)
5768
+ break;
5769
+ const absPath = safeJoin(cwd, rawPath);
5770
+ if (!absPath || !existsSync(absPath))
5771
+ continue;
5772
+ const st = statSync(absPath);
5773
+ if (!st.isFile())
5774
+ continue;
5775
+ if (st.size > MAX_UNTRACKED_DIFF_BYTES) {
5776
+ chunks.push(formatBinaryAddedDiff(rawPath));
5777
+ continue;
5778
+ }
5779
+ const buf = readFileSync(absPath);
5780
+ if (buf.includes(0)) {
5781
+ chunks.push(formatBinaryAddedDiff(rawPath));
5782
+ continue;
5783
+ }
5784
+ totalBytes += buf.length;
5785
+ chunks.push(formatAddedFileDiff(rawPath, buf.toString('utf-8')));
5786
+ }
5787
+ if (chunks.length === 0)
5788
+ return '';
5789
+ const omitted = listed.stdout.split('\n').filter(Boolean).length - chunks.length;
5790
+ if (omitted > 0) {
5791
+ chunks.push(`diff --git a/${filePath} b/${filePath}\n# ${omitted} additional untracked file${omitted === 1 ? '' : 's'} omitted from preview`);
5792
+ }
5793
+ return chunks.join('\n');
5794
+ }
5795
+ function formatAddedFileDiff(filePath, content) {
5796
+ const normalized = content.replace(/\r\n/g, '\n');
5797
+ const hasFinalNewline = normalized.endsWith('\n');
5798
+ const lines = normalized.length === 0
5799
+ ? []
5800
+ : normalized.split('\n').slice(0, hasFinalNewline ? -1 : undefined);
5801
+ const hunk = lines.length > 0
5802
+ ? [`@@ -0,0 +1,${lines.length} @@`, ...lines.map((line) => `+${line}`)]
5803
+ : [];
5804
+ if (!hasFinalNewline && normalized.length > 0)
5805
+ hunk.push('\');
5806
+ return [
5807
+ `diff --git a/${filePath} b/${filePath}`,
5808
+ 'new file mode 100644',
5809
+ 'index 0000000..0000000',
5810
+ '--- /dev/null',
5811
+ `+++ b/${filePath}`,
5812
+ ...hunk,
5813
+ ].join('\n');
5814
+ }
5815
+ function formatBinaryAddedDiff(filePath) {
5816
+ return [
5817
+ `diff --git a/${filePath} b/${filePath}`,
5818
+ 'new file mode 100644',
5819
+ 'index 0000000..0000000',
5820
+ '--- /dev/null',
5821
+ `+++ b/${filePath}`,
5822
+ `Binary file ${filePath} added`,
5823
+ ].join('\n');
5264
5824
  }
5265
5825
  // ── git write operations ──────────────────────────────────────────────────
5266
5826
  // Each helper validates inputs, shells out via execFile (no shell expansion),
@@ -5290,89 +5850,118 @@ function gitErrorOutput(res) {
5290
5850
  return (res.stderr || res.stdout || '').trim();
5291
5851
  }
5292
5852
  async function gitStage(cwd, paths) {
5293
- const v = validatePaths(cwd, paths);
5853
+ const gitRoot = await resolveGitRoot(cwd);
5854
+ if (!gitRoot)
5855
+ return { ok: false, error: 'Not a git repository' };
5856
+ const v = validatePaths(gitRoot, paths);
5294
5857
  if (!v.ok)
5295
5858
  return { ok: false, error: v.error };
5296
- const res = await execGit(cwd, ['add', '--', ...v.paths]);
5859
+ const res = await execGit(gitRoot, ['add', '--', ...v.paths]);
5297
5860
  return res.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(res) };
5298
5861
  }
5299
5862
  async function gitUnstage(cwd, paths) {
5300
- const v = validatePaths(cwd, paths);
5863
+ const gitRoot = await resolveGitRoot(cwd);
5864
+ if (!gitRoot)
5865
+ return { ok: false, error: 'Not a git repository' };
5866
+ const v = validatePaths(gitRoot, paths);
5301
5867
  if (!v.ok)
5302
5868
  return { ok: false, error: v.error };
5303
5869
  // `restore --staged` works with or without HEAD; for an initial commit (no
5304
5870
  // HEAD yet) git's `rm --cached` is the fallback. Try restore first.
5305
- const res = await execGit(cwd, ['restore', '--staged', '--', ...v.paths]);
5871
+ const res = await execGit(gitRoot, ['restore', '--staged', '--', ...v.paths]);
5306
5872
  if (res.code === 0)
5307
5873
  return { ok: true };
5308
- const fallback = await execGit(cwd, ['rm', '--cached', '-r', '--', ...v.paths]);
5874
+ const fallback = await execGit(gitRoot, ['rm', '--cached', '-r', '--', ...v.paths]);
5309
5875
  return fallback.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(fallback) };
5310
5876
  }
5311
5877
  async function gitDiscard(cwd, paths) {
5312
- const v = validatePaths(cwd, paths);
5878
+ const gitRoot = await resolveGitRoot(cwd);
5879
+ if (!gitRoot)
5880
+ return { ok: false, error: 'Not a git repository' };
5881
+ const v = validatePaths(gitRoot, paths);
5313
5882
  if (!v.ok)
5314
5883
  return { ok: false, error: v.error };
5315
5884
  // For tracked files: `restore --worktree` reverts to HEAD. For untracked
5316
5885
  // files: that's a no-op and we delete them via `clean -f`. Run both so
5317
5886
  // the caller doesn't have to know which list each path is in.
5318
- const restore = await execGit(cwd, ['restore', '--worktree', '--', ...v.paths]);
5319
- const clean = await execGit(cwd, ['clean', '-f', '--', ...v.paths]);
5887
+ const restore = await execGit(gitRoot, ['restore', '--worktree', '--', ...v.paths]);
5888
+ const clean = await execGit(gitRoot, ['clean', '-f', '--', ...v.paths]);
5320
5889
  if (restore.code !== 0 && clean.code !== 0) {
5321
5890
  return { ok: false, error: gitErrorOutput(restore) || gitErrorOutput(clean) };
5322
5891
  }
5323
5892
  return { ok: true };
5324
5893
  }
5325
5894
  async function gitCommit(cwd, message, stageAll) {
5895
+ const gitRoot = await resolveGitRoot(cwd);
5896
+ if (!gitRoot)
5897
+ return { ok: false, error: 'Not a git repository' };
5326
5898
  const trimmed = message.trim();
5327
5899
  if (!trimmed)
5328
5900
  return { ok: false, error: 'Commit message required' };
5329
5901
  if (stageAll) {
5330
- const add = await execGit(cwd, ['add', '-A']);
5902
+ const add = await execGit(gitRoot, ['add', '-A']);
5331
5903
  if (add.code !== 0)
5332
5904
  return { ok: false, error: gitErrorOutput(add) };
5333
5905
  }
5334
- const res = await execGit(cwd, ['commit', '-m', trimmed]);
5906
+ const res = await execGit(gitRoot, ['commit', '-m', trimmed]);
5335
5907
  if (res.code !== 0)
5336
5908
  return { ok: false, error: gitErrorOutput(res) };
5337
- const hashRes = await execGit(cwd, ['rev-parse', 'HEAD']);
5909
+ const hashRes = await execGit(gitRoot, ['rev-parse', 'HEAD']);
5338
5910
  return { ok: true, hash: hashRes.code === 0 ? hashRes.stdout.trim() : undefined };
5339
5911
  }
5340
5912
  async function gitPush(cwd) {
5341
- const res = await execGit(cwd, ['push']);
5913
+ const gitRoot = await resolveGitRoot(cwd);
5914
+ if (!gitRoot)
5915
+ return { ok: false, error: 'Not a git repository' };
5916
+ const branch = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
5917
+ const upstream = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
5918
+ const remotes = await execGit(gitRoot, ['remote']);
5919
+ const remote = remotes.stdout.split('\n').map((s) => s.trim()).find(Boolean) ?? 'origin';
5920
+ const branchName = branch.code === 0 ? branch.stdout.trim() : '';
5921
+ const args = upstream.code === 0 || !branchName || branchName === 'HEAD'
5922
+ ? ['push']
5923
+ : ['push', '-u', remote, branchName];
5924
+ const res = await execGit(gitRoot, args);
5342
5925
  return res.code === 0
5343
5926
  ? { ok: true, output: gitErrorOutput(res) }
5344
5927
  : { ok: false, error: gitErrorOutput(res) };
5345
5928
  }
5346
5929
  async function gitPull(cwd) {
5930
+ const gitRoot = await resolveGitRoot(cwd);
5931
+ if (!gitRoot)
5932
+ return { ok: false, error: 'Not a git repository' };
5347
5933
  // `--ff-only` keeps the operation non-destructive: if the local branch has
5348
5934
  // diverged from upstream, we surface the error rather than auto-merging.
5349
5935
  // The user can resolve via the terminal or a future merge UI.
5350
- const res = await execGit(cwd, ['pull', '--ff-only']);
5936
+ const res = await execGit(gitRoot, ['pull', '--ff-only']);
5351
5937
  return res.code === 0
5352
5938
  ? { ok: true, output: gitErrorOutput(res) }
5353
5939
  : { ok: false, error: gitErrorOutput(res) };
5354
5940
  }
5355
5941
  async function readGitBranches(cwd) {
5356
- const isRepo = await execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
5357
- if (isRepo.code !== 0)
5942
+ const gitRoot = await resolveGitRoot(cwd);
5943
+ if (!gitRoot)
5358
5944
  return { inRepo: false, current: null, branches: [] };
5359
- const cur = await execGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
5360
- const list = await execGit(cwd, ['branch', '--list', '--format=%(refname:short)']);
5945
+ const cur = await execGit(gitRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
5946
+ const list = await execGit(gitRoot, ['branch', '--list', '--format=%(refname:short)']);
5361
5947
  const branches = list.code === 0
5362
5948
  ? list.stdout.split('\n').map((s) => s.trim()).filter(Boolean)
5363
5949
  : [];
5364
5950
  return { inRepo: true, current: cur.code === 0 ? cur.stdout.trim() : null, branches };
5365
5951
  }
5366
5952
  async function readGitRemote(cwd) {
5367
- const isRepo = await execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
5368
- if (isRepo.code !== 0)
5953
+ const gitRoot = await resolveGitRoot(cwd);
5954
+ if (!gitRoot)
5369
5955
  return { inRepo: false, url: null, name: null };
5370
- const remoteName = await execGit(cwd, ['config', '--get', 'remote.pushDefault']);
5956
+ const remoteName = await execGit(gitRoot, ['config', '--get', 'remote.pushDefault']);
5371
5957
  const name = remoteName.code === 0 && remoteName.stdout.trim() ? remoteName.stdout.trim() : 'origin';
5372
- const url = await execGit(cwd, ['remote', 'get-url', name]);
5958
+ const url = await execGit(gitRoot, ['remote', 'get-url', name]);
5373
5959
  return { inRepo: true, url: url.code === 0 ? url.stdout.trim() : null, name };
5374
5960
  }
5375
5961
  async function gitCreateBranch(cwd, name, checkout) {
5962
+ const gitRoot = await resolveGitRoot(cwd);
5963
+ if (!gitRoot)
5964
+ return { ok: false, error: 'Not a git repository' };
5376
5965
  const trimmed = name.trim();
5377
5966
  // Branch names can't start with `-` (would be parsed as a flag) and must be
5378
5967
  // non-empty. git itself enforces the rest of the ref-name rules.
@@ -5381,17 +5970,20 @@ async function gitCreateBranch(cwd, name, checkout) {
5381
5970
  if (trimmed.startsWith('-'))
5382
5971
  return { ok: false, error: 'Invalid branch name' };
5383
5972
  const res = checkout
5384
- ? await execGit(cwd, ['checkout', '-b', trimmed])
5385
- : await execGit(cwd, ['branch', trimmed]);
5973
+ ? await execGit(gitRoot, ['checkout', '-b', trimmed])
5974
+ : await execGit(gitRoot, ['branch', trimmed]);
5386
5975
  return res.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(res) };
5387
5976
  }
5388
5977
  async function gitCheckout(cwd, name) {
5978
+ const gitRoot = await resolveGitRoot(cwd);
5979
+ if (!gitRoot)
5980
+ return { ok: false, error: 'Not a git repository' };
5389
5981
  const trimmed = name.trim();
5390
5982
  if (!trimmed)
5391
5983
  return { ok: false, error: 'Branch name required' };
5392
5984
  if (trimmed.startsWith('-'))
5393
5985
  return { ok: false, error: 'Invalid branch name' };
5394
- const res = await execGit(cwd, ['checkout', trimmed]);
5986
+ const res = await execGit(gitRoot, ['checkout', trimmed]);
5395
5987
  return res.code === 0 ? { ok: true } : { ok: false, error: gitErrorOutput(res) };
5396
5988
  }
5397
5989
  async function readHeadBlob(cwd, filePath) {
@@ -5521,6 +6113,92 @@ function isAiPinRefreshDue(lastRefreshedAt) {
5521
6113
  return true;
5522
6114
  return Date.now() - last >= 24 * 60 * 60 * 1000;
5523
6115
  }
6116
+ function buildAgentSchemaContext(question, rows) {
6117
+ const byRelation = new Map();
6118
+ for (const row of rows) {
6119
+ if (!row || typeof row !== 'object')
6120
+ continue;
6121
+ const record = row;
6122
+ const schema = stringFromRecord(record, 'table_schema');
6123
+ const table = stringFromRecord(record, 'table_name');
6124
+ const column = stringFromRecord(record, 'column_name');
6125
+ if (!schema || !table || !column)
6126
+ continue;
6127
+ const relation = `${schema}.${table}`;
6128
+ const current = byRelation.get(relation) ?? {
6129
+ relation,
6130
+ schema,
6131
+ name: table,
6132
+ source: 'runtime information_schema',
6133
+ columns: [],
6134
+ };
6135
+ if (current.columns.length < 80) {
6136
+ current.columns.push({
6137
+ name: column,
6138
+ type: stringFromRecord(record, 'data_type'),
6139
+ });
6140
+ }
6141
+ byRelation.set(relation, current);
6142
+ }
6143
+ const tokens = agentSchemaTokens(question);
6144
+ return Array.from(byRelation.values())
6145
+ .map((table) => ({ table, score: scoreAgentSchemaTable(table, tokens) }))
6146
+ .filter((entry) => entry.score > 0)
6147
+ .sort((a, b) => b.score - a.score || a.table.relation.localeCompare(b.table.relation))
6148
+ .slice(0, 12)
6149
+ .map((entry) => entry.table);
6150
+ }
6151
+ function scoreAgentSchemaTable(table, tokens) {
6152
+ let score = 0;
6153
+ const relationTokens = agentSchemaTokens(`${table.schema ?? ''} ${table.name} ${table.relation}`);
6154
+ for (const token of tokens) {
6155
+ if (relationTokens.has(token))
6156
+ score += 8;
6157
+ }
6158
+ for (const column of table.columns) {
6159
+ const columnTokens = agentSchemaTokens(column.name);
6160
+ for (const token of tokens) {
6161
+ if (columnTokens.has(token))
6162
+ score += 3;
6163
+ }
6164
+ }
6165
+ if (/(customer|order|revenue|product|location|date|month)/i.test(table.name))
6166
+ score += 1;
6167
+ return score;
6168
+ }
6169
+ function agentSchemaTokens(value) {
6170
+ const tokens = new Set();
6171
+ for (const raw of value.toLowerCase().match(/[a-z0-9_]+/g) ?? []) {
6172
+ for (const part of raw.split('_')) {
6173
+ const normalized = normalizeAgentSchemaToken(part);
6174
+ if (!normalized || normalized.length < 3 || AGENT_SCHEMA_STOPWORDS.has(normalized))
6175
+ continue;
6176
+ tokens.add(normalized);
6177
+ }
6178
+ }
6179
+ return tokens;
6180
+ }
6181
+ const AGENT_SCHEMA_STOPWORDS = new Set([
6182
+ 'all', 'and', 'are', 'can', 'data', 'for', 'from', 'have', 'how', 'many', 'me',
6183
+ 'show', 'the', 'this', 'who', 'with', 'value',
6184
+ ]);
6185
+ function normalizeAgentSchemaToken(token) {
6186
+ if (token === 'orders')
6187
+ return 'order';
6188
+ if (token === 'customers')
6189
+ return 'customer';
6190
+ if (token === 'products')
6191
+ return 'product';
6192
+ if (token.endsWith('ies') && token.length > 4)
6193
+ return `${token.slice(0, -3)}y`;
6194
+ if (token.endsWith('s') && token.length > 4)
6195
+ return token.slice(0, -1);
6196
+ return token;
6197
+ }
6198
+ function stringFromRecord(record, key) {
6199
+ const value = record[key];
6200
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
6201
+ }
5524
6202
  function isMemoryScope(value) {
5525
6203
  return value === 'thread'
5526
6204
  || value === 'notebook'