geotechcli 0.3.0 → 0.4.1
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 +510 -91
- package/dist/commands/ai.js.map +1 -1
- package/dist/commands/config.js +1 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +7 -2
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/liquefaction.d.ts.map +1 -1
- package/dist/commands/liquefaction.js +19 -12
- package/dist/commands/liquefaction.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/status.d.ts.map +1 -1
- package/dist/commands/status.js +60 -9
- package/dist/commands/status.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 +131 -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 +0 -1
- package/dist/ui/terminal.d.ts.map +1 -1
- package/dist/ui/terminal.js +36 -70
- package/dist/ui/terminal.js.map +1 -1
- package/dist/util/vision-output.d.ts +40 -0
- package/dist/util/vision-output.d.ts.map +1 -0
- package/dist/util/vision-output.js +139 -0
- package/dist/util/vision-output.js.map +1 -0
- package/dist/util/viz.d.ts +31 -0
- package/dist/util/viz.d.ts.map +1 -0
- package/dist/util/viz.js +320 -0
- package/dist/util/viz.js.map +1 -0
- package/package.json +54 -52
package/dist/commands/ai.js
CHANGED
|
@@ -1,10 +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
|
-
import { buildLLMConfig, analyzeCoreBox, classifyRMRFromImage, classifySoilFromDescription, interpretBoreholeLog, queryGBRDocument, interpretSensorImage, runAgent, runSwarm, AgentConversation, loadProject, getProjectAgentContext, addAgentSession, addArtifact, addNote, saveNamedDataset, saveDerivedParameter, setActiveAnalysisContext, generateReport, renderReportAsPdf, renderReportAsDocx, } from '@geotechcli/core';
|
|
6
|
-
import { heading, keyValue, renderJSON, success, error, warn, renderTable } from '../ui/terminal.js';
|
|
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';
|
|
5
|
+
import { heading, keyValue, renderJSON, success, error, warn, renderTable, info } from '../ui/terminal.js';
|
|
7
6
|
import { addGlobalFlags, getGlobalFlags } from '../util/flags.js';
|
|
7
|
+
import { estimateHostedBetaVisionBodyBytes, formatByteSize, HOSTED_BETA_REQUEST_LIMIT_BYTES, readVisionInput, readVisionPdfPageInputs, resolveStructuredOutputTarget, HOSTED_BETA_REQUEST_SAFE_BYTES, } from '../util/vision-output.js';
|
|
8
8
|
async function checkQuota(_callType) {
|
|
9
9
|
// Strong-beta hosted limits are enforced server-side by the beta proxy.
|
|
10
10
|
// Keep the CLI permissive here so successful completions, retries, and
|
|
@@ -32,11 +32,251 @@ function loadImageBase64(filePath) {
|
|
|
32
32
|
mimeType: mimeMap[ext] ?? 'image/png',
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
+
function describeVisionInput(file) {
|
|
36
|
+
if (file.kind !== 'pdf') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log(chalk.yellow(' PDF input detected.'));
|
|
41
|
+
console.log(chalk.gray(' GLM vision works best with PNG or JPG images.'));
|
|
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.'));
|
|
44
|
+
console.log('');
|
|
45
|
+
}
|
|
46
|
+
function ensureHostedBetaVisionPayloadWithinLimit(file, details) {
|
|
47
|
+
const estimatedBytes = estimateHostedBetaVisionBodyBytes({
|
|
48
|
+
prompt: details.prompt,
|
|
49
|
+
systemPrompt: details.systemPrompt,
|
|
50
|
+
imageBase64: file.base64,
|
|
51
|
+
mimeType: file.mimeType,
|
|
52
|
+
model: details.model,
|
|
53
|
+
temperature: details.temperature,
|
|
54
|
+
maxTokens: details.maxTokens,
|
|
55
|
+
jsonMode: false,
|
|
56
|
+
});
|
|
57
|
+
if (estimatedBytes <= HOSTED_BETA_REQUEST_SAFE_BYTES) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const fileSize = formatByteSize(file.fileBytes);
|
|
61
|
+
const payloadSize = formatByteSize(estimatedBytes);
|
|
62
|
+
const limitSize = formatByteSize(HOSTED_BETA_REQUEST_SAFE_BYTES);
|
|
63
|
+
const capSize = formatByteSize(HOSTED_BETA_REQUEST_LIMIT_BYTES);
|
|
64
|
+
const baseMessage = file.kind === 'pdf'
|
|
65
|
+
? 'PDF vision inputs are uploaded as a single base64 payload and are too large for the hosted beta proxy.'
|
|
66
|
+
: 'Image vision inputs are uploaded as a single base64 payload and are too large for the hosted beta proxy.';
|
|
67
|
+
const mitigation = file.kind === 'pdf'
|
|
68
|
+
? 'Export one page as PNG or JPG, or split the PDF into smaller files, then retry.'
|
|
69
|
+
: 'Resize or crop the image to the relevant region, then retry.';
|
|
70
|
+
throw new Error(`${baseMessage} File: ${file.filePath} (${fileSize}). Estimated request body: ${payloadSize}. Safe limit: ${limitSize}. Hosted beta cap: ${capSize}.\n${mitigation}`);
|
|
71
|
+
}
|
|
72
|
+
function maybeCheckHostedBetaVisionPayload(config, file, details) {
|
|
73
|
+
if (config.provider !== 'hosted-beta') {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
ensureHostedBetaVisionPayloadWithinLimit(file, {
|
|
77
|
+
...details,
|
|
78
|
+
model: config.visionModelId ?? DEFAULT_LLM_VISION_MODEL,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
35
81
|
function formatMaybe(value, suffix = '') {
|
|
36
82
|
if (value == null || value === '')
|
|
37
83
|
return 'Unavailable';
|
|
38
84
|
return `${value}${suffix}`;
|
|
39
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
|
+
}
|
|
40
280
|
function sanitizeErrorMessage(message) {
|
|
41
281
|
return message.replace(/(?:Bearer |sk-|zhipu-|api[_-]?key[=: ]*)[^\s"'\]},]*/gi, '***REDACTED***');
|
|
42
282
|
}
|
|
@@ -52,7 +292,13 @@ function handleCommandError(err, flags, code = 'command_failed') {
|
|
|
52
292
|
}
|
|
53
293
|
// Provide specific guidance for common vision errors
|
|
54
294
|
if (code.includes('vision') || code.includes('corebox') || code.includes('rmr') || code.includes('sensor') || code.includes('borehole')) {
|
|
55
|
-
|
|
295
|
+
const lowered = message.toLowerCase();
|
|
296
|
+
if (lowered.includes('no content') ||
|
|
297
|
+
lowered.includes('empty') ||
|
|
298
|
+
lowered.includes('upstream') ||
|
|
299
|
+
lowered.includes('hosted beta proxy') ||
|
|
300
|
+
lowered.includes('too large for the hosted beta proxy') ||
|
|
301
|
+
lowered.includes('safe limit')) {
|
|
56
302
|
error(message);
|
|
57
303
|
console.log('');
|
|
58
304
|
console.log(chalk.gray(' Vision troubleshooting tips:'));
|
|
@@ -69,10 +315,15 @@ function handleCommandError(err, flags, code = 'command_failed') {
|
|
|
69
315
|
function renderWarnings(warnings) {
|
|
70
316
|
if (warnings.length === 0)
|
|
71
317
|
return;
|
|
318
|
+
const uniqueWarnings = [...new Set(warnings.map((warning) => warning.trim()).filter(Boolean))];
|
|
319
|
+
const visibleWarnings = uniqueWarnings.slice(0, 5);
|
|
72
320
|
console.log(chalk.yellow(' Warnings:'));
|
|
73
|
-
for (const warning of
|
|
321
|
+
for (const warning of visibleWarnings) {
|
|
74
322
|
console.log(chalk.yellow(` - ${warning}`));
|
|
75
323
|
}
|
|
324
|
+
if (uniqueWarnings.length > visibleWarnings.length) {
|
|
325
|
+
console.log(chalk.yellow(` - ${uniqueWarnings.length - visibleWarnings.length} more warning(s) omitted.`));
|
|
326
|
+
}
|
|
76
327
|
}
|
|
77
328
|
function renderParseSafety(result) {
|
|
78
329
|
keyValue('Parse status', result.parseStatus);
|
|
@@ -98,17 +349,34 @@ function persistSessionToProject(projectId, mode, query, session) {
|
|
|
98
349
|
corrections: session.corrections,
|
|
99
350
|
}
|
|
100
351
|
: undefined;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
query,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
352
|
+
if (mode === 'swarm') {
|
|
353
|
+
const swarmSession = session;
|
|
354
|
+
addAgentSession(projectId, buildSwarmSessionProjectRecord(query, swarmSession, {
|
|
355
|
+
mode,
|
|
356
|
+
answer,
|
|
357
|
+
summary: answer?.slice(0, 240) ?? `${mode} session for: ${query.slice(0, 120)}`,
|
|
358
|
+
metadata: reviewMetadata,
|
|
359
|
+
}));
|
|
360
|
+
const caseFileResult = persistSwarmCaseFile(projectId, query, swarmSession);
|
|
361
|
+
const evidenceRecords = persistCaseFileEvidence(projectId, caseFileResult.scenarioId);
|
|
362
|
+
addNote(projectId, `Updated swarm case file "${caseFileResult.scenarioId}" from the latest session.`);
|
|
363
|
+
if (evidenceRecords.length > 0) {
|
|
364
|
+
addNote(projectId, `Captured ${evidenceRecords.length} evidence record${evidenceRecords.length === 1 ? '' : 's'} for scenario "${caseFileResult.scenarioId}".`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
addAgentSession(projectId, {
|
|
369
|
+
mode,
|
|
370
|
+
query,
|
|
371
|
+
answer,
|
|
372
|
+
summary: answer?.slice(0, 240) ?? `${mode} session for: ${query.slice(0, 120)}`,
|
|
373
|
+
stepCount: session.steps.length,
|
|
374
|
+
tokens: session.totalTokens,
|
|
375
|
+
latencyMs: session.totalLatencyMs,
|
|
376
|
+
context: session.context,
|
|
377
|
+
metadata: reviewMetadata,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
112
380
|
saveNamedDataset(projectId, {
|
|
113
381
|
name: 'latest-agent-context',
|
|
114
382
|
kind: 'agent-context',
|
|
@@ -151,6 +419,26 @@ function persistOutputArtifact(projectId, kind, title, path, metadata) {
|
|
|
151
419
|
metadata,
|
|
152
420
|
});
|
|
153
421
|
}
|
|
422
|
+
function buildAnalysisDocument(title, task, answer, mode) {
|
|
423
|
+
const content = [
|
|
424
|
+
`## Task`,
|
|
425
|
+
task,
|
|
426
|
+
'',
|
|
427
|
+
`## ${mode === 'swarm' ? 'Swarm Report' : 'Analysis'}`,
|
|
428
|
+
answer,
|
|
429
|
+
].join('\n');
|
|
430
|
+
return {
|
|
431
|
+
title,
|
|
432
|
+
sections: [
|
|
433
|
+
{
|
|
434
|
+
title: 'Summary',
|
|
435
|
+
content,
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
fullMarkdown: `# ${title}\n\n${content}\n`,
|
|
439
|
+
latencyMs: 0,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
154
442
|
// ---------------------------------------------------------------------------
|
|
155
443
|
// Vision Commands
|
|
156
444
|
// ---------------------------------------------------------------------------
|
|
@@ -159,24 +447,31 @@ export function registerVisionCommand(program) {
|
|
|
159
447
|
.description('AI vision analysis for geotechnical images');
|
|
160
448
|
// Core box analysis
|
|
161
449
|
const coreboxCmd = new Command('corebox')
|
|
162
|
-
.description('Analyze core box image
|
|
450
|
+
.description('Analyze a core box image for RQD, fracture spacing, and weathering')
|
|
163
451
|
.argument('<image>', 'Path to core box image')
|
|
164
452
|
.action(async (imagePath, opts) => {
|
|
165
453
|
const flags = getGlobalFlags(opts);
|
|
166
454
|
if (!(await checkQuota('visionCalls')))
|
|
167
455
|
return;
|
|
168
|
-
const spinner = flags
|
|
456
|
+
const spinner = startProgress(flags, 'Analyzing core box image...');
|
|
169
457
|
try {
|
|
170
|
-
const
|
|
458
|
+
const file = readVisionInput(imagePath);
|
|
459
|
+
describeVisionInput(file);
|
|
171
460
|
const config = buildLLMConfig();
|
|
172
|
-
|
|
461
|
+
maybeCheckHostedBetaVisionPayload(config, file, {
|
|
462
|
+
prompt: 'Analyze this geotechnical image.',
|
|
463
|
+
systemPrompt: 'You are analyzing a geotechnical image.',
|
|
464
|
+
temperature: 0.1,
|
|
465
|
+
maxTokens: 900,
|
|
466
|
+
});
|
|
467
|
+
const result = await analyzeCoreBox(file.base64, file.mimeType, config);
|
|
173
468
|
spinner?.succeed(`Analysis complete (${result.latencyMs}ms)`);
|
|
174
469
|
if (flags.json) {
|
|
175
470
|
renderJSON(result);
|
|
176
471
|
return;
|
|
177
472
|
}
|
|
178
473
|
heading('Core Box Analysis');
|
|
179
|
-
|
|
474
|
+
renderParseSafetyCompact(result);
|
|
180
475
|
keyValue('RQD', formatMaybe(result.rqd, '%'));
|
|
181
476
|
keyValue('Fracture spacing', formatMaybe(result.fractureSpacing));
|
|
182
477
|
keyValue('Weathering grade', formatMaybe(result.weatheringGrade));
|
|
@@ -191,31 +486,38 @@ export function registerVisionCommand(program) {
|
|
|
191
486
|
}
|
|
192
487
|
catch (err) {
|
|
193
488
|
spinner?.fail('Analysis failed');
|
|
194
|
-
|
|
489
|
+
handleCommandErrorClean(err, flags, 'corebox_analysis_failed');
|
|
195
490
|
}
|
|
196
491
|
});
|
|
197
492
|
addGlobalFlags(coreboxCmd);
|
|
198
493
|
vision.addCommand(coreboxCmd);
|
|
199
494
|
// Hybrid RMR from image
|
|
200
495
|
const rmrImageCmd = new Command('rmr')
|
|
201
|
-
.description('Hybrid RMR: vision extracts features
|
|
496
|
+
.description('Hybrid RMR: vision extracts features, then deterministic RMR scoring')
|
|
202
497
|
.argument('<image>', 'Path to rock face / core image')
|
|
203
498
|
.action(async (imagePath, opts) => {
|
|
204
499
|
const flags = getGlobalFlags(opts);
|
|
205
500
|
if (!(await checkQuota('visionCalls')))
|
|
206
501
|
return;
|
|
207
|
-
const spinner = flags
|
|
502
|
+
const spinner = startProgress(flags, 'Extracting rock mass parameters from image...');
|
|
208
503
|
try {
|
|
209
|
-
const
|
|
504
|
+
const file = readVisionInput(imagePath);
|
|
505
|
+
describeVisionInput(file);
|
|
210
506
|
const config = buildLLMConfig();
|
|
211
|
-
|
|
507
|
+
maybeCheckHostedBetaVisionPayload(config, file, {
|
|
508
|
+
prompt: 'Estimate rock mass parameters from this image.',
|
|
509
|
+
systemPrompt: 'You are analyzing a geotechnical image.',
|
|
510
|
+
temperature: 0.1,
|
|
511
|
+
maxTokens: 700,
|
|
512
|
+
});
|
|
513
|
+
const result = await classifyRMRFromImage(file.base64, file.mimeType, config);
|
|
212
514
|
spinner?.succeed(`RMR classification complete (${result.latencyMs}ms)`);
|
|
213
515
|
if (flags.json) {
|
|
214
516
|
renderJSON(result);
|
|
215
517
|
return;
|
|
216
518
|
}
|
|
217
519
|
heading('Hybrid RMR Classification (Vision + Deterministic)');
|
|
218
|
-
|
|
520
|
+
renderParseSafetyCompact(result);
|
|
219
521
|
console.log('');
|
|
220
522
|
console.log(chalk.gray(' Vision-extracted parameters:'));
|
|
221
523
|
keyValue(' Estimated UCS', formatMaybe(result.visionExtraction.estimatedUCS, ' MPa'));
|
|
@@ -237,7 +539,7 @@ export function registerVisionCommand(program) {
|
|
|
237
539
|
}
|
|
238
540
|
catch (err) {
|
|
239
541
|
spinner?.fail('RMR classification failed');
|
|
240
|
-
|
|
542
|
+
handleCommandErrorClean(err, flags, 'rmr_classification_failed');
|
|
241
543
|
}
|
|
242
544
|
});
|
|
243
545
|
addGlobalFlags(rmrImageCmd);
|
|
@@ -250,18 +552,25 @@ export function registerVisionCommand(program) {
|
|
|
250
552
|
const flags = getGlobalFlags(opts);
|
|
251
553
|
if (!(await checkQuota('visionCalls')))
|
|
252
554
|
return;
|
|
253
|
-
const spinner = flags
|
|
555
|
+
const spinner = startProgress(flags, 'Interpreting sensor data...');
|
|
254
556
|
try {
|
|
255
|
-
const
|
|
557
|
+
const file = readVisionInput(imagePath);
|
|
558
|
+
describeVisionInput(file);
|
|
256
559
|
const config = buildLLMConfig();
|
|
257
|
-
|
|
560
|
+
maybeCheckHostedBetaVisionPayload(config, file, {
|
|
561
|
+
prompt: 'Interpret this sensor data image.',
|
|
562
|
+
systemPrompt: 'You are analyzing a geotechnical image.',
|
|
563
|
+
temperature: 0.1,
|
|
564
|
+
maxTokens: 700,
|
|
565
|
+
});
|
|
566
|
+
const result = await interpretSensorImage(file.base64, file.mimeType, config);
|
|
258
567
|
spinner?.succeed(`Interpretation complete (${result.latencyMs}ms)`);
|
|
259
568
|
if (flags.json) {
|
|
260
569
|
renderJSON(result);
|
|
261
570
|
return;
|
|
262
571
|
}
|
|
263
|
-
heading(`Sensor Interpretation
|
|
264
|
-
|
|
572
|
+
heading(`Sensor Interpretation - ${result.sensorType ?? 'Unknown'}`);
|
|
573
|
+
renderParseSafetyCompact(result);
|
|
265
574
|
keyValue('Sensor type', formatMaybe(result.sensorType));
|
|
266
575
|
keyValue('Measurements', formatMaybe(result.measurements));
|
|
267
576
|
console.log('');
|
|
@@ -277,7 +586,7 @@ export function registerVisionCommand(program) {
|
|
|
277
586
|
}
|
|
278
587
|
catch (err) {
|
|
279
588
|
spinner?.fail('Sensor interpretation failed');
|
|
280
|
-
|
|
589
|
+
handleCommandErrorClean(err, flags, 'sensor_interpretation_failed');
|
|
281
590
|
}
|
|
282
591
|
});
|
|
283
592
|
addGlobalFlags(sensorCmd);
|
|
@@ -291,18 +600,63 @@ export function registerVisionCommand(program) {
|
|
|
291
600
|
const flags = getGlobalFlags(opts);
|
|
292
601
|
if (!(await checkQuota('visionCalls')))
|
|
293
602
|
return;
|
|
294
|
-
const spinner = flags
|
|
603
|
+
const spinner = startProgress(flags, 'Extracting borehole log data...');
|
|
295
604
|
try {
|
|
296
|
-
const
|
|
605
|
+
const file = readVisionInput(filePath);
|
|
606
|
+
describeVisionInput(file);
|
|
297
607
|
const config = buildLLMConfig();
|
|
298
|
-
const
|
|
299
|
-
|
|
608
|
+
const requestDetails = {
|
|
609
|
+
prompt: 'Extract structured borehole log data.',
|
|
610
|
+
systemPrompt: 'You are analyzing a geotechnical image.',
|
|
611
|
+
temperature: 0.1,
|
|
612
|
+
maxTokens: 900,
|
|
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
|
+
}
|
|
300
654
|
if (flags.json) {
|
|
301
655
|
renderJSON(result);
|
|
302
656
|
return;
|
|
303
657
|
}
|
|
304
|
-
heading(`Borehole Log
|
|
305
|
-
|
|
658
|
+
heading(`Borehole Log - ${result.boreholeId}`);
|
|
659
|
+
renderParseSafetyCompact(result);
|
|
306
660
|
keyValue('Total depth', formatMaybe(result.totalDepth, ' m'));
|
|
307
661
|
keyValue('Water table', result.waterTableDepth != null ? `${result.waterTableDepth} m` : 'Not detected');
|
|
308
662
|
renderTable(['From (m)', 'To (m)', 'Description', 'USCS', 'SPT-N'], result.layers.map((l) => [
|
|
@@ -323,7 +677,7 @@ export function registerVisionCommand(program) {
|
|
|
323
677
|
}
|
|
324
678
|
catch (err) {
|
|
325
679
|
spinner?.fail('Extraction failed');
|
|
326
|
-
|
|
680
|
+
handleCommandErrorClean(err, flags, 'borehole_extraction_failed');
|
|
327
681
|
}
|
|
328
682
|
});
|
|
329
683
|
addGlobalFlags(logCmd);
|
|
@@ -342,7 +696,7 @@ export function registerAIClassifyCommand(program) {
|
|
|
342
696
|
const description = descParts.join(' ');
|
|
343
697
|
if (!(await checkQuota('llmCalls')))
|
|
344
698
|
return;
|
|
345
|
-
const spinner = flags
|
|
699
|
+
const spinner = startProgress(flags, 'Classifying soil from description...');
|
|
346
700
|
try {
|
|
347
701
|
const config = buildLLMConfig();
|
|
348
702
|
const result = await classifySoilFromDescription(description, config);
|
|
@@ -353,7 +707,7 @@ export function registerAIClassifyCommand(program) {
|
|
|
353
707
|
}
|
|
354
708
|
heading('AI Soil Classification');
|
|
355
709
|
keyValue('Input', `"${description}"`);
|
|
356
|
-
|
|
710
|
+
renderParseSafetyCompact(result);
|
|
357
711
|
keyValue('USCS symbol', formatMaybe(result.uscsSymbol));
|
|
358
712
|
keyValue('USCS name', formatMaybe(result.uscsName));
|
|
359
713
|
keyValue('Friction angle', formatMaybe(result.estimatedProperties.frictionAngle, ' deg'));
|
|
@@ -367,7 +721,7 @@ export function registerAIClassifyCommand(program) {
|
|
|
367
721
|
}
|
|
368
722
|
catch (err) {
|
|
369
723
|
spinner?.fail('Classification failed');
|
|
370
|
-
|
|
724
|
+
handleCommandErrorClean(err, flags, 'ai_classification_failed');
|
|
371
725
|
}
|
|
372
726
|
});
|
|
373
727
|
addGlobalFlags(cmd);
|
|
@@ -388,11 +742,18 @@ export function registerGBRCommand(program) {
|
|
|
388
742
|
const question = questionParts.join(' ');
|
|
389
743
|
if (!(await checkQuota('llmCalls')))
|
|
390
744
|
return;
|
|
391
|
-
const spinner = flags
|
|
745
|
+
const spinner = startProgress(flags, `Querying GBR: "${question.slice(0, 50)}..."`);
|
|
392
746
|
try {
|
|
393
|
-
const
|
|
747
|
+
const file = readVisionInput(opts.doc);
|
|
748
|
+
describeVisionInput(file);
|
|
394
749
|
const config = buildLLMConfig();
|
|
395
|
-
|
|
750
|
+
maybeCheckHostedBetaVisionPayload(config, file, {
|
|
751
|
+
prompt: 'Answer questions about this GBR document.',
|
|
752
|
+
systemPrompt: 'You are analyzing a geotechnical document image.',
|
|
753
|
+
temperature: 0.1,
|
|
754
|
+
maxTokens: 700,
|
|
755
|
+
});
|
|
756
|
+
const result = await queryGBRDocument(question, file.base64, file.mimeType, config);
|
|
396
757
|
spinner?.succeed(`Answer ready (${result.latencyMs}ms)`);
|
|
397
758
|
if (flags.json) {
|
|
398
759
|
renderJSON({ question, answer: result.answer, latencyMs: result.latencyMs });
|
|
@@ -406,7 +767,7 @@ export function registerGBRCommand(program) {
|
|
|
406
767
|
}
|
|
407
768
|
catch (err) {
|
|
408
769
|
spinner?.fail('GBR query failed');
|
|
409
|
-
|
|
770
|
+
handleCommandErrorClean(err, flags, 'gbr_query_failed');
|
|
410
771
|
}
|
|
411
772
|
});
|
|
412
773
|
addGlobalFlags(chatCmd);
|
|
@@ -505,9 +866,9 @@ function renderSwarmStep(step, json, quiet = false) {
|
|
|
505
866
|
}
|
|
506
867
|
export function registerAgentCommand(program) {
|
|
507
868
|
const cmd = new Command('agent')
|
|
508
|
-
.description('Agentic AI
|
|
869
|
+
.description('Agentic AI - reasons about your problem and executes real calculations')
|
|
509
870
|
.argument('<task...>', 'Engineering task in natural language')
|
|
510
|
-
.option('--swarm', 'Use multi-agent swarm (
|
|
871
|
+
.option('--swarm', 'Use multi-agent swarm (Bieniawski -> Terzaghi -> Hoek)')
|
|
511
872
|
.option('--project <id>', 'Load and persist context to a stored project')
|
|
512
873
|
.action(async (taskParts, opts) => {
|
|
513
874
|
const flags = getGlobalFlags(opts);
|
|
@@ -518,10 +879,10 @@ export function registerAgentCommand(program) {
|
|
|
518
879
|
if (!flags.json) {
|
|
519
880
|
console.log('');
|
|
520
881
|
if (useSwarm) {
|
|
521
|
-
console.log(chalk.gray(' Swarm activated
|
|
882
|
+
console.log(chalk.gray(' Swarm activated - Bieniawski, Terzaghi, and Hoek coordinated by Mohr'));
|
|
522
883
|
}
|
|
523
884
|
else {
|
|
524
|
-
console.log(chalk.gray(' Agent activated
|
|
885
|
+
console.log(chalk.gray(' Agent activated - Terzaghi is planning and executing'));
|
|
525
886
|
}
|
|
526
887
|
console.log('');
|
|
527
888
|
}
|
|
@@ -535,7 +896,7 @@ export function registerAgentCommand(program) {
|
|
|
535
896
|
if (useSwarm) {
|
|
536
897
|
// Multi-agent swarm mode
|
|
537
898
|
const session = await runSwarm(task, config, (step) => {
|
|
538
|
-
|
|
899
|
+
renderSwarmStepPlain(step, flags.json, flags.quiet);
|
|
539
900
|
}, projectState?.context);
|
|
540
901
|
const answer = session.steps.find((s) => s.type === 'answer');
|
|
541
902
|
if (projectState) {
|
|
@@ -570,18 +931,34 @@ export function registerAgentCommand(program) {
|
|
|
570
931
|
const toolCalls = session.steps.filter((s) => s.type === 'tool_call').length;
|
|
571
932
|
const agents = [...new Set(session.steps.map((s) => s.agent))];
|
|
572
933
|
console.log(chalk.gray(` (${agents.length} agents, ${toolCalls} tools executed, review: ${session.reviewPassed ? 'PASSED' : 'ISSUES NOTED'}, ${session.totalTokens} tokens)`));
|
|
573
|
-
console.log(chalk.cyan('\n
|
|
934
|
+
console.log(chalk.cyan('\n Continue interactively with: ') + chalk.white(`geotech chat${opts.project ? ` --project ${opts.project}` : ''}`));
|
|
574
935
|
}
|
|
575
936
|
if (flags.output && answer) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
937
|
+
const outputTarget = resolveStructuredOutputTarget({
|
|
938
|
+
outputPath: flags.output,
|
|
939
|
+
defaultBaseName: 'swarm-report',
|
|
940
|
+
});
|
|
941
|
+
if (outputTarget.warning) {
|
|
942
|
+
warn(outputTarget.warning);
|
|
943
|
+
}
|
|
944
|
+
if (outputTarget.kind === 'pdf' || outputTarget.kind === 'docx') {
|
|
945
|
+
const document = buildAnalysisDocument('Swarm Report', task, answer.content, 'swarm');
|
|
946
|
+
const buffer = outputTarget.kind === 'pdf'
|
|
947
|
+
? await renderReportAsPdf(document)
|
|
948
|
+
: await renderReportAsDocx(document);
|
|
949
|
+
writeFileSync(outputTarget.outputPath, buffer);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
writeFileSync(outputTarget.outputPath, answer.content);
|
|
953
|
+
}
|
|
954
|
+
persistOutputArtifact(projectState?.id, 'swarm-report-file', 'swarm report output', outputTarget.outputPath, { task });
|
|
955
|
+
success(`Report saved to ${outputTarget.outputPath}`);
|
|
579
956
|
}
|
|
580
957
|
}
|
|
581
958
|
else {
|
|
582
959
|
// Single-agent ReAct mode (default)
|
|
583
960
|
const session = await runAgent(task, config, (step) => {
|
|
584
|
-
|
|
961
|
+
renderAgentStepPlain(step, flags.json, flags.quiet);
|
|
585
962
|
}, projectState?.context);
|
|
586
963
|
const answer = session.steps.find((s) => s.type === 'answer');
|
|
587
964
|
if (projectState) {
|
|
@@ -611,12 +988,28 @@ export function registerAgentCommand(program) {
|
|
|
611
988
|
console.log(answer.content);
|
|
612
989
|
console.log('');
|
|
613
990
|
console.log(chalk.gray(` (${session.steps.filter((s) => s.type === 'tool_call').length} tools executed, ${session.totalTokens} tokens, ${session.totalLatencyMs}ms)`));
|
|
614
|
-
console.log(chalk.cyan('\n
|
|
991
|
+
console.log(chalk.cyan('\n Continue interactively with: ') + chalk.white(`geotech chat${opts.project ? ` --project ${opts.project}` : ''}`));
|
|
615
992
|
}
|
|
616
993
|
if (flags.output && answer) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
994
|
+
const outputTarget = resolveStructuredOutputTarget({
|
|
995
|
+
outputPath: flags.output,
|
|
996
|
+
defaultBaseName: 'agent-analysis',
|
|
997
|
+
});
|
|
998
|
+
if (outputTarget.warning) {
|
|
999
|
+
warn(outputTarget.warning);
|
|
1000
|
+
}
|
|
1001
|
+
if (outputTarget.kind === 'pdf' || outputTarget.kind === 'docx') {
|
|
1002
|
+
const document = buildAnalysisDocument('Agent Analysis', task, answer.content, 'single');
|
|
1003
|
+
const buffer = outputTarget.kind === 'pdf'
|
|
1004
|
+
? await renderReportAsPdf(document)
|
|
1005
|
+
: await renderReportAsDocx(document);
|
|
1006
|
+
writeFileSync(outputTarget.outputPath, buffer);
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
writeFileSync(outputTarget.outputPath, answer.content);
|
|
1010
|
+
}
|
|
1011
|
+
persistOutputArtifact(projectState?.id, 'agent-report-file', 'agent analysis output', outputTarget.outputPath, { task });
|
|
1012
|
+
success(`Report saved to ${outputTarget.outputPath}`);
|
|
620
1013
|
}
|
|
621
1014
|
}
|
|
622
1015
|
if (!flags.json) {
|
|
@@ -624,7 +1017,7 @@ export function registerAgentCommand(program) {
|
|
|
624
1017
|
}
|
|
625
1018
|
}
|
|
626
1019
|
catch (err) {
|
|
627
|
-
|
|
1020
|
+
handleCommandErrorClean(err, flags, useSwarm ? 'swarm_failed' : 'agent_failed');
|
|
628
1021
|
}
|
|
629
1022
|
});
|
|
630
1023
|
addGlobalFlags(cmd);
|
|
@@ -635,12 +1028,12 @@ export function registerAgentCommand(program) {
|
|
|
635
1028
|
// ---------------------------------------------------------------------------
|
|
636
1029
|
export function registerChatCommand(program) {
|
|
637
1030
|
const cmd = new Command('chat')
|
|
638
|
-
.description('Interactive agentic session
|
|
1031
|
+
.description('Interactive agentic session - type natural language, agent executes tools with memory')
|
|
639
1032
|
.option('--project <id>', 'Load and persist context to a stored project')
|
|
640
1033
|
.action(async (opts) => {
|
|
641
1034
|
const { createInterface } = await import('node:readline');
|
|
642
1035
|
console.log('');
|
|
643
|
-
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'));
|
|
644
1037
|
console.log(chalk.gray(' Type engineering questions. The agent will reason and execute calculations.'));
|
|
645
1038
|
console.log(chalk.gray(' Commands: /context (show memory), /clear (reset), /exit (quit)'));
|
|
646
1039
|
console.log('');
|
|
@@ -731,7 +1124,7 @@ export function registerChatCommand(program) {
|
|
|
731
1124
|
console.log('');
|
|
732
1125
|
try {
|
|
733
1126
|
const session = await conversation.ask(input, config, (step) => {
|
|
734
|
-
|
|
1127
|
+
renderAgentStepPlain(step, false, false);
|
|
735
1128
|
});
|
|
736
1129
|
if (projectState) {
|
|
737
1130
|
persistSessionToProject(projectState.id, 'chat', input, session);
|
|
@@ -762,53 +1155,79 @@ export function registerChatCommand(program) {
|
|
|
762
1155
|
export function registerReportCommand(program) {
|
|
763
1156
|
const cmd = new Command('report')
|
|
764
1157
|
.description('Generate AI-powered geotechnical report from analysis data')
|
|
765
|
-
.
|
|
1158
|
+
.option('--data <file>', 'JSON file with analysis results')
|
|
1159
|
+
.option('--from-case-file <scenarioId>', 'Assemble a deterministic report from a stored case file')
|
|
1160
|
+
.option('--project-id <id>', 'Stored project id required with --from-case-file')
|
|
766
1161
|
.option('--type <type>', 'Report type: borehole|site-investigation|tunnel-design|foundation|slope|custom', 'site-investigation')
|
|
767
1162
|
.option('--project <name>', 'Project name')
|
|
768
1163
|
.option('--location <loc>', 'Project location')
|
|
769
1164
|
.option('--format <ext>', 'Export format: md|pdf|docx', 'md')
|
|
770
1165
|
.action(async (opts) => {
|
|
771
1166
|
const flags = getGlobalFlags(opts);
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const spinner = flags.json ? null : ora({ text: 'Generating report...', indent: 2 }).start();
|
|
1167
|
+
const useCaseFile = typeof opts.fromCaseFile === 'string' && opts.fromCaseFile.trim().length > 0;
|
|
1168
|
+
let spinner = null;
|
|
775
1169
|
try {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1170
|
+
if (!useCaseFile && !opts.data) {
|
|
1171
|
+
throw new Error('Provide either --data <file> or --from-case-file <scenarioId>.');
|
|
1172
|
+
}
|
|
1173
|
+
if (useCaseFile && !opts.projectId) {
|
|
1174
|
+
throw new Error('--project-id is required when using --from-case-file.');
|
|
1175
|
+
}
|
|
1176
|
+
if (!useCaseFile && !(await checkQuota('llmCalls'))) {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
spinner = startProgress(flags, useCaseFile ? 'Assembling case-file report...' : 'Generating report...');
|
|
1180
|
+
const report = useCaseFile
|
|
1181
|
+
? await generateReportFromCaseFile({
|
|
1182
|
+
projectId: opts.projectId,
|
|
1183
|
+
scenarioId: opts.fromCaseFile,
|
|
1184
|
+
projectName: opts.project,
|
|
1185
|
+
location: opts.location,
|
|
1186
|
+
})
|
|
1187
|
+
: await (async () => {
|
|
1188
|
+
const data = JSON.parse(readFileSync(opts.data, 'utf-8'));
|
|
1189
|
+
const config = buildLLMConfig();
|
|
1190
|
+
return generateReport(data, {
|
|
1191
|
+
type: opts.type,
|
|
1192
|
+
projectName: opts.project,
|
|
1193
|
+
location: opts.location,
|
|
1194
|
+
}, config);
|
|
1195
|
+
})();
|
|
1196
|
+
spinner?.succeed(`${useCaseFile ? 'Case-file report assembled' : 'Report generated'}: ${report.sections.length} sections (${report.latencyMs}ms)`);
|
|
784
1197
|
if (flags.json) {
|
|
785
1198
|
renderJSON(report);
|
|
786
1199
|
return;
|
|
787
1200
|
}
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1201
|
+
const baseName = (opts.project
|
|
1202
|
+
? opts.project.replace(/\s+/g, '_')
|
|
1203
|
+
: useCaseFile
|
|
1204
|
+
? `${opts.projectId}_${opts.fromCaseFile}`
|
|
1205
|
+
: 'report').toLowerCase();
|
|
1206
|
+
const outputTarget = resolveStructuredOutputTarget({
|
|
1207
|
+
outputPath: flags.output,
|
|
1208
|
+
requestedFormat: opts.format,
|
|
1209
|
+
defaultBaseName: baseName,
|
|
1210
|
+
});
|
|
1211
|
+
if (outputTarget.warning) {
|
|
1212
|
+
warn(outputTarget.warning);
|
|
793
1213
|
}
|
|
794
|
-
if (
|
|
1214
|
+
if (outputTarget.kind === 'pdf') {
|
|
795
1215
|
const buf = await renderReportAsPdf(report);
|
|
796
|
-
writeFileSync(
|
|
1216
|
+
writeFileSync(outputTarget.outputPath, buf);
|
|
797
1217
|
}
|
|
798
|
-
else if (
|
|
1218
|
+
else if (outputTarget.kind === 'docx') {
|
|
799
1219
|
const buf = await renderReportAsDocx(report);
|
|
800
|
-
writeFileSync(
|
|
1220
|
+
writeFileSync(outputTarget.outputPath, buf);
|
|
801
1221
|
}
|
|
802
1222
|
else {
|
|
803
|
-
|
|
804
|
-
writeFileSync(outputFile, report.fullMarkdown);
|
|
1223
|
+
writeFileSync(outputTarget.outputPath, report.fullMarkdown);
|
|
805
1224
|
}
|
|
806
|
-
success(`Report saved to ${
|
|
1225
|
+
success(`Report saved to ${outputTarget.outputPath}`);
|
|
807
1226
|
console.log('');
|
|
808
1227
|
}
|
|
809
1228
|
catch (err) {
|
|
810
1229
|
spinner?.fail('Report generation failed');
|
|
811
|
-
|
|
1230
|
+
handleCommandErrorClean(err, flags, 'report_generation_failed');
|
|
812
1231
|
}
|
|
813
1232
|
});
|
|
814
1233
|
addGlobalFlags(cmd);
|