u1s1-cli 1.11.8 → 1.13.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 (49) hide show
  1. package/dist/agent-setup.d.ts +19 -3
  2. package/dist/agent-setup.js +85 -10
  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 +140 -49
  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.d.ts +2 -2
  27. package/dist/mcp/command.js +109 -69
  28. package/dist/mcp/config.d.ts +22 -6
  29. package/dist/mcp/config.js +0 -0
  30. package/dist/mcp/extension.js +5 -3
  31. package/dist/mcp/tools.d.ts +2 -0
  32. package/dist/mcp/tools.js +10 -5
  33. package/dist/model.js +11 -10
  34. package/dist/nudges.d.ts +8 -2
  35. package/dist/nudges.js +22 -3
  36. package/dist/search-tools.js +7 -6
  37. package/dist/shell-doctor.js +9 -8
  38. package/dist/subagent.js +5 -4
  39. package/dist/tools.js +53 -37
  40. package/dist/update.js +29 -28
  41. package/dist/usage.d.ts +1 -1
  42. package/dist/usage.js +96 -43
  43. package/dist/web.js +2 -1
  44. package/dist/workflow/runner.js +33 -29
  45. package/dist/workflow/templates.js +70 -45
  46. package/dist/workflow/tool.js +15 -12
  47. package/package.json +4 -4
  48. package/scripts/patch-pi.js +347 -278
  49. package/scripts/render-regression.mjs +55 -4
@@ -1,5 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { formatHomePath } from "../brand.js";
3
+ import { isEnglish, tr } from "../i18n.js";
3
4
  import { agentDir } from "../config.js";
4
5
  import { claudeAdapter, hydrateClaudeSession } from "./claude.js";
5
6
  import { codexAdapter, hydrateCodexSession } from "./codex.js";
@@ -65,7 +66,7 @@ function parseSourceArg(raw) {
65
66
  };
66
67
  const id = aliases[raw.toLowerCase()];
67
68
  if (!id) {
68
- console.error(` 不认识来源「${raw}」,可选: claude / codex / all`);
69
+ console.error(tr(` 不认识来源「${raw}」,可选: claude / codex / all`, ` Unknown source "${raw}"; choose claude / codex / all`));
69
70
  process.exit(1);
70
71
  }
71
72
  return [id];
@@ -88,7 +89,7 @@ function parseFlags(args) {
88
89
  else if (a === "--cwd") {
89
90
  const next = args[++i];
90
91
  if (!next) {
91
- console.error(" --cwd 后面要跟一个目录");
92
+ console.error(tr(" --cwd 后面要跟一个目录", " --cwd requires a directory"));
92
93
  process.exit(1);
93
94
  }
94
95
  cwd = next;
@@ -97,20 +98,20 @@ function parseFlags(args) {
97
98
  const next = args[++i];
98
99
  const n = Number(next);
99
100
  if (!Number.isFinite(n) || n <= 0) {
100
- console.error(" --limit 要是正整数");
101
+ console.error(tr(" --limit 要是正整数", " --limit must be a positive integer"));
101
102
  process.exit(1);
102
103
  }
103
104
  limit = Math.floor(n);
104
105
  }
105
106
  else if (a.startsWith("-")) {
106
- console.error(` 不认识参数 ${a}`);
107
+ console.error(tr(` 不认识参数 ${a}`, ` Unknown option ${a}`));
107
108
  process.exit(1);
108
109
  }
109
110
  else if (!sourceRaw) {
110
111
  sourceRaw = a;
111
112
  }
112
113
  else {
113
- console.error(` 多余参数 ${a}`);
114
+ console.error(tr(` 多余参数 ${a}`, ` Unexpected argument ${a}`));
114
115
  process.exit(1);
115
116
  }
116
117
  }
@@ -123,6 +124,24 @@ function parseFlags(args) {
123
124
  };
124
125
  }
