@seeed-studio/solution-cli 0.1.0 → 0.3.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.
@@ -0,0 +1,516 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import https from 'node:https';
5
+ import http from 'node:http';
6
+ import { createCatalogDto } from './catalog-dto.js';
7
+ import { assertCompatibility, COMPATIBILITY_STATUS } from './compatibility.js';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const DATA_FILE = path.join(__dirname, '..', 'catalog', 'knowledge-core.json');
12
+
13
+ let cachedKnowledgeCore = null;
14
+
15
+ /**
16
+ * Appends UTM tracking parameters to a URL while preserving existing query parameters.
17
+ *
18
+ * @param {string} urlStr - Target URL
19
+ * @param {Object} utmParams
20
+ * @param {string} [utmParams.source='sensecraft_agent']
21
+ * @param {string} [utmParams.medium='solution_consult']
22
+ * @param {string} [utmParams.campaign='']
23
+ * @param {string} [utmParams.content='']
24
+ * @returns {string} URL with appended UTM parameters
25
+ */
26
+ export function appendUtm(urlStr, {
27
+ source = 'sensecraft_agent',
28
+ medium = 'solution_consult',
29
+ campaign = '',
30
+ content = '',
31
+ } = {}) {
32
+ if (!urlStr || typeof urlStr !== 'string') return urlStr || '';
33
+ try {
34
+ const url = new URL(urlStr);
35
+ if (!url.searchParams.has('utm_source') && source) {
36
+ url.searchParams.set('utm_source', source);
37
+ }
38
+ if (!url.searchParams.has('utm_medium') && medium) {
39
+ url.searchParams.set('utm_medium', medium);
40
+ }
41
+ if (!url.searchParams.has('utm_campaign') && campaign) {
42
+ url.searchParams.set('utm_campaign', campaign);
43
+ }
44
+ if (!url.searchParams.has('utm_content') && content) {
45
+ url.searchParams.set('utm_content', content);
46
+ }
47
+ return url.toString();
48
+ } catch {
49
+ return urlStr;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Loads the bundled offline knowledge core file.
55
+ *
56
+ * @returns {Object} Bundled knowledge core data
57
+ */
58
+ export function loadKnowledgeCore() {
59
+ if (cachedKnowledgeCore) return cachedKnowledgeCore;
60
+ if (!fs.existsSync(DATA_FILE)) {
61
+ throw new Error(`Knowledge core manifest not found at ${DATA_FILE}. Run "npm run build:knowledge" to generate.`);
62
+ }
63
+ const raw = fs.readFileSync(DATA_FILE, 'utf-8');
64
+ cachedKnowledgeCore = JSON.parse(raw);
65
+ return cachedKnowledgeCore;
66
+ }
67
+
68
+ /**
69
+ * Performs an HTTP GET request with a timeout.
70
+ *
71
+ * @param {string} urlStr
72
+ * @param {Object} headers
73
+ * @param {number} timeoutMs
74
+ * @returns {Promise<Object>}
75
+ */
76
+ function fetchJson(urlStr, headers = {}, timeoutMs = 2500) {
77
+ return new Promise((resolve, reject) => {
78
+ const url = new URL(urlStr);
79
+ const client = url.protocol === 'https:' ? https : http;
80
+ const req = client.get(url, { headers, timeout: timeoutMs }, (res) => {
81
+ let body = '';
82
+ res.on('data', (chunk) => { body += chunk; });
83
+ res.on('end', () => {
84
+ if (res.statusCode >= 200 && res.statusCode < 300) {
85
+ try {
86
+ resolve(JSON.parse(body));
87
+ } catch (e) {
88
+ reject(new Error(`Invalid JSON from ${urlStr}: ${e.message}`));
89
+ }
90
+ } else {
91
+ reject(new Error(`HTTP ${res.statusCode} from ${urlStr}`));
92
+ }
93
+ });
94
+ });
95
+ req.on('timeout', () => {
96
+ req.destroy();
97
+ reject(new Error(`Timeout after ${timeoutMs}ms from ${urlStr}`));
98
+ });
99
+ req.on('error', reject);
100
+ });
101
+ }
102
+
103
+ const CONFIG_FILE = path.resolve(__dirname, '../catalog/client-config.json');
104
+ let cachedClientConfig = null;
105
+
106
+ /**
107
+ * Loads build-time baked client configuration if present.
108
+ *
109
+ * @returns {Object|null}
110
+ */
111
+ export function loadClientConfig() {
112
+ if (cachedClientConfig) return cachedClientConfig;
113
+ try {
114
+ if (fs.existsSync(CONFIG_FILE)) {
115
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
116
+ cachedClientConfig = JSON.parse(raw);
117
+ return cachedClientConfig;
118
+ }
119
+ } catch {
120
+ // Graceful fallback if missing or invalid
121
+ }
122
+ return null;
123
+ }
124
+
125
+ /**
126
+ * Searches Seeed Solutions Hub scenarios and reference designs.
127
+ *
128
+ * @param {Object} options
129
+ * @param {string} options.query - Free text search query
130
+ * @param {'zh' | 'en'} [options.locale='zh'] - Language preference
131
+ * @param {number} [options.limit=5] - Maximum number of results
132
+ * @returns {Promise<Object>} Standardized Catalog DTO
133
+ */
134
+ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
135
+ const qLower = (query || '').toLowerCase().trim();
136
+ const clientConfig = loadClientConfig();
137
+ const host = process.env.TYPESENSE_HOST || clientConfig?.typesense?.host || 'https://rel-search.seeedstudio.com';
138
+ const keySolution = process.env.TYPESENSE_SEARCH_KEY_SOLUTION || process.env.TYPESENSE_SEARCH_KEY || clientConfig?.typesense?.search_key_solution;
139
+ const keyRefDesign = process.env.TYPESENSE_SEARCH_KEY_REFERENCE_DESIGN || process.env.TYPESENSE_SEARCH_KEY || clientConfig?.typesense?.search_key_reference_design;
140
+
141
+ // Attempt online Typesense search if keys are provided and not explicitly running offline
142
+ if ((keySolution || keyRefDesign) && !process.env.SENSECRAFT_OFFLINE_ONLY) {
143
+ try {
144
+ const core = loadKnowledgeCore();
145
+ const onlinePromises = [];
146
+
147
+ if (keySolution) {
148
+ const solUrl = `${host}/collections/Solution/documents/search?q=${encodeURIComponent(query)}&query_by=title_zh_hans,title_en,excerpt_zh_hans,excerpt_en,display_tags_zh_hans&per_page=${limit}`;
149
+ onlinePromises.push(
150
+ fetchJson(solUrl, { 'X-TYPESENSE-API-KEY': keySolution }, 2500)
151
+ .then((data) => ({ type: 'scenario', hits: data?.hits || [] }))
152
+ .catch(() => ({ type: 'scenario', hits: [] }))
153
+ );
154
+ }
155
+
156
+ if (keyRefDesign) {
157
+ const rdUrl = `${host}/collections/reference_design/documents/search?q=${encodeURIComponent(query)}&query_by=name_zh_hans,name_en,summary_zh_hans,summary_en,tags&per_page=${limit}`;
158
+ onlinePromises.push(
159
+ fetchJson(rdUrl, { 'X-TYPESENSE-API-KEY': keyRefDesign }, 2500)
160
+ .then((data) => ({ type: 'reference_design', hits: data?.hits || [] }))
161
+ .catch(() => ({ type: 'reference_design', hits: [] }))
162
+ );
163
+ }
164
+
165
+ const results = await Promise.all(onlinePromises);
166
+ const onlineRecords = [];
167
+
168
+ for (const res of results) {
169
+ if (res.type === 'scenario') {
170
+ for (const hit of res.hits) {
171
+ const doc = hit.document || {};
172
+ const docId = doc.id || doc.slug;
173
+ const title = locale === 'en'
174
+ ? (doc.title_en || doc.title_zh_hans || docId)
175
+ : (doc.title_zh_hans || doc.title_en || docId);
176
+ const summary = locale === 'en'
177
+ ? (doc.excerpt_en || doc.seo_description_en || doc.excerpt_zh_hans || '')
178
+ : (doc.excerpt_zh_hans || doc.seo_description_zh_hans || doc.excerpt_en || '');
179
+ const tags = (locale === 'en' ? doc.display_tags_en : doc.display_tags_zh_hans) || doc.display_tags || [];
180
+ const pb = core.diagnostic_playbooks?.[docId] || doc.diagnostic_playbook || null;
181
+
182
+ onlineRecords.push({
183
+ type: 'scenario',
184
+ id: docId,
185
+ title,
186
+ summary,
187
+ tags,
188
+ family_ids: doc.family_ids || [],
189
+ reference_designs: doc.reference_designs || [],
190
+ landing_url: appendUtm(`https://www.seeedstudio.com/solutions/${docId}`, {
191
+ source: 'sensecraft_agent',
192
+ medium: 'solution_consult',
193
+ campaign: docId,
194
+ content: 'scenario_landing',
195
+ }),
196
+ repo_url: '',
197
+ diagnostic_playbook: pb,
198
+ source: 'typesense',
199
+ });
200
+ }
201
+ } else if (res.type === 'reference_design') {
202
+ for (const hit of res.hits) {
203
+ const doc = hit.document || {};
204
+ const docId = doc.id;
205
+ const title = locale === 'en'
206
+ ? (doc.name_en || doc.name_zh_hans || docId)
207
+ : (doc.name_zh_hans || doc.name_en || docId);
208
+ const summary = locale === 'en'
209
+ ? (doc.summary_en || doc.tagline_en || doc.summary_zh_hans || '')
210
+ : (doc.summary_zh_hans || doc.tagline_zh_hans || doc.summary_en || '');
211
+
212
+ let pb = doc.diagnostic_playbook || null;
213
+ if (!pb) {
214
+ for (const sc of core.scenarios || []) {
215
+ if ((sc.reference_designs || []).includes(docId)) {
216
+ pb = sc.diagnostic_playbook || core.diagnostic_playbooks?.[sc.id] || null;
217
+ break;
218
+ }
219
+ }
220
+ }
221
+
222
+ onlineRecords.push({
223
+ type: 'reference_design',
224
+ id: docId,
225
+ title,
226
+ summary,
227
+ presets: doc.presets || [],
228
+ landing_url: appendUtm(`https://www.seeedstudio.com/solutions/reference-designs/${docId}`, {
229
+ source: 'sensecraft_agent',
230
+ medium: 'solution_consult',
231
+ campaign: docId,
232
+ content: 'solution_landing',
233
+ }),
234
+ wiki_url: doc.wiki_url ? appendUtm(doc.wiki_url, {
235
+ source: 'sensecraft_agent',
236
+ medium: 'tech_wiki',
237
+ campaign: docId,
238
+ }) : '',
239
+ repo_url: doc.repo_url || `https://github.com/suharvest/sensecraft-solutions/tree/main/solutions/${docId}`,
240
+ diagnostic_playbook: pb,
241
+ source: 'typesense',
242
+ });
243
+ }
244
+ }
245
+ }
246
+
247
+ if (onlineRecords.length > 0) {
248
+ return createCatalogDto({
249
+ source: 'typesense',
250
+ freshness: 'FRESH',
251
+ records: onlineRecords.slice(0, limit),
252
+ });
253
+ }
254
+ } catch {
255
+ // Graceful fallback to offline core with notice
256
+ }
257
+ }
258
+
259
+ // Offline fallback search over knowledge-core
260
+ const core = loadKnowledgeCore();
261
+ const matchedScenarios = [];
262
+ const matchedRefDesigns = [];
263
+
264
+ const tokens = qLower.split(/\s+/).filter(Boolean);
265
+
266
+ for (const s of core.scenarios || []) {
267
+ const haystack = [
268
+ s.id,
269
+ s.title?.zh,
270
+ s.title?.en,
271
+ s.subtitle?.zh,
272
+ s.subtitle?.en,
273
+ s.summary,
274
+ ...(s.tags || []),
275
+ ].join(' ').toLowerCase();
276
+
277
+ const score = tokens.reduce((acc, token) => (haystack.includes(token) ? acc + 1 : acc), 0);
278
+ if (tokens.length === 0 || score > 0) {
279
+ matchedScenarios.push({
280
+ type: 'scenario',
281
+ id: s.id,
282
+ title: s.title?.[locale] || s.title?.zh || s.title?.en || s.id,
283
+ summary: s.subtitle?.[locale] || s.subtitle?.zh || s.summary || '',
284
+ tags: s.tags || [],
285
+ family_ids: s.family_ids || [],
286
+ reference_designs: s.reference_designs || [],
287
+ landing_url: appendUtm(s.landing_url || `https://www.seeedstudio.com/solutions/${s.id}`, {
288
+ source: 'sensecraft_agent',
289
+ medium: 'solution_consult',
290
+ campaign: s.id,
291
+ content: 'scenario_landing',
292
+ }),
293
+ repo_url: s.repo_url || '',
294
+ diagnostic_playbook: s.diagnostic_playbook || null,
295
+ score,
296
+ });
297
+ }
298
+ }
299
+
300
+ for (const rd of core.reference_designs || []) {
301
+ const haystack = [
302
+ rd.id,
303
+ rd.title?.zh,
304
+ rd.title?.en,
305
+ rd.summary?.zh,
306
+ rd.summary?.en,
307
+ ...(rd.presets || []),
308
+ ].join(' ').toLowerCase();
309
+
310
+ const score = tokens.reduce((acc, token) => (haystack.includes(token) ? acc + 1 : acc), 0);
311
+ if (tokens.length === 0 || score > 0) {
312
+ matchedRefDesigns.push({
313
+ type: 'reference_design',
314
+ id: rd.id,
315
+ title: rd.title?.[locale] || rd.title?.zh || rd.title?.en || rd.id,
316
+ summary: rd.summary?.[locale] || rd.summary?.zh || rd.summary?.en || '',
317
+ presets: rd.presets || [],
318
+ landing_url: appendUtm(rd.landing_url || `https://www.seeedstudio.com/solutions/reference-designs/${rd.id}`, {
319
+ source: 'sensecraft_agent',
320
+ medium: 'solution_consult',
321
+ campaign: rd.id,
322
+ content: 'solution_landing',
323
+ }),
324
+ wiki_url: rd.wiki_url ? appendUtm(rd.wiki_url, {
325
+ source: 'sensecraft_agent',
326
+ medium: 'tech_wiki',
327
+ campaign: rd.id,
328
+ }) : '',
329
+ repo_url: rd.repo_url || `https://github.com/suharvest/sensecraft-solutions/tree/main/solutions/${rd.id}`,
330
+ diagnostic_playbook: rd.diagnostic_playbook || null,
331
+ score,
332
+ });
333
+ }
334
+ }
335
+
336
+ // Sort by score descending and take top results
337
+ matchedScenarios.sort((a, b) => b.score - a.score);
338
+ matchedRefDesigns.sort((a, b) => b.score - a.score);
339
+
340
+ const combined = [...matchedScenarios, ...matchedRefDesigns].slice(0, limit);
341
+
342
+ return createCatalogDto({
343
+ source: 'bundled_manifest',
344
+ freshness: 'FRESH',
345
+ records: combined,
346
+ warnings: (keySolution || keyRefDesign)
347
+ ? ['Online Typesense request timed out or returned no hits. Displaying verified local catalog.']
348
+ : ['No Typesense API key configured or baked. Displaying verified local catalog.'],
349
+ });
350
+ }
351
+
352
+ /**
353
+ * Queries product family information, SKUs, and official purchase links.
354
+ *
355
+ * @param {string} familyId
356
+ * @param {Object} [options]
357
+ * @param {string} [options.locale='zh']
358
+ * @returns {Object} Catalog DTO with product details
359
+ */
360
+ export function queryProductFamily(familyId, { locale = 'zh' } = {}) {
361
+ const core = loadKnowledgeCore();
362
+ const family = core.product_families?.[familyId];
363
+
364
+ if (!family) {
365
+ return createCatalogDto({
366
+ source: 'bundled_manifest',
367
+ freshness: 'FRESH',
368
+ records: [],
369
+ warnings: [`Product family "${familyId}" not found in local catalog.`],
370
+ });
371
+ }
372
+
373
+ return createCatalogDto({
374
+ source: 'bundled_manifest',
375
+ freshness: 'FRESH',
376
+ records: [{
377
+ family_id: family.id,
378
+ title: family.title?.[locale] || family.title?.zh || family.title?.en || family.id,
379
+ default_sku: family.default_sku,
380
+ regions: family.regions || [],
381
+ skus_count: (family.skus || []).length,
382
+ skus: (family.skus || []).slice(0, 10).map((s) => ({
383
+ ...s,
384
+ url: appendUtm(s.url, {
385
+ source: 'sensecraft_agent',
386
+ medium: 'bom_recommendation',
387
+ campaign: family.id,
388
+ content: `sku_${s.sku}`,
389
+ }),
390
+ })),
391
+ physical_spec: family.physical_spec || null,
392
+ required_accessories_count: (family.required_accessories || []).length,
393
+ optional_accessories_count: (family.optional_accessories || []).length,
394
+ }],
395
+ });
396
+ }
397
+
398
+ /**
399
+ * Queries and verifies accessories for a product family, running physical compatibility checks.
400
+ *
401
+ * @param {string} familyId
402
+ * @param {Object} [options]
403
+ * @param {string} [options.sku] - Specific host SKU
404
+ * @param {string} [options.locale='zh']
405
+ * @returns {Object} Catalog DTO with structured, verified accessories
406
+ */
407
+ export function queryAccessories(familyId, { sku = null, locale = 'zh' } = {}) {
408
+ const core = loadKnowledgeCore();
409
+ const family = core.product_families?.[familyId];
410
+
411
+ if (!family) {
412
+ return createCatalogDto({
413
+ source: 'bundled_manifest',
414
+ freshness: 'FRESH',
415
+ records: [],
416
+ warnings: [`Product family "${familyId}" not found in local catalog.`],
417
+ });
418
+ }
419
+
420
+ const hostPhysicalSpec = family.physical_spec || null;
421
+
422
+ // Process required accessories
423
+ const required = (family.required_accessories || []).map((acc) => {
424
+ const comp = assertCompatibility(hostPhysicalSpec, acc);
425
+ return {
426
+ category: 'REQUIRED',
427
+ id: acc.id,
428
+ name: acc.name,
429
+ sku: acc.sku,
430
+ url: appendUtm(acc.url, {
431
+ source: 'sensecraft_agent',
432
+ medium: 'bom_recommendation',
433
+ campaign: family.id,
434
+ content: `required_${acc.id}`,
435
+ }),
436
+ qty: acc.qty || 1,
437
+ reason: acc.reason || 'Required for standard operation',
438
+ applies_when: acc.applies_when || null,
439
+ compatibility: {
440
+ is_compatible: comp.isCompatible,
441
+ status: comp.status,
442
+ details: comp.dimensions,
443
+ },
444
+ };
445
+ });
446
+
447
+ // Process optional accessories
448
+ const optional = (family.optional_accessories || []).map((acc) => {
449
+ const comp = assertCompatibility(hostPhysicalSpec, acc);
450
+ return {
451
+ category: 'OPTIONAL',
452
+ id: acc.id,
453
+ name: acc.name,
454
+ sku: acc.sku,
455
+ url: appendUtm(acc.url, {
456
+ source: 'sensecraft_agent',
457
+ medium: 'bom_recommendation',
458
+ campaign: family.id,
459
+ content: `optional_${acc.id}`,
460
+ }),
461
+ type: acc.type || 'peripheral',
462
+ compatibility: {
463
+ is_compatible: comp.isCompatible,
464
+ status: comp.status,
465
+ details: comp.dimensions,
466
+ },
467
+ };
468
+ });
469
+
470
+ return createCatalogDto({
471
+ source: 'bundled_manifest',
472
+ freshness: 'FRESH',
473
+ records: [{
474
+ family_id: family.id,
475
+ host_title: family.title?.[locale] || family.title?.zh || family.title?.en,
476
+ host_sku: sku || family.default_sku,
477
+ physical_spec: hostPhysicalSpec,
478
+ accessories: {
479
+ required,
480
+ optional,
481
+ },
482
+ }],
483
+ });
484
+ }
485
+
486
+ /**
487
+ * Generates an immutable DeploymentLockfile to isolate deployment apply from external network.
488
+ *
489
+ * @param {Object} options
490
+ * @param {string} options.solutionId
491
+ * @param {string} options.solutionVersion
492
+ * @param {Array<string>} options.selectedHardware
493
+ * @param {Array<Object>} [options.verifiedCompatibility=[]]
494
+ * @returns {Object} DeploymentLockfile
495
+ */
496
+ export function createDeploymentLockfile({
497
+ solutionId,
498
+ solutionVersion = '1.0.0',
499
+ selectedHardware = [],
500
+ verifiedCompatibility = [],
501
+ }) {
502
+ return {
503
+ $schema: 'https://spec.seeedstudio.com/schemas/v2/deployment-lock.json',
504
+ solution_id: solutionId,
505
+ solution_version: solutionVersion,
506
+ locked_at: new Date().toISOString(),
507
+ dataset_version: '2026-10-13.1',
508
+ selected_hardware: selectedHardware,
509
+ compatibility_assertions: verifiedCompatibility,
510
+ unresolved_requirements: [],
511
+ network_policy: {
512
+ commercial_queries_allowed: false,
513
+ apply_isolated: true,
514
+ },
515
+ };
516
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seeed-studio/solution-cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Zero-friction installer and CLI runner for SenseCraft Engine and AI Agent Skills (Work Buddy, Claude Code, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "./lib/installer.js",
@@ -17,10 +17,14 @@
17
17
  "files": [
18
18
  "bin",
19
19
  "lib",
20
+ "catalog",
20
21
  "skills",
22
+ "docs",
21
23
  "README.md"
22
24
  ],
23
25
  "scripts": {
26
+ "build": "node scripts/bake-client-config.mjs",
27
+ "prepack": "node scripts/bake-client-config.mjs",
24
28
  "test": "node --test tests/cli.test.mjs",
25
29
  "install:self": "node bin/cli.js install",
26
30
  "status": "node bin/cli.js status",
@@ -1,16 +1,25 @@
1
1
  ---
2
2
  name: sensecraft
3
- description: Deploy, manage, and troubleshoot SenseCraft / Seeed Studio edge IoT solutions (智慧农业, 摔倒检测, 边缘安全, 智慧仓库, 语音助手, people counting, face recognition 等) on hardware devices (NVIDIA Jetson, Raspberry Pi 树莓派, reComputer, reCamera, SenseCAP Watcher, Grove Vision AI, ESP32) using the `sensecraft-engine` command-line engine (no desktop app needed). Use whenever the user asks to deploy (部署), install (安装方案), test, inspect, or troubleshoot a SenseCraft edge IoT solution or configure devices and sensors. Changing a solution's configuration needs the desktop app (references/app-only.md).
3
+ description: Consult, select, deploy, manage, and troubleshoot SenseCraft / Seeed Studio edge IoT solutions (智慧农业, 摔倒检测, 边缘安全, 智慧仓库, 语音助手, people counting, face recognition 等) and hardware products (reComputer Jetson, reCamera, SenseCAP, Grove Vision AI, ESP32) using the `sensecraft-engine` and `solution-cli` toolset. Use whenever the user asks for edge IoT solutions, hardware recommendations, accessory compatibility, deployment, or troubleshooting.
4
4
  ---
5
5
 
6
6
  # SenseCraft Solution
7
7
 
8
- This skill drives the `sensecraft-engine` command-line engine. It works in any
9
- agent that can read files and run shell commands. Every step is a
10
- `sensecraft-engine` command; the engine starts its own background instance when
11
- a command needs one. Do not call the instance's HTTP port with `curl`: use the
12
- CLI, and `sensecraft-engine api <METHOD> <path>` for anything without its own
13
- command.
8
+ This skill drives the `sensecraft-engine` execution engine and `solution-cli` toolset. It operates across the complete lifecycle: **Solution Consultation ➔ Hardware & Accessory BOM Selection ➔ Local Deployment ➔ Self-Healing Diagnostics**.
9
+
10
+ ## Workflow Routing & Progressive Disclosure
11
+
12
+ Do NOT read all reference files at once. Determine the user's workflow state and intent, then load **at most ONE** reference file on demand:
13
+
14
+ | User State & Intent | Reference to Load | Primary CLI Tool to Call | Guardrails & Forbidden Actions |
15
+ |---|---|---|---|
16
+ | **DISCOVERY / CONSULT**<br>“我想做防跌倒监控”、“有智慧农场方案吗” | `references/consult-and-select.md` | `solution-cli hub search "<keyword>" --json` | **DO NOT run deploy**; user is exploring scenarios and has not confirmed hardware. |
17
+ | **PROCUREMENT / VERIFY_BOM**<br>“买什么硬件?”、“J4012 需要配什么配件?” | `references/hardware-bom.md` | `solution-cli product accessories <id> --json` | **DO NOT fabricate** third-party SKUs or prices. Always distinguish REQUIRED vs OPTIONAL accessories. |
18
+ | **ONBOARDING / PRECHECK**<br>“设备到了,插上了网线/USB”、“环境自检” | `references/diagnose.md` | `solution-cli doctor` / `sensecraft-engine device scan` | Freeze hardware requirements into DeploymentLockfile before proceeding to apply. |
19
+ | **DEPLOYING / APPLY**<br>“开始安装”、“部署 fall_detection” | `references/deploy.md` | `sensecraft-engine deploy --solution <id>` | **DEPLOYMENT CRITICAL SECTION**: Zero external commercial queries allowed during apply. |
20
+ | **MANAGE / RECONFIG**<br>“改一下 MQTT 地址”、“查看已安装应用”、“重启服务”、“修改配置” | `references/device-management.md` | `curl BASE/api/docker-devices/local/config/<id>` | **Check before write**: Only alter `reconfigurable: true` fields; check current value before modifying. |
21
+ | **CUSTOMIZE / SECONDARY_DEV**<br>“我想改源码”、“换成自研模型”、“改前端看板”、“GitHub 仓库” | `references/secondary-development.md` | `git clone <repo_url>` / `sensecraft-engine --solutions-dir` | **Never edit cached content**: Clone to user workspace, pair-program, test locally with `--solutions-dir`. |
22
+ | **TROUBLESHOOTING**<br>“部署报错”、“黑屏”、“无视频流” | `references/error-catalog.md` | `sensecraft-engine logs` / `solution-cli product verify-compatibility` | Max 3 fix-and-retry rounds. Follow local auto-patching rule. |
14
23
 
15
24
  Capabilities that need the **SenseCraft Solution desktop app** (editing a
16
25
  solution's configuration, the app's `/api/ai/context`, diagnosing through the
@@ -79,6 +88,20 @@ daemon is a host prerequisite, not evidence that a deployment has passed.
79
88
  6. **Temp files:** keep request bodies and scratch files in the current
80
89
  working directory under `./.sensecraft-work/`, never in `/tmp`. Remove the
81
90
  directory when the task ends, also after a failure.
91
+ 7. **Conversational tone & soft referral:** Agent is an IoT domain expert and
92
+ answers technical, business, hardware, and configuration questions directly
93
+ and thoroughly using local knowledge and tools. Do NOT dump raw link lists
94
+ or write pushy sales pitches. External links (`landing_url`, `wiki_url`,
95
+ product purchase URLs, `repo_url`) must be smoothly integrated as
96
+ supplemental reading / procurement / code exploration. All URLs generated by
97
+ `solution-cli` automatically carry silent UTM parameters
98
+ (`utm_source=sensecraft_agent`) for analytics attribution.
99
+ 8. **Proactive diagnosis & domain-specific questioning:** During solution consultation,
100
+ Agent must avoid one-way monologues. Proactively inspect the `diagnostic_playbook`
101
+ returned by `solution-cli hub search` (or cross-reference `references/consult-and-select.md` §3).
102
+ Ask 2–3 targeted domain questions explaining the engineering rationale (topology,
103
+ asset reuse, power/cabling, inspection speed/materials) to help the user converge
104
+ on the optimal hardware architecture and budget.
82
105
 
83
106
  ## Step 0 — Install the engine and the solutions (every task)
84
107