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.
- package/dist/commands/ai.d.ts.map +1 -1
- package/dist/commands/ai.js +281 -50
- package/dist/commands/ai.js.map +1 -1
- package/dist/commands/classify.d.ts.map +1 -1
- package/dist/commands/classify.js +28 -2
- package/dist/commands/classify.js.map +1 -1
- package/dist/commands/liquefaction.d.ts.map +1 -1
- package/dist/commands/liquefaction.js +29 -1
- package/dist/commands/liquefaction.js.map +1 -1
- package/dist/commands/pile.d.ts.map +1 -1
- package/dist/commands/pile.js +33 -1
- package/dist/commands/pile.js.map +1 -1
- package/dist/commands/settlement.d.ts.map +1 -1
- package/dist/commands/settlement.js +4 -0
- package/dist/commands/settlement.js.map +1 -1
- package/dist/commands/viz.d.ts +3 -0
- package/dist/commands/viz.d.ts.map +1 -0
- package/dist/commands/viz.js +209 -0
- package/dist/commands/viz.js.map +1 -0
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/ui/terminal.d.ts +21 -1
- package/dist/ui/terminal.d.ts.map +1 -1
- package/dist/ui/terminal.js +141 -70
- package/dist/ui/terminal.js.map +1 -1
- package/dist/util/vision-output.d.ts +5 -0
- package/dist/util/vision-output.d.ts.map +1 -1
- package/dist/util/vision-output.js +25 -0
- package/dist/util/vision-output.js.map +1 -1
- package/dist/util/viz.d.ts +53 -0
- package/dist/util/viz.d.ts.map +1 -0
- package/dist/util/viz.js +600 -0
- package/dist/util/viz.js.map +1 -0
- package/package.json +7 -5
|
@@ -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;
|
|
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"}
|
package/dist/commands/ai.js
CHANGED
|
@@ -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
|
|
44
|
-
console.log(chalk.gray('
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
375
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
|
|
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
|
|
423
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1230
|
+
handleCommandErrorClean(err, flags, 'report_generation_failed');
|
|
1000
1231
|
}
|
|
1001
1232
|
});
|
|
1002
1233
|
addGlobalFlags(cmd);
|