@seeed-studio/solution-cli 0.4.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/docs/cli-guide.md CHANGED
@@ -28,24 +28,26 @@ Agent 的默认工作流是“先咨询,再选型,再检查,最后在用
28
28
 
29
29
  ## 2. 安装与 Agent 技能挂载
30
30
 
31
- ### 2.1 按需精准安装(推荐:只针对当前 Agent 安装,避免环境污染)
31
+ ### 2.1 按需精准安装(推荐:npm 全局安装并挂载,避免 PATH 缺失报错)
32
32
 
33
33
  你可以直接复制下表中对应的专属命令发给对应的 AI 助手,或在终端执行:
34
34
 
35
35
  | 目标 AI 宿主平台 | 专属安装命令(直接复制发给 AI 或在终端运行) | 技能默认配置路径 |
36
36
  |---|---|---|
37
- | **豆包工作 (Doubao Work)** | `npx -y @seeed-studio/solution-cli install --agent doubao` | `~/.doubao/skills` |
38
- | **WorkBuddy** | `npx -y @seeed-studio/solution-cli install --agent workbuddy` | `~/.workbuddy/skills` |
39
- | **Claude Code** | `npx -y @seeed-studio/solution-cli install --agent claude` | `~/.claude/skills` |
40
- | **OpenAI Codex** | `npx -y @seeed-studio/solution-cli install --agent codex` | `~/.agents/skills` 或 `~/.codex/skills` |
41
- | **Google Antigravity** | `npx -y @seeed-studio/solution-cli install --agent antigravity` | `~/.gemini/config/skills` |
42
- | **CodeBuddy** | `npx -y @seeed-studio/solution-cli install --agent codebuddy` | `~/.codebuddy/skills` |
43
- | **全部 Agent (全量覆盖)** | `npx -y @seeed-studio/solution-cli install --agent all` | 上述全部已检测目录 |
44
- | **自定义目录 (如 Cursor)** | `npx -y @seeed-studio/solution-cli install --skills-dir <路径>` | `<自定义路径>/solution-cli` |
37
+ | **豆包工作 (Doubao Work)** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent doubao` | `~/.doubao/skills` |
38
+ | **Claude Code** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent claude` | `~/.claude/skills` |
39
+ | **OpenAI Codex** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent codex` | `~/.agents/skills` 或 `~/.codex/skills` |
40
+ | **Google Antigravity** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent antigravity` | `~/.gemini/config/skills` |
41
+ | **WorkBuddy** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent workbuddy` | `~/.workbuddy/skills` |
42
+ | **CodeBuddy** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent codebuddy` | `~/.codebuddy/skills` |
43
+ | **Hermes Agent** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent hermes` | `$HERMES_HOME/skills` (默认 `~/.hermes/skills`) |
44
+ | **全部 Agent (全量覆盖)** | `npm install -g @seeed-studio/solution-cli && solution-cli install --agent all` | 上述全部已检测目录 |
45
+ | **自定义目录 (如 Cursor)** | `npm install -g @seeed-studio/solution-cli && solution-cli install --skills-dir <路径>` | `<自定义路径>/solution-cli` |
45
46
 
46
47
  > [!TIP]
47
- > - **丢给 AI 极简执行**:例如在豆包工作中直接发送:*“请帮我运行:`npx -y @seeed-studio/solution-cli install --agent doubao`”*,AI 将直接在后台非交互执行并自动就绪。
48
- > - **严格隔离**:指定 `--agent <id>` 时,安装器依据 ledger 记录仅写入该 Agent 对应的目录,绝不修改其他 Agent 的技能或配置。
48
+ > - **为什么推荐 npm 全局安装?** 将 `solution-cli` 加入全局 PATH,AI Agent 在后续对话中调用方案/硬件查询时直接本地毫秒级响应,避免因 PATH 缺失触发 `command not found`。
49
+ > - **发给 AI 助手**:例如在豆包工作中直接发送:*“请帮我运行:`npm install -g @seeed-studio/solution-cli && solution-cli install --agent doubao`”*。
50
+ > - **免全局安装(npx 备选)**:如无全局 npm 写入权限,可使用 npx:`npx -y @seeed-studio/solution-cli install --agent <id>`。
49
51
 
50
52
  ### 2.2 本地源码部署与定制开发 (Local Deployment)
51
53
 
@@ -59,6 +61,27 @@ node bin/cli.js install --agent <doubao|claude|codex|antigravity|codebuddy>
59
61
  node bin/cli.js install --skills-dir ~/.cursor/skills
60
62
  ```
61
63
 
64
+ 省略 `--agent` 时,终端交互运行会进入选择器;非交互运行会安装到已检测的宿主,未检测到宿主时会选择全部宿主。建议始终明确指定当前 Agent。重复执行会复用相同或更新的签名 Engine、升级旧版本,并保留用户修改的 Skill。`--force` 会覆盖已有 Skill 修改,只在用户明确要求时使用。
65
+
66
+ 安装完成后刷新宿主的 Skill 列表或完全退出并重新打开 Agent 应用,确认宿主的实际 Skill 列表中已出现 `solution-cli`,再新建会话直接描述需求。若宿主未自动发现技能,发送:“请使用 solution-cli Skill,先查询方案,再告诉我缺哪些部署信息。”安装器只写入被选中的宿主。
67
+
68
+ ### 2.3 全局安装与 Profile
69
+
70
+ 需要长期使用 `solution-cli` 命令时可以全局安装:
71
+
72
+ ```bash
73
+ npm install -g --registry=https://registry.npmjs.org/ @seeed-studio/solution-cli@latest
74
+ solution-cli status
75
+ ```
76
+
77
+ 不想修改全局 npm 环境时,使用 `npx --package` 临时调用:
78
+
79
+ ```bash
80
+ npx -y --registry=https://registry.npmjs.org/ --package=@seeed-studio/solution-cli@latest solution-cli hub search "农业" --json
81
+ ```
82
+
83
+ Hermes 的已配置 profile 可通过 `--agent hermes:<profile>` 选择(安装、更新、卸载通用):具名 profile 使用 `<root>/profiles/<profile>/skills/solution-cli`,`hermes:default` 使用 `<root>/skills/solution-cli`;`<root>` 是 Hermes 安装根目录。Profile ID 为 1–64 位小写字母、数字、`_` 或 `-`,以字母或数字开头。安装器仅复制 Skill,不创建或切换 profile。
84
+
62
85
  ## 3. 核心功能、使用方法与具体产出
63
86
 
64
87
  安装后可直接在命令行运行 `solution-cli` 与 `sensecraft-engine`;若未执行安装,也可随时通过 `npx -y @seeed-studio/solution-cli <command>` 免安装即用。
package/lib/agent.js CHANGED
@@ -5,6 +5,53 @@ import readline from 'node:readline';
5
5
  import crypto from 'node:crypto';
6
6
  import { loadLedger } from './ledger.js';
7
7
 
8
+ // Match Hermes' native data home and get_default_hermes_root() semantics.
9
+ function getHermesPaths() {
10
+ const home = os.homedir();
11
+ const suffix = process.env.HERMES_DATA_DIR_SUFFIX || '';
12
+ const nativeHome = process.platform === 'win32'
13
+ ? path.join(process.env.LOCALAPPDATA?.trim() || path.join(home, 'AppData', 'Local'), `hermes${suffix}`)
14
+ : path.join(home, `.hermes${suffix}`);
15
+ const expand = value => value.replace(/\$(\w+)|\$\{([^}]+)\}/g,
16
+ (match, bare, braced) => process.env[bare || braced] ?? match)
17
+ .replace(/^~(?=$|[\\/])/, () => home);
18
+ const activeHome = path.resolve(expand(process.env.HERMES_HOME?.trim() || nativeHome));
19
+ const real = value => fs.existsSync(value) ? fs.realpathSync(value) : path.resolve(value);
20
+ const relative = path.relative(real(nativeHome), real(activeHome));
21
+ const inNativeHome = relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
22
+ const root = inNativeHome ? nativeHome
23
+ : path.basename(path.dirname(activeHome)) === 'profiles' ? path.dirname(path.dirname(activeHome)) : activeHome;
24
+ return { activeHome, root };
25
+ }
26
+
27
+ function hermesTarget(id = 'hermes') {
28
+ const { activeHome, root } = getHermesPaths();
29
+ let targetHome = activeHome;
30
+ if (id !== 'hermes') {
31
+ const profile = id.slice('hermes:'.length);
32
+ if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(profile)) {
33
+ throw new Error(`Invalid Hermes profile: "${profile}". Use 1-64 lowercase letters, numbers, '-' or '_', starting with a letter or number.`);
34
+ }
35
+ targetHome = profile === 'default' ? root : path.join(root, 'profiles', profile);
36
+ }
37
+ return {
38
+ id,
39
+ name: id === 'hermes' ? 'Hermes Agent' : `Hermes Agent (${id.slice(7)})`,
40
+ homeDirs: [targetHome],
41
+ getSkillsDir: () => path.join(targetHome, 'skills'),
42
+ };
43
+ }
44
+
45
+ function scanAgent(agent) {
46
+ const skillsDir = agent.getSkillsDir();
47
+ return {
48
+ ...agent,
49
+ detected: agent.homeDirs.some(dir => fs.existsSync(dir)) || fs.existsSync(skillsDir),
50
+ installed: fs.existsSync(path.join(skillsDir, 'solution-cli', 'SKILL.md')),
51
+ skillsDir,
52
+ };
53
+ }
54
+
8
55
  export function getKnownAgents() {
9
56
  const home = os.homedir();
10
57
 
@@ -68,7 +115,8 @@ export function getKnownAgents() {
68
115
  name: 'Google Antigravity (AGY)',
69
116
  homeDirs: [path.join(home, '.gemini', 'config'), path.join(home, '.gemini', 'antigravity-cli')],
70
117
  getSkillsDir: () => path.join(home, '.gemini', 'config', 'skills')
71
- }
118
+ },
119
+ hermesTarget()
72
120
  ];
73
121
  }
74
122
 
