geotechcli 0.2.0
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 +8 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +780 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/bearing.d.ts +3 -0
- package/dist/commands/bearing.d.ts.map +1 -0
- package/dist/commands/bearing.js +64 -0
- package/dist/commands/bearing.js.map +1 -0
- package/dist/commands/bridge.d.ts +3 -0
- package/dist/commands/bridge.d.ts.map +1 -0
- package/dist/commands/bridge.js +87 -0
- package/dist/commands/bridge.js.map +1 -0
- package/dist/commands/classify.d.ts +3 -0
- package/dist/commands/classify.d.ts.map +1 -0
- package/dist/commands/classify.js +122 -0
- package/dist/commands/classify.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +97 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/export.d.ts +3 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +85 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/liquefaction.d.ts +3 -0
- package/dist/commands/liquefaction.d.ts.map +1 -0
- package/dist/commands/liquefaction.js +94 -0
- package/dist/commands/liquefaction.js.map +1 -0
- package/dist/commands/pile.d.ts +3 -0
- package/dist/commands/pile.d.ts.map +1 -0
- package/dist/commands/pile.js +88 -0
- package/dist/commands/pile.js.map +1 -0
- package/dist/commands/retaining.d.ts +3 -0
- package/dist/commands/retaining.d.ts.map +1 -0
- package/dist/commands/retaining.js +71 -0
- package/dist/commands/retaining.js.map +1 -0
- package/dist/commands/slope.d.ts +3 -0
- package/dist/commands/slope.d.ts.map +1 -0
- package/dist/commands/slope.js +80 -0
- package/dist/commands/slope.js.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +76 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/tunnel.d.ts +3 -0
- package/dist/commands/tunnel.d.ts.map +1 -0
- package/dist/commands/tunnel.js +145 -0
- package/dist/commands/tunnel.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/ui/terminal.d.ts +21 -0
- package/dist/ui/terminal.d.ts.map +1 -0
- package/dist/ui/terminal.js +153 -0
- package/dist/ui/terminal.js.map +1 -0
- package/dist/util/flags.d.ts +13 -0
- package/dist/util/flags.d.ts.map +1 -0
- package/dist/util/flags.js +25 -0
- package/dist/util/flags.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
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, } from '@geotechcli/core';
|
|
6
|
+
import { heading, keyValue, renderJSON, success, error, warn, renderTable } from '../ui/terminal.js';
|
|
7
|
+
import { addGlobalFlags, getGlobalFlags } from '../util/flags.js';
|
|
8
|
+
async function checkQuota(_callType) {
|
|
9
|
+
// Strong-beta hosted limits are enforced server-side by the beta proxy.
|
|
10
|
+
// Keep the CLI permissive here so successful completions, retries, and
|
|
11
|
+
// daily limits are handled by the hosted gateway instead of stale local state.
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
function loadImageBase64(filePath) {
|
|
15
|
+
const buffer = readFileSync(filePath);
|
|
16
|
+
const ext = filePath.split('.').pop()?.toLowerCase() ?? 'png';
|
|
17
|
+
const mimeMap = {
|
|
18
|
+
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png',
|
|
19
|
+
gif: 'image/gif', webp: 'image/webp', pdf: 'application/pdf',
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
base64: buffer.toString('base64'),
|
|
23
|
+
mimeType: mimeMap[ext] ?? 'image/png',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function formatMaybe(value, suffix = '') {
|
|
27
|
+
if (value == null || value === '')
|
|
28
|
+
return 'Unavailable';
|
|
29
|
+
return `${value}${suffix}`;
|
|
30
|
+
}
|
|
31
|
+
function sanitizeErrorMessage(message) {
|
|
32
|
+
return message.replace(/(?:Bearer |sk-|zhipu-|api[_-]?key[=: ]*)[^\s"'\]},]*/gi, '***REDACTED***');
|
|
33
|
+
}
|
|
34
|
+
function getErrorMessage(err) {
|
|
35
|
+
return sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
|
|
36
|
+
}
|
|
37
|
+
function handleCommandError(err, flags, code = 'command_failed') {
|
|
38
|
+
const message = getErrorMessage(err);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
if (flags.json) {
|
|
41
|
+
renderJSON({
|
|
42
|
+
error: {
|
|
43
|
+
code,
|
|
44
|
+
message,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
error(message);
|
|
50
|
+
}
|
|
51
|
+
function renderWarnings(warnings) {
|
|
52
|
+
if (warnings.length === 0)
|
|
53
|
+
return;
|
|
54
|
+
console.log(chalk.yellow(' Warnings:'));
|
|
55
|
+
for (const warning of warnings) {
|
|
56
|
+
console.log(chalk.yellow(` - ${warning}`));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function renderParseSafety(result) {
|
|
60
|
+
keyValue('Parse status', result.parseStatus);
|
|
61
|
+
keyValue('Confidence', `${result.confidence}%`);
|
|
62
|
+
keyValue('Auto proceed', result.canAutoProceed ? 'Yes' : 'No');
|
|
63
|
+
renderWarnings(result.warnings);
|
|
64
|
+
}
|
|
65
|
+
function loadProjectState(projectId) {
|
|
66
|
+
if (!projectId)
|
|
67
|
+
return null;
|
|
68
|
+
const project = loadProject(projectId);
|
|
69
|
+
return {
|
|
70
|
+
id: project.meta.id,
|
|
71
|
+
name: project.meta.name,
|
|
72
|
+
context: getProjectAgentContext(project.meta.id),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function persistSessionToProject(projectId, mode, query, session) {
|
|
76
|
+
const answer = session.steps.find((step) => step.type === 'answer')?.content;
|
|
77
|
+
const reviewMetadata = 'reviewPassed' in session
|
|
78
|
+
? {
|
|
79
|
+
reviewPassed: session.reviewPassed,
|
|
80
|
+
corrections: session.corrections,
|
|
81
|
+
}
|
|
82
|
+
: undefined;
|
|
83
|
+
addAgentSession(projectId, {
|
|
84
|
+
mode,
|
|
85
|
+
query,
|
|
86
|
+
answer,
|
|
87
|
+
summary: answer?.slice(0, 240) ?? `${mode} session for: ${query.slice(0, 120)}`,
|
|
88
|
+
stepCount: session.steps.length,
|
|
89
|
+
tokens: session.totalTokens,
|
|
90
|
+
latencyMs: session.totalLatencyMs,
|
|
91
|
+
context: session.context,
|
|
92
|
+
metadata: reviewMetadata,
|
|
93
|
+
});
|
|
94
|
+
saveNamedDataset(projectId, {
|
|
95
|
+
name: 'latest-agent-context',
|
|
96
|
+
kind: 'agent-context',
|
|
97
|
+
data: session.context,
|
|
98
|
+
source: mode,
|
|
99
|
+
});
|
|
100
|
+
saveDerivedParameter(projectId, {
|
|
101
|
+
name: 'last-agent-mode',
|
|
102
|
+
value: mode,
|
|
103
|
+
source: 'cli',
|
|
104
|
+
});
|
|
105
|
+
setActiveAnalysisContext(projectId, {
|
|
106
|
+
currentTask: query,
|
|
107
|
+
lastAgentMode: mode,
|
|
108
|
+
lastAnswer: answer,
|
|
109
|
+
context: session.context,
|
|
110
|
+
relatedDatasets: Object.keys(session.context),
|
|
111
|
+
});
|
|
112
|
+
addNote(projectId, `Persisted ${mode} agent session for "${query.slice(0, 120)}"`);
|
|
113
|
+
if (answer) {
|
|
114
|
+
addArtifact(projectId, {
|
|
115
|
+
kind: mode === 'swarm' ? 'swarm-report' : 'agent-report',
|
|
116
|
+
title: `${mode} analysis`,
|
|
117
|
+
content: answer,
|
|
118
|
+
mimeType: 'text/plain',
|
|
119
|
+
metadata: {
|
|
120
|
+
query,
|
|
121
|
+
tokens: session.totalTokens,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function persistOutputArtifact(projectId, kind, title, path, metadata) {
|
|
127
|
+
if (!projectId)
|
|
128
|
+
return;
|
|
129
|
+
addArtifact(projectId, {
|
|
130
|
+
kind,
|
|
131
|
+
title,
|
|
132
|
+
path,
|
|
133
|
+
metadata,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Vision Commands
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
export function registerVisionCommand(program) {
|
|
140
|
+
const vision = new Command('vision')
|
|
141
|
+
.description('AI vision analysis for geotechnical images');
|
|
142
|
+
// Core box analysis
|
|
143
|
+
const coreboxCmd = new Command('corebox')
|
|
144
|
+
.description('Analyze core box image → RQD, fracture spacing, weathering')
|
|
145
|
+
.argument('<image>', 'Path to core box image')
|
|
146
|
+
.action(async (imagePath, opts) => {
|
|
147
|
+
const flags = getGlobalFlags(opts);
|
|
148
|
+
if (!(await checkQuota('visionCalls')))
|
|
149
|
+
return;
|
|
150
|
+
const spinner = flags.json ? null : ora({ text: 'Analyzing core box image...', indent: 2 }).start();
|
|
151
|
+
try {
|
|
152
|
+
const { base64, mimeType } = loadImageBase64(imagePath);
|
|
153
|
+
const config = buildLLMConfig();
|
|
154
|
+
const result = await analyzeCoreBox(base64, mimeType, config);
|
|
155
|
+
spinner?.succeed(`Analysis complete (${result.latencyMs}ms)`);
|
|
156
|
+
if (flags.json) {
|
|
157
|
+
renderJSON(result);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
heading('Core Box Analysis');
|
|
161
|
+
renderParseSafety(result);
|
|
162
|
+
keyValue('RQD', formatMaybe(result.rqd, '%'));
|
|
163
|
+
keyValue('Fracture spacing', formatMaybe(result.fractureSpacing));
|
|
164
|
+
keyValue('Weathering grade', formatMaybe(result.weatheringGrade));
|
|
165
|
+
keyValue('Rock type', formatMaybe(result.rockType));
|
|
166
|
+
keyValue('Core recovery', formatMaybe(result.coreRecovery, '%'));
|
|
167
|
+
keyValue('Discontinuities', formatMaybe(result.discontinuities));
|
|
168
|
+
if (flags.output) {
|
|
169
|
+
writeFileSync(flags.output, JSON.stringify(result, null, 2));
|
|
170
|
+
success(`Results saved to ${flags.output}`);
|
|
171
|
+
}
|
|
172
|
+
console.log('');
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
spinner?.fail('Analysis failed');
|
|
176
|
+
handleCommandError(err, flags, 'corebox_analysis_failed');
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
addGlobalFlags(coreboxCmd);
|
|
180
|
+
vision.addCommand(coreboxCmd);
|
|
181
|
+
// Hybrid RMR from image
|
|
182
|
+
const rmrImageCmd = new Command('rmr')
|
|
183
|
+
.description('Hybrid RMR: vision extracts features → deterministic RMR scoring')
|
|
184
|
+
.argument('<image>', 'Path to rock face / core image')
|
|
185
|
+
.action(async (imagePath, opts) => {
|
|
186
|
+
const flags = getGlobalFlags(opts);
|
|
187
|
+
if (!(await checkQuota('visionCalls')))
|
|
188
|
+
return;
|
|
189
|
+
const spinner = flags.json ? null : ora({ text: 'Extracting rock mass parameters from image...', indent: 2 }).start();
|
|
190
|
+
try {
|
|
191
|
+
const { base64, mimeType } = loadImageBase64(imagePath);
|
|
192
|
+
const config = buildLLMConfig();
|
|
193
|
+
const result = await classifyRMRFromImage(base64, mimeType, config);
|
|
194
|
+
spinner?.succeed(`RMR classification complete (${result.latencyMs}ms)`);
|
|
195
|
+
if (flags.json) {
|
|
196
|
+
renderJSON(result);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
heading('Hybrid RMR Classification (Vision + Deterministic)');
|
|
200
|
+
renderParseSafety(result);
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(chalk.gray(' Vision-extracted parameters:'));
|
|
203
|
+
keyValue(' Estimated UCS', formatMaybe(result.visionExtraction.estimatedUCS, ' MPa'));
|
|
204
|
+
keyValue(' Estimated RQD', formatMaybe(result.visionExtraction.estimatedRQD, '%'));
|
|
205
|
+
keyValue(' Estimated spacing', formatMaybe(result.visionExtraction.estimatedSpacing, ' m'));
|
|
206
|
+
keyValue(' Joint condition', formatMaybe(result.visionExtraction.jointCondition));
|
|
207
|
+
keyValue(' Groundwater', formatMaybe(result.visionExtraction.groundwaterCondition));
|
|
208
|
+
console.log('');
|
|
209
|
+
console.log(chalk.gray(' Deterministic RMR result:'));
|
|
210
|
+
if (result.rmrResult) {
|
|
211
|
+
keyValue(' Total RMR', `${result.rmrResult.totalRating}/100`);
|
|
212
|
+
keyValue(' Rock class', `Class ${result.rmrResult.classNumber}: ${result.rmrResult.rockClass}`);
|
|
213
|
+
keyValue(' Support', result.rmrResult.supportRecommendation);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
console.log(chalk.yellow(' Deterministic RMR scoring was skipped because the vision extraction was incomplete or low confidence.'));
|
|
217
|
+
}
|
|
218
|
+
console.log('');
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
spinner?.fail('RMR classification failed');
|
|
222
|
+
handleCommandError(err, flags, 'rmr_classification_failed');
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
addGlobalFlags(rmrImageCmd);
|
|
226
|
+
vision.addCommand(rmrImageCmd);
|
|
227
|
+
// Sensor interpretation
|
|
228
|
+
const sensorCmd = new Command('sensor')
|
|
229
|
+
.description('Interpret sensor data image (piezometer, inclinometer, etc.)')
|
|
230
|
+
.argument('<image>', 'Path to sensor data image')
|
|
231
|
+
.action(async (imagePath, opts) => {
|
|
232
|
+
const flags = getGlobalFlags(opts);
|
|
233
|
+
if (!(await checkQuota('visionCalls')))
|
|
234
|
+
return;
|
|
235
|
+
const spinner = flags.json ? null : ora({ text: 'Interpreting sensor data...', indent: 2 }).start();
|
|
236
|
+
try {
|
|
237
|
+
const { base64, mimeType } = loadImageBase64(imagePath);
|
|
238
|
+
const config = buildLLMConfig();
|
|
239
|
+
const result = await interpretSensorImage(base64, mimeType, config);
|
|
240
|
+
spinner?.succeed(`Interpretation complete (${result.latencyMs}ms)`);
|
|
241
|
+
if (flags.json) {
|
|
242
|
+
renderJSON(result);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
heading(`Sensor Interpretation — ${result.sensorType ?? 'Unknown'}`);
|
|
246
|
+
renderParseSafety(result);
|
|
247
|
+
keyValue('Sensor type', formatMaybe(result.sensorType));
|
|
248
|
+
keyValue('Measurements', formatMaybe(result.measurements));
|
|
249
|
+
console.log('');
|
|
250
|
+
console.log(chalk.white(' Interpretation:'));
|
|
251
|
+
console.log(` ${formatMaybe(result.interpretation)}`);
|
|
252
|
+
console.log('');
|
|
253
|
+
console.log(chalk.white(' Evaluation:'));
|
|
254
|
+
console.log(` ${formatMaybe(result.evaluation)}`);
|
|
255
|
+
console.log('');
|
|
256
|
+
console.log(chalk.white(' Recommendations:'));
|
|
257
|
+
console.log(` ${formatMaybe(result.recommendations)}`);
|
|
258
|
+
console.log('');
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
spinner?.fail('Sensor interpretation failed');
|
|
262
|
+
handleCommandError(err, flags, 'sensor_interpretation_failed');
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
addGlobalFlags(sensorCmd);
|
|
266
|
+
vision.addCommand(sensorCmd);
|
|
267
|
+
// Borehole log extraction
|
|
268
|
+
const logCmd = new Command('log')
|
|
269
|
+
.description('Extract structured data from borehole log image/PDF')
|
|
270
|
+
.argument('<file>', 'Path to borehole log image or PDF')
|
|
271
|
+
.option('--borehole-id <id>', 'Override borehole ID')
|
|
272
|
+
.action(async (filePath, opts) => {
|
|
273
|
+
const flags = getGlobalFlags(opts);
|
|
274
|
+
if (!(await checkQuota('visionCalls')))
|
|
275
|
+
return;
|
|
276
|
+
const spinner = flags.json ? null : ora({ text: 'Extracting borehole log data...', indent: 2 }).start();
|
|
277
|
+
try {
|
|
278
|
+
const { base64, mimeType } = loadImageBase64(filePath);
|
|
279
|
+
const config = buildLLMConfig();
|
|
280
|
+
const result = await interpretBoreholeLog(base64, mimeType, config, opts.boreholeId);
|
|
281
|
+
spinner?.succeed(`Extraction complete: ${result.layers.length} layers (${result.latencyMs}ms)`);
|
|
282
|
+
if (flags.json) {
|
|
283
|
+
renderJSON(result);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
heading(`Borehole Log — ${result.boreholeId}`);
|
|
287
|
+
renderParseSafety(result);
|
|
288
|
+
keyValue('Total depth', formatMaybe(result.totalDepth, ' m'));
|
|
289
|
+
keyValue('Water table', result.waterTableDepth != null ? `${result.waterTableDepth} m` : 'Not detected');
|
|
290
|
+
renderTable(['From (m)', 'To (m)', 'Description', 'USCS', 'SPT-N'], result.layers.map((l) => [
|
|
291
|
+
l.depthFrom != null ? l.depthFrom : '-',
|
|
292
|
+
l.depthTo != null ? l.depthTo : '-',
|
|
293
|
+
(l.description ?? 'Unavailable').slice(0, 40),
|
|
294
|
+
l.uscsSymbol || '-',
|
|
295
|
+
l.sptN ?? '-',
|
|
296
|
+
]));
|
|
297
|
+
console.log('');
|
|
298
|
+
console.log(chalk.white(' Summary:'));
|
|
299
|
+
console.log(` ${formatMaybe(result.summary)}`);
|
|
300
|
+
if (flags.output) {
|
|
301
|
+
writeFileSync(flags.output, JSON.stringify(result, null, 2));
|
|
302
|
+
success(`Results saved to ${flags.output}`);
|
|
303
|
+
}
|
|
304
|
+
console.log('');
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
spinner?.fail('Extraction failed');
|
|
308
|
+
handleCommandError(err, flags, 'borehole_extraction_failed');
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
addGlobalFlags(logCmd);
|
|
312
|
+
vision.addCommand(logCmd);
|
|
313
|
+
program.addCommand(vision);
|
|
314
|
+
}
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// Soil Classification from Natural Language
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
export function registerAIClassifyCommand(program) {
|
|
319
|
+
const cmd = new Command('ai-classify')
|
|
320
|
+
.description('Classify soil from natural language description (AI-powered)')
|
|
321
|
+
.argument('<description...>', 'Soil description in natural language')
|
|
322
|
+
.action(async (descParts, opts) => {
|
|
323
|
+
const flags = getGlobalFlags(opts);
|
|
324
|
+
const description = descParts.join(' ');
|
|
325
|
+
if (!(await checkQuota('llmCalls')))
|
|
326
|
+
return;
|
|
327
|
+
const spinner = flags.json ? null : ora({ text: 'Classifying soil from description...', indent: 2 }).start();
|
|
328
|
+
try {
|
|
329
|
+
const config = buildLLMConfig();
|
|
330
|
+
const result = await classifySoilFromDescription(description, config);
|
|
331
|
+
spinner?.succeed('Classification complete');
|
|
332
|
+
if (flags.json) {
|
|
333
|
+
renderJSON(result);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
heading('AI Soil Classification');
|
|
337
|
+
keyValue('Input', `"${description}"`);
|
|
338
|
+
renderParseSafety(result);
|
|
339
|
+
keyValue('USCS symbol', formatMaybe(result.uscsSymbol));
|
|
340
|
+
keyValue('USCS name', formatMaybe(result.uscsName));
|
|
341
|
+
keyValue('Friction angle', formatMaybe(result.estimatedProperties.frictionAngle, ' deg'));
|
|
342
|
+
keyValue('Cohesion', formatMaybe(result.estimatedProperties.cohesion, ' kPa'));
|
|
343
|
+
keyValue('Unit weight', formatMaybe(result.estimatedProperties.unitWeight, ' kN/m3'));
|
|
344
|
+
keyValue('Permeability', formatMaybe(result.estimatedProperties.permeability));
|
|
345
|
+
console.log('');
|
|
346
|
+
console.log(chalk.white(' Notes:'));
|
|
347
|
+
console.log(` ${formatMaybe(result.engineeringNotes)}`);
|
|
348
|
+
console.log('');
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
spinner?.fail('Classification failed');
|
|
352
|
+
handleCommandError(err, flags, 'ai_classification_failed');
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
addGlobalFlags(cmd);
|
|
356
|
+
program.addCommand(cmd);
|
|
357
|
+
}
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// GBR Document Q&A
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
export function registerGBRCommand(program) {
|
|
362
|
+
const gbr = new Command('gbr')
|
|
363
|
+
.description('Geotechnical Baseline Report Q&A');
|
|
364
|
+
const chatCmd = new Command('chat')
|
|
365
|
+
.description('Ask questions about a GBR document')
|
|
366
|
+
.requiredOption('--doc <file>', 'Path to GBR PDF or image')
|
|
367
|
+
.argument('<question...>', 'Question about the GBR')
|
|
368
|
+
.action(async (questionParts, opts) => {
|
|
369
|
+
const flags = getGlobalFlags(opts);
|
|
370
|
+
const question = questionParts.join(' ');
|
|
371
|
+
if (!(await checkQuota('llmCalls')))
|
|
372
|
+
return;
|
|
373
|
+
const spinner = flags.json ? null : ora({ text: `Querying GBR: "${question.slice(0, 50)}..."`, indent: 2 }).start();
|
|
374
|
+
try {
|
|
375
|
+
const { base64, mimeType } = loadImageBase64(opts.doc);
|
|
376
|
+
const config = buildLLMConfig();
|
|
377
|
+
const result = await queryGBRDocument(question, base64, mimeType, config);
|
|
378
|
+
spinner?.succeed(`Answer ready (${result.latencyMs}ms)`);
|
|
379
|
+
if (flags.json) {
|
|
380
|
+
renderJSON({ question, answer: result.answer, latencyMs: result.latencyMs });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
heading('GBR Q&A');
|
|
384
|
+
console.log(chalk.cyan(` Q: ${question}`));
|
|
385
|
+
console.log('');
|
|
386
|
+
console.log(` ${result.answer}`);
|
|
387
|
+
console.log('');
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
spinner?.fail('GBR query failed');
|
|
391
|
+
handleCommandError(err, flags, 'gbr_query_failed');
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
addGlobalFlags(chatCmd);
|
|
395
|
+
gbr.addCommand(chatCmd);
|
|
396
|
+
program.addCommand(gbr);
|
|
397
|
+
}
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// Agentic CLI — real tool-calling brain with ReAct loop
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
function renderAgentStep(step, json, quiet = false) {
|
|
402
|
+
if (json || quiet)
|
|
403
|
+
return;
|
|
404
|
+
const icons = {
|
|
405
|
+
thought: '🤔',
|
|
406
|
+
tool_call: '🔧',
|
|
407
|
+
tool_result: '📊',
|
|
408
|
+
answer: '✅',
|
|
409
|
+
error: '❌',
|
|
410
|
+
};
|
|
411
|
+
const icon = icons[step.type] ?? '•';
|
|
412
|
+
switch (step.type) {
|
|
413
|
+
case 'thought':
|
|
414
|
+
console.log(chalk.gray(` ${icon} [Thinking] ${step.content.slice(0, 200)}`));
|
|
415
|
+
break;
|
|
416
|
+
case 'tool_call':
|
|
417
|
+
console.log(chalk.cyan(` ${icon} [Tool] ${step.toolName}(${JSON.stringify(step.toolArgs).slice(0, 100)})`));
|
|
418
|
+
break;
|
|
419
|
+
case 'tool_result':
|
|
420
|
+
if (step.toolResult?.success) {
|
|
421
|
+
console.log(chalk.green(` ${icon} [Result] ${step.content}`));
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
console.log(chalk.red(` ${icon} [Error] ${step.content}`));
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
case 'answer':
|
|
428
|
+
// Final answer rendered separately
|
|
429
|
+
break;
|
|
430
|
+
case 'error':
|
|
431
|
+
console.log(chalk.red(` ${icon} ${step.content}`));
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function renderSwarmStep(step, json, quiet = false) {
|
|
436
|
+
if (json || quiet)
|
|
437
|
+
return;
|
|
438
|
+
const agentColors = {
|
|
439
|
+
orchestrator: chalk.white,
|
|
440
|
+
interpretation: chalk.blue,
|
|
441
|
+
simulation: chalk.cyan,
|
|
442
|
+
reviewer: chalk.yellow,
|
|
443
|
+
};
|
|
444
|
+
const icons = {
|
|
445
|
+
thought: '🤔',
|
|
446
|
+
tool_call: '🔧',
|
|
447
|
+
tool_result: '📊',
|
|
448
|
+
handoff: '🔀',
|
|
449
|
+
review: '✅',
|
|
450
|
+
correction: '🔄',
|
|
451
|
+
answer: '📋',
|
|
452
|
+
error: '❌',
|
|
453
|
+
};
|
|
454
|
+
const color = agentColors[step.agent] ?? chalk.gray;
|
|
455
|
+
const icon = icons[step.type] ?? '•';
|
|
456
|
+
const tag = color(`[${step.agent}]`);
|
|
457
|
+
switch (step.type) {
|
|
458
|
+
case 'thought':
|
|
459
|
+
console.log(chalk.gray(` ${icon} ${tag} ${step.content.slice(0, 180)}`));
|
|
460
|
+
break;
|
|
461
|
+
case 'tool_call':
|
|
462
|
+
console.log(chalk.cyan(` ${icon} ${tag} ${step.toolName}(${JSON.stringify(step.toolArgs).slice(0, 80)})`));
|
|
463
|
+
break;
|
|
464
|
+
case 'tool_result':
|
|
465
|
+
if (step.toolResult?.success) {
|
|
466
|
+
console.log(chalk.green(` ${icon} ${tag} ${step.content.slice(0, 150)}`));
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
console.log(chalk.red(` ${icon} ${tag} ${step.content.slice(0, 150)}`));
|
|
470
|
+
}
|
|
471
|
+
break;
|
|
472
|
+
case 'handoff':
|
|
473
|
+
console.log(chalk.magenta(` ${icon} ${tag} ${step.content}`));
|
|
474
|
+
break;
|
|
475
|
+
case 'review':
|
|
476
|
+
console.log(chalk.green(` ${icon} ${tag} ${step.content}`));
|
|
477
|
+
break;
|
|
478
|
+
case 'correction':
|
|
479
|
+
console.log(chalk.red(` ${icon} ${tag} ${step.content.slice(0, 200)}`));
|
|
480
|
+
break;
|
|
481
|
+
case 'answer':
|
|
482
|
+
break;
|
|
483
|
+
case 'error':
|
|
484
|
+
console.log(chalk.red(` ${icon} ${tag} ${step.content}`));
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
export function registerAgentCommand(program) {
|
|
489
|
+
const cmd = new Command('agent')
|
|
490
|
+
.description('Agentic AI — reasons about your problem and executes real calculations')
|
|
491
|
+
.argument('<task...>', 'Engineering task in natural language')
|
|
492
|
+
.option('--swarm', 'Use multi-agent swarm (Interpretation → Simulation → Reviewer)')
|
|
493
|
+
.option('--project <id>', 'Load and persist context to a stored project')
|
|
494
|
+
.action(async (taskParts, opts) => {
|
|
495
|
+
const flags = getGlobalFlags(opts);
|
|
496
|
+
const task = taskParts.join(' ');
|
|
497
|
+
const useSwarm = opts.swarm === true;
|
|
498
|
+
if (!(await checkQuota('agentCalls')))
|
|
499
|
+
return;
|
|
500
|
+
if (!flags.json) {
|
|
501
|
+
console.log('');
|
|
502
|
+
if (useSwarm) {
|
|
503
|
+
console.log(chalk.gray(' Swarm activated — Interpretation → Simulation → Reviewer'));
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
console.log(chalk.gray(' Agent activated — planning and executing...'));
|
|
507
|
+
}
|
|
508
|
+
console.log('');
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
const config = buildLLMConfig();
|
|
512
|
+
const projectState = loadProjectState(opts.project);
|
|
513
|
+
if (projectState && !flags.json) {
|
|
514
|
+
console.log(chalk.gray(` Project context loaded: ${projectState.name} (${projectState.id})`));
|
|
515
|
+
console.log('');
|
|
516
|
+
}
|
|
517
|
+
if (useSwarm) {
|
|
518
|
+
// Multi-agent swarm mode
|
|
519
|
+
const session = await runSwarm(task, config, (step) => {
|
|
520
|
+
renderSwarmStep(step, flags.json, flags.quiet);
|
|
521
|
+
}, projectState?.context);
|
|
522
|
+
const answer = session.steps.find((s) => s.type === 'answer');
|
|
523
|
+
if (projectState) {
|
|
524
|
+
persistSessionToProject(projectState.id, 'swarm', task, session);
|
|
525
|
+
}
|
|
526
|
+
if (flags.json) {
|
|
527
|
+
renderJSON({
|
|
528
|
+
task,
|
|
529
|
+
mode: 'swarm',
|
|
530
|
+
answer: answer?.content ?? '',
|
|
531
|
+
reviewPassed: session.reviewPassed,
|
|
532
|
+
corrections: session.corrections,
|
|
533
|
+
steps: session.steps.map((s) => ({
|
|
534
|
+
agent: s.agent,
|
|
535
|
+
type: s.type,
|
|
536
|
+
content: s.content,
|
|
537
|
+
toolName: s.toolName,
|
|
538
|
+
toolArgs: s.toolArgs,
|
|
539
|
+
toolResult: s.toolResult ? { success: s.toolResult.success, summary: s.toolResult.summary } : undefined,
|
|
540
|
+
})),
|
|
541
|
+
context: session.context,
|
|
542
|
+
tokens: session.totalTokens,
|
|
543
|
+
latencyMs: session.totalLatencyMs,
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (answer) {
|
|
548
|
+
console.log('');
|
|
549
|
+
heading('Swarm Report');
|
|
550
|
+
console.log(answer.content);
|
|
551
|
+
console.log('');
|
|
552
|
+
const toolCalls = session.steps.filter((s) => s.type === 'tool_call').length;
|
|
553
|
+
const agents = [...new Set(session.steps.map((s) => s.agent))];
|
|
554
|
+
console.log(chalk.gray(` (${agents.length} agents, ${toolCalls} tools executed, review: ${session.reviewPassed ? 'PASSED' : 'ISSUES NOTED'}, ${session.totalTokens} tokens)`));
|
|
555
|
+
}
|
|
556
|
+
if (flags.output && answer) {
|
|
557
|
+
writeFileSync(flags.output, answer.content);
|
|
558
|
+
persistOutputArtifact(projectState?.id, 'swarm-report-file', 'swarm report output', flags.output, { task });
|
|
559
|
+
success(`Report saved to ${flags.output}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
// Single-agent ReAct mode (default)
|
|
564
|
+
const session = await runAgent(task, config, (step) => {
|
|
565
|
+
renderAgentStep(step, flags.json, flags.quiet);
|
|
566
|
+
}, projectState?.context);
|
|
567
|
+
const answer = session.steps.find((s) => s.type === 'answer');
|
|
568
|
+
if (projectState) {
|
|
569
|
+
persistSessionToProject(projectState.id, 'single', task, session);
|
|
570
|
+
}
|
|
571
|
+
if (flags.json) {
|
|
572
|
+
renderJSON({
|
|
573
|
+
task,
|
|
574
|
+
mode: 'single',
|
|
575
|
+
answer: answer?.content ?? '',
|
|
576
|
+
steps: session.steps.map((s) => ({
|
|
577
|
+
type: s.type,
|
|
578
|
+
content: s.content,
|
|
579
|
+
toolName: s.toolName,
|
|
580
|
+
toolArgs: s.toolArgs,
|
|
581
|
+
toolResult: s.toolResult ? { success: s.toolResult.success, summary: s.toolResult.summary } : undefined,
|
|
582
|
+
})),
|
|
583
|
+
context: session.context,
|
|
584
|
+
tokens: session.totalTokens,
|
|
585
|
+
latencyMs: session.totalLatencyMs,
|
|
586
|
+
});
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (answer) {
|
|
590
|
+
console.log('');
|
|
591
|
+
heading('Agent Analysis');
|
|
592
|
+
console.log(answer.content);
|
|
593
|
+
console.log('');
|
|
594
|
+
console.log(chalk.gray(` (${session.steps.filter((s) => s.type === 'tool_call').length} tools executed, ${session.totalTokens} tokens, ${session.totalLatencyMs}ms)`));
|
|
595
|
+
}
|
|
596
|
+
if (flags.output && answer) {
|
|
597
|
+
writeFileSync(flags.output, answer.content);
|
|
598
|
+
persistOutputArtifact(projectState?.id, 'agent-report-file', 'agent analysis output', flags.output, { task });
|
|
599
|
+
success(`Report saved to ${flags.output}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (!flags.json) {
|
|
603
|
+
console.log('');
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
handleCommandError(err, flags, useSwarm ? 'swarm_failed' : 'agent_failed');
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
addGlobalFlags(cmd);
|
|
611
|
+
program.addCommand(cmd);
|
|
612
|
+
}
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
// Interactive REPL Chat — conversational agentic session with memory
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
export function registerChatCommand(program) {
|
|
617
|
+
const cmd = new Command('chat')
|
|
618
|
+
.description('Interactive agentic session — type natural language, agent executes tools with memory')
|
|
619
|
+
.option('--project <id>', 'Load and persist context to a stored project')
|
|
620
|
+
.action(async (opts) => {
|
|
621
|
+
const { createInterface } = await import('node:readline');
|
|
622
|
+
console.log('');
|
|
623
|
+
console.log(chalk.bold.cyan(' geotech') + chalk.bold.white('CLI') + chalk.gray(' Agent — Interactive Mode'));
|
|
624
|
+
console.log(chalk.gray(' Type engineering questions. The agent will reason and execute calculations.'));
|
|
625
|
+
console.log(chalk.gray(' Commands: /context (show memory), /clear (reset), /exit (quit)'));
|
|
626
|
+
console.log('');
|
|
627
|
+
let config;
|
|
628
|
+
try {
|
|
629
|
+
config = buildLLMConfig();
|
|
630
|
+
}
|
|
631
|
+
catch (err) {
|
|
632
|
+
error(err instanceof Error ? err.message : String(err));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (config.provider !== 'hosted-beta' && !config.apiKey) {
|
|
636
|
+
warn('No provider API key set. Run: geotech config set llm.api_key <key>');
|
|
637
|
+
warn('Or switch back to hosted beta with: geotech config set llm.provider hosted-beta');
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const projectState = loadProjectState(opts.project);
|
|
641
|
+
if (projectState) {
|
|
642
|
+
console.log(chalk.gray(` Project context loaded: ${projectState.name} (${projectState.id})`));
|
|
643
|
+
console.log('');
|
|
644
|
+
}
|
|
645
|
+
const conversation = new AgentConversation({
|
|
646
|
+
context: projectState?.context,
|
|
647
|
+
});
|
|
648
|
+
const rl = createInterface({
|
|
649
|
+
input: process.stdin,
|
|
650
|
+
output: process.stdout,
|
|
651
|
+
prompt: chalk.cyan(' geotech') + chalk.white(' > '),
|
|
652
|
+
});
|
|
653
|
+
rl.prompt();
|
|
654
|
+
rl.on('line', async (line) => {
|
|
655
|
+
const input = line.trim();
|
|
656
|
+
if (!input) {
|
|
657
|
+
rl.prompt();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
// REPL commands
|
|
661
|
+
if (input === '/exit' || input === '/quit') {
|
|
662
|
+
console.log(chalk.gray(' Goodbye.'));
|
|
663
|
+
rl.close();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (input === '/context') {
|
|
667
|
+
const ctx = conversation.getContext();
|
|
668
|
+
if (Object.keys(ctx).length === 0) {
|
|
669
|
+
console.log(chalk.gray(' No context yet. Run some analyses first.'));
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
console.log(chalk.gray(' Session context (accumulated results):'));
|
|
673
|
+
for (const [key, value] of Object.entries(ctx)) {
|
|
674
|
+
const summary = typeof value === 'object' && value !== null && 'steps' in value
|
|
675
|
+
? value.steps?.slice(-1)[0] ?? key
|
|
676
|
+
: key;
|
|
677
|
+
console.log(chalk.gray(` • ${key}: `) + chalk.white(String(typeof summary === 'string' ? summary : key)));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
console.log('');
|
|
681
|
+
rl.prompt();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (input === '/clear') {
|
|
685
|
+
conversation.clearContext();
|
|
686
|
+
if (projectState) {
|
|
687
|
+
setActiveAnalysisContext(projectState.id, {
|
|
688
|
+
currentTask: 'interactive-chat',
|
|
689
|
+
lastAgentMode: 'chat',
|
|
690
|
+
lastAnswer: undefined,
|
|
691
|
+
context: {},
|
|
692
|
+
relatedDatasets: [],
|
|
693
|
+
});
|
|
694
|
+
saveNamedDataset(projectState.id, {
|
|
695
|
+
name: 'latest-agent-context',
|
|
696
|
+
kind: 'agent-context',
|
|
697
|
+
data: {},
|
|
698
|
+
source: 'chat-clear',
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
console.log(chalk.gray(' Context cleared.'));
|
|
702
|
+
console.log('');
|
|
703
|
+
rl.prompt();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
// Check quota
|
|
707
|
+
if (!(await checkQuota('agentCalls'))) {
|
|
708
|
+
rl.prompt();
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
console.log('');
|
|
712
|
+
try {
|
|
713
|
+
const session = await conversation.ask(input, config, (step) => {
|
|
714
|
+
renderAgentStep(step, false, false);
|
|
715
|
+
});
|
|
716
|
+
if (projectState) {
|
|
717
|
+
persistSessionToProject(projectState.id, 'chat', input, session);
|
|
718
|
+
}
|
|
719
|
+
const answer = session.steps.find((s) => s.type === 'answer');
|
|
720
|
+
if (answer) {
|
|
721
|
+
console.log('');
|
|
722
|
+
console.log(chalk.white(answer.content));
|
|
723
|
+
console.log('');
|
|
724
|
+
console.log(chalk.gray(` (${session.steps.filter((s) => s.type === 'tool_call').length} tools, ${session.totalTokens} tokens)`));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
catch (err) {
|
|
728
|
+
error(err instanceof Error ? err.message : String(err));
|
|
729
|
+
}
|
|
730
|
+
console.log('');
|
|
731
|
+
rl.prompt();
|
|
732
|
+
});
|
|
733
|
+
rl.on('close', () => {
|
|
734
|
+
process.exit(0);
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
program.addCommand(cmd);
|
|
738
|
+
}
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
740
|
+
// Report Generation
|
|
741
|
+
// ---------------------------------------------------------------------------
|
|
742
|
+
export function registerReportCommand(program) {
|
|
743
|
+
const cmd = new Command('report')
|
|
744
|
+
.description('Generate AI-powered geotechnical report from analysis data')
|
|
745
|
+
.requiredOption('--data <file>', 'JSON file with analysis results')
|
|
746
|
+
.option('--type <type>', 'Report type: borehole|site-investigation|tunnel-design|foundation|slope|custom', 'site-investigation')
|
|
747
|
+
.option('--project <name>', 'Project name')
|
|
748
|
+
.option('--location <loc>', 'Project location')
|
|
749
|
+
.action(async (opts) => {
|
|
750
|
+
const flags = getGlobalFlags(opts);
|
|
751
|
+
if (!(await checkQuota('llmCalls')))
|
|
752
|
+
return;
|
|
753
|
+
const spinner = flags.json ? null : ora({ text: 'Generating report...', indent: 2 }).start();
|
|
754
|
+
try {
|
|
755
|
+
const data = JSON.parse(readFileSync(opts.data, 'utf-8'));
|
|
756
|
+
const config = buildLLMConfig();
|
|
757
|
+
const report = await generateReport(data, {
|
|
758
|
+
type: opts.type,
|
|
759
|
+
projectName: opts.project,
|
|
760
|
+
location: opts.location,
|
|
761
|
+
}, config);
|
|
762
|
+
spinner?.succeed(`Report generated: ${report.sections.length} sections (${report.latencyMs}ms)`);
|
|
763
|
+
if (flags.json) {
|
|
764
|
+
renderJSON(report);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const outputFile = flags.output ?? 'report.md';
|
|
768
|
+
writeFileSync(outputFile, report.fullMarkdown);
|
|
769
|
+
success(`Report saved to ${outputFile}`);
|
|
770
|
+
console.log('');
|
|
771
|
+
}
|
|
772
|
+
catch (err) {
|
|
773
|
+
spinner?.fail('Report generation failed');
|
|
774
|
+
handleCommandError(err, flags, 'report_generation_failed');
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
addGlobalFlags(cmd);
|
|
778
|
+
program.addCommand(cmd);
|
|
779
|
+
}
|
|
780
|
+
//# sourceMappingURL=ai.js.map
|