geotechcli 0.4.0 → 0.4.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.
@@ -1 +1 @@
1
- {"version":3,"file":"ai.d.ts","sourceRoot":"","sources":["../../src/commands/ai.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+WpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyN5D;AAMD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwChE;AAMD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgDzD;AAkGD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA8K3D;AAMD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0I1D;AAMD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA4F5D"}
1
+ {"version":3,"file":"ai.d.ts","sourceRoot":"","sources":["../../src/commands/ai.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6lBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA4Q5D;AAMD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwChE;AAMD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgDzD;AAkGD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA8K3D;AAMD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0I1D;AAMD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuF5D"}
@@ -1,11 +1,10 @@
1
1
  import { Command } from 'commander';
2
2
  import { readFileSync, writeFileSync } from 'node:fs';
3
- import ora from 'ora';
4
3
  import chalk from 'chalk';
5
4
  import { buildLLMConfig, DEFAULT_LLM_VISION_MODEL, analyzeCoreBox, classifyRMRFromImage, classifySoilFromDescription, interpretBoreholeLog, queryGBRDocument, interpretSensorImage, runAgent, runSwarm, AgentConversation, loadProject, getProjectAgentContext, addAgentSession, addArtifact, addNote, saveNamedDataset, saveDerivedParameter, setActiveAnalysisContext, generateReport, generateReportFromCaseFile, renderReportAsPdf, renderReportAsDocx, buildSwarmSessionProjectRecord, persistSwarmCaseFile, persistCaseFileEvidence, } from '@geotechcli/core';
6
- import { heading, keyValue, renderJSON, success, error, warn, renderTable } from '../ui/terminal.js';
5
+ import { heading, keyValue, renderJSON, success, error, warn, renderTable, info } from '../ui/terminal.js';
7
6
  import { addGlobalFlags, getGlobalFlags } from '../util/flags.js';
8
- import { estimateHostedBetaVisionBodyBytes, formatByteSize, HOSTED_BETA_REQUEST_LIMIT_BYTES, readVisionInput, resolveStructuredOutputTarget, HOSTED_BETA_REQUEST_SAFE_BYTES, } from '../util/vision-output.js';
7
+ import { estimateHostedBetaVisionBodyBytes, formatByteSize, HOSTED_BETA_REQUEST_LIMIT_BYTES, readVisionInput, readVisionPdfPageInputs, resolveStructuredOutputTarget, HOSTED_BETA_REQUEST_SAFE_BYTES, } from '../util/vision-output.js';
9
8
  async function checkQuota(_callType) {
10
9
  // Strong-beta hosted limits are enforced server-side by the beta proxy.
11
10
  // Keep the CLI permissive here so successful completions, retries, and
@@ -40,8 +39,8 @@ function describeVisionInput(file) {
40
39
  console.log('');
41
40
  console.log(chalk.yellow(' PDF input detected.'));
42
41
  console.log(chalk.gray(' GLM vision works best with PNG or JPG images.'));
43
- console.log(chalk.gray(' For PDFs, export a single page to PNG/JPG first, or split the PDF into smaller files.'));
44
- console.log(chalk.gray(' The CLI will block oversized PDFs before upload to avoid the hosted-beta body limit.'));
42
+ console.log(chalk.gray(' For borehole logs, the CLI can split multi-page PDFs into page-level requests automatically.'));
43
+ console.log(chalk.gray(' Oversized PDF pages will still be blocked before upload to avoid the hosted-beta body limit.'));
45
44
  console.log('');
46
45
  }
47
46
  function ensureHostedBetaVisionPayloadWithinLimit(file, details) {
@@ -84,6 +83,200 @@ function formatMaybe(value, suffix = '') {
84
83
  return 'Unavailable';
85
84
  return `${value}${suffix}`;
86
85
  }
86
+ function startProgress(flags, text) {
87
+ if (flags.json || flags.quiet) {
88
+ return null;
89
+ }
90
+ info(text);
91
+ return {
92
+ succeed(message) {
93
+ success(message);
94
+ },
95
+ fail(message) {
96
+ error(message);
97
+ },
98
+ };
99
+ }
100
+ const SWARM_AGENT_LABELS = {
101
+ orchestrator: 'Mohr',
102
+ interpretation: 'Bieniawski',
103
+ simulation: 'Terzaghi',
104
+ reviewer: 'Hoek',
105
+ };
106
+ function formatToolPreview(args, limit) {
107
+ if (!args) {
108
+ return '';
109
+ }
110
+ const serialized = JSON.stringify(args);
111
+ return serialized.length > limit ? `${serialized.slice(0, limit)}...` : serialized;
112
+ }
113
+ function renderWarningsCompact(warnings) {
114
+ if (warnings.length === 0)
115
+ return;
116
+ const uniqueWarnings = [...new Set(warnings.map((warning) => warning.trim()).filter(Boolean))];
117
+ const visibleWarnings = uniqueWarnings.slice(0, 5);
118
+ console.log(chalk.yellow(' Warnings:'));
119
+ for (const warning of visibleWarnings) {
120
+ console.log(chalk.yellow(` - ${warning}`));
121
+ }
122
+ if (uniqueWarnings.length > visibleWarnings.length) {
123
+ console.log(chalk.yellow(` - ${uniqueWarnings.length - visibleWarnings.length} more warning(s) omitted.`));
124
+ }
125
+ }
126
+ function renderParseSafetyCompact(result) {
127
+ keyValue('Parse status', result.parseStatus);
128
+ keyValue('Confidence', `${result.confidence}%`);
129
+ keyValue('Auto proceed', result.canAutoProceed ? 'Yes' : 'No');
130
+ renderWarningsCompact(result.warnings);
131
+ }
132
+ function mergeBoreholeLayers(layers) {
133
+ const deduped = new Map();
134
+ for (const layer of layers) {
135
+ const key = [
136
+ layer.depthFrom ?? 'na',
137
+ layer.depthTo ?? 'na',
138
+ (layer.description ?? '').trim().toLowerCase(),
139
+ (layer.uscsSymbol ?? '').trim().toUpperCase(),
140
+ layer.sptN ?? 'na',
141
+ ].join('|');
142
+ if (!deduped.has(key)) {
143
+ deduped.set(key, layer);
144
+ }
145
+ }
146
+ return [...deduped.values()].sort((left, right) => {
147
+ const leftDepth = left.depthFrom ?? Number.POSITIVE_INFINITY;
148
+ const rightDepth = right.depthFrom ?? Number.POSITIVE_INFINITY;
149
+ return leftDepth - rightDepth;
150
+ });
151
+ }
152
+ function mergeBoreholeInterpretations(pages, overrideBoreholeId) {
153
+ const validPages = pages.filter(({ result }) => result.layers.length > 0 || result.totalDepth != null || result.summary);
154
+ const sourcePages = validPages.length > 0 ? validPages : pages;
155
+ const mergedLayers = mergeBoreholeLayers(sourcePages.flatMap(({ result }) => result.layers));
156
+ const summaries = [...new Set(sourcePages.map(({ result }) => result.summary?.trim()).filter((value) => Boolean(value)))];
157
+ const warnings = [...new Set(pages.flatMap(({ pageNumber, result }) => result.warnings.map((warning) => `Page ${pageNumber}: ${warning}`)))];
158
+ const confidences = sourcePages.map(({ result }) => result.confidence);
159
+ const averageConfidence = confidences.length > 0
160
+ ? Math.round(confidences.reduce((sum, value) => sum + value, 0) / confidences.length)
161
+ : 0;
162
+ const totalDepth = sourcePages.reduce((maxDepth, { result }) => {
163
+ if (result.totalDepth == null)
164
+ return maxDepth;
165
+ return maxDepth == null ? result.totalDepth : Math.max(maxDepth, result.totalDepth);
166
+ }, null);
167
+ const waterTableDepth = sourcePages.reduce((selected, { result }) => {
168
+ if (result.waterTableDepth == null)
169
+ return selected;
170
+ return selected == null ? result.waterTableDepth : Math.min(selected, result.waterTableDepth);
171
+ }, null);
172
+ const parseStatus = mergedLayers.length > 0 && totalDepth != null
173
+ ? 'parsed'
174
+ : mergedLayers.length > 0 || summaries.length > 0 || totalDepth != null
175
+ ? 'partial'
176
+ : 'failed';
177
+ return {
178
+ boreholeId: overrideBoreholeId
179
+ ?? sourcePages.map(({ result }) => result.boreholeId).find((value) => value && value !== 'BH-unknown')
180
+ ?? 'BH-unknown',
181
+ totalDepth,
182
+ waterTableDepth,
183
+ layers: mergedLayers,
184
+ summary: summaries.length > 0 ? summaries.join(' ') : null,
185
+ rawLLMText: pages.map(({ pageNumber, result }) => `[Page ${pageNumber}]\n${result.rawLLMText}`).join('\n\n'),
186
+ latencyMs: pages.reduce((sum, { result }) => sum + result.latencyMs, 0),
187
+ parseStatus,
188
+ confidence: averageConfidence,
189
+ warnings,
190
+ canAutoProceed: parseStatus === 'parsed' && averageConfidence >= 70,
191
+ };
192
+ }
193
+ function handleCommandErrorClean(err, flags, code = 'command_failed') {
194
+ const message = getErrorMessage(err);
195
+ process.exitCode = 1;
196
+ if (flags.json) {
197
+ renderJSON({ error: { code, message } });
198
+ return;
199
+ }
200
+ if (code.includes('vision') || code.includes('corebox') || code.includes('rmr') || code.includes('sensor') || code.includes('borehole')) {
201
+ const lowered = message.toLowerCase();
202
+ if (lowered.includes('no content') ||
203
+ lowered.includes('empty') ||
204
+ lowered.includes('upstream') ||
205
+ lowered.includes('hosted beta proxy') ||
206
+ lowered.includes('too large for the hosted beta proxy') ||
207
+ lowered.includes('safe limit')) {
208
+ error(message);
209
+ console.log('');
210
+ console.log(chalk.gray(' Vision troubleshooting tips:'));
211
+ console.log(chalk.gray(' - Use PNG or JPG images (not PDF or BMP)'));
212
+ console.log(chalk.gray(' - Ensure the image is well-lit and clearly shows the subject'));
213
+ console.log(chalk.gray(' - Try a smaller image file (< 5 MB)'));
214
+ console.log(chalk.gray(' - Wait a moment and retry; the AI provider may be busy'));
215
+ console.log(chalk.gray(' - Run with --verbose to see the raw response'));
216
+ return;
217
+ }
218
+ }
219
+ error(message);
220
+ }
221
+ function renderAgentStepPlain(step, json, quiet = false) {
222
+ if (json || quiet)
223
+ return;
224
+ switch (step.type) {
225
+ case 'thought':
226
+ return;
227
+ case 'tool_call':
228
+ console.log(chalk.cyan(` [Terzaghi] Tool: ${step.toolName}(${formatToolPreview(step.toolArgs, 120)})`));
229
+ return;
230
+ case 'tool_result':
231
+ if (step.toolResult?.success) {
232
+ console.log(chalk.green(` [Terzaghi] Result: ${step.content}`));
233
+ }
234
+ else {
235
+ console.log(chalk.red(` [Terzaghi] Error: ${step.content}`));
236
+ }
237
+ return;
238
+ case 'answer':
239
+ return;
240
+ case 'error':
241
+ console.log(chalk.red(` [Terzaghi] Error: ${step.content}`));
242
+ return;
243
+ }
244
+ }
245
+ function renderSwarmStepPlain(step, json, quiet = false) {
246
+ if (json || quiet)
247
+ return;
248
+ const label = SWARM_AGENT_LABELS[step.agent] ?? step.agent;
249
+ const tag = `[${label}]`;
250
+ switch (step.type) {
251
+ case 'thought':
252
+ return;
253
+ case 'tool_call':
254
+ console.log(chalk.cyan(` ${tag} Tool: ${step.toolName}(${formatToolPreview(step.toolArgs, 100)})`));
255
+ return;
256
+ case 'tool_result':
257
+ if (step.toolResult?.success) {
258
+ console.log(chalk.green(` ${tag} Result: ${step.content.slice(0, 180)}`));
259
+ }
260
+ else {
261
+ console.log(chalk.red(` ${tag} Error: ${step.content.slice(0, 180)}`));
262
+ }
263
+ return;
264
+ case 'handoff':
265
+ console.log(chalk.magenta(` ${tag} Handoff: ${step.content}`));
266
+ return;
267
+ case 'review':
268
+ console.log(chalk.green(` ${tag} Review: ${step.content}`));
269
+ return;
270
+ case 'correction':
271
+ console.log(chalk.red(` ${tag} Correction: ${step.content.slice(0, 200)}`));
272
+ return;
273
+ case 'answer':
274
+ return;
275
+ case 'error':
276
+ console.log(chalk.red(` ${tag} Error: ${step.content}`));
277
+ return;
278
+ }
279
+ }
87
280
  function sanitizeErrorMessage(message) {
88
281
  return message.replace(/(?:Bearer |sk-|zhipu-|api[_-]?key[=: ]*)[^\s"'\]},]*/gi, '***REDACTED***');
89
282
  }
@@ -122,10 +315,15 @@ function handleCommandError(err, flags, code = 'command_failed') {
122
315
  function renderWarnings(warnings) {
123
316
  if (warnings.length === 0)
124
317
  return;
318
+ const uniqueWarnings = [...new Set(warnings.map((warning) => warning.trim()).filter(Boolean))];
319
+ const visibleWarnings = uniqueWarnings.slice(0, 5);
125
320
  console.log(chalk.yellow(' Warnings:'));
126
- for (const warning of warnings) {
321
+ for (const warning of visibleWarnings) {
127
322
  console.log(chalk.yellow(` - ${warning}`));
128
323
  }
324
+ if (uniqueWarnings.length > visibleWarnings.length) {
325
+ console.log(chalk.yellow(` - ${uniqueWarnings.length - visibleWarnings.length} more warning(s) omitted.`));
326
+ }
129
327
  }
130
328
  function renderParseSafety(result) {
131
329
  keyValue('Parse status', result.parseStatus);
@@ -249,13 +447,13 @@ export function registerVisionCommand(program) {
249
447
  .description('AI vision analysis for geotechnical images');
250
448
  // Core box analysis
251
449
  const coreboxCmd = new Command('corebox')
252
- .description('Analyze core box image RQD, fracture spacing, weathering')
450
+ .description('Analyze a core box image for RQD, fracture spacing, and weathering')
253
451
  .argument('<image>', 'Path to core box image')
254
452
  .action(async (imagePath, opts) => {
255
453
  const flags = getGlobalFlags(opts);
256
454
  if (!(await checkQuota('visionCalls')))
257
455
  return;
258
- const spinner = flags.json ? null : ora({ text: 'Analyzing core box image...', indent: 2 }).start();
456
+ const spinner = startProgress(flags, 'Analyzing core box image...');
259
457
  try {
260
458
  const file = readVisionInput(imagePath);
261
459
  describeVisionInput(file);
@@ -273,7 +471,7 @@ export function registerVisionCommand(program) {
273
471
  return;
274
472
  }
275
473
  heading('Core Box Analysis');
276
- renderParseSafety(result);
474
+ renderParseSafetyCompact(result);
277
475
  keyValue('RQD', formatMaybe(result.rqd, '%'));
278
476
  keyValue('Fracture spacing', formatMaybe(result.fractureSpacing));
279
477
  keyValue('Weathering grade', formatMaybe(result.weatheringGrade));
@@ -288,20 +486,20 @@ export function registerVisionCommand(program) {
288
486
  }
289
487
  catch (err) {
290
488
  spinner?.fail('Analysis failed');
291
- handleCommandError(err, flags, 'corebox_analysis_failed');
489
+ handleCommandErrorClean(err, flags, 'corebox_analysis_failed');
292
490
  }
293
491
  });
294
492
  addGlobalFlags(coreboxCmd);
295
493
  vision.addCommand(coreboxCmd);
296
494
  // Hybrid RMR from image
297
495
  const rmrImageCmd = new Command('rmr')
298
- .description('Hybrid RMR: vision extracts features deterministic RMR scoring')
496
+ .description('Hybrid RMR: vision extracts features, then deterministic RMR scoring')
299
497
  .argument('<image>', 'Path to rock face / core image')
300
498
  .action(async (imagePath, opts) => {
301
499
  const flags = getGlobalFlags(opts);
302
500
  if (!(await checkQuota('visionCalls')))
303
501
  return;
304
- const spinner = flags.json ? null : ora({ text: 'Extracting rock mass parameters from image...', indent: 2 }).start();
502
+ const spinner = startProgress(flags, 'Extracting rock mass parameters from image...');
305
503
  try {
306
504
  const file = readVisionInput(imagePath);
307
505
  describeVisionInput(file);
@@ -319,7 +517,7 @@ export function registerVisionCommand(program) {
319
517
  return;
320
518
  }
321
519
  heading('Hybrid RMR Classification (Vision + Deterministic)');
322
- renderParseSafety(result);
520
+ renderParseSafetyCompact(result);
323
521
  console.log('');
324
522
  console.log(chalk.gray(' Vision-extracted parameters:'));
325
523
  keyValue(' Estimated UCS', formatMaybe(result.visionExtraction.estimatedUCS, ' MPa'));
@@ -341,7 +539,7 @@ export function registerVisionCommand(program) {
341
539
  }
342
540
  catch (err) {
343
541
  spinner?.fail('RMR classification failed');
344
- handleCommandError(err, flags, 'rmr_classification_failed');
542
+ handleCommandErrorClean(err, flags, 'rmr_classification_failed');
345
543
  }
346
544
  });
347
545
  addGlobalFlags(rmrImageCmd);
@@ -354,7 +552,7 @@ export function registerVisionCommand(program) {
354
552
  const flags = getGlobalFlags(opts);
355
553
  if (!(await checkQuota('visionCalls')))
356
554
  return;
357
- const spinner = flags.json ? null : ora({ text: 'Interpreting sensor data...', indent: 2 }).start();
555
+ const spinner = startProgress(flags, 'Interpreting sensor data...');
358
556
  try {
359
557
  const file = readVisionInput(imagePath);
360
558
  describeVisionInput(file);
@@ -371,8 +569,8 @@ export function registerVisionCommand(program) {
371
569
  renderJSON(result);
372
570
  return;
373
571
  }
374
- heading(`Sensor Interpretation ${result.sensorType ?? 'Unknown'}`);
375
- renderParseSafety(result);
572
+ heading(`Sensor Interpretation - ${result.sensorType ?? 'Unknown'}`);
573
+ renderParseSafetyCompact(result);
376
574
  keyValue('Sensor type', formatMaybe(result.sensorType));
377
575
  keyValue('Measurements', formatMaybe(result.measurements));
378
576
  console.log('');
@@ -388,7 +586,7 @@ export function registerVisionCommand(program) {
388
586
  }
389
587
  catch (err) {
390
588
  spinner?.fail('Sensor interpretation failed');
391
- handleCommandError(err, flags, 'sensor_interpretation_failed');
589
+ handleCommandErrorClean(err, flags, 'sensor_interpretation_failed');
392
590
  }
393
591
  });
394
592
  addGlobalFlags(sensorCmd);
@@ -402,25 +600,63 @@ export function registerVisionCommand(program) {
402
600
  const flags = getGlobalFlags(opts);
403
601
  if (!(await checkQuota('visionCalls')))
404
602
  return;
405
- const spinner = flags.json ? null : ora({ text: 'Extracting borehole log data...', indent: 2 }).start();
603
+ const spinner = startProgress(flags, 'Extracting borehole log data...');
406
604
  try {
407
605
  const file = readVisionInput(filePath);
408
606
  describeVisionInput(file);
409
607
  const config = buildLLMConfig();
410
- maybeCheckHostedBetaVisionPayload(config, file, {
608
+ const requestDetails = {
411
609
  prompt: 'Extract structured borehole log data.',
412
610
  systemPrompt: 'You are analyzing a geotechnical image.',
413
611
  temperature: 0.1,
414
612
  maxTokens: 900,
415
- });
416
- const result = await interpretBoreholeLog(file.base64, file.mimeType, config, opts.boreholeId);
417
- spinner?.succeed(`Extraction complete: ${result.layers.length} layers (${result.latencyMs}ms)`);
613
+ };
614
+ let result;
615
+ if (file.kind === 'pdf') {
616
+ const pageInputs = await readVisionPdfPageInputs(filePath);
617
+ if (!flags.json && !flags.quiet && pageInputs.length > 1) {
618
+ info(`PDF contains ${pageInputs.length} pages. Processing borehole log pages sequentially.`);
619
+ }
620
+ const pageResults = [];
621
+ const pageFailures = [];
622
+ for (const pageInput of pageInputs) {
623
+ if (!flags.json && !flags.quiet && pageInputs.length > 1) {
624
+ info(`Processing PDF page ${pageInput.pageNumber}/${pageInput.totalPages}...`);
625
+ }
626
+ try {
627
+ maybeCheckHostedBetaVisionPayload(config, pageInput, requestDetails);
628
+ const pageResult = await interpretBoreholeLog(pageInput.base64, pageInput.mimeType, config, opts.boreholeId);
629
+ pageResults.push({
630
+ pageNumber: pageInput.pageNumber,
631
+ result: pageResult,
632
+ });
633
+ }
634
+ catch (pageError) {
635
+ pageFailures.push(`Page ${pageInput.pageNumber}: ${getErrorMessage(pageError)}`);
636
+ }
637
+ }
638
+ if (pageResults.length === 0) {
639
+ throw new Error(pageFailures.length > 0
640
+ ? `No PDF pages could be processed successfully.\n${pageFailures.join('\n')}`
641
+ : 'No PDF pages could be processed successfully.');
642
+ }
643
+ result = mergeBoreholeInterpretations(pageResults, opts.boreholeId);
644
+ if (pageFailures.length > 0) {
645
+ result.warnings = [...result.warnings, ...pageFailures];
646
+ }
647
+ spinner?.succeed(`Extraction complete: ${result.layers.length} layers from ${pageResults.length}/${pageInputs.length} page(s) (${result.latencyMs}ms)`);
648
+ }
649
+ else {
650
+ maybeCheckHostedBetaVisionPayload(config, file, requestDetails);
651
+ result = await interpretBoreholeLog(file.base64, file.mimeType, config, opts.boreholeId);
652
+ spinner?.succeed(`Extraction complete: ${result.layers.length} layers (${result.latencyMs}ms)`);
653
+ }
418
654
  if (flags.json) {
419
655
  renderJSON(result);
420
656
  return;
421
657
  }
422
- heading(`Borehole Log ${result.boreholeId}`);
423
- renderParseSafety(result);
658
+ heading(`Borehole Log - ${result.boreholeId}`);
659
+ renderParseSafetyCompact(result);
424
660
  keyValue('Total depth', formatMaybe(result.totalDepth, ' m'));
425
661
  keyValue('Water table', result.waterTableDepth != null ? `${result.waterTableDepth} m` : 'Not detected');
426
662
  renderTable(['From (m)', 'To (m)', 'Description', 'USCS', 'SPT-N'], result.layers.map((l) => [
@@ -441,7 +677,7 @@ export function registerVisionCommand(program) {
441
677
  }
442
678
  catch (err) {
443
679
  spinner?.fail('Extraction failed');
444
- handleCommandError(err, flags, 'borehole_extraction_failed');
680
+ handleCommandErrorClean(err, flags, 'borehole_extraction_failed');
445
681
  }
446
682
  });
447
683
  addGlobalFlags(logCmd);
@@ -460,7 +696,7 @@ export function registerAIClassifyCommand(program) {
460
696
  const description = descParts.join(' ');
461
697
  if (!(await checkQuota('llmCalls')))
462
698
  return;
463
- const spinner = flags.json ? null : ora({ text: 'Classifying soil from description...', indent: 2 }).start();
699
+ const spinner = startProgress(flags, 'Classifying soil from description...');
464
700
  try {
465
701
  const config = buildLLMConfig();
466
702
  const result = await classifySoilFromDescription(description, config);
@@ -471,7 +707,7 @@ export function registerAIClassifyCommand(program) {
471
707
  }
472
708
  heading('AI Soil Classification');
473
709
  keyValue('Input', `"${description}"`);
474
- renderParseSafety(result);
710
+ renderParseSafetyCompact(result);
475
711
  keyValue('USCS symbol', formatMaybe(result.uscsSymbol));
476
712
  keyValue('USCS name', formatMaybe(result.uscsName));
477
713
  keyValue('Friction angle', formatMaybe(result.estimatedProperties.frictionAngle, ' deg'));
@@ -485,7 +721,7 @@ export function registerAIClassifyCommand(program) {
485
721
  }
486
722
  catch (err) {
487
723
  spinner?.fail('Classification failed');
488
- handleCommandError(err, flags, 'ai_classification_failed');
724
+ handleCommandErrorClean(err, flags, 'ai_classification_failed');
489
725
  }
490
726
  });
491
727
  addGlobalFlags(cmd);
@@ -506,7 +742,7 @@ export function registerGBRCommand(program) {
506
742
  const question = questionParts.join(' ');
507
743
  if (!(await checkQuota('llmCalls')))
508
744
  return;
509
- const spinner = flags.json ? null : ora({ text: `Querying GBR: "${question.slice(0, 50)}..."`, indent: 2 }).start();
745
+ const spinner = startProgress(flags, `Querying GBR: "${question.slice(0, 50)}..."`);
510
746
  try {
511
747
  const file = readVisionInput(opts.doc);
512
748
  describeVisionInput(file);
@@ -531,7 +767,7 @@ export function registerGBRCommand(program) {
531
767
  }
532
768
  catch (err) {
533
769
  spinner?.fail('GBR query failed');
534
- handleCommandError(err, flags, 'gbr_query_failed');
770
+ handleCommandErrorClean(err, flags, 'gbr_query_failed');
535
771
  }
536
772
  });
537
773
  addGlobalFlags(chatCmd);
@@ -630,9 +866,9 @@ function renderSwarmStep(step, json, quiet = false) {
630
866
  }
631
867
  export function registerAgentCommand(program) {
632
868
  const cmd = new Command('agent')
633
- .description('Agentic AI reasons about your problem and executes real calculations')
869
+ .description('Agentic AI - reasons about your problem and executes real calculations')
634
870
  .argument('<task...>', 'Engineering task in natural language')
635
- .option('--swarm', 'Use multi-agent swarm (Interpretation Simulation Reviewer)')
871
+ .option('--swarm', 'Use multi-agent swarm (Bieniawski -> Terzaghi -> Hoek)')
636
872
  .option('--project <id>', 'Load and persist context to a stored project')
637
873
  .action(async (taskParts, opts) => {
638
874
  const flags = getGlobalFlags(opts);
@@ -643,10 +879,10 @@ export function registerAgentCommand(program) {
643
879
  if (!flags.json) {
644
880
  console.log('');
645
881
  if (useSwarm) {
646
- console.log(chalk.gray(' Swarm activated Interpretation Simulation Reviewer'));
882
+ console.log(chalk.gray(' Swarm activated - Bieniawski, Terzaghi, and Hoek coordinated by Mohr'));
647
883
  }
648
884
  else {
649
- console.log(chalk.gray(' Agent activated planning and executing...'));
885
+ console.log(chalk.gray(' Agent activated - Terzaghi is planning and executing'));
650
886
  }
651
887
  console.log('');
652
888
  }
@@ -660,7 +896,7 @@ export function registerAgentCommand(program) {
660
896
  if (useSwarm) {
661
897
  // Multi-agent swarm mode
662
898
  const session = await runSwarm(task, config, (step) => {
663
- renderSwarmStep(step, flags.json, flags.quiet);
899
+ renderSwarmStepPlain(step, flags.json, flags.quiet);
664
900
  }, projectState?.context);
665
901
  const answer = session.steps.find((s) => s.type === 'answer');
666
902
  if (projectState) {
@@ -695,7 +931,7 @@ export function registerAgentCommand(program) {
695
931
  const toolCalls = session.steps.filter((s) => s.type === 'tool_call').length;
696
932
  const agents = [...new Set(session.steps.map((s) => s.agent))];
697
933
  console.log(chalk.gray(` (${agents.length} agents, ${toolCalls} tools executed, review: ${session.reviewPassed ? 'PASSED' : 'ISSUES NOTED'}, ${session.totalTokens} tokens)`));
698
- console.log(chalk.cyan('\n Hint: To continue this session interactively, run: ') + chalk.white(`geotech chat${opts.project ? ` --project ${opts.project}` : ''}`));
934
+ console.log(chalk.cyan('\n Continue interactively with: ') + chalk.white(`geotech chat${opts.project ? ` --project ${opts.project}` : ''}`));
699
935
  }
700
936
  if (flags.output && answer) {
701
937
  const outputTarget = resolveStructuredOutputTarget({
@@ -722,7 +958,7 @@ export function registerAgentCommand(program) {
722
958
  else {
723
959
  // Single-agent ReAct mode (default)
724
960
  const session = await runAgent(task, config, (step) => {
725
- renderAgentStep(step, flags.json, flags.quiet);
961
+ renderAgentStepPlain(step, flags.json, flags.quiet);
726
962
  }, projectState?.context);
727
963
  const answer = session.steps.find((s) => s.type === 'answer');
728
964
  if (projectState) {
@@ -752,7 +988,7 @@ export function registerAgentCommand(program) {
752
988
  console.log(answer.content);
753
989
  console.log('');
754
990
  console.log(chalk.gray(` (${session.steps.filter((s) => s.type === 'tool_call').length} tools executed, ${session.totalTokens} tokens, ${session.totalLatencyMs}ms)`));
755
- console.log(chalk.cyan('\n Hint: To continue this session interactively, run: ') + chalk.white(`geotech chat${opts.project ? ` --project ${opts.project}` : ''}`));
991
+ console.log(chalk.cyan('\n Continue interactively with: ') + chalk.white(`geotech chat${opts.project ? ` --project ${opts.project}` : ''}`));
756
992
  }
757
993
  if (flags.output && answer) {
758
994
  const outputTarget = resolveStructuredOutputTarget({
@@ -781,7 +1017,7 @@ export function registerAgentCommand(program) {
781
1017
  }
782
1018
  }
783
1019
  catch (err) {
784
- handleCommandError(err, flags, useSwarm ? 'swarm_failed' : 'agent_failed');
1020
+ handleCommandErrorClean(err, flags, useSwarm ? 'swarm_failed' : 'agent_failed');
785
1021
  }
786
1022
  });
787
1023
  addGlobalFlags(cmd);
@@ -792,12 +1028,12 @@ export function registerAgentCommand(program) {
792
1028
  // ---------------------------------------------------------------------------
793
1029
  export function registerChatCommand(program) {
794
1030
  const cmd = new Command('chat')
795
- .description('Interactive agentic session type natural language, agent executes tools with memory')
1031
+ .description('Interactive agentic session - type natural language, agent executes tools with memory')
796
1032
  .option('--project <id>', 'Load and persist context to a stored project')
797
1033
  .action(async (opts) => {
798
1034
  const { createInterface } = await import('node:readline');
799
1035
  console.log('');
800
- console.log(chalk.bold.cyan(' geotech') + chalk.bold.white('CLI') + chalk.gray(' Agent Interactive Mode'));
1036
+ console.log(chalk.bold.cyan(' geotech') + chalk.bold.white('CLI') + chalk.gray(' Agent - Interactive Mode'));
801
1037
  console.log(chalk.gray(' Type engineering questions. The agent will reason and execute calculations.'));
802
1038
  console.log(chalk.gray(' Commands: /context (show memory), /clear (reset), /exit (quit)'));
803
1039
  console.log('');
@@ -888,7 +1124,7 @@ export function registerChatCommand(program) {
888
1124
  console.log('');
889
1125
  try {
890
1126
  const session = await conversation.ask(input, config, (step) => {
891
- renderAgentStep(step, false, false);
1127
+ renderAgentStepPlain(step, false, false);
892
1128
  });
893
1129
  if (projectState) {
894
1130
  persistSessionToProject(projectState.id, 'chat', input, session);
@@ -940,12 +1176,7 @@ export function registerReportCommand(program) {
940
1176
  if (!useCaseFile && !(await checkQuota('llmCalls'))) {
941
1177
  return;
942
1178
  }
943
- spinner = flags.json
944
- ? null
945
- : ora({
946
- text: useCaseFile ? 'Assembling case-file report...' : 'Generating report...',
947
- indent: 2,
948
- }).start();
1179
+ spinner = startProgress(flags, useCaseFile ? 'Assembling case-file report...' : 'Generating report...');
949
1180
  const report = useCaseFile
950
1181
  ? await generateReportFromCaseFile({
951
1182
  projectId: opts.projectId,
@@ -996,7 +1227,7 @@ export function registerReportCommand(program) {
996
1227
  }
997
1228
  catch (err) {
998
1229
  spinner?.fail('Report generation failed');
999
- handleCommandError(err, flags, 'report_generation_failed');
1230
+ handleCommandErrorClean(err, flags, 'report_generation_failed');
1000
1231
  }
1001
1232
  });
1002
1233
  addGlobalFlags(cmd);