@@ -78,6 +126,10 @@ export function scanLocalAgents() {
78
126
  try {
79
127
  const ledger = loadLedger();
80
128
  for (const [targetId, inst] of Object.entries(ledger?.installations || {})) {
129
+ if (targetId.startsWith('hermes:') && inst && !agents.some(a => a.id === targetId)) {
130
+ const target = hermesTarget(targetId);
131
+ if (fs.existsSync(path.join(target.getSkillsDir(), 'solution-cli', 'SKILL.md'))) agents.push(target);
132
+ }
81
133
  if (targetId.startsWith('custom_') && inst) {
82
134
  const skillsDir = inst.skills_dir || (inst.target_dir ? path.dirname(inst.target_dir) : (inst.targetDir ? path.dirname(inst.targetDir) : null));
83
135
  if (skillsDir && fs.existsSync(skillsDir) && !agents.some(a => a.id === targetId)) {
@@ -92,17 +144,7 @@ export function scanLocalAgents() {
92
144
  }
93
145
  } catch {}
94
146
 
95
- return agents.map(a => {
96
- const skillsDir = a.getSkillsDir();
97
- const detected = a.homeDirs.some(dir => fs.existsSync(dir)) || fs.existsSync(skillsDir);
98
- const installed = fs.existsSync(path.join(skillsDir, 'solution-cli', 'SKILL.md'));
99
- return {
100
- ...a,
101
- detected,
102
- installed,
103
- skillsDir
104
- };
105
- });
147
+ return agents.map(scanAgent);
106
148
  }
107
149
 
108
150
  export function formatAgentList(scannedAgents) {
@@ -147,8 +189,16 @@ export async function promptAgentSelection(scannedAgents, cliArg, autoYes = fals
147
189
  if (cliArg.toLowerCase() === 'all') {
148
190
  return scannedAgents;
149
191
  }
150
- const requested = cliArg.split(',').map(s => s.trim().toLowerCase());
151
- const matched = scannedAgents.filter(a => requested.includes(a.id) || (a.aliases && a.aliases.some(alias => requested.includes(alias))));
192
+ const requested = cliArg.split(',').map(s => s.trim());
193
+ // Validate profile IDs before normalizing other hosts or accepting a partial match.
194
+ for (const id of requested) {
195
+ if (id.toLowerCase().startsWith('hermes:')) {
196
+ const target = scanAgent(hermesTarget(`hermes:${id.slice(7)}`));
197
+ if (!scannedAgents.some(a => a.id === target.id)) scannedAgents.push(target);
198
+ }
199
+ }
200
+ const normalized = requested.map(id => id.toLowerCase());
201
+ const matched = scannedAgents.filter(a => normalized.includes(a.id) || a.aliases?.some(alias => normalized.includes(alias)));
152
202
  if (matched.length === 0) {
153
203
  throw new Error(`No matching agents for: "${cliArg}". Available: ${scannedAgents.map(a => a.id).join(', ')}`);
154
204
  }
package/lib/knowledge.js CHANGED
@@ -711,6 +711,35 @@ export function getHubRecord(id, { type = null, locale = 'zh', section = null, s
711
711
  return row;
712
712
  }
713
713
 
714
+ /**
715
+ * Detects if a search query matches known hardware product families, models, or SKUs.
716
+ *
717
+ * @param {string[]} tokens
718
+ * @param {string} query
719
+ * @param {Object} core
720
+ * @returns {string|null} Helpful redirect note if hardware is detected
721
+ */
722
+ function detectHardwareIntent(tokens, query, core) {
723
+ if (!tokens || tokens.length === 0) return null;
724
+ const families = Object.values(core?.product_families || {});
725
+ const matchedHw = families.find((fam) => {
726
+ const fid = (fam.id || '').toLowerCase();
727
+ const zh = (fam.title?.zh || '').toLowerCase();
728
+ const en = (fam.title?.en || '').toLowerCase();
729
+ if (tokens.some((t) => fid.includes(t) || zh.includes(t) || en.includes(t))) return true;
730
+ return (fam.skus || []).some((s) => {
731
+ const sNum = String(s.sku || '').toLowerCase();
732
+ const sModel = String(s.model || '').toLowerCase();
733
+ const sName = String(s.name || '').toLowerCase();
734
+ return tokens.some((t) => sNum === t || (sModel && sModel.includes(t)) || sName.includes(t));
735
+ });
736
+ });
737
+ if (matchedHw) {
738
+ return `💡 提示: 检测到硬件设备/型号关键词 "${query}"。若需查询该硬件的物理接口、包装清单(是否带电源)或官方落地应用方案,请使用: solution-cli product search "${query}"`;
739
+ }
740
+ return null;
741
+ }
742
+
714
743
  /**
715
744
  * Searches Seeed Solutions Hub scenarios and reference designs.
716
745
  *
@@ -725,6 +754,7 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
725
754
  locale = normalizedLocale;
726
755
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) throw new Error('Limit must be an integer from 1 to 200.');
727
756
  const qLower = (query || '').toLowerCase().trim();
757
+ const tokens = qLower.split(/\s+/).filter(Boolean);
728
758
  const clientConfig = loadClientConfig();
729
759
  const host = process.env.TYPESENSE_HOST || clientConfig?.typesense?.host || 'https://rel-search.seeedstudio.com';
730
760
  const keySolution = process.env.TYPESENSE_SEARCH_KEY_SOLUTION || process.env.TYPESENSE_SEARCH_KEY || clientConfig?.typesense?.search_key_solution;
@@ -896,7 +926,6 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
896
926
 
897
927
  // If one collection returned results but the other was unavailable/failed/empty, supplement from bundled catalog
898
928
  if ((onlineScenarios.length > 0 && onlineRefDesigns.length === 0) || (onlineRefDesigns.length > 0 && onlineScenarios.length === 0)) {
899
- const tokens = qLower.split(/\s+/).filter(Boolean);
900
929
  if (onlineScenarios.length === 0) {
901
930
  for (const s of core.scenarios || []) {
902
931
  if (s.published === false || s.visibility === 'staging' || s.visibility === 'draft') continue;
@@ -939,12 +968,17 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
939
968
  }
940
969
 
941
970
  if (mergedRecords.length > 0) {
971
+ const onlineWarnings = ['Online search results are partial and do not replace the bundled catalog; use hub get for verified section content.'];
972
+ const hwHint = detectHardwareIntent(tokens, query, core);
973
+ if (hwHint) {
974
+ onlineWarnings.push(hwHint);
975
+ }
942
976
  return createCatalogDto({
943
977
  source: (onlineScenarios.some(r => r.source === 'typesense') || onlineRefDesigns.some(r => r.source === 'typesense')) ? 'typesense' : 'bundled_manifest',
944
978
  freshness: 'UNKNOWN',
945
979
  datasetVersion: core.dataset_version,
946
980
  scope: 'online_partial_with_bundled_fallback',
947
- warnings: ['Online search results are partial and do not replace the bundled catalog; use hub get for verified section content.'],
981
+ warnings: onlineWarnings,
948
982
  records: mergedRecords.map(compactSearchRecord),
949
983
  });
950
984
  }
@@ -958,8 +992,6 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
958
992
  const matchedScenarios = [];
959
993
  const matchedRefDesigns = [];
960
994
 
961
- const tokens = qLower.split(/\s+/).filter(Boolean);
962
-
963
995
  for (const s of core.scenarios || []) {
964
996
  if (s.published === false || s.visibility === 'staging' || s.visibility === 'draft') continue;
965
997
  const locKey = locale === 'zh' ? 'zh_hans' : locale;
@@ -1042,17 +1074,25 @@ export async function searchHub({ query = '', locale = 'zh', limit = 5 } = {}) {
1042
1074
  }
1043
1075
  }
1044
1076
 
1077
+ const warnings = process.env.SENSECRAFT_OFFLINE_ONLY
1078
+ ? ['Offline-only mode: displaying the verified bundled catalog; no online request was attempted.']
1079
+ : ((keySolution || keyRefDesign)
1080
+ ? ['Online Typesense request timed out or returned no hits. Displaying verified local catalog.']
1081
+ : ['No Typesense API key configured or baked. Displaying verified local catalog.']);
1082
+
1083
+ // Detect if query looks like a hardware device, model, or SKU keyword
1084
+ const hwHint = detectHardwareIntent(tokens, query, core);
1085
+ if (hwHint && (combined.length === 0 || (combined[0] && combined[0].score < 2))) {
1086
+ warnings.push(hwHint);
1087
+ }
1088
+
1045
1089
  return createCatalogDto({
1046
1090
  source: 'bundled_manifest',
1047
1091
  freshness: 'UNKNOWN',
1048
1092
  datasetVersion: core.dataset_version,
1049
1093
  scope: 'bundled_catalog_snapshot',
1050
1094
  records: combined.map(compactSearchRecord),
1051
- warnings: process.env.SENSECRAFT_OFFLINE_ONLY
1052
- ? ['Offline-only mode: displaying the verified bundled catalog; no online request was attempted.']
1053
- : ((keySolution || keyRefDesign)
1054
- ? ['Online Typesense request timed out or returned no hits. Displaying verified local catalog.']
1055
- : ['No Typesense API key configured or baked. Displaying verified local catalog.']),
1095
+ warnings,
1056
1096
  });
1057
1097
  }
1058
1098
 
@@ -1083,8 +1123,12 @@ export function queryProductFamily(familyId, { locale = 'zh' } = {}) {
1083
1123
  records: [{
1084
1124
  family_id: family.id,
1085
1125
  title: family.title?.[locale] || family.title?.zh || family.title?.en || family.id,
1126
+ summary: family.summary || null,
1086
1127
  default_sku: family.default_sku,
1087
1128
  regions: family.regions || [],
1129
+ interfaces: family.interfaces || [],
1130
+ protocols: family.protocols || [],
1131
+ supported_scenarios: family.supported_scenarios || [],
1088
1132
  skus_count: (family.skus || []).length,
1089
1133
  skus: (family.skus || []).slice(0, 10).map((s) => ({
1090
1134
  ...s,
@@ -1102,6 +1146,261 @@ export function queryProductFamily(familyId, { locale = 'zh' } = {}) {
1102
1146
  });
1103
1147
  }
1104
1148
 
1149
+ /**
1150
+ * Searches product families and member SKUs by keywords, model name, interface, or SKU number.
1151
+ *
1152
+ * @param {Object} options
1153
+ * @param {string} options.query - Free text query (e.g. "J4012 Super", "RS485", "114110314")
1154
+ * @param {'zh' | 'en' | 'ja'} [options.locale='zh']
1155
+ * @param {number} [options.limit=5]
1156
+ * @returns {Object} Catalog DTO
1157
+ */
1158
+ export function searchProducts({ query = '', locale = 'zh', limit = 5 } = {}) {
1159
+ const core = loadKnowledgeCore();
1160
+ const families = Object.values(core.product_families || {});
1161
+ const qLower = (query || '').toLowerCase().trim();
1162
+ const tokens = qLower.split(/\s+/).filter(Boolean);
1163
+
1164
+ if (tokens.length === 0) {
1165
+ return createCatalogDto({
1166
+ source: 'bundled_manifest',
1167
+ freshness: 'FRESH',
1168
+ records: [],
1169
+ warnings: ['Query cannot be empty.'],
1170
+ });
1171
+ }
1172
+
1173
+ const matches = [];
1174
+
1175
+ for (const fam of families) {
1176
+ let score = 0;
1177
+ const titleZh = (fam.title?.zh || '').toLowerCase();
1178
+ const titleEn = (fam.title?.en || '').toLowerCase();
1179
+ const fid = (fam.id || '').toLowerCase();
1180
+ const famHaystack = [fid, titleZh, titleEn].join(' ');
1181
+
1182
+ // Check SKU matching
1183
+ const matchedSkus = [];
1184
+ for (const sku of fam.skus || []) {
1185
+ const sNum = String(sku.sku || '').toLowerCase();
1186
+ const sModel = String(sku.model || '').toLowerCase();
1187
+ const sName = String(sku.name || '').toLowerCase();
1188
+ // Strip negative clauses like "(Super Mode NOT Supported)" from positive keyword matching
1189
+ const sNameClean = sName
1190
+ .replace(/[(\(][^)\)]*not\s+supported[^)\)]*[)\)]?/gi, '')
1191
+ .replace(/super\s+mode\s+not\s+supported/gi, '')
1192
+ .replace(/\b(?:not\s+supported|unsupported)\b/gi, '');
1193
+ const sAttrs = Object.values(sku.attrs || {}).map((v) => String(v).toLowerCase());
1194
+ const skuIfaces = (sku.specs?.connectivity?.interfaces || sku.interfaces || []).map((i) => String(i).toLowerCase());
1195
+ const skuProtos = (sku.specs?.connectivity?.protocols || sku.protocols || []).map((p) => String(p).toLowerCase());
1196
+ const skuIoPorts = (sku.specs?.io_ports || []).map((p) => String(p.type || '').toLowerCase());
1197
+
1198
+ const skuHaystack = [sNum, sModel, sNameClean, ...sAttrs, ...skuIfaces, ...skuProtos, ...skuIoPorts].join(' ');
1199
+ const isSingleSkuLookup = tokens.length === 1 && (sNum === tokens[0] || sNum === qLower);
1200
+ const skuMatchesAll = tokens.every((t) => skuHaystack.includes(t));
1201
+ const singleTokenHit = tokens.length === 1 && ((sModel && sModel.includes(tokens[0])) || skuHaystack.includes(tokens[0]));
1202
+
1203
+ if (isSingleSkuLookup || skuMatchesAll) {
1204
+ matchedSkus.push(sku);
1205
+ } else if (tokens.length > 1) {
1206
+ const combinedMatchesAll = tokens.every((t) => (famHaystack + ' ' + skuHaystack).includes(t));
1207
+ const nonFamTokens = tokens.filter((t) => !famHaystack.includes(t));
1208
+ if (combinedMatchesAll && (nonFamTokens.length === 0 || nonFamTokens.every((t) => skuHaystack.includes(t)))) {
1209
+ matchedSkus.push(sku);
1210
+ }
1211
+ } else if (singleTokenHit) {
1212
+ matchedSkus.push(sku);
1213
+ }
1214
+ }
1215
+
1216
+ // Require all query tokens to be satisfied either in the family or across member SKUs
1217
+ const famMatchesAll = tokens.every((t) => famHaystack.includes(t));
1218
+ const allTokensSatisfiedInFamily = tokens.every((t) => {
1219
+ return famHaystack.includes(t) || (fam.skus || []).some((s) => {
1220
+ const sNameClean = String(s.name || '')
1221
+ .toLowerCase()
1222
+ .replace(/[(\(][^)\)]*not\s+supported[^)\)]*[)\)]?/gi, '')
1223
+ .replace(/super\s+mode\s+not\s+supported/gi, '');
1224
+ const sIfaces = (s.specs?.connectivity?.interfaces || s.interfaces || []).map((i) => String(i).toLowerCase());
1225
+ const sProtos = (s.specs?.connectivity?.protocols || s.protocols || []).map((p) => String(p).toLowerCase());
1226
+ const sPorts = (s.specs?.io_ports || []).map((p) => String(p.type || '').toLowerCase());
1227
+ return [s.sku, s.model, sNameClean, ...Object.values(s.attrs || {}), ...sIfaces, ...sProtos, ...sPorts].join(' ').toLowerCase().includes(t);
1228
+ });
1229
+ });
1230
+
1231
+ if (!allTokensSatisfiedInFamily) {
1232
+ continue;
1233
+ }
1234
+
1235
+ if (matchedSkus.length > 0 || famMatchesAll) {
1236
+ score = matchedSkus.length * 10 + (famMatchesAll ? 5 : 0);
1237
+ matches.push({
1238
+ family: fam,
1239
+ score,
1240
+ matchedSkus: matchedSkus.length > 0 ? matchedSkus : (fam.skus || []).slice(0, 3),
1241
+ });
1242
+ }
1243
+ }
1244
+
1245
+ matches.sort((a, b) => b.score - a.score);
1246
+
1247
+ const records = matches.slice(0, limit).map(({ family, matchedSkus }) => {
1248
+ return {
1249
+ family_id: family.id,
1250
+ title: family.title?.[locale] || family.title?.zh || family.title?.en || family.id,
1251
+ summary: family.summary || null,
1252
+ default_sku: family.default_sku,
1253
+ interfaces: family.interfaces || [],
1254
+ protocols: family.protocols || [],
1255
+ supported_scenarios: family.supported_scenarios || [],
1256
+ matched_skus: matchedSkus.map((s) => {
1257
+ const specs = s.specs || {};
1258
+ const pwr = specs.power || {};
1259
+ const pkg = specs.package_contents || [];
1260
+ const pkgText = pkg.join(' ').toLowerCase();
1261
+
1262
+ // Require positive evidence of an included operational battery (not just battery_powered capability or RTC battery)
1263
+ const hasIncludedBattery = pkg.some((item) => {
1264
+ if (!/\bbattery\b/i.test(item)) return false;
1265
+ if (/(?:not\s+included|sold\s+separately|cr1220|rtc|coin)/i.test(item)) return false;
1266
+ if (/(?:without|w\/o|no|exclude[ds]?)\s+[^()]*\bbattery/i.test(item)) return false;
1267
+ return true;
1268
+ });
1269
+
1270
+ let adapterIncluded = pwr.adapter_included;
1271
+ if (adapterIncluded === undefined || adapterIncluded === null) {
1272
+ if (s.attrs?.power_adapter === 'not_included' || /w\/?o\s+power/i.test(s.name)) {
1273
+ adapterIncluded = false;
1274
+ } else if (s.attrs?.power_adapter === 'included' || /power\s+adapter/i.test(pkgText)) {
1275
+ adapterIncluded = true;
1276
+ } else {
1277
+ adapterIncluded = hasIncludedBattery ? false : null;
1278
+ }
1279
+ }
1280
+
1281
+ const cordExplicitlyExcluded =
1282
+ s.attrs?.power_adapter === 'included_no_cord' ||
1283
+ s.attrs?.power_cord === 'not_included' ||
1284
+ /without\s+power\s+cord/i.test(pkgText) ||
1285
+ /no\s+power\s+cord/i.test(pkgText) ||
1286
+ /w\/?o\s+power\s+cord/i.test(pkgText) ||
1287
+ /power\s+cord\s+(?:sold\s+separately|not\s+included|optional|excluded)/i.test(pkgText) ||
1288
+ /without\s+power\s+cord/i.test(pwr.adapter_spec || '') ||
1289
+ /without\s+power\s+cord/i.test(s.name || '');
1290
+
1291
+ let powerCordIncluded = null;
1292
+ if (cordExplicitlyExcluded) {
1293
+ powerCordIncluded = false;
1294
+ } else if (s.attrs?.power_cord === 'included') {
1295
+ powerCordIncluded = true;
1296
+ } else if (pkg.length > 0) {
1297
+ const hasExplicitCord = pkg.some((item) => {
1298
+ const itemLower = item.toLowerCase();
1299
+ return /power\s+cord/i.test(itemLower) &&
1300
+ !/(?:without|sold\s+separately|not\s+included|optional|no\s+|w\/o|separately)/i.test(itemLower);
1301
+ });
1302
+ if (hasExplicitCord) {
1303
+ powerCordIncluded = true;
1304
+ } else if (adapterIncluded === true) {
1305
+ const isIntegratedPlug =
1306
+ /universal|wall\s*mount|plug/i.test(pwr.adapter_spec || '') ||
1307
+ /universal\s+power\s+supply/i.test(pkgText) ||
1308
+ /(?:with\s+[^()]*|\b(?:interchangeable|multi-country|global)\s+)plugs?\b/i.test(pkgText);
1309
+ if (isIntegratedPlug) {
1310
+ powerCordIncluded = true;
1311
+ } else {
1312
+ powerCordIncluded = null;
1313
+ }
1314
+ }
1315
+ }
1316
+
1317
+ let outOfTheBoxReady = false;
1318
+ let readinessSummary = '';
1319
+
1320
+ const isDevBoard = /xiao|seeeduino|arduino/i.test(family.id) || /board|development\s+kit/i.test(s.name) || (pwr.input_voltage && /3\.7|5v/i.test(pwr.input_voltage));
1321
+
1322
+ if (hasIncludedBattery) {
1323
+ outOfTheBoxReady = true;
1324
+ readinessSummary = `随附配套电池供电(${pwr.input_connector ? '支持 ' + pwr.input_connector : '支持太阳能/USB充电'}),开箱即可直接上电运行。`;
1325
+ } else if (adapterIncluded === false) {
1326
+ outOfTheBoxReady = false;
1327
+ if (isDevBoard) {
1328
+ readinessSummary = `开发板/套件(仅含主板与配件),需通过 ${pwr.input_connector || 'USB-C'} 供电或自备外接电源/电池。`;
1329
+ } else {
1330
+ readinessSummary = `仅含主机,不随附电源适配器。用户买回去无法直接开机,必须加配 ${pwr.adapter_spec || '合适规格电源适配器'}。`;
1331
+ }
1332
+ } else if (adapterIncluded === true) {
1333
+ if (powerCordIncluded === false) {
1334
+ outOfTheBoxReady = false;
1335
+ readinessSummary = `随附电源适配器(${pwr.adapter_spec || '标配'}),但不随附 AC 电源线,开箱前需自备或加配电源线。`;
1336
+ } else if (powerCordIncluded === true) {
1337
+ outOfTheBoxReady = true;
1338
+ readinessSummary = `随附电源适配器与电源线(或一体式插头),开箱即用。`;
1339
+ } else {
1340
+ outOfTheBoxReady = false;
1341
+ readinessSummary = `随附电源适配器(${pwr.adapter_spec || '标配'}),包装清单未明确电源线随附情况,请核对是否需自备电源线。`;
1342
+ }
1343
+ } else {
1344
+ outOfTheBoxReady = false;
1345
+ readinessSummary = pwr.input_voltage ? `供电规格: ${pwr.input_voltage} ${pwr.input_connector || ''},请核对是否自备电源。` : '电源随附情况未声明。';
1346
+ }
1347
+
1348
+ // Get matching required accessories
1349
+ const requiredAccessories = (family.required_accessories || []).filter((acc) => {
1350
+ if (!acc.applies_when) return true;
1351
+ const attrVal = s.attrs?.[acc.applies_when.attr];
1352
+ const target = acc.applies_when.value;
1353
+ if (Array.isArray(target)) return target.includes(attrVal);
1354
+ return attrVal === target;
1355
+ }).map((acc) => ({
1356
+ id: acc.id,
1357
+ name: acc.name,
1358
+ sku: acc.sku,
1359
+ reason: acc.reason,
1360
+ url: acc.url,
1361
+ }));
1362
+
1363
+ return {
1364
+ sku: s.sku,
1365
+ model: s.model || null,
1366
+ name: s.name,
1367
+ url: appendUtm(s.url, {
1368
+ source: 'sensecraft_agent',
1369
+ medium: 'product_search',
1370
+ campaign: family.id,
1371
+ content: `sku_${s.sku}`,
1372
+ }),
1373
+ package_profile: {
1374
+ power_adapter_included: adapterIncluded,
1375
+ power_cord_included: powerCordIncluded,
1376
+ out_of_the_box_ready: outOfTheBoxReady,
1377
+ readiness_summary: readinessSummary,
1378
+ },
1379
+ required_accessories: requiredAccessories,
1380
+ power: {
1381
+ adapter_included: adapterIncluded,
1382
+ adapter_spec: pwr.adapter_spec || null,
1383
+ input_voltage: pwr.input_voltage || null,
1384
+ input_connector: pwr.input_connector || null,
1385
+ },
1386
+ package_contents: pkg,
1387
+ interfaces: specs.connectivity?.interfaces || family.interfaces || [],
1388
+ io_ports: specs.io_ports || [],
1389
+ expansion_slots: specs.expansion_slots || [],
1390
+ ai_performance: specs.ai_performance || null,
1391
+ };
1392
+ }),
1393
+ };
1394
+ });
1395
+
1396
+ return createCatalogDto({
1397
+ source: 'bundled_manifest',
1398
+ freshness: 'FRESH',
1399
+ records,
1400
+ warnings: records.length === 0 ? [`未找到与 "${query}" 匹配的硬件产品或 SKU。`] : [],
1401
+ });
1402
+ }
1403
+
1105
1404
  /**
1106
1405
  * Queries and verifies accessories for a product family, running physical compatibility checks.
1107
1406
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@seeed-studio/solution-cli",
3
- "version": "0.4.0",
4
- "description": "Installer for the Rust deployment engine and Solution CLI Agent skills (Work Buddy, Claude Code, Codex, CodeBuddy)",
3
+ "version": "0.4.1",
4
+ "description": "Installer for the Rust deployment engine and Solution CLI Agent skills (Doubao Work, WorkBuddy, Claude Code, Codex, CodeBuddy, Hermes)",
5
5
  "type": "module",
6
6
  "main": "./lib/installer.js",
7
7
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "scripts": {
23
23
  "build": "node scripts/bake-client-config.mjs",
24
24
  "prepack": "node scripts/bake-client-config.mjs",
25
- "test": "node --test tests/cli.test.mjs",
25
+ "test": "node --test",
26
26
  "install:self": "node bin/cli.js install",
27
27
  "status": "node bin/cli.js status",
28
28
  "doctor": "node bin/cli.js doctor"
@@ -36,6 +36,7 @@
36
36
  "claude-code",
37
37
  "codex",
38
38
  "codebuddy",
39
+ "hermes",
39
40
  "skills",
40
41
  "edge-ai",
41
42
  "jetson",
@@ -64,14 +64,24 @@ path is usable only when the matching signed local content is present; otherwise
64
64
  report the source asset as unavailable instead of treating the URL as an image that
65
65
  has been reviewed.
66
66
 
67
- ## Workflow Routing & Progressive Disclosure
67
+ ## Workflow Routing & Intent Classification
68
+
69
+ ```mermaid
70
+ flowchart TD
71
+ User["用户提问: 'J4012 Super 包含电源吗?买回去能直接用吗?'"] --> AgentIntent{"意图识别 (SKILL.md)"}
72
+ AgentIntent -- "问行业/业务方案 (跌倒/农业/质检/选型)" --> HubSearch["hub search '<方案关键词>'"]
73
+ AgentIntent -- "问硬件单品/型号/包装清单/电源/接口/能干嘛" --> ProdSearch["product search '<型号/SKU>'"]
74
+ HubSearch -- "误把硬件当方案搜" --> Fallback["Fallback: 检测到硬件型号,引导使用 product search"]
75
+ ProdSearch --> ClearOutput["直出: 电源随附情况 + 开箱即用判定 + 必选配件 + 接口 + 落地场景"]
76
+ ```
68
77
 
69
- 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:
78
+ Do NOT read all reference files at once. Determine the user's workflow state and intent, then execute the corresponding command and load **at most ONE** reference file on demand:
70
79
 
71
80
  | User State & Intent | Reference to Load | Primary CLI Tool to Call | Guardrails & Forbidden Actions |
72
81
  |---|---|---|---|
73
- | **DISCOVERY / CONSULT**<br>“我想做防跌倒监控”、“有智慧农场方案吗” | `references/consult-and-select.md` | `solution-cli hub search "<keyword>" --json`, then `hub get <id> --sections architecture,playbook --json` | **DO NOT run deploy**; search exposes `available_sections` and evidence status (`documented`/`generic`/`inferred`). Batch fetch architecture and playbook in 1 roundtrip. |
74
- | **PROCUREMENT / VERIFY_BOM**<br>“买什么硬件?”、“J4012 需要配什么配件?” | `references/hardware-bom.md` | `solution-cli product accessories <id> [--category <power,camera>] --compact --json` | **DO NOT fabricate** third-party SKUs or prices. Always distinguish REQUIRED vs OPTIONAL accessories. Use `--compact` and `--category` to save tokens. |
82
+ | **HARDWARE_SPEC / BOX_CONTENTS / POWER_READINESS**<br>“J4012 Super 包含电源吗?”、“盒子里有什么”、“带不带电源”、“买回去能否直接用/能否开箱即用”、“发货清单”、“有哪些接口”、“能干嘛” | `references/hardware-bom.md` | `solution-cli product search "<型号/SKU/关键词>" --json` | **STRICT HARD REDLINE**: 严禁凭常识臆测硬件包材(如擅自推测 Super 版本带电源)或谎称没有清单。包装随附情况、电源线适配器状态、开箱即用性必须严格以 `product search` 返回的 `package_profile`、`package_contents` 和 `interfaces` 为准!回答“设备能干嘛”直接使用返回的 `supported_scenarios`。 |
83
+ | **DISCOVERY / CONSULT**<br>“我想做防跌倒监控”、“有智慧农场方案吗”、“怎么做园区安防” | `references/consult-and-select.md` | `solution-cli hub search "<keyword>" --json`, then `hub get <id> --sections architecture,playbook --json` | **DO NOT run deploy**; search exposes `available_sections` and evidence status (`documented`/`generic`/`inferred`). 若返回 warning 提示包含硬件关键词,立即按提示切换至 `product search`。 |
84
+ | **PROCUREMENT / VERIFY_BOM**<br>“买什么硬件?”、“J4012 需要配什么配件?”、“推荐镜头/天线” | `references/hardware-bom.md` | `solution-cli product accessories <id> [--category <power,camera>] --compact --json` | **DO NOT fabricate** third-party SKUs or prices. Always distinguish REQUIRED vs OPTIONAL accessories. Use `--compact` and `--category` to save tokens. |
75
85
  | **ONBOARDING / PRECHECK**<br>“设备到了,插上了网线/USB”、“环境自检”、“发现新设备” | `references/diagnose.md` & `references/device-management.md#0` | `solution-cli doctor` / `sensecraft-engine device scan` | **Early & Proactive**: Check device firmware & drivers early. Intercept and prompt for reCamera firmware/patch or SBC env fixes before deploy. |
76
86
  | **DEPLOYING / APPLY**<br>“开始安装”、“部署 fall_detection” | `references/deploy.md` | `solution-cli hub get <id> --section deployment --preset <id> --json` then `sensecraft-engine deploy <id>` | **DEPLOYMENT CRITICAL SECTION**: Zero external commercial queries allowed during apply. Filter guide with `--preset` to reduce token bloat. |
77
87
  | **INTEGRATION / INGESTION**<br>“怎么接到我们系统”、“MQTT主题是什么”、“Webhook格式”、“接入 Home Assistant” | `references/device-management.md#5` | `solution-cli hub get <id> --section integration --json` | **System Ingestion**: Provide exact MQTT topics, payload schemas, Webhook endpoints, and port mappings directly from verified integration contract. Batch with `--sections architecture,integration` if needed. |