125
126
  function printHelp() {
127
+ if (isEnglish()) {
128
+ console.log("");
129
+ console.log(" Import past conversations and skills from Claude Code / Codex into u1s1");
130
+ console.log("");
131
+ console.log(" Usage:");
132
+ console.log(" u1s1 import Import conversations for the current directory");
133
+ console.log(" u1s1 import claude Claude Code only");
134
+ console.log(" u1s1 import codex Codex only");
135
+ console.log(" u1s1 import --all Import every conversation found on this machine");
136
+ console.log(" u1s1 import --cwd <dir> Project directory (default: current directory)");
137
+ console.log(" u1s1 import --dry-run Show what would be imported without writing anything");
138
+ console.log(" u1s1 import --force Re-import conversations already imported");
139
+ console.log(" u1s1 import skills Import skills (SKILL.md) from Claude Code / Codex and others; see u1s1 import skills -h");
140
+ console.log("");
141
+ console.log(" After importing, run u1s1 in that project and type /resume to see them.");
142
+ console.log("");
143
+ return;
144
+ }
126
145
  console.log("");
127
146
  console.log(" 把 Claude Code / Codex 的历史对话和技能导入 u1s1");
128
147
  console.log("");
@@ -179,65 +198,82 @@ function pendingSessions(sessions, index, force) {
179
198
  return { pending, alreadyImported };
180
199
  }
181
200
  function printNoSessions(opts) {
182
- const where = opts.cwd ? `当前目录 ${formatHomePath(opts.cwd)}` : "这台电脑";
183
201
  const who = opts.sources.map(sourceLabel).join(" / ");
184
202
  console.log("");
185
- console.log(` 在${where}没找到 ${who} 的历史对话。`);
203
+ if (isEnglish()) {
204
+ const where = opts.cwd ? `the current directory ${formatHomePath(opts.cwd)}` : "this computer";
205
+ console.log(` No ${who} conversations found in ${where}.`);
206
+ }
207
+ else {
208
+ const where = opts.cwd ? `当前目录 ${formatHomePath(opts.cwd)}` : "这台电脑";
209
+ console.log(` 在${where}没找到 ${who} 的历史对话。`);
210
+ }
186
211
  if (opts.cwd)
187
- console.log(" 想全盘扫一遍可以: u1s1 import --all");
212
+ console.log(tr(" 想全盘扫一遍可以: u1s1 import --all", " To scan everything: u1s1 import --all"));
188
213
  console.log("");
189
214
  }
190
215
  async function printSessionPreview({ sessions, pending, alreadyImported, opts, }) {
191
216
  console.log("");
192
- console.log(` 找到 ${sessions.length} 段对话`
193
- + (opts.cwd ? `(${formatHomePath(opts.cwd)})` : "")
194
- + (alreadyImported ? `,其中 ${alreadyImported} 段以前导过` : "")
195
- + "。");
217
+ console.log(isEnglish()
218
+ ? ` Found ${sessions.length} conversation(s)`
219
+ + (opts.cwd ? ` (${formatHomePath(opts.cwd)})` : "")
220
+ + (alreadyImported ? `, ${alreadyImported} already imported` : "")
221
+ + "."
222
+ : ` 找到 ${sessions.length} 段对话`
223
+ + (opts.cwd ? `(${formatHomePath(opts.cwd)})` : "")
224
+ + (alreadyImported ? `,其中 ${alreadyImported} 段以前导过` : "")
225
+ + "。");
196
226
  const preview = await Promise.all(pending.slice(0, 12).map(hydrateSession));
197
227
  for (const session of preview) {
198
- const title = session.title ? oneLine(session.title, 56) : "(无标题)";
228
+ const title = session.title ? oneLine(session.title, 56) : tr("(无标题)", "(untitled)");
199
229
  console.log(` · [${sourceLabel(session.source)}] ${title}`);
200
230
  if (!opts.cwd && session.cwd)
201
231
  console.log(` ${formatHomePath(session.cwd)}`);
202
232
  }
203
233
  if (pending.length > preview.length) {
204
- console.log(` …还有 ${pending.length - preview.length} 段`);
234
+ console.log(tr(` …还有 ${pending.length - preview.length} 段`, ` …and ${pending.length - preview.length} more`));
205
235
  }
206
236
  }
207
237
  async function shouldCancelImport(pending, opts, assumeYes) {
208
238
  if (pending.length === 0) {
209
239
  console.log("");
210
- console.log(" 没有新的可导。想重导一遍就加 --force。");
240
+ console.log(tr(" 没有新的可导。想重导一遍就加 --force。", " Nothing new to import. Add --force to re-import."));
211
241
  console.log("");
212
242
  return true;
213
243
  }
214
244
  if (opts.dryRun) {
215
245
  console.log("");
216
- console.log(` 预演结束,以上 ${pending.length} 段还没真正导入。`);
246
+ console.log(tr(` 预演结束,以上 ${pending.length} 段还没真正导入。`, ` Dry run complete; the ${pending.length} conversation(s) above were not imported.`));
217
247
  console.log("");
218
248
  return true;
219
249
  }
220
- if (assumeYes || await confirm(` 导入这 ${pending.length} 段?(回车=好 / n=取消) `)) {
250
+ if (assumeYes || await confirm(tr(` 导入这 ${pending.length} 段?(回车=好 / n=取消) `, ` Import these ${pending.length}? (Enter = yes / n = cancel) `))) {
221
251
  return false;
222
252
  }
223
- console.log(" 已取消。");
253
+ console.log(tr(" 已取消。", " Canceled."));
224
254
  return true;
225
255
  }
226
256
  function printImportSummary(summary) {
227
257
  console.log("");
228
- console.log(` ✓ 导入 ${summary.imported} 段`
229
- + (summary.skipped ? `,跳过 ${summary.skipped}` : "")
230
- + (summary.empty ? `,空对话 ${summary.empty}` : "")
231
- + (summary.errors ? `,失败 ${summary.errors}` : "")
232
- + "。");
258
+ console.log(isEnglish()
259
+ ? ` ✓ Imported ${summary.imported}`
260
+ + (summary.skipped ? `, skipped ${summary.skipped}` : "")
261
+ + (summary.empty ? `, empty ${summary.empty}` : "")
262
+ + (summary.errors ? `, failed ${summary.errors}` : "")
263
+ + "."
264
+ : ` ✓ 导入 ${summary.imported} 段`
265
+ + (summary.skipped ? `,跳过 ${summary.skipped}` : "")
266
+ + (summary.empty ? `,空对话 ${summary.empty}` : "")
267
+ + (summary.errors ? `,失败 ${summary.errors}` : "")
268
+ + "。");
233
269
  if (summary.imported > 0) {
234
- console.log(" 进对应项目跑 u1s1,输入 /resume 就能接着聊。");
270
+ console.log(tr(" 进对应项目跑 u1s1,输入 /resume 就能接着聊。", " Run u1s1 in that project and type /resume to pick up where you left off."));
235
271
  }
236
272
  for (const item of summary.items) {
237
273
  if (item.status === "error")
238
- console.log(` × ${item.title}: ${item.detail ?? "失败"}`);
274
+ console.log(` × ${item.title}: ${item.detail ?? tr("失败", "failed")}`);
239
275
  else if (item.status === "skipped" && item.detail)
240
- console.log(` · 跳过 ${item.title}: ${item.detail}`);
276
+ console.log(tr(` · 跳过 ${item.title}: ${item.detail}`, ` · Skipped ${item.title}: ${item.detail}`));
241
277
  }
242
278
  console.log("");
243
279
  }
@@ -292,7 +328,7 @@ function runImport(sessions, index, opts) {
292
328
  title,
293
329
  cwd: session.cwd,
294
330
  status: "skipped",
295
- detail: `原始记录太大(${formatBytes(size)}),先跳过`,
331
+ detail: tr(`原始记录太大(${formatBytes(size)}),先跳过`, `source log too large (${formatBytes(size)}), skipped`),
296
332
  });
297
333
  skipped += 1;
298
334
  continue;
@@ -305,7 +341,7 @@ function runImport(sessions, index, opts) {
305
341
  title,
306
342
  cwd: converted.cwd,
307
343
  status: "skipped",
308
- detail: "不属于当前目录",
344
+ detail: tr("不属于当前目录", "not in the current directory"),
309
345
  });
310
346
  skipped += 1;
311
347
  continue;
@@ -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) {