@descix/cli 1.0.5 → 1.0.7

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 (46) hide show
  1. package/README.md +130 -34
  2. package/agent-assets/guides/dynamic-site-pattern.md +10 -14
  3. package/agent-assets/guides/knowledge-only-pattern.md +11 -15
  4. package/agent-assets/guides/microservice-pattern.md +12 -14
  5. package/agent-assets/guides/static-site-pattern.md +12 -15
  6. package/agent-assets/instructions/onboarding-flow.md +4 -4
  7. package/agent-assets/reference/app-creation.md +36 -101
  8. package/agent-assets/reference/architecture.md +24 -27
  9. package/agent-assets/reference/local-dev.md +0 -4
  10. package/agent-assets/reference/mcp-integration.md +14 -22
  11. package/agent-assets/reference/workspace-config.md +97 -340
  12. package/agent-assets/rules/descix_mcp.template.mdc +1 -1
  13. package/bin/descix.js +159 -98
  14. package/lib/api-client.js +30 -22
  15. package/lib/commands/airdrop.js +10 -0
  16. package/lib/commands/auth.js +33 -3
  17. package/lib/commands/briefer/index.js +8 -5
  18. package/lib/commands/config.js +13 -31
  19. package/lib/commands/corpus.js +161 -54
  20. package/lib/commands/health.js +116 -21
  21. package/lib/commands/init.js +25 -48
  22. package/lib/commands/kb.js +14 -4
  23. package/lib/commands/update.js +0 -1
  24. package/lib/core/Chunker.js +1 -1
  25. package/lib/core/CorpusWalker.js +238 -47
  26. package/lib/core/Hydrator.js +88 -11
  27. package/lib/core/ManifestLoader.js +68 -0
  28. package/lib/core/Syncer.js +147 -46
  29. package/lib/interactive.js +158 -0
  30. package/lib/origin.js +3 -3
  31. package/lib/wallet-file.js +36 -37
  32. package/lib/wizard/setup.js +104 -22
  33. package/lib/workspace-config.js +325 -167
  34. package/lib/workspace-identity.js +0 -1
  35. package/package.json +3 -3
  36. package/templates/agent-claude.md +0 -3
  37. package/templates/agent-cline.md +0 -3
  38. package/templates/agent-copilot.md +0 -3
  39. package/templates/agent-cursor.md +0 -3
  40. package/templates/drive/app/kb/General/README.md +1 -1
  41. package/templates/drive_templates/agent_app_template/kb/General/README.md +1 -1
  42. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +3 -3
  43. package/templates/scaffolds/microservice/defaults-config.json +3 -1
  44. package/templates/scaffolds/microservice/manifest.json +9 -5
  45. package/templates/scaffolds/microservice/package.json +1 -1
  46. package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +2 -2
@@ -3,7 +3,12 @@
3
3
  *
4
4
  * WS-CLI-HEALTH-ENV-PIVOT (2026-05-26):
5
5
  * `descix health --env <env>` pivots the probe surface based on env:
6
- * - dev (default): existing local-port checks (UNCHANGED behavior)
6
+ * - dev (default): BOUND (lsof) AND SERVES (a route probe) per service, plus a gateway
7
+ * row from env.gateway.port. A bound port is not a health check: measured
8
+ * 2026-09-16, a port answering 500 to every request reported PASS, and the
9
+ * gateway had no row. A microservice's health path is read from its OWN served
10
+ * manifest (`GET /manifest` → service.healthEndpoint — cloud declares
11
+ * /api/health, powch /health), never assumed.
7
12
  * - demo: gcloud probes against demo cloud resources + synthetic HTTPS
8
13
  * probes against *.demo.descix.net hosts
9
14
  * - prod: gcloud probes against prod cloud resources + synthetic HTTPS
@@ -28,6 +33,7 @@
28
33
  import chalk from 'chalk';
29
34
  import { exec } from 'child_process';
30
35
  import { promisify } from 'util';
36
+ import { localUpstreamOrigin, resolveGatewayPort } from '@descix/app-sdk/dev';
31
37
  import https from 'https';
32
38
  import http from 'http';
33
39
  import { WorkspaceConfig } from '../workspace-config.js';
@@ -67,17 +73,22 @@ async function defaultExec(cmd) {
67
73
  }
68
74
 
69
75
  /**
70
- * Default HTTPS probe. Resolves with { status, error? } — never rejects.
71
- * Tests replace this with a mock.
76
+ * Default HTTP(S) probe. Resolves with { status, body, error? } — never rejects.
77
+ * `insecure` tolerates the workspace's self-signed dev certs and is set ONLY by the DEV
78
+ * path; cloud probes keep TLS validation. Tests replace this with a mock.
72
79
  */
73
- function defaultHttpsProbe(url, timeoutMs = 8000) {
80
+ function defaultHttpsProbe(url, timeoutMs = 8000, { insecure = false } = {}) {
74
81
  return new Promise((resolve) => {
75
82
  try {
76
83
  const parsed = new URL(url);
77
84
  const client = parsed.protocol === 'http:' ? http : https;
78
- const req = client.get(url, { timeout: timeoutMs }, (res) => {
79
- res.resume();
80
- resolve({ status: res.statusCode });
85
+ const opts = { timeout: timeoutMs };
86
+ if (insecure && parsed.protocol === 'https:') opts.rejectUnauthorized = false;
87
+ const req = client.get(url, opts, (res) => {
88
+ let body = '';
89
+ res.setEncoding('utf8');
90
+ res.on('data', (chunk) => { body += chunk; });
91
+ res.on('end', () => resolve({ status: res.statusCode, body }));
81
92
  });
82
93
  req.on('error', (err) => resolve({ status: 0, error: err.message }));
83
94
  req.on('timeout', () => { req.destroy(); resolve({ status: 0, error: 'timeout' }); });
@@ -90,29 +101,34 @@ function defaultHttpsProbe(url, timeoutMs = 8000) {
90
101
  // ─── DEV path (UNCHANGED behavior) ───────────────────────────────────────────
91
102
 
92
103
  /**
93
- * Build the service registry from workspace.json for DEV port checks.
94
- * Returns an array of { name, appId, port, type, protocol? }.
104
+ * Build the service registry from workspace.json for DEV checks.
105
+ * Returns an array of { name, appId, port, type, origin }. `origin` — scheme included — comes
106
+ * from the ONE owner of a local upstream's origin (@descix/app-sdk localOrigin), never a
107
+ * protocol default re-derived here. The gateway is a row too: it is the process a local platform
108
+ * developer most needs to see, and it used to be structurally invisible to this command.
95
109
  */
96
110
  function buildServiceRegistry(wsConfig) {
97
111
  const services = [];
98
112
  const env = wsConfig.env || {};
99
113
 
100
114
  if (env.platform) {
115
+ const name = env.platform.appId || 'platform';
101
116
  if (env.platform.microservice?.port) {
102
117
  services.push({
103
- name: env.platform.appId || 'platform',
118
+ name,
104
119
  appId: env.platform.appId,
105
120
  port: env.platform.microservice.port,
106
121
  type: 'microservice',
122
+ origin: localUpstreamOrigin(env.platform.microservice, 'env.platform.microservice'),
107
123
  });
108
124
  }
109
125
  if (env.platform.site?.port) {
110
126
  services.push({
111
- name: `${env.platform.appId || 'platform'}-site`,
127
+ name: `${name}-site`,
112
128
  appId: env.platform.appId,
113
129
  port: env.platform.site.port,
114
130
  type: 'site',
115
- protocol: env.platform.site.protocol || 'https',
131
+ origin: localUpstreamOrigin(env.platform.site, 'env.platform.site'),
116
132
  });
117
133
  }
118
134
  }
@@ -124,6 +140,7 @@ function buildServiceRegistry(wsConfig) {
124
140
  appId: product.appId,
125
141
  port: product.microservice.port,
126
142
  type: 'microservice',
143
+ origin: localUpstreamOrigin(product.microservice, `env.products[${product.appId}].microservice`),
127
144
  });
128
145
  }
129
146
  if (product.site?.port) {
@@ -132,14 +149,82 @@ function buildServiceRegistry(wsConfig) {
132
149
  appId: product.appId,
133
150
  port: product.site.port,
134
151
  type: 'site',
135
- protocol: product.site.protocol || 'https',
152
+ origin: localUpstreamOrigin(product.site, `env.products[${product.appId}].site`),
136
153
  });
137
154
  }
138
155
  }
139
156
 
157
+ // The gateway — port from its ONE owner (resolveGatewayPort: --port > env.gateway.port >
158
+ // the SDK default), origin from the same owner as every other row.
159
+ const { port: gatewayPort } = resolveGatewayPort(wsConfig);
160
+ services.push({
161
+ name: 'gateway',
162
+ appId: null,
163
+ port: gatewayPort,
164
+ type: 'gateway',
165
+ origin: localUpstreamOrigin({ port: gatewayPort }, 'env.gateway'),
166
+ });
167
+
140
168
  return services;
141
169
  }
142
170
 
171
+ /** What a DEV run reads, and what it does not — printed on green AND red, and carried in JSON. */
172
+ const DEV_PROBE_BOUNDARY =
173
+ 'Probes: bound = lsof on the port; serves = microservices: GET /manifest then GET its ' +
174
+ 'service.healthEndpoint (2xx); sites: GET / (2xx); gateway: GET /__descix/app-binding.json ' +
175
+ '(a JSON binding, not HTML). Not probed: authentication, any command, credits, the mesh ' +
176
+ 'registration, or the deployed cloud — use --env demo|prod for that.';
177
+
178
+ /** A JSON body, or null when it is not one (a vite dev server answers 200 HTML for any path). */
179
+ function parseJsonBody(body) {
180
+ try {
181
+ const v = JSON.parse(body);
182
+ return v && typeof v === 'object' ? v : null;
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+
188
+ const is2xx = (status) => typeof status === 'number' && status >= 200 && status < 300;
189
+
190
+ /**
191
+ * DEV mode: does the bound process actually SERVE? Resolves { serves, probe } where `probe` says
192
+ * exactly what was asked and what came back, so a FAIL line names its own reason.
193
+ */
194
+ async function checkServesDev(svc, httpsProbeFn) {
195
+ const opts = { insecure: true };
196
+ try {
197
+ if (svc.type === 'microservice') {
198
+ const m = await httpsProbeFn(`${svc.origin}/manifest`, 8000, opts);
199
+ if (!is2xx(m.status)) {
200
+ return { serves: false, probe: `GET /manifest -> ${m.status || m.error || 'unreachable'}` };
201
+ }
202
+ const manifest = parseJsonBody(m.body);
203
+ const healthEndpoint = manifest?.service?.healthEndpoint;
204
+ if (typeof healthEndpoint !== 'string' || healthEndpoint === '') {
205
+ return { serves: false, probe: 'GET /manifest -> 200 but it names no service.healthEndpoint' };
206
+ }
207
+ const h = await httpsProbeFn(`${svc.origin}${healthEndpoint}`, 8000, opts);
208
+ return { serves: is2xx(h.status), probe: `GET ${healthEndpoint} -> ${h.status || h.error || 'unreachable'}` };
209
+ }
210
+ if (svc.type === 'gateway') {
211
+ const b = await httpsProbeFn(`${svc.origin}/__descix/app-binding.json`, 8000, opts);
212
+ const binding = is2xx(b.status) ? parseJsonBody(b.body) : null;
213
+ const isBinding = !!binding && typeof binding.mode === 'string';
214
+ return {
215
+ serves: isBinding,
216
+ probe: `GET /__descix/app-binding.json -> ${b.status || b.error || 'unreachable'}` +
217
+ (is2xx(b.status) && !isBinding ? ' (not a binding: something else is on this port)' : ''),
218
+ };
219
+ }
220
+ // site
221
+ const s = await httpsProbeFn(`${svc.origin}/`, 8000, opts);
222
+ return { serves: is2xx(s.status) && !!s.body, probe: `GET / -> ${s.status || s.error || 'unreachable'}` };
223
+ } catch (err) {
224
+ return { serves: false, probe: `probe threw: ${err.message}` };
225
+ }
226
+ }
227
+
143
228
  /**
144
229
  * DEV mode: check if a local port is listening (lsof).
145
230
  */
@@ -402,16 +487,16 @@ export async function runHealth(options = {}) {
402
487
  }
403
488
 
404
489
  if (env === 'dev') {
405
- return runHealthDev(wsConfig, filterName, jsonOutput, execFn);
490
+ return runHealthDev(wsConfig, filterName, jsonOutput, execFn, httpsProbeFn);
406
491
  }
407
492
 
408
493
  return runHealthCloud(env, wsConfig, filterName, jsonOutput, execFn, httpsProbeFn);
409
494
  }
410
495
 
411
496
  /**
412
- * DEV path — unchanged from pre-pivot behavior.
497
+ * DEV path — bound AND serves, per service, plus the gateway.
413
498
  */
414
- async function runHealthDev(wsConfig, filterName, jsonOutput, execFn) {
499
+ async function runHealthDev(wsConfig, filterName, jsonOutput, execFn, httpsProbeFn) {
415
500
  let services = buildServiceRegistry(wsConfig);
416
501
 
417
502
  if (filterName) {
@@ -432,13 +517,20 @@ async function runHealthDev(wsConfig, filterName, jsonOutput, execFn) {
432
517
  const results = [];
433
518
  const startTime = Date.now();
434
519
  for (const svc of services) {
435
- const check = await checkPortDev(svc.port, execFn);
520
+ const bound = (await checkPortDev(svc.port, execFn)).healthy;
521
+ const { serves, probe } = bound
522
+ ? await checkServesDev(svc, httpsProbeFn)
523
+ : { serves: false, probe: 'not bound' };
436
524
  results.push({
437
525
  service: svc.name,
438
526
  appId: svc.appId,
439
527
  type: svc.type,
440
528
  port: svc.port,
441
- ...check,
529
+ bound,
530
+ serves,
531
+ serves_probe: probe,
532
+ healthy: bound && serves,
533
+ method: 'port+route',
442
534
  });
443
535
  }
444
536
  const totalTime = Date.now() - startTime;
@@ -447,7 +539,8 @@ async function runHealthDev(wsConfig, filterName, jsonOutput, execFn) {
447
539
  environment: 'dev',
448
540
  timestamp: new Date().toISOString(),
449
541
  check_duration_ms: totalTime,
450
- probe_surface: 'local-port',
542
+ probe_surface: 'local-port+route',
543
+ probe_boundary: DEV_PROBE_BOUNDARY,
451
544
  services: results,
452
545
  all_healthy: results.every(r => r.healthy),
453
546
  summary: {
@@ -466,10 +559,12 @@ async function runHealthDev(wsConfig, filterName, jsonOutput, execFn) {
466
559
  console.log(chalk.cyan('='.repeat(50)));
467
560
  console.log(chalk.gray(` Timestamp: ${output.timestamp}`));
468
561
  console.log(chalk.gray(` Check duration: ${output.check_duration_ms}ms`));
469
- console.log(chalk.gray(` Probe surface: local-port (lsof)\n`));
562
+ console.log(chalk.gray(` Probe surface: local-port+route`));
563
+ console.log(chalk.gray(` ${DEV_PROBE_BOUNDARY}\n`));
470
564
 
471
565
  for (const r of results) {
472
- const probeDetail = `${r.type}, port:${r.port}`;
566
+ const yn = (v) => (v ? 'yes' : 'no');
567
+ const probeDetail = `${r.type}, port:${r.port}, bound:${yn(r.bound)}, serves:${yn(r.serves)}, ${r.serves_probe}`;
473
568
  console.log(formatProbeLine(r.service, 'dev', r.healthy, probeDetail));
474
569
  }
475
570
 
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  import chalk from 'chalk';
8
- import readline from 'readline';
9
8
  import path from 'path';
10
9
  import fs from 'fs/promises';
11
10
  import { fileURLToPath } from 'url';
@@ -13,56 +12,33 @@ import { dirname, join } from 'path';
13
12
  // clone.js is imported dynamically inside the invite flow to avoid circular deps
14
13
  import { WorkspaceConfig } from '../workspace-config.js';
15
14
  import { generateAgentFiles } from '../agent-files.js';
15
+ // The canonical KB-sync surface is owned by retired-kb-sync.js. Consume the constant: a literal
16
+ // spelled here is a second derivation of the same fact, and the removed verb this replaced
17
+ // reached a developer who had done everything right.
18
+ import { CANONICAL_KB_SYNC } from './retired-kb-sync.js';
19
+ // "May I prompt?" has ONE OWNER. init does not derive it, and holds no TTY check of its own.
20
+ import { createPromptSession } from '../interactive.js';
16
21
 
17
22
  const __filename = fileURLToPath(import.meta.url);
18
23
  const __dirname = dirname(__filename);
19
24
 
20
- /**
21
- * Create readline interface for interactive prompts
22
- */
23
- function createPrompt() {
24
- return readline.createInterface({
25
- input: process.stdin,
26
- output: process.stdout
27
- });
28
- }
29
-
30
- /**
31
- * Ask for text input with optional default
32
- */
33
- function askInput(rl, question, defaultValue = '') {
34
- return new Promise((resolve) => {
35
- const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
36
- rl.question(prompt, (answer) => {
37
- resolve(answer.trim() || defaultValue);
38
- });
39
- });
40
- }
41
-
42
- /**
43
- * Ask a yes/no question
44
- */
45
- function askYesNo(rl, question, defaultYes = true) {
46
- return new Promise((resolve) => {
47
- const suffix = defaultYes ? '(Y/n)' : '(y/N)';
48
- rl.question(`${question} ${suffix}: `, (answer) => {
49
- const trimmed = answer.trim().toLowerCase();
50
- if (trimmed === '') {
51
- resolve(defaultYes);
52
- } else {
53
- resolve(trimmed === 'y' || trimmed === 'yes');
54
- }
55
- });
56
- });
57
- }
58
-
59
25
  /**
60
26
  * Run the init command
61
27
  * @param {Object} apiClient - API client (optional, for listing communities)
62
28
  * @param {Object} options - Command options
63
29
  */
64
30
  export async function runInit(apiClient, options = {}) {
65
- const rl = createPrompt();
31
+ // The session is GATED AT CONSTRUCTION: under a non-TTY this throws NonInteractiveError,
32
+ // which bin/descix.js's init action prints and exits 1 on. It can no longer hang on an open
33
+ // pipe/FIFO, nor exit 0 writing nothing on /dev/null.
34
+ const rl = createPromptSession({
35
+ what: 'descix init',
36
+ nonInteractiveForm: [
37
+ 'descix init has no non-interactive form today. Run it in a terminal.',
38
+ '',
39
+ 'Nothing has been changed. An existing .descix/workspace.json is left as it is.'
40
+ ]
41
+ });
66
42
  const projectPath = options.path ? path.resolve(options.path) : process.cwd();
67
43
 
68
44
  // If --from-invite provided, resolve the invite first to pre-fill context
@@ -130,7 +106,7 @@ export async function runInit(apiClient, options = {}) {
130
106
  }
131
107
 
132
108
  if (hasExisting && !options.force) {
133
- const overwrite = await askYesNo(rl, chalk.yellow('Workspace already initialized. Overwrite?'), false);
109
+ const overwrite = await rl.askYesNo(chalk.yellow('Workspace already initialized. Overwrite?'), false);
134
110
  if (!overwrite) {
135
111
  console.log(chalk.gray('\nInitialization cancelled.\n'));
136
112
  rl.close();
@@ -156,7 +132,7 @@ export async function runInit(apiClient, options = {}) {
156
132
  // ignore
157
133
  }
158
134
  }
159
- communityId = await askInput(rl, chalk.white('Community ID'));
135
+ communityId = await rl.ask(chalk.white('Community ID'));
160
136
  if (!communityId) {
161
137
  console.log(chalk.red('\n❌ Community ID is required.\n'));
162
138
  rl.close();
@@ -167,7 +143,7 @@ export async function runInit(apiClient, options = {}) {
167
143
  const defaultAppName = path.basename(projectPath).toLowerCase().replace(/[^a-z0-9]/g, '_');
168
144
  let appName = options.app;
169
145
  if (!appName) {
170
- appName = await askInput(rl, chalk.white('App name'), defaultAppName);
146
+ appName = await rl.ask(chalk.white('App name'), defaultAppName);
171
147
  if (!appName) {
172
148
  console.log(chalk.red('\n❌ App name is required.\n'));
173
149
  rl.close();
@@ -181,7 +157,7 @@ export async function runInit(apiClient, options = {}) {
181
157
  console.log(chalk.white(` Community: ${communityId}`));
182
158
  console.log(chalk.white(` App: ${appName} (${appId})`));
183
159
 
184
- const proceed = await askYesNo(rl, chalk.white('\nProceed?'), true);
160
+ const proceed = await rl.askYesNo(chalk.white('\nProceed?'), true);
185
161
  if (!proceed) {
186
162
  console.log(chalk.gray('\nCancelled.\n'));
187
163
  rl.close();
@@ -208,8 +184,9 @@ export async function runInit(apiClient, options = {}) {
208
184
 
209
185
  console.log(chalk.cyan('\n─── Next Steps ───\n'));
210
186
  console.log(chalk.white(` 1. descix app init -a ${appId} -c ${communityId} # register app on platform`));
211
- console.log(chalk.white(` 2. descix update kb -c ${communityId} -a ${appId} # sync KB to Pinecone`));
212
- console.log(chalk.white(` 3. descix chat -c ${communityId} -a ${appId} -q "test" # verify RAG\n`));
187
+ console.log(chalk.gray(` 2. Create a corpus manifest at .descix/manifests/General.json`));
188
+ console.log(chalk.white(` 3. ${CANONICAL_KB_SYNC} -a ${appId} # sync KB to Pinecone`));
189
+ console.log(chalk.white(` 4. descix chat -c ${communityId} -a ${appId} -q "test" # verify RAG\n`));
213
190
  console.log(chalk.green('✅ Workspace initialized.\n'));
214
191
  rl.close();
215
192
  return { created: ['.descix/workspace.json'], skipped: [], warnings: [] };
@@ -250,7 +227,7 @@ export async function initWorkspace(options) {
250
227
  created,
251
228
  skipped: [],
252
229
  warnings: [],
253
- nextSteps: [`descix app init -a ${appId} -c ${communityId}`, `descix update kb -c ${communityId} -a ${appId}`]
230
+ nextSteps: [`descix app init -a ${appId} -c ${communityId}`, `${CANONICAL_KB_SYNC} -a ${appId}`]
254
231
  };
255
232
  }
256
233
 
@@ -22,11 +22,17 @@ import ora from 'ora';
22
22
  import * as path from 'path';
23
23
  import * as fs from 'fs/promises';
24
24
  import inquirer from 'inquirer';
25
+ // "May I prompt?" has ONE OWNER. --interactive is an opt-IN, not a guarantee a human is there.
26
+ import { requireInteractive } from '../interactive.js';
25
27
  import { WorkspaceConfig } from '../workspace-config.js';
26
28
  import { hydrateKb, pushStaging, checkStagingFiles } from '../core/Hydrator.js';
27
29
  import { processKb } from '../core/Chunker.js';
28
30
  import { syncKb, getSyncStatus } from '../core/Syncer.js';
29
31
  import * as driveADC from '../google-storage-adc.js';
32
+ // The one canonical KB-sync surface, from its owner. Never spell it as a literal here: these
33
+ // commands' next steps are read by a developer as instructions, so they must name the live verb
34
+ // and nothing else.
35
+ import { CANONICAL_KB_SYNC } from './retired-kb-sync.js';
30
36
 
31
37
  // ============ Pull Command ============
32
38
 
@@ -110,8 +116,7 @@ export async function runKbPull(apiClient, options) {
110
116
  if (!options.quiet) {
111
117
  console.log(chalk.cyan('\n📋 Next steps:'));
112
118
  console.log(chalk.gray(' 1. Review files in kb/' + kbId + '/'));
113
- console.log(chalk.gray(' 2. Run "descix kb chunk" to generate chunks'));
114
- console.log(chalk.gray(' 3. Run "descix kb sync" to push to Pinecone\n'));
119
+ console.log(chalk.gray(` 2. Run "${CANONICAL_KB_SYNC} -a <app_id>" to push to Pinecone\n`));
115
120
  }
116
121
 
117
122
  return result;
@@ -173,6 +178,11 @@ export async function runKbPush(apiClient, options) {
173
178
  // Create conflict prompt callback for interactive mode
174
179
  const onConflictPrompt = options.interactive ? async (fileName, fileInfo) => {
175
180
  spinner.stop();
181
+ requireInteractive({
182
+ what: 'descix kb push --interactive',
183
+ question: `File "${fileName}" exists in Drive. Overwrite or skip?`,
184
+ nonInteractiveForm: ['descix kb push # without --interactive, conflicts resolve without a prompt']
185
+ });
176
186
  const { action } = await inquirer.prompt([{
177
187
  type: 'list',
178
188
  name: 'action',
@@ -220,8 +230,8 @@ export async function runKbPush(apiClient, options) {
220
230
  // Show next steps
221
231
  if (!options.quiet) {
222
232
  console.log(chalk.cyan('\n📋 Next steps:'));
223
- console.log(chalk.gray(' 1. Run "descix kb pull" to sync all Drive content'));
224
- console.log(chalk.gray(' 2. Run "descix kb chunk" to generate chunks\n'));
233
+ console.log(chalk.gray(' 1. Run "descix drive pull" to sync all Drive content'));
234
+ console.log(chalk.gray(` 2. Run "${CANONICAL_KB_SYNC} -a <app_id>" to push to Pinecone\n`));
225
235
  }
226
236
 
227
237
  return result;
@@ -4,7 +4,6 @@
4
4
  * Commands for updating resources based on workspace.json context detection:
5
5
  * descix update # Auto-detect what to update
6
6
  * descix update app # Sync app assets to Drive
7
- * descix update kb # Sync KB + vectorize
8
7
  * descix update site # Deploy site to GCS
9
8
  * descix update all # Update everything
10
9
  *
@@ -938,7 +938,7 @@ export async function processKb(config, options = {}) {
938
938
  try {
939
939
  await fs.access(srcDir);
940
940
  } catch {
941
- throw new Error(`Source directory not found: ${srcDir}\nRun "descix kb pull" first.`);
941
+ throw new Error(`Source directory not found: ${srcDir}\nRun "descix drive pull" first.`);
942
942
  }
943
943
 
944
944
  await fs.mkdir(chunksDir, { recursive: true });