@harrylabsj/kiwi 0.7.16 → 0.7.18

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/cli.js CHANGED
@@ -27,7 +27,9 @@
27
27
  * kiwi init|up|status|logs|down --dir <instance-dir>
28
28
  * Managed-local product lifecycle.
29
29
  */
30
- import { existsSync, realpathSync } from "node:fs";
30
+ import { spawn, spawnSync } from "node:child_process";
31
+ import { existsSync, mkdirSync, realpathSync, writeFileSync } from "node:fs";
32
+ import { homedir, userInfo } from "node:os";
31
33
  import path from "node:path";
32
34
  import { fileURLToPath } from "node:url";
33
35
  import { startA2aNode } from "./a2a/node.js";
@@ -55,11 +57,12 @@ import { parseLogLines, runLogs } from "./supervisor/logs.js";
55
57
  import { StackConfigError } from "./supervisor/stack-config.js";
56
58
  import { cmdProductDoctor, productHelp, PRODUCT_VERSION, DEFAULT_CATALOG_URL, DEFAULT_PROFILE_PATH, } from "./product-cli.js";
57
59
  import { cmdWeixin, weixinUsage } from "./weixin/cli-weixin.js";
58
- import { merchantInit, slugifyMerchantId } from "./product-init.js";
60
+ import { DEFAULT_SHOPPING_DB_PATH, loadMerchantCredentials, merchantInit, slugifyMerchantId } from "./product-init.js";
59
61
  import readline from "node:readline";
60
62
  import { buyerInit, buyerSearch, buyerTasks } from "./product-buyer.js";
61
63
  import { merchantPublish } from "./product-publish.js";
62
64
  import { catalogServe } from "./product-catalog.js";
