u1s1-cli 1.2.4 → 1.2.6

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.
@@ -89,4 +89,10 @@ export declare function ensureWorkflowPromptTemplate(): void;
89
89
  export declare function scrubForeignProviderEnv(): void;
90
90
  /** 把各端点密钥放进环境(TUI 直接 process.env;web 传给子进程)。 */
91
91
  export declare function endpointKeyEnv(): Record<string, string>;
92
- export declare function ensureProviderModels(cfg: CliConfig): void;
92
+ export interface ProviderModelsFileOptions {
93
+ /** Destination for the composed provider config. Defaults to the shared agent models file. */
94
+ outputPath?: string;
95
+ /** Existing config to preserve. Defaults to outputPath. */
96
+ sourcePath?: string;
97
+ }
98
+ export declare function ensureProviderModels(cfg: CliConfig, options?: ProviderModelsFileOptions): void;
@@ -6,7 +6,7 @@ const BRAND_APPEND = `## u1s1
6
6
 
7
7
  You are u1s1 — a plain-spoken AI coding buddy for programming beginners. Most users are unfamiliar with jargon:
8
8
  - When introducing yourself or asked "who are you", say you are u1s1, never "pi"; only mention the underlying pi engine if the user explicitly asks about internals.
9
- - Reply in Chinese by default; keep code, commands, and verbatim error messages in English.
9
+ - Reply in the language of the user's latest request by default. If that is ambiguous, follow the conversation language, then the UI locale when available, and otherwise use Chinese. Never infer language from IP or location. Keep code, commands, and verbatim error messages in English.
10
10
  - Explain problems in plain language without jargon; use a one-sentence analogy when helpful.
11
11
  - Before making changes, briefly state what you plan to do; afterwards summarize what changed in one or two sentences.
12
12
  - If the user's request is vague, assume the most likely intent and confirm it instead of asking a long list of questions.
@@ -206,10 +206,10 @@ export function writeWebToolsExtension(cfg, features) {
206
206
  mkdirSync(dir, { recursive: true });
207
207
  const toolsUrl = new URL("./tools.js", import.meta.url).href;
208
208
  const searchLine = features.webSearch
209
- ? ` pi.registerTool(tools.createSearchTool({ baseUrl: ${JSON.stringify(cfg.baseUrl)}, apiKey: process.env.U1S1_API_KEY }));\n`
209
+ ? ` pi.registerTool(tools.createSearchTool({ baseUrl, apiKey: process.env.U1S1_API_KEY }));\n`
210
210
  : "";
211
211
  const imageLine = features.imageGen
212
- ? ` pi.registerTool(tools.createImageTool({ baseUrl: ${JSON.stringify(cfg.baseUrl)}, apiKey: process.env.U1S1_API_KEY }));\n`
212
+ ? ` pi.registerTool(tools.createImageTool({ baseUrl, apiKey: process.env.U1S1_API_KEY }));\n`
213
213
  : "";
214
214
  // spawn_subagent / run_workflow 只给主会话注册;子 agent 环境里跳过,杜绝孙 agent 递归 spawn
215
215
  const subagentBlock = ` if (process.env.U1S1_IN_SUBAGENT !== "1") {\n` +
@@ -227,9 +227,10 @@ export function writeWebToolsExtension(cfg, features) {
227
227
  writeFileSync(join(dir, "u1s1-tools.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
228
228
  `export default async function (pi) {\n` +
229
229
  ` if (process.env.U1S1_TOOLS_VIA_EXTENSION !== "1") return;\n` +
230
+ ` const baseUrl = process.env.U1S1_SIGNING_PROXY_URL || ${JSON.stringify(cfg.baseUrl)};\n` +
230
231
  ` const tools = await import(${JSON.stringify(toolsUrl)});\n` +
231
232
  searchLine +
232
- ` pi.registerTool(tools.createFetchTool({ baseUrl: ${JSON.stringify(cfg.baseUrl)}, apiKey: process.env.U1S1_API_KEY, renderFallback: ${features.webFetchRender} }));\n` +
233
+ ` pi.registerTool(tools.createFetchTool({ baseUrl, apiKey: process.env.U1S1_API_KEY, renderFallback: ${features.webFetchRender} }));\n` +
233
234
  imageLine +
234
235
  subagentBlock +
235
236
  `}\n`);
@@ -528,13 +529,14 @@ export function endpointKeyEnv() {
528
529
  }
529
530
  return env;
530
531
  }
531
- export function ensureProviderModels(cfg) {
532
- mkdirSync(agentDir, { recursive: true });
533
- const p = join(agentDir, "models.json");
532
+ export function ensureProviderModels(cfg, options = {}) {
533
+ const outputPath = options.outputPath ?? join(agentDir, "models.json");
534
+ const sourcePath = options.sourcePath ?? outputPath;
535
+ mkdirSync(dirname(outputPath), { recursive: true });
534
536
  let root = {};
535
- if (existsSync(p)) {
537
+ if (existsSync(sourcePath)) {
536
538
  try {
537
- root = JSON.parse(readFileSync(p, "utf8"));
539
+ root = JSON.parse(readFileSync(sourcePath, "utf8"));
538
540
  }
539
541
  catch {
540
542
  root = {};
@@ -563,5 +565,5 @@ export function ensureProviderModels(cfg) {
563
565
  providers[ep.id] = endpointProviderEntry(ep);
564
566
  }
565
567
  root["providers"] = providers;
566
- writeFileSync(p, JSON.stringify(root, null, 2) + "\n");
568
+ writeFileSync(outputPath, JSON.stringify(root, null, 2) + "\n", { mode: 0o600 });
567
569
  }
package/dist/embed.d.ts CHANGED
@@ -6,6 +6,6 @@
6
6
  export { agentDir, loadConfig, saveConfig, u1s1Dir, VERSION, type CliConfig, } from "./config.js";
7
7
  export { fetchMe, fetchModels } from "./api.js";
8
8
  export { apiOrigin, pollDeviceLogin, startDeviceLogin, type DeviceStart } from "./login.js";
9
- export { prepareWebEnv } from "./web.js";
10
- export { applyWebUiBranding, applyWebUiFrontend } from "./webui-brand.js";
9
+ export { prepareWebEnv, refreshWebModels } from "./web.js";
10
+ export { applyWebUiBranding } from "./webui-brand.js";
11
11
  export { DASHBOARD_URL } from "./brand.js";
package/dist/embed.js CHANGED
@@ -6,6 +6,6 @@
6
6
  export { agentDir, loadConfig, saveConfig, u1s1Dir, VERSION, } from "./config.js";
7
7
  export { fetchMe, fetchModels } from "./api.js";
8
8
  export { apiOrigin, pollDeviceLogin, startDeviceLogin } from "./login.js";
9
- export { prepareWebEnv } from "./web.js";
10
- export { applyWebUiBranding, applyWebUiFrontend } from "./webui-brand.js";
9
+ export { prepareWebEnv, refreshWebModels } from "./web.js";
10
+ export { applyWebUiBranding } from "./webui-brand.js";
11
11
  export { DASHBOARD_URL } from "./brand.js";
package/dist/index.js CHANGED
@@ -212,6 +212,7 @@ async function runAgent(cfg, args) {
212
212
  // must be set before pi reads them (getAgentDir() reads at call time, env at import is fine too)
213
213
  process.env["PI_CODING_AGENT_DIR"] = agentDir;
214
214
  process.env["U1S1_API_KEY"] = officialCfg.apiKey;
215
+ process.env["U1S1_SIGNING_PROXY_URL"] = officialCfg.baseUrl;
215
216
  process.env["U1S1_TOOLS_VIA_EXTENSION"] = "1";
216
217
  // 自定义端点的密钥走环境变量引用(models.json 里只有 $VAR,不落明文)
217
218
  Object.assign(process.env, endpointKeyEnv());
@@ -8,7 +8,8 @@
8
8
  *
9
9
  * 整个探测是异步的:调用方先拿 Promise 与拉模型列表等网络请求并行,进 pi 前
10
10
  * 再 await——首启的 where/验活成本(Defender 首扫 bash.exe 可达秒级)藏进
11
- * 网络等待里。验活结果持久化到 shellPath,之后每次启动只花 existsSync。
11
+ * 网络等待里。Git Bash 验活结果持久化到 shellPath,之后每次启动只花
12
+ * existsSync;缓存的是 WSL 时仍会检查 PATH,以便发现自定义目录的 Git。
12
13
  */
13
14
  export type ShellDoctorResult = {
14
15
  status: "posix";
@@ -23,4 +24,6 @@ export type ShellDoctorResult = {
23
24
  } | {
24
25
  status: "unknown";
25
26
  };
27
+ /** 从 git.exe 路径反推 Git Bash 候选;用 win32 保证非 Windows CI 也能覆盖路径规则。 */
28
+ export declare function gitBashPathsFromGitExecutables(gitPaths: string[], skip: string[], pathExists?: (path: string) => boolean): string[];
26
29
  export declare function ensureUsableShell(): Promise<ShellDoctorResult>;
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
3
+ import { join, win32 } from "node:path";
4
4
  import { agentDir, agentSettingsFile } from "./config.js";
5
5
  /** System32/Sysnative 的 bash.exe 是 WSL 转发器,不是真 bash(与 pi 同款判断)。 */
6
6
  function isWslRelayBash(p) {
@@ -91,21 +91,38 @@ function knownGitBashPaths() {
91
91
  }
92
92
  return out;
93
93
  }
94
- /** 慢路径候选:where git.exe 反推安装根,兜住 portable 等非常规安装。 */
95
- async function discoveredGitBashPaths(skip) {
94
+ /** git.exe 路径反推 Git Bash 候选;用 win32 保证非 Windows CI 也能覆盖路径规则。 */
95
+ export function gitBashPathsFromGitExecutables(gitPaths, skip, pathExists = existsSync) {
96
96
  const out = [];
97
97
  // git.exe 在 <root>\cmd、<root>\bin 或 <root>\mingw64\bin 下,向上找 root
98
- for (const gitPath of await runWhere("git.exe")) {
99
- const dir = dirname(gitPath);
100
- for (const root of [dirname(dir), dirname(dirname(dir))]) {
101
- for (const p of [join(root, "bin", "bash.exe"), join(root, "usr", "bin", "bash.exe")]) {
102
- if (!out.includes(p) && !skip.includes(p) && existsSync(p))
98
+ for (const gitPath of gitPaths) {
99
+ const dir = win32.dirname(gitPath);
100
+ for (const root of [win32.dirname(dir), win32.dirname(win32.dirname(dir))]) {
101
+ for (const p of [win32.join(root, "bin", "bash.exe"), win32.join(root, "usr", "bin", "bash.exe")]) {
102
+ if (!out.includes(p) && !skip.includes(p) && pathExists(p))
103
103
  out.push(p);
104
104
  }
105
105
  }
106
106
  }
107
107
  return out;
108
108
  }
109
+ /** 慢路径候选:where git.exe 反推安装根,兜住 portable 等非常规安装。 */
110
+ async function discoveredGitBashPaths(skip) {
111
+ return gitBashPathsFromGitExecutables(await runWhere("git.exe"), skip);
112
+ }
113
+ /** 固定位置与 PATH 反推共用同一验活流程,缓存 WSL 和首次探测都走这里。 */
114
+ async function findUsableGitBash() {
115
+ const known = knownGitBashPaths();
116
+ for (const candidate of known) {
117
+ if (await bashWorks(candidate))
118
+ return candidate;
119
+ }
120
+ for (const candidate of await discoveredGitBashPaths(known)) {
121
+ if (await bashWorks(candidate))
122
+ return candidate;
123
+ }
124
+ return undefined;
125
+ }
109
126
  /** 持久化写 settings.json:写入时现读现改,避免覆盖并行流程刚写的其他键。 */
110
127
  function persistShellPath(p) {
111
128
  let settings = {};
@@ -139,17 +156,16 @@ export async function ensureUsableShell() {
139
156
  }
140
157
  }
141
158
  // 已配置且文件还在:信它,不每次启动都探活(坏了 pi 会在会话里报错)。
142
- // 配置的是 WSL 转发器时,零成本看一眼 Git Bash 是否新装了,装了就换轨。
159
+ // 配置的是 WSL 转发器时,同时检查固定位置与 PATH 里的 Git 安装,找到就换轨。
143
160
  const configured = settings["shellPath"];
144
161
  if (typeof configured === "string" && configured && existsSync(configured)) {
145
162
  if (!isWslRelayBash(configured))
146
163
  return { status: "bash", shellPath: configured };
147
- for (const candidate of knownGitBashPaths()) {
148
- if (await bashWorks(candidate)) {
149
- persistShellPath(candidate);
150
- console.error(` 检测到 Git Bash,命令改回 Windows 本机执行: ${candidate}`);
151
- return { status: "bash", shellPath: candidate };
152
- }
164
+ const candidate = await findUsableGitBash();
165
+ if (candidate) {
166
+ persistShellPath(candidate);
167
+ console.error(` 检测到 Git Bash,命令改回 Windows 本机执行: ${candidate}`);
168
+ return { status: "bash", shellPath: candidate };
153
169
  }
154
170
  adviseInstallGitBash();
155
171
  return { status: "wsl", shellPath: configured };
@@ -163,20 +179,14 @@ export async function ensureUsableShell() {
163
179
  }
164
180
  return { status: "bash", shellPath: candidate };
165
181
  };
166
- const known = knownGitBashPaths();
167
- for (const candidate of known) {
168
- if (await bashWorks(candidate))
169
- return adopt(candidate);
170
- }
171
- for (const candidate of await discoveredGitBashPaths(known)) {
172
- if (await bashWorks(candidate))
173
- return adopt(candidate);
174
- }
182
+ const gitBash = await findUsableGitBash();
183
+ if (gitBash)
184
+ return adopt(gitBash);
175
185
  // 没有 Git Bash:看 PATH 上还有什么(与 pi 的兜底一致,但这里要验活)
176
186
  const fallback = (await runWhere("bash.exe")).find((p) => existsSync(p));
177
187
  if (fallback && (await bashWorks(fallback))) {
178
188
  // WSL 转发器也持久化:冷启 WSL 验活要好几秒,不能每次启动都付。装了
179
- // Git Bash 后的换轨由上面的 configured-WSL 分支负责(零成本 existsSync)。
189
+ // Git Bash 后的换轨由上面的 configured-WSL 分支负责。
180
190
  persistShellPath(fallback);
181
191
  if (isWslRelayBash(fallback)) {
182
192
  adviseInstallGitBash();
package/dist/web.d.ts CHANGED
@@ -1,4 +1,10 @@
1
1
  import { type CliConfig } from "./config.js";
2
+ /**
3
+ * Refresh the web/Desktop process-local model snapshot while preserving user
4
+ * providers from the shared agent config. The u1s1 provider always remains
5
+ * pinned to this process's signing proxy.
6
+ */
7
+ export declare function refreshWebModels(): string | undefined;
2
8
  /**
3
9
  * Desktop App 启动 agent server 前的公共准备:品牌 prompt、模型列表、
4
10
  * auth.json 凭据、联网工具扩展、默认模型,并返回子进程需要的环境变量。
package/dist/web.js CHANGED
@@ -1,4 +1,5 @@
1
- import { mkdirSync } from "node:fs";
1
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
2
3
  import { join } from "node:path";
3
4
  import { cleanupBrandThemes, endpointKeyEnv, ensureAuthCredential, ensureBrandPrompt, ensureWorkflowPromptTemplate, ensureDefaultSettings, ensureProviderModels, scrubForeignProviderEnv, writeAttributionExtension, writeWebToolsExtension, } from "./agent-setup.js";
4
5
  import { agentDir, apiModelToDef, MODELS, resolvePreferredModel, setModelsFromApi, u1s1Dir, writeAgentDefaultModel, } from "./config.js";
@@ -6,6 +7,33 @@ import { ensureSearchTools } from "./search-tools.js";
6
7
  import { ensureUsableShell } from "./shell-doctor.js";
7
8
  import { fetchModels, loadCustomEndpoints } from "./api.js";
8
9
  import { ensureSigningProxy } from "./device-auth.js";
10
+ let webModelsDir;
11
+ let webOfficialCfg;
12
+ function processModelsPath() {
13
+ if (!webModelsDir) {
14
+ webModelsDir = mkdtempSync(join(tmpdir(), "u1s1-web-"));
15
+ process.once("exit", () => {
16
+ if (webModelsDir)
17
+ rmSync(webModelsDir, { recursive: true, force: true });
18
+ });
19
+ }
20
+ return join(webModelsDir, "models.json");
21
+ }
22
+ /**
23
+ * Refresh the web/Desktop process-local model snapshot while preserving user
24
+ * providers from the shared agent config. The u1s1 provider always remains
25
+ * pinned to this process's signing proxy.
26
+ */
27
+ export function refreshWebModels() {
28
+ if (!webOfficialCfg)
29
+ return undefined;
30
+ const outputPath = processModelsPath();
31
+ ensureProviderModels(webOfficialCfg, {
32
+ outputPath,
33
+ sourcePath: join(agentDir, "models.json"),
34
+ });
35
+ return outputPath;
36
+ }
9
37
  /**
10
38
  * Desktop App 启动 agent server 前的公共准备:品牌 prompt、模型列表、
11
39
  * auth.json 凭据、联网工具扩展、默认模型,并返回子进程需要的环境变量。
@@ -41,10 +69,11 @@ export async function prepareWebEnv(cfg) {
41
69
  ensureDefaultSettings(MODELS);
42
70
  const signing = await ensureSigningProxy(cfg, "desktop");
43
71
  const officialCfg = { ...cfg, baseUrl: signing.baseUrl, apiKey: signing.localKey };
72
+ webOfficialCfg = officialCfg;
73
+ const modelsPath = refreshWebModels();
44
74
  ensureBrandPrompt(await shellReady);
45
75
  // /workflow 提示词模板与 TUI 同源,Desktop App 也要有
46
76
  ensureWorkflowPromptTemplate();
47
- ensureProviderModels(officialCfg);
48
77
  // pi-web-ui 靠 auth.json 判断「已配置」,否则网页会弹 pi 安装引导
49
78
  ensureAuthCredential();
50
79
  // 联网工具经 agentDir/extensions 投影,和 TUI 共用一份注册
@@ -67,6 +96,8 @@ export async function prepareWebEnv(cfg) {
67
96
  return {
68
97
  PI_CODING_AGENT_DIR: agentDir,
69
98
  U1S1_API_KEY: officialCfg.apiKey,
99
+ U1S1_MODELS_PATH: modelsPath,
100
+ U1S1_SIGNING_PROXY_URL: officialCfg.baseUrl,
70
101
  U1S1_TOOLS_VIA_EXTENSION: "1",
71
102
  PI_WEB_DATA_DIR: dataDir,
72
103
  // 自定义端点的密钥经环境变量传给 App 子进程(models.json 里只有 $VAR 引用)
@@ -1,9 +1,2 @@
1
- /**
2
- * u1s1 自有前端(packages/webui,Codex 布局复刻)整体替换 pi-web-ui 的
3
- * web/dist。产物随 CLI 包发布在 <cliPkg>/webui-dist;index.html 内容一致
4
- * 说明已同步过,跳过。目标目录先整体删除再拷贝 —— 断开 pnpm 硬链接,
5
- * 也顺带清掉上游的旧 assets。返回 false 表示产物缺失(回退到品牌补丁)。
6
- */
7
- export declare function applyWebUiFrontend(binPath: string): boolean;
8
1
  /** binPath = <pkg>/bin/pi-web-ui.mjs → 前端产物在 <pkg>/web/dist。 */
9
2
  export declare function applyWebUiBranding(binPath: string): void;
@@ -1,33 +1,5 @@
1
- import { cpSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- /**
5
- * u1s1 自有前端(packages/webui,Codex 布局复刻)整体替换 pi-web-ui 的
6
- * web/dist。产物随 CLI 包发布在 <cliPkg>/webui-dist;index.html 内容一致
7
- * 说明已同步过,跳过。目标目录先整体删除再拷贝 —— 断开 pnpm 硬链接,
8
- * 也顺带清掉上游的旧 assets。返回 false 表示产物缺失(回退到品牌补丁)。
9
- */
10
- export function applyWebUiFrontend(binPath) {
11
- const src = fileURLToPath(new URL("../webui-dist/", import.meta.url));
12
- const srcIndex = join(src, "index.html");
13
- if (!existsSync(srcIndex))
14
- return false;
15
- const dist = join(dirname(binPath), "..", "web", "dist");
16
- const distIndex = join(dist, "index.html");
17
- try {
18
- if (existsSync(distIndex) &&
19
- readFileSync(distIndex, "utf8") === readFileSync(srcIndex, "utf8")) {
20
- return true;
21
- }
22
- rmSync(dist, { recursive: true, force: true });
23
- cpSync(src, dist, { recursive: true });
24
- return true;
25
- }
26
- catch (e) {
27
- console.error(" 前端替换失败,退回默认界面:", e.message);
28
- return false;
29
- }
30
- }
31
3
  /**
32
4
  * pi-web-ui 前端产物的启动时品牌补丁。不 fork 上游,只在 Desktop App
33
5
  * 启动前改它 web/dist 里的静态文件:重写 <title>、换 favicon、注入
@@ -41,9 +13,9 @@ export function applyWebUiFrontend(binPath) {
41
13
  */
42
14
  const PATCH_MARK = "u1s1-brand-v2";
43
15
  const PAGE_TITLE = "u1s1 Desktop App — 有一说一";
44
- const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
45
- <rect width="64" height="64" rx="14" fill="#101418"/>
46
- <text x="32" y="42" font-family="ui-monospace,Menlo,Consolas,monospace" font-size="26" font-weight="700" fill="#4ea1ff" text-anchor="middle">u1</text>
16
+ const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
17
+ <rect width="100" height="100" rx="18" fill="#e8442e"/>
18
+ <text x="50" y="68" font-family="ui-monospace,Menlo,Consolas,monospace" font-size="52" font-weight="700" fill="#faf5ec" text-anchor="middle">u1</text>
47
19
  </svg>
48
20
  `;
49
21
  /** 页面 chrome 品牌替换:改 .brand-logo/.brand-name,盯住 title(React 可能改回去)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {