u1s1-cli 1.11.8 → 1.12.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.
Files changed (45) hide show
  1. package/dist/agent-setup.d.ts +5 -0
  2. package/dist/agent-setup.js +31 -7
  3. package/dist/api.js +42 -34
  4. package/dist/bench-report.js +12 -9
  5. package/dist/bench.js +34 -31
  6. package/dist/brand.d.ts +2 -1
  7. package/dist/brand.js +38 -12
  8. package/dist/config.d.ts +11 -1
  9. package/dist/config.js +25 -8
  10. package/dist/context-usage.js +12 -9
  11. package/dist/deploy.js +49 -45
  12. package/dist/device-auth.js +7 -5
  13. package/dist/error-humanize.d.ts +1 -0
  14. package/dist/error-humanize.js +18 -6
  15. package/dist/feedback.js +48 -31
  16. package/dist/i18n.d.ts +21 -0
  17. package/dist/i18n.js +46 -0
  18. package/dist/import/index.js +64 -28
  19. package/dist/import/skills.js +72 -41
  20. package/dist/import/util.js +3 -2
  21. package/dist/import/write.js +2 -1
  22. package/dist/index.js +139 -48
  23. package/dist/login.js +39 -28
  24. package/dist/loop.js +18 -12
  25. package/dist/mcp/client.js +15 -14
  26. package/dist/mcp/command.js +76 -48
  27. package/dist/mcp/extension.js +5 -3
  28. package/dist/mcp/tools.js +6 -5
  29. package/dist/model.js +11 -10
  30. package/dist/nudges.d.ts +8 -2
  31. package/dist/nudges.js +22 -3
  32. package/dist/search-tools.js +7 -6
  33. package/dist/shell-doctor.js +9 -8
  34. package/dist/subagent.js +5 -4
  35. package/dist/tools.js +53 -37
  36. package/dist/update.js +29 -28
  37. package/dist/usage.d.ts +1 -1
  38. package/dist/usage.js +96 -43
  39. package/dist/web.js +2 -1
  40. package/dist/workflow/runner.js +33 -29
  41. package/dist/workflow/templates.js +70 -45
  42. package/dist/workflow/tool.js +15 -12
  43. package/package.json +2 -2
  44. package/scripts/patch-pi.js +243 -278
  45. package/scripts/render-regression.mjs +55 -4
@@ -4,13 +4,17 @@ import { basename, join, resolve } from "node:path";
4
4
  import { loadSkillsFromDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
5
5
  import { formatHomePath } from "../brand.js";
6
6
  import { agentDir } from "../config.js";
7
+ import { isEnglish, tr } from "../i18n.js";
7
8
  import { asRecord, asString, formatBytes, listHomeClaudeDirs, oneLine, uniqueExistingDirs, confirm } from "./util.js";
8
9
  import { readJsonIfExists, writeJson } from "./write.js";
9
- const TOOL_LABEL = {
10
- claude: "Claude Code",
11
- codex: "Codex",
12
- custom: "自定义目录",
13
- };
10
+ // 用时求值:界面语言要等配置读完才定
11
+ function toolLabel(tool) {
12
+ if (tool === "claude")
13
+ return "Claude Code";
14
+ if (tool === "codex")
15
+ return "Codex";
16
+ return tr("自定义目录", "Custom directory");
17
+ }
14
18
  const SCRIPT_EXTENSIONS = new Set([".sh", ".bash", ".zsh", ".ps1", ".bat", ".cmd", ".py", ".js", ".mjs", ".cjs", ".ts", ".rb", ".pl"]);
15
19
  /** 超过这个体积的技能目录多半塞了数据集或依赖,先不动。 */
16
20
  export const MAX_SKILL_BYTES = 20 * 1024 * 1024;
@@ -38,12 +42,12 @@ export function defaultSkillSources(cwd, home = homedir()) {
38
42
  if (!dir || seen.has(dir))
39
43
  continue;
40
44
  seen.add(dir);
41
- out.push({ tool: c.tool, label: TOOL_LABEL[c.tool], dir, scope: c.scope });
45
+ out.push({ tool: c.tool, label: toolLabel(c.tool), dir, scope: c.scope });
42
46
  }
43
47
  return out;
44
48
  }