65
+ import { extractPublicDomain, runMerchantSetupPublic, SetupPublicError, validatePublicDomain } from "./product-setup-public.js";
63
66
  const USAGE = `kiwi ${PRODUCT_VERSION} — commerce negotiation agent runtime
64
67
 
65
68
  Usage:
@@ -93,6 +96,8 @@ Usage:
93
96
  Product layer (product-strategy rev1.1 §10/§19):
94
97
  kiwi buyer --help Kiwi Buyer command tree
95
98
  kiwi merchant --help Kiwi Merchant command tree
99
+ kiwi setup-hermes One-click: wire kiwi-buyer-mcp into Hermes
100
+ (hermes mcp add + skills install)
96
101
  kiwi network --help Kiwi Network command tree
97
102
  kiwi demo [a|b] One-command local multi-merchant demo
98
103
  (Issue 13; 1 buyer + catalog + 3 merchants
@@ -155,6 +160,10 @@ export function parseArgs(argv) {
155
160
  let relogin = false;
156
161
  let qrScale = 1;
157
162
  let noQr = false;
163
+ let domain;
164
+ let check = false;
165
+ let caddyfile;
166
+ let file;
158
167
  for (let i = 0; i < argv.length; i++) {
159
168
  const arg = argv[i];
160
169
  if (arg === "--profile") {
@@ -268,6 +277,18 @@ export function parseArgs(argv) {
268
277
  else if (arg === "--no-qr") {
269
278
  noQr = true;
270
279
  }
280
+ else if (arg === "--domain") {
281
+ domain = argv[++i];
282
+ }
283
+ else if (arg === "--check") {
284
+ check = true;
285
+ }
286
+ else if (arg === "--caddyfile") {
287
+ caddyfile = argv[++i];
288
+ }
289
+ else if (arg === "--file") {
290
+ file = argv[++i];
291
+ }
271
292
  else if (arg !== undefined && arg.startsWith("--profile=")) {
272
293
  profile = arg.slice("--profile=".length);
273
294
  }
@@ -289,6 +310,15 @@ export function parseArgs(argv) {
289
310
  else if (arg !== undefined && arg.startsWith("--data-dir=")) {
290
311
  dataDir = arg.slice("--data-dir=".length);
291
312
  }
313
+ else if (arg !== undefined && arg.startsWith("--domain=")) {
314
+ domain = arg.slice("--domain=".length);
315
+ }
316
+ else if (arg !== undefined && arg.startsWith("--caddyfile=")) {
317
+ caddyfile = arg.slice("--caddyfile=".length);
318
+ }
319
+ else if (arg !== undefined && arg.startsWith("--file=")) {
320
+ file = arg.slice("--file=".length);
321
+ }
292
322
  else if (arg !== undefined && !arg.startsWith("-")) {
293
323
  command.push(arg);
294
324
  }
@@ -311,6 +341,7 @@ export function parseArgs(argv) {
311
341
  relogin,
312
342
  qrScale,
313
343
  noQr,
344
+ check,
314
345
  };
315
346
  if (profile !== undefined)
316
347
  out.profile = profile;
@@ -354,6 +385,12 @@ export function parseArgs(argv) {
354
385
  out.port = port;
355
386
  if (catalogDb !== undefined)
356
387
  out.catalogDb = catalogDb;
388
+ if (domain !== undefined)
389
+ out.domain = domain;
390
+ if (caddyfile !== undefined)
391
+ out.caddyfile = caddyfile;
392
+ if (file !== undefined)
393
+ out.file = file;
357
394
  if (catalogHost !== undefined)
358
395
  out.catalogHost = catalogHost;
359
396
  return out;
@@ -370,6 +407,20 @@ function requireProfile(args) {
370
407
  }
371
408
  return loadProfile(args.profile);
372
409
  }
410
+ /**
411
+ * 读取 profile:`--profile` 优先;缺省回退 `KIWI_DEFAULT_PROFILE` env 或
412
+ * `DEFAULT_PROFILE_PATH`(`kiwi merchant init` 写入的默认路径)。文件不存在 →
413
+ * fail-closed 提示先 init。供商家命令(start/publish/setup-public)参数化使用,
414
+ * 让第 4-5 步不带 `--profile`。
415
+ */
416
+ function requireProfileOrDefault(args) {
417
+ if (args.profile)
418
+ return loadProfile(args.profile);
419
+ const fallback = process.env.KIWI_DEFAULT_PROFILE ?? DEFAULT_PROFILE_PATH;
420
+ if (existsSync(fallback))
421
+ return loadProfile(fallback);
422
+ throw new ProfileError(`未找到商家配置(${fallback})——先运行 kiwi merchant init,或用 --profile <file> 指定`);
423
+ }
373
424
  function printReportJsonl(report) {
374
425
  process.stdout.write(`${JSON.stringify(report)}\n`);
375
426
  }
@@ -555,11 +606,12 @@ export function resolveServeDataDir(dataDir, agentId) {
555
606
  return dataDir ?? path.resolve(".kiwi", "agents", agentId);
556
607
  }
557
608
  async function cmdAgentServe(args) {
558
- const profile = requireProfile(args);
609
+ const profile = requireProfileOrDefault(args);
559
610
  if (profile.role !== "merchant") {
560
611
  process.stderr.write("kiwi agent serve 需要 merchant profile(role: merchant)\n");
561
612
  return EXIT.CONFIG;
562
613
  }
614
+ loadMerchantCredentials(); // 加载 init 写入的 credentials.env(KIWI_MERCHANT_TOKEN)
563
615
  const catalog = args.catalog ?? process.env.KIWI_CATALOG_URL ?? DEFAULT_CATALOG_URL;
564
616
  const merchantToken = process.env.KIWI_MERCHANT_TOKEN || "";
565
617
  // 审查 P1-09:serve 的初始 A2A 节点必须用稳定 dataDir——此前初始节点缺省走
@@ -567,11 +619,13 @@ async function cmdAgentServe(args) {
567
619
  // negotiation 可重开、已发 conditional offer 返回 offer_unknown。稳定目录
568
620
  // 让重启可恢复状态(resolveServeDataDir 见上)。
569
621
  const serveDataDir = resolveServeDataDir(args.dataDir, profile.agent_id);
622
+ const profilePublicUrl = profile.merchant_public?.public_url;
570
623
  let node = await startA2aNode({
571
624
  profile,
572
625
  catalog,
573
- preferredPort: args.port,
626
+ preferredPort: args.port ?? profile.merchant_public?.a2a_port,
574
627
  dataDir: serveDataDir,
628
+ ...(profilePublicUrl ? { publicBaseUrl: `https://${profilePublicUrl}` } : {}),
575
629
  ...(merchantToken ? { ownerToken: merchantToken } : {}),
576
630
  ownerTokenSecret: process.env.KIWI_CATALOG_OWNER_TOKEN_SECRET,
577
631
  });
@@ -824,6 +878,9 @@ async function cmdBuyerInit(args) {
824
878
  ...(args.force ? { force: true } : {}),
825
879
  });
826
880
  printJson(report);
881
+ if (report.ok) {
882
+ process.stdout.write("✓ 商家配置完成。下一步:`kiwi merchant up` 上线(需先装 Caddy、DNS 指向服务器);或 `kiwi merchant setup-public` 查看公网配置。\n");
883
+ }
827
884
  return report.ok ? EXIT.OK : EXIT.CONFIG;
828
885
  }
829
886
  /** `kiwi buyer search`(D4):Product-first 搜索(M3 链路)。 */
@@ -890,6 +947,10 @@ async function routeMerchant(sub, args) {
890
947
  return await cmdMerchantInit(args);
891
948
  if (sub === "publish")
892
949
  return await cmdMerchantPublish(args);
950
+ if (sub === "setup-public")
951
+ return await cmdMerchantSetupPublic(args);
952
+ if (sub === "up")
953
+ return await cmdMerchantUp(args);
893
954
  if (sub === "listings")
894
955
  return notImplementedProduct("kiwi merchant listings", "D2");
895
956
  if (sub === "status")
@@ -921,9 +982,15 @@ async function cmdMerchantInit(args) {
921
982
  // --merchant-id / --name flag 优先,env 回退;TTY 下交互提示,缺省自动派生。
922
983
  let merchantId = args.merchantId ?? process.env.KIWI_MERCHANT_ID ?? "";
923
984
  let name = args.merchantName ?? process.env.KIWI_MERCHANT_NAME ?? "";
985
+ let publicUrl = "";
986
+ let merchantToken = "";
924
987
  if (process.stdin.isTTY) {
925
988
  merchantId = await promptLine(`merchant_id(回车自动生成${name !== "" ? `,建议 ${slugifyMerchantId(name)}` : ""}): `, merchantId);
926
989
  name = await promptLine(`商家名称(回车用缺省): `, name);
990
+ publicUrl = await promptLine("公网域名(可选,回车跳过;如 merchant.example.com): ", "");
991
+ if (publicUrl !== "")
992
+ publicUrl = publicUrl.trim().toLowerCase();
993
+ merchantToken = await promptLine("商家令牌(从商家后台获取,可稍后设置;直接回车跳过): ", "");
927
994
  // TTY 下可全回车:merchant_id 自动生成(避免固定名碰撞)。
928
995
  if (merchantId === "")
929
996
  merchantId = `merchant-${Math.random().toString(36).slice(2, 8)}`;
@@ -945,6 +1012,8 @@ async function cmdMerchantInit(args) {
945
1012
  const report = await merchantInit({
946
1013
  merchantName: name,
947
1014
  ...(merchantId !== "" ? { merchantId } : {}),
1015
+ ...(publicUrl !== "" ? { publicUrl } : {}),
1016
+ ...(merchantToken !== "" ? { merchantToken } : {}),
948
1017
  shoppingCliUrl: process.env.SHOPPING_CLI_URL ?? "http://127.0.0.1:8765",
949
1018
  shoppingCliDb: process.env.SHOPPING_DB_PATH,
950
1019
  catalogUrl: args.catalog ?? process.env.KIWI_CATALOG_URL ?? DEFAULT_CATALOG_URL,
@@ -965,27 +1034,35 @@ async function cmdMerchantInit(args) {
965
1034
  * fail-closed:任一步失败 → 非零退出 + 分步报告,不假装全成功。
966
1035
  */
967
1036
  async function cmdMerchantPublish(args) {
968
- const profile = requireProfile(args);
1037
+ const profile = requireProfileOrDefault(args);
969
1038
  if (profile.role !== "merchant") {
970
1039
  process.stderr.write("kiwi merchant publish 需要 merchant profile(role: merchant)\n");
971
1040
  return EXIT.CONFIG;
972
1041
  }
973
- const shoppingCliDb = args.shoppingCliDb ?? process.env.SHOPPING_DB_PATH;
974
- if (!shoppingCliDb) {
975
- process.stderr.write("--shopping-cli-db <path>(或 SHOPPING_DB_PATH)是必需的:shopping-cli SQLite 数据库路径\n");
976
- return EXIT.CONFIG;
1042
+ loadMerchantCredentials(); // 加载 init 写入的 credentials.env(KIWI_MERCHANT_TOKEN)
1043
+ // 缺省路径与 shopping-cli 的 DEFAULT_DB_PATH 一致——商家无需设置 SHOPPING_DB_PATH。
1044
+ const shoppingCliDb = args.shoppingCliDb ?? process.env.SHOPPING_DB_PATH ?? DEFAULT_SHOPPING_DB_PATH;
1045
+ // `--file <csv>`:先导入商品(shopping-cli import-csv-excel)再发布。
1046
+ if (args.file !== undefined && args.file !== "") {
1047
+ const imp = spawnSync("shopping-cli", ["import-csv-excel", "--file", args.file, "--merchant", profile.agent_id, "--format", "json"], { env: { ...process.env, SHOPPING_DB_PATH: shoppingCliDb }, stdio: "inherit" });
1048
+ if (imp.status !== 0) {
1049
+ process.stderr.write(`商品导入失败(shopping-cli import-csv-excel 退出码 ${imp.status ?? "?"})——请检查 CSV 格式\n`);
1050
+ return EXIT.CONFIG;
1051
+ }
1052
+ process.stdout.write(`已导入商品:${args.file}\n`);
977
1053
  }
978
1054
  if (!existsSync(shoppingCliDb)) {
979
- process.stderr.write(`shopping-cli 数据库不存在:${shoppingCliDb}\n`);
1055
+ process.stderr.write(`shopping-cli 数据库不存在:${shoppingCliDb}(先运行 shopping-cli import-csv-excel 导入商品)\n`);
980
1056
  return EXIT.CONFIG;
981
1057
  }
982
1058
  const ownerTokenSecret = process.env.KIWI_CATALOG_OWNER_TOKEN_SECRET;
983
- const merchantToken = process.env.KIWI_MERCHANT_TOKEN || "";
1059
+ const merchantTokenEnv = profile.merchant_public?.merchant_token_env;
1060
+ const merchantToken = (merchantTokenEnv ? process.env[merchantTokenEnv] : undefined) || process.env.KIWI_MERCHANT_TOKEN || "";
984
1061
  if (!merchantToken && !ownerTokenSecret) {
985
1062
  process.stderr.write("需要 KIWI_MERCHANT_TOKEN(随机 token,推荐)或 KIWI_CATALOG_OWNER_TOKEN_SECRET(legacy HMAC)\n");
986
1063
  return EXIT.CONFIG;
987
1064
  }
988
- const catalog = args.catalog ?? process.env.KIWI_CATALOG_URL ?? DEFAULT_CATALOG_URL;
1065
+ const catalog = args.catalog ?? profile.merchant_public?.catalog_url ?? process.env.KIWI_CATALOG_URL ?? DEFAULT_CATALOG_URL;
989
1066
  const report = await merchantPublish({
990
1067
  profile,
991
1068
  catalogBaseUrl: catalog,
@@ -1033,6 +1110,225 @@ async function cmdDemo(args) {
1033
1110
  return EXIT.TRANSIENT;
1034
1111
  }
1035
1112
  }
1113
+ /**
1114
+ * `kiwi merchant setup-public`(D3)——公网 A2A 暴露引导向导。
1115
+ * 检测公网 IP、检查域名 DNS、生成 Caddy 反代配置、输出启动与验证命令。
1116
+ * well-known 文件由跑着的 `kiwi merchant start` 节点自动生成,商家无需手写。
1117
+ */
1118
+ async function cmdMerchantSetupPublic(args) {
1119
+ const profile = requireProfileOrDefault(args); // 无 --profile 时回退缺省 ~/.kiwi/kiwi.yaml
1120
+ const port = args.port ?? profile.merchant_public?.a2a_port ?? (Number(process.env.KIWI_A2A_PORT ?? "") || 9000); // 与 startA2aNode 缺省一致
1121
+ // 单一来源:--domain 显式 > profile.merchant_public.public_url(init 引导写入)>
1122
+ // KIWI_A2A_PUBLIC_URL env 提取;TTY 下再交互提示。
1123
+ let domain = args.domain ?? profile.merchant_public?.public_url ?? extractPublicDomain(process.env.KIWI_A2A_PUBLIC_URL) ?? "";
1124
+ if (domain === "" && process.stdin.isTTY) {
1125
+ domain = await promptLine("公网域名(如 merchant.example.com,TLS 证书就用它): ", "");
1126
+ }
1127
+ if (domain === "") {
1128
+ process.stderr.write("--domain <公网域名> 必填(TTY 下可交互输入)。例:kiwi merchant setup-public --domain merchant.example.com\n");
1129
+ return EXIT.CONFIG;
1130
+ }
1131
+ let normalized;
1132
+ try {
1133
+ normalized = validatePublicDomain(domain);
1134
+ }
1135
+ catch (err) {
1136
+ if (err instanceof SetupPublicError) {
1137
+ process.stderr.write(`${err.message}\n`);
1138
+ return EXIT.CONFIG;
1139
+ }
1140
+ throw err;
1141
+ }
1142
+ const caddyfilePath = args.caddyfile ?? "Caddyfile.kiwi";
1143
+ try {
1144
+ const report = await runMerchantSetupPublic({
1145
+ domain: normalized,
1146
+ port,
1147
+ caddyfilePath,
1148
+ merchantAgentId: profile.agent_id,
1149
+ profilePath: args.profile,
1150
+ checkNow: args.check,
1151
+ });
1152
+ process.stdout.write(`merchant ${report.merchantAgentId} · 端口 ${report.port} · 域名 ${report.domain}\n`);
1153
+ process.stdout.write(`公网 IP:${report.publicIp ?? "(未能自动检测,请手动确认)"}\n`);
1154
+ const dnsMsg = {
1155
+ ok: `DNS 已指向本机(${report.dns.resolved})`,
1156
+ mismatch: `DNS 解析到 ${report.dns.resolved},与公网 IP ${report.dns.expected} 不一致——请到域名服务商把 ${report.domain} 的 A 记录指向 ${report.dns.expected}`,
1157
+ unresolved: `域名 ${report.domain} 当前无法解析——请先到域名服务商添加 A 记录指向服务器公网 IP`,
1158
+ skipped: `无法对比 DNS(未检测到公网 IP),请手动确认 ${report.domain} 的 A 记录指向本机`,
1159
+ };
1160
+ process.stdout.write(`DNS:${dnsMsg[report.dns.status]}\n`);
1161
+ process.stdout.write(`已写入 ${report.caddyfilePath}:\n${report.caddyfile}`);
1162
+ for (const line of report.instructions)
1163
+ process.stdout.write(`${line}\n`);
1164
+ if (report.check !== null) {
1165
+ const status = report.check.httpStatus;
1166
+ process.stdout.write(`\n--check:${status === 200 ? "✓" : status === null ? "✗ 无法连接" : `✗ HTTP ${status}`} ${report.check.url}\n`);
1167
+ }
1168
+ return EXIT.OK;
1169
+ }
1170
+ catch (err) {
1171
+ if (err instanceof SetupPublicError) {
1172
+ process.stderr.write(`${err.message}\n`);
1173
+ return EXIT.CONFIG;
1174
+ }
1175
+ throw err;
1176
+ }
1177
+ }
1178
+ /**
1179
+ * `kiwi merchant up`(D3)——一条命令上线:setup-public(生成 Caddyfile)→ 起 Caddy
1180
+ * 反代(子进程)→ 起 A2A 节点 → 退出时清理 Caddy。把第 4 步三命令合成一个。
1181
+ */
1182
+ async function cmdMerchantUp(args) {
1183
+ const profile = requireProfileOrDefault(args);
1184
+ if (profile.role !== "merchant") {
1185
+ process.stderr.write("kiwi merchant up 需要 merchant profile(role: merchant)\n");
1186
+ return EXIT.CONFIG;
1187
+ }
1188
+ loadMerchantCredentials(); // 加载 init 写入的 credentials.env(KIWI_MERCHANT_TOKEN)
1189
+ // ── 域名 / 端口 / Caddyfile(profile 兜底)──
1190
+ const domain = args.domain ?? profile.merchant_public?.public_url ?? extractPublicDomain(process.env.KIWI_A2A_PUBLIC_URL) ?? "";
1191
+ if (domain === "") {
1192
+ process.stderr.write("需要公网域名——先 `kiwi merchant init` 填公网域名,或 --domain <域名>\n");
1193
+ return EXIT.CONFIG;
1194
+ }
1195
+ const port = args.port ?? profile.merchant_public?.a2a_port ?? (Number(process.env.KIWI_A2A_PORT ?? "") || 9000);
1196
+ const caddyfilePath = args.caddyfile ?? "Caddyfile.kiwi";
1197
+ // ── setup-public:检测 IP / DNS 检查 / 生成 Caddyfile(幂等)──
1198
+ await runMerchantSetupPublic({
1199
+ domain,
1200
+ port,
1201
+ caddyfilePath,
1202
+ merchantAgentId: profile.agent_id,
1203
+ profilePath: args.profile,
1204
+ });
1205
+ process.stdout.write(`[merchant up] 域名 ${domain} · 端口 ${port} · Caddyfile ${caddyfilePath}\n`);
1206
+ // ── 检查 Caddy 是否安装(fail-closed)──
1207
+ const caddyOk = spawnSync("caddy", ["version"], { stdio: "ignore" }).status === 0;
1208
+ if (!caddyOk) {
1209
+ process.stderr.write("未检测到 Caddy(反代 + TLS)。请先安装:`brew install caddy`(或 https://caddyserver.com/download),然后重试。\n");
1210
+ return EXIT.CONFIG;
1211
+ }
1212
+ // ── 起 Caddy 反代(子进程,日志透传)──
1213
+ const caddy = spawn("caddy", ["run", "--config", caddyfilePath], { stdio: "inherit" });
1214
+ // ── 起 A2A 节点(与 merchant start --no-chat 同一路径)──
1215
+ const catalog = args.catalog ?? profile.merchant_public?.catalog_url ?? process.env.KIWI_CATALOG_URL ?? DEFAULT_CATALOG_URL;
1216
+ const merchantToken = process.env.KIWI_MERCHANT_TOKEN || "";
1217
+ const serveDataDir = resolveServeDataDir(args.dataDir, profile.agent_id);
1218
+ let node = await startA2aNode({
1219
+ profile,
1220
+ catalog,
1221
+ preferredPort: port,
1222
+ dataDir: serveDataDir,
1223
+ ...(domain ? { publicBaseUrl: `https://${domain}` } : {}),
1224
+ ...(merchantToken ? { ownerToken: merchantToken } : {}),
1225
+ ownerTokenSecret: process.env.KIWI_CATALOG_OWNER_TOKEN_SECRET,
1226
+ });
1227
+ process.stdout.write(`[merchant up] A2A server: ${node.agentCardUrl}(local ${node.url})· catalog: ${node.catalogAgentId ?? "?"}\n`);
1228
+ process.stdout.write("[merchant up] 已上线 — Ctrl+C to stop(会同时停掉 Caddy 与节点)\n");
1229
+ const shutdown = async () => {
1230
+ await node?.stop().catch(() => undefined);
1231
+ node = null;
1232
+ caddy.kill(); // SIGTERM → Caddy 优雅退出
1233
+ process.exit(0);
1234
+ };
1235
+ process.on("SIGINT", shutdown);
1236
+ process.on("SIGTERM", shutdown);
1237
+ await new Promise(() => { });
1238
+ return EXIT.OK;
1239
+ }
1240
+ /**
1241
+ * `kiwi setup-hermes`(买家侧)——一键把 Kiwi 买家连接器接入 Hermes:
1242
+ * 配置 kiwi-buyer-mcp MCP server + 安装 kiwi-buyer skill。之后 Hermes 重启
1243
+ * 即可调用 9 个采购工具。等价于手动执行:
1244
+ * hermes mcp add kiwi-buyer-mcp --command node --args <cli> mcp serve ...
1245
+ * hermes skills install <SKILL.md URL>
1246
+ */
1247
+ async function cmdSetupHermes() {
1248
+ // 1. 检查 Hermes CLI
1249
+ const hermesOk = spawnSync("hermes", ["--version"], { stdio: "ignore" }).status === 0;
1250
+ if (!hermesOk) {
1251
+ process.stderr.write("未检测到 Hermes CLI(hermes)——请先安装 Hermes,再运行本命令。\n");
1252
+ return EXIT.CONFIG;
1253
+ }
1254
+ // 2. 本机 kiwi cli 路径(dist/cli.js)、principal、db 路径、skill 落盘路径
1255
+ const cliPath = fileURLToPath(import.meta.url);
1256
+ let username = "";
1257
+ try {
1258
+ username = userInfo().username;
1259
+ }
1260
+ catch {
1261
+ username = process.env.USER ?? "user";
1262
+ }
1263
+ const dbPath = path.join(homedir(), ".kiwi", "buyer.sqlite");
1264
+ const skillUrl = "https://raw.githubusercontent.com/harrylabsj/kiwi/main/integrations/hosts/hermes/SKILL.md";
1265
+ const skillPath = path.join(homedir(), ".hermes", "skills", "kiwi-buyer", "SKILL.md");
1266
+ const configured = [];
1267
+ const already = [];
1268
+ // 3. MCP server:已存在则跳过,否则添加(避免 overwrite 交互提示)
1269
+ const mcpList = spawnSync("hermes", ["mcp", "list"], { encoding: "utf-8" });
1270
+ const mcpExists = (mcpList.stdout ?? "").includes("kiwi-buyer-mcp");
1271
+ if (mcpExists) {
1272
+ already.push("MCP server kiwi-buyer-mcp");
1273
+ }
1274
+ else {
1275
+ process.stdout.write("[setup-hermes] 配置 kiwi-buyer-mcp MCP server ...\n");
1276
+ const mcp = spawnSync("hermes", [
1277
+ "mcp",
1278
+ "add",
1279
+ "kiwi-buyer-mcp",
1280
+ "--command",
1281
+ "node",
1282
+ "--args",
1283
+ cliPath,
1284
+ "mcp",
1285
+ "serve",
1286
+ "--db",
1287
+ dbPath,
1288
+ "--principal",
1289
+ `hermes:${username}`,
1290
+ "--agent",
1291
+ "buyer-agent:hermes",
1292
+ ], { stdio: "inherit" });
1293
+ if (mcp.status !== 0) {
1294
+ process.stderr.write(`hermes mcp add 失败(退出码 ${mcp.status ?? "?"})\n`);
1295
+ return EXIT.CONFIG;
1296
+ }
1297
+ configured.push("MCP server kiwi-buyer-mcp");
1298
+ }
1299
+ // 4. kiwi-buyer skill:已存在则跳过;否则 hermes install(失败则直接 fetch 落盘兜底)
1300
+ if (existsSync(skillPath)) {
1301
+ already.push("skill kiwi-buyer");
1302
+ }
1303
+ else {
1304
+ process.stdout.write("[setup-hermes] 安装 kiwi-buyer skill ...\n");
1305
+ const skill = spawnSync("hermes", ["skills", "install", skillUrl, "--yes"], { stdio: "inherit" });
1306
+ if (skill.status !== 0 || !existsSync(skillPath)) {
1307
+ // 兜底:直接 fetch SKILL.md 写入 ~/.hermes/skills/kiwi-buyer/(Hermes skills 为 local 存储)
1308
+ process.stdout.write("[setup-hermes] hermes skills install 不可用,改用直接写入 ...\n");
1309
+ try {
1310
+ const res = await fetch(skillUrl, { signal: AbortSignal.timeout(15000) });
1311
+ if (!res.ok)
1312
+ throw new Error(`HTTP ${res.status}`);
1313
+ const content = await res.text();
1314
+ mkdirSync(path.dirname(skillPath), { recursive: true, mode: 0o700 });
1315
+ writeFileSync(skillPath, content, { mode: 0o600 });
1316
+ }
1317
+ catch (err) {
1318
+ process.stderr.write(`安装 kiwi-buyer skill 失败:${err instanceof Error ? err.message : String(err)}\n`);
1319
+ return EXIT.CONFIG;
1320
+ }
1321
+ }
1322
+ configured.push("skill kiwi-buyer");
1323
+ }
1324
+ // 5. 报告
1325
+ for (const item of already)
1326
+ process.stdout.write(` ✓ ${item} 已配置,跳过\n`);
1327
+ for (const item of configured)
1328
+ process.stdout.write(` ✓ ${item} 已配置\n`);
1329
+ process.stdout.write("✓ Kiwi 已接入 Hermes。请重启 Hermes(或开新会话),然后直接说出采购需求(如「帮我买 200 个 USB-C 扩展坞,7 天内到」)。\n");
1330
+ return EXIT.OK;
1331
+ }
1036
1332
  function notImplementedProduct(name, target) {
1037
1333
  process.stderr.write(`${name} 尚未实现(产品层完成定义 ${target},见 docs/kiwi-product-layer-refactor-rev1.1.md §19)。\n`);
1038
1334
  return EXIT.CONFIG;
@@ -1087,6 +1383,8 @@ export async function main(argv = process.argv.slice(2)) {
1087
1383
  return await routeBuyer(sub, args);
1088
1384
  if (cmd === "merchant")
1089
1385
  return await routeMerchant(sub, args);
1386
+ if (cmd === "setup-hermes")
1387
+ return await cmdSetupHermes();
1090
1388
  if (cmd === "network")
1091
1389
  return await routeNetwork(sub);
1092
1390
  if (cmd === "demo")