45
49
  export function customSkillSource(dir) {
46
- return { tool: "custom", label: TOOL_LABEL.custom, dir: resolve(dir), scope: "custom" };
50
+ return { tool: "custom", label: toolLabel("custom"), dir: resolve(dir), scope: "custom" };
47
51
  }
48
52
  function walkStats(dir, stats, depth = 0) {
49
53
  if (stats.truncated || depth > 16)
@@ -87,9 +91,9 @@ function walkStats(dir, stats, depth = 0) {
87
91
  function skillHints(stats) {
88
92
  const hints = [];
89
93
  if (stats.scripts > 0)
90
- hints.push(`含 ${stats.scripts} 个脚本,只导可信来源`);
94
+ hints.push(tr(`含 ${stats.scripts} 个脚本,只导可信来源`, `contains ${stats.scripts} script(s); only import from trusted sources`));
91
95
  if (stats.bytes > LARGE_SKILL_BYTES)
92
- hints.push(`体积 ${formatBytes(stats.bytes)}`);
96
+ hints.push(tr(`体积 ${formatBytes(stats.bytes)}`, `size ${formatBytes(stats.bytes)}`));
93
97
  return hints;
94
98
  }
95
99
  const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -137,7 +141,8 @@ export function discoverSkills(sources) {
137
141
  const singleFile = basename(skill.filePath) !== "SKILL.md";
138
142
  const name = singleFile ? singleFileSkillName(skill.filePath, skill.name) : skill.name;
139
143
  if (!SKILL_NAME_RE.test(name) || name.length > 64) {
140
- problems.push({ path: skill.filePath, reason: `技能名「${name}」不合规:只能用小写字母、数字和连字符` });
144
+ problems.push({ path: skill.filePath, reason: tr(`技能名「${name}」不合规:只能用小写字母、数字和连字符`, `invalid skill name "${name}": only lowercase letters, digits and hyphens are allowed`),
145
+ });
141
146
  continue;
142
147
  }
143
148
  const stats = { files: 0, bytes: 0, scripts: 0, truncated: false };
@@ -156,7 +161,7 @@ export function discoverSkills(sources) {
156
161
  if (stats.truncated) {
157
162
  problems.push({
158
163
  path: singleFile ? skill.filePath : skill.baseDir,
159
- reason: `技能目录太大(超过 ${formatBytes(MAX_SKILL_BYTES)} 或 ${MAX_SKILL_FILES} 个文件),先跳过`,
164
+ reason: tr(`技能目录太大(超过 ${formatBytes(MAX_SKILL_BYTES)} 或 ${MAX_SKILL_FILES} 个文件),先跳过`, `skill directory too large (over ${formatBytes(MAX_SKILL_BYTES)} or ${MAX_SKILL_FILES} files), skipped`),
160
165
  });
161
166
  continue;
162
167
  }
@@ -227,7 +232,7 @@ export function planSkillImport(skills, opts) {
227
232
  export function copySkill(skill, destDir) {
228
233
  const dest = join(destDir, skill.name);
229
234
  if (resolve(skill.baseDir) === resolve(dest))
230
- throw new Error("来源就是目标目录");
235
+ throw new Error(tr("来源就是目标目录", "Source is the destination directory"));
231
236
  mkdirSync(destDir, { recursive: true });
232
237
  rmSync(dest, { recursive: true, force: true });
233
238
  if (skill.singleFile) {
@@ -270,10 +275,13 @@ export function runSkillImport(pending, index, destDir) {
270
275
  export function removeImportedSkill(name, index, destDir) {
271
276
  const record = index.items[name];
272
277
  if (!record)
273
- return { ok: false, reason: "不是 u1s1 import skills 导入的技能,请手动处理" };
278
+ return { ok: false, reason: tr("不是 u1s1 import skills 导入的技能,请手动处理", "not imported by u1s1 import skills; please handle it manually") };
274
279
  const expected = join(destDir, name);
275
280
  if (resolve(record.destPath) !== resolve(expected)) {
276
- return { ok: false, reason: `记录的位置 ${record.destPath} 与技能目录不一致,请手动处理` };
281
+ return {
282
+ ok: false,
283
+ reason: tr(`记录的位置 ${record.destPath} 与技能目录不一致,请手动处理`, `recorded location ${record.destPath} doesn't match the skills directory; please handle it manually`),
284
+ };
277
285
  }
278
286
  try {
279
287
  if (existsSync(expected) && lstatSync(expected).isDirectory())
@@ -299,7 +307,7 @@ function parseSkillsArgs(args) {
299
307
  const needValue = (flag, i) => {
300
308
  const next = args[i + 1];
301
309
  if (!next || next.startsWith("-")) {
302
- console.error(` ${flag} 后面要跟一个值`);
310
+ console.error(tr(` ${flag} 后面要跟一个值`, ` ${flag} requires a value`));
303
311
  process.exit(1);
304
312
  }
305
313
  return next;
@@ -321,7 +329,7 @@ function parseSkillsArgs(args) {
321
329
  else if (a === "--remove")
322
330
  opts.remove.push(needValue(a, i++));
323
331
  else if (a.startsWith("-")) {
324
- console.error(` 不认识参数 ${a}`);
332
+ console.error(tr(` 不认识参数 ${a}`, ` Unknown option ${a}`));
325
333
  process.exit(1);
326
334
  }
327
335
  else {
@@ -342,12 +350,32 @@ function parseToolArg(raw) {
342
350
  };
343
351
  const tool = aliases[raw.toLowerCase()];
344
352
  if (!tool) {
345
- console.error(` 不认识来源「${raw}」,可选: claude / codex;其他工具用 --from 目录`);
353
+ console.error(tr(` 不认识来源「${raw}」,可选: claude / codex;其他工具用 --from 目录`, ` Unknown source "${raw}"; choose claude / codex, or use --from <dir> for other tools`));
346
354
  process.exit(1);
347
355
  }
348
356
  return tool;
349
357
  }
350
358
  export function printSkillsHelp() {
359
+ if (isEnglish()) {
360
+ console.log("");
361
+ console.log(" Import skills (SKILL.md) from Claude Code / Codex and other tools into u1s1");
362
+ console.log("");
363
+ console.log(" Usage:");
364
+ console.log(" u1s1 import skills Find Claude Code / Codex skills on this machine");
365
+ console.log(" u1s1 import skills claude Claude Code only");
366
+ console.log(" u1s1 import skills codex Codex only");
367
+ console.log(" u1s1 import skills --from <dir> Other tools: a directory containing SKILL.md files (repeatable)");
368
+ console.log(" u1s1 import skills --cwd <dir> Look for project skills in this directory (default: current directory)");
369
+ console.log(" u1s1 import skills --dry-run Show what would be imported without writing anything");
370
+ console.log(" u1s1 import skills --update Re-copy skills already imported (same as --force)");
371
+ console.log(" u1s1 import skills --list List imported skills");
372
+ console.log(" u1s1 import skills --remove <name> Remove an imported skill");
373
+ console.log("");
374
+ console.log(` Skills are copied to ${formatHomePath(skillsDestDir())} and are available from your next chat:`);
375
+ console.log(" the model invokes them automatically based on their description, or type /skill:<name> to invoke one manually.");
376
+ console.log("");
377
+ return;
378
+ }
351
379
  console.log("");
352
380
  console.log(" 把 Claude Code / Codex 等工具的技能(SKILL.md)导入 u1s1");
353
381
  console.log("");
@@ -373,37 +401,38 @@ function printList(index) {
373
401
  const items = Object.values(index.items).sort((a, b) => a.name.localeCompare(b.name));
374
402
  console.log("");
375
403
  if (items.length === 0) {
376
- console.log(" 还没导入过技能。跑 u1s1 import skills 试试。");
404
+ console.log(tr(" 还没导入过技能。跑 u1s1 import skills 试试。", " No skills imported yet. Try u1s1 import skills."));
377
405
  console.log("");
378
406
  return;
379
407
  }
380
- console.log(` 已导入 ${items.length} 个技能(${formatHomePath(skillsDestDir())}):`);
408
+ console.log(tr(` 已导入 ${items.length} 个技能(${formatHomePath(skillsDestDir())}):`, ` ${items.length} imported skill(s) (${formatHomePath(skillsDestDir())}):`));
381
409
  for (const item of items) {
382
- const missing = existsSync(item.destPath) ? "" : "(目录已不在)";
383
- console.log(` · ${item.name} [${TOOL_LABEL[item.tool]}] ${missing}`);
384
- console.log(` 来自 ${formatHomePath(item.sourcePath)}`);
410
+ const missing = existsSync(item.destPath) ? "" : tr("(目录已不在)", "(directory missing)");
411
+ console.log(` · ${item.name} [${toolLabel(item.tool)}] ${missing}`);
412
+ console.log(tr(` 来自 ${formatHomePath(item.sourcePath)}`, ` from ${formatHomePath(item.sourcePath)}`));
385
413
  }
386
414
  console.log("");
387
415
  }
388
416
  function printPreview(skills, plan, problems, sources) {
389
417
  console.log("");
390
- console.log(` 在 ${sources.length} 个目录里找到 ${skills.length} 个技能:`);
418
+ console.log(tr(` 在 ${sources.length} 个目录里找到 ${skills.length} 个技能:`, ` Found ${skills.length} skill(s) in ${sources.length} director${sources.length === 1 ? "y" : "ies"}:`));
391
419
  for (const source of sources)
392
420
  console.log(` ${formatHomePath(source.dir)} [${source.label}]`);
393
421
  console.log("");
394
422
  for (const skill of plan.pending) {
395
423
  console.log(` + ${skill.name} [${skill.label}] ${oneLine(skill.description, 60)}`);
396
- console.log(` ${describeSource(skill)}${skill.hints.length ? ` ⚠ ${skill.hints.join(";")}` : ""}`);
424
+ console.log(` ${describeSource(skill)}${skill.hints.length ? ` ⚠ ${skill.hints.join(tr(";", "; "))}` : ""}`);
397
425
  }
398
426
  for (const skill of plan.alreadyImported)
399
- console.log(` · ${skill.name} 已导过(加 --update 重导)`);
427
+ console.log(tr(` · ${skill.name} 已导过(加 --update 重导)`, ` · ${skill.name} already imported (add --update to re-import)`));
400
428
  for (const skill of plan.conflicts) {
401
- console.log(` · ${skill.name} 目标目录已有同名技能(不是本命令导的),不覆盖`);
429
+ console.log(tr(` · ${skill.name} 目标目录已有同名技能(不是本命令导的),不覆盖`, ` · ${skill.name} a skill with this name already exists (not imported by this command); not overwriting`));
430
+ }
431
+ for (const skill of plan.duplicates) {
432
+ console.log(tr(` · ${skill.name} 与前面同名,只保留先发现的(${describeSource(skill)})`, ` · ${skill.name} duplicate name; keeping the first one found (${describeSource(skill)})`));
402
433
  }
403
- for (const skill of plan.duplicates)
404
- console.log(` · ${skill.name} 与前面同名,只保留先发现的(${describeSource(skill)})`);
405
434
  for (const problem of problems) {
406
- console.log(` × ${formatHomePath(problem.path)} 不兼容,跳过: ${oneLine(problem.reason, 80)}`);
435
+ console.log(tr(` × ${formatHomePath(problem.path)} 不兼容,跳过: ${oneLine(problem.reason, 80)}`, ` × ${formatHomePath(problem.path)} incompatible, skipped: ${oneLine(problem.reason, 80)}`));
407
436
  }
408
437
  }
409
438
  export async function importSkillsCommand(args) {
@@ -418,7 +447,7 @@ export async function importSkillsCommand(args) {
418
447
  for (const name of opts.remove) {
419
448
  const result = removeImportedSkill(name, index, destDir);
420
449
  if (result.ok)
421
- console.log(` ✓ 已删除技能 ${name}`);
450
+ console.log(tr(` ✓ 已删除技能 ${name}`, ` ✓ Removed skill ${name}`));
422
451
  else
423
452
  console.log(` × ${name}: ${result.reason}`);
424
453
  }
@@ -435,11 +464,11 @@ export async function importSkillsCommand(args) {
435
464
  ];
436
465
  const missingFrom = opts.from.filter((dir) => !existsSync(dir));
437
466
  for (const dir of missingFrom)
438
- console.log(` --from 目录不存在: ${dir}`);
467
+ console.log(tr(` --from 目录不存在: ${dir}`, ` --from directory doesn't exist: ${dir}`));
439
468
  if (sources.length === 0) {
440
469
  console.log("");
441
- console.log(" 没找到 Claude Code / Codex 的技能目录(~/.claude/skills、~/.codex/skills、项目里的 .claude/skills 或 .agents/skills)。");
442
- console.log(" 其他工具的技能可以用 --from 指定目录,只要里面有 SKILL.md。");
470
+ console.log(tr(" 没找到 Claude Code / Codex 的技能目录(~/.claude/skills、~/.codex/skills、项目里的 .claude/skills 或 .agents/skills)。", " No Claude Code / Codex skill directories found (~/.claude/skills, ~/.codex/skills, or .claude/skills / .agents/skills in the project)."));
471
+ console.log(tr(" 其他工具的技能可以用 --from 指定目录,只要里面有 SKILL.md。", " For other tools, point --from at any directory containing SKILL.md files."));
443
472
  console.log("");
444
473
  return;
445
474
  }
@@ -448,18 +477,20 @@ export async function importSkillsCommand(args) {
448
477
  printPreview(skills, plan, problems, sources);
449
478
  if (plan.pending.length === 0) {
450
479
  console.log("");
451
- console.log(skills.length === 0 ? " 这些目录里没有能用的技能。" : " 没有新的可导。想重导一遍就加 --update。");
480
+ console.log(skills.length === 0
481
+ ? tr(" 这些目录里没有能用的技能。", " No usable skills in these directories.")
482
+ : tr(" 没有新的可导。想重导一遍就加 --update。", " Nothing new to import. Add --update to re-import."));
452
483
  console.log("");
453
484
  return;
454
485
  }
455
486
  if (opts.dryRun) {
456
487
  console.log("");
457
- console.log(` 预演结束,以上 ${plan.pending.length} 个还没真正导入。`);
488
+ console.log(tr(` 预演结束,以上 ${plan.pending.length} 个还没真正导入。`, ` Dry run complete; the ${plan.pending.length} skill(s) above were not imported.`));
458
489
  console.log("");
459
490
  return;
460
491
  }
461
- if (!opts.yes && !(await confirm(` 导入这 ${plan.pending.length} 个技能?(回车=好 / n=取消) `))) {
462
- console.log(" 已取消。");
492
+ if (!opts.yes && !(await confirm(tr(` 导入这 ${plan.pending.length} 个技能?(回车=好 / n=取消) `, ` Import these ${plan.pending.length} skill(s)? (Enter = yes / n = cancel) `)))) {
493
+ console.log(tr(" 已取消。", " Canceled."));
463
494
  return;
464
495
  }
465
496
  const results = runSkillImport(plan.pending, index, destDir);
@@ -467,12 +498,12 @@ export async function importSkillsCommand(args) {
467
498
  const imported = results.filter((r) => r.status === "imported").length;
468
499
  const errors = results.filter((r) => r.status === "error");
469
500
  console.log("");
470
- console.log(` ✓ 导入 ${imported} 个技能到 ${formatHomePath(destDir)}${errors.length ? `,失败 ${errors.length}` : ""}。`);
501
+ console.log(tr(` ✓ 导入 ${imported} 个技能到 ${formatHomePath(destDir)}${errors.length ? `,失败 ${errors.length}` : ""}。`, ` ✓ Imported ${imported} skill(s) to ${formatHomePath(destDir)}${errors.length ? `, ${errors.length} failed` : ""}.`));
471
502
  for (const item of errors)
472
- console.log(` × ${item.name}: ${item.detail ?? "失败"}`);
503
+ console.log(` × ${item.name}: ${item.detail ?? tr("失败", "failed")}`);
473
504
  if (imported > 0) {
474
- console.log(" 下次进 u1s1 对话就能用:模型会按说明自动调用,或输入 /skill:名称 手动调用。");
475
- console.log(` 管理: u1s1 import skills --list / --update / --remove 名称`);
505
+ console.log(tr(" 下次进 u1s1 对话就能用:模型会按说明自动调用,或输入 /skill:名称 手动调用。", " Available from your next u1s1 chat: the model invokes them based on their description, or type /skill:<name>."));
506
+ console.log(tr(" 管理: u1s1 import skills --list / --update / --remove 名称", " Manage: u1s1 import skills --list / --update / --remove <name>"));
476
507
  }
477
508
  console.log("");
478
509
  }
@@ -3,6 +3,7 @@ import { stdin as input, stdout as output } from "node:process";
3
3
  import { closeSync, existsSync, openSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
+ import { tr } from "../i18n.js";
6
7
  export const MAX_TOOL_RESULT_CHARS = 80_000;
7
8
  export const MAX_TEXT_CHARS = 200_000;
8
9
  export const PREVIEW_TITLE_CHARS = 48;
@@ -133,11 +134,11 @@ export function truncateText(text, max) {
133
134
  if (text.length <= max)
134
135
  return text;
135
136
  const omitted = text.length - max;
136
- return `${text.slice(0, max)}\n\n[已截断,原文还有约 ${formatBytes(omitted)} ]`;
137
+ return `${text.slice(0, max)}\n\n${tr(`[已截断,原文还有约 ${formatBytes(omitted)} ]`, `[truncated, about ${formatBytes(omitted)} more omitted]`)}`;
137
138
  }
138
139
  export function formatBytes(n) {
139
140
  if (n < 1024)
140
- return `${n} 字`;
141
+ return tr(`${n} 字`, `${n} B`);
141
142
  if (n < 1024 * 1024)
142
143
  return `${(n / 1024).toFixed(1)} KB`;
143
144
  return `${(n / (1024 * 1024)).toFixed(1)} MB`;
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { SessionManager } from "@earendil-works/pi-coding-agent";
4
4
  import { agentDir, loadConfig, resolvePreferredModel } from "../config.js";
5
+ import { tr } from "../i18n.js";
5
6
  const EMPTY_USAGE = {
6
7
  input: 0,
7
8
  output: 0,
@@ -78,7 +79,7 @@ export function writeConvertedSession(converted, destCwd) {
78
79
  sm.appendModelChange(pref.provider, pref.id);
79
80
  const dest = sm.getSessionFile();
80
81
  if (!dest)
81
- throw new Error("会话写盘失败");
82
+ throw new Error(tr("会话写盘失败", "Failed to write session to disk"));
82
83
  return dest;
83
84
  }
84
85
  export function readJsonIfExists(path) {