u1s1-cli 0.16.3 → 0.16.5
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/agent-setup.js +20 -14
- package/dist/index.js +0 -2
- package/dist/loop.js +9 -5
- package/dist/tools.js +3 -3
- package/package.json +3 -2
- package/scripts/render-regression.mjs +129 -0
- package/dist/templates.d.ts +0 -2
- package/dist/templates.js +0 -48
package/dist/agent-setup.js
CHANGED
|
@@ -3,18 +3,18 @@ import { join } from "node:path";
|
|
|
3
3
|
import { agentDir, CUSTOM_ENDPOINTS, ENDPOINT_ID_RE, endpointKeyEnvName, MODELS, PROVIDER_ID, VERSION, } from "./config.js";
|
|
4
4
|
const BRAND_APPEND = `## u1s1
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
6
|
+
You are u1s1 — a plain-spoken AI coding buddy for programming beginners. Most users are unfamiliar with jargon:
|
|
7
|
+
- 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.
|
|
8
|
+
- Reply in Chinese by default; keep code, commands, and verbatim error messages in English.
|
|
9
|
+
- Explain problems in plain language without jargon; use a one-sentence analogy when helpful.
|
|
10
|
+
- Before making changes, briefly state what you plan to do; afterwards summarize what changed in one or two sentences.
|
|
11
|
+
- If the user's request is vague, assume the most likely intent and confirm it instead of asking a long list of questions.
|
|
12
12
|
`;
|
|
13
13
|
const EXEC_RULES = `
|
|
14
|
-
##
|
|
14
|
+
## Command execution
|
|
15
15
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
16
|
+
- Run any command yourself with the bash tool and show its output; never ask the user to run it manually.
|
|
17
|
+
- Only hand a command to the user when it needs admin rights, interactive input (logins, installing system components), or a new terminal window — and always give the full command ready to paste.
|
|
18
18
|
`;
|
|
19
19
|
/**
|
|
20
20
|
* pi 的系统提示不含任何 OS/平台信息,模型只能拿 uname 之类盲探,Windows 上
|
|
@@ -23,18 +23,18 @@ const EXEC_RULES = `
|
|
|
23
23
|
*/
|
|
24
24
|
function envSection(shell) {
|
|
25
25
|
if (shell.status === "posix") {
|
|
26
|
-
return `\n##
|
|
26
|
+
return `\n## Environment\n\n- Operating system: ${process.platform === "darwin" ? "macOS" : "Linux"}.\n`;
|
|
27
27
|
}
|
|
28
|
-
const lines = ["", "##
|
|
28
|
+
const lines = ["", "## Environment", "", "- Operating system: Windows. Trust this section over probing; do not detect the OS with uname."];
|
|
29
29
|
switch (shell.status) {
|
|
30
30
|
case "bash":
|
|
31
|
-
lines.push(`-
|
|
31
|
+
lines.push(`- Commands run through bash (${shell.shellPath}); POSIX tools like ls/grep/curl are available.`, 'To inspect native Windows state or launch Windows programs, use cmd.exe /c "..." or powershell.exe -NoProfile -Command "...".', "- Work on Windows itself by default; only consider WSL when the user asks or the task truly requires Linux.");
|
|
32
32
|
break;
|
|
33
33
|
case "wsl":
|
|
34
|
-
lines.push("-
|
|
34
|
+
lines.push("- Note: commands run through WSL's bash, so they execute inside a Linux distro, not on Windows itself.", "- Access Windows files via /mnt/c/... paths; invoke Windows programs by their .exe names (cmd.exe, powershell.exe).", '- When appropriate, suggest the user install Git for Windows (run winget install --id Git.Git in PowerShell) and restart u1s1 so commands run natively on Windows again.');
|
|
35
35
|
break;
|
|
36
36
|
case "none":
|
|
37
|
-
lines.push("-
|
|
37
|
+
lines.push("- Critical: no usable bash exists — every bash tool call will fail immediately, and retrying is pointless.", '- Top priority is guiding the user to fix it: have them run winget install --id Git.Git in PowerShell to install Git for Windows, then restart u1s1.', "- Until then, only help the user with other tools such as file read/write.");
|
|
38
38
|
break;
|
|
39
39
|
case "unknown":
|
|
40
40
|
break;
|
|
@@ -77,6 +77,12 @@ export function ensureDefaultSettings() {
|
|
|
77
77
|
settings["hideThinkingBlock"] = true;
|
|
78
78
|
changed = true;
|
|
79
79
|
}
|
|
80
|
+
// 精简 UI 配套:组件变矮(状态栏消失、思考区伸缩)时清掉残留空行,
|
|
81
|
+
// 否则增量打印会在聊天流里越攒越多幽灵空行
|
|
82
|
+
if (!("clearOnShrink" in settings)) {
|
|
83
|
+
settings["clearOnShrink"] = true;
|
|
84
|
+
changed = true;
|
|
85
|
+
}
|
|
80
86
|
// 启动时是否显示品牌横幅(logo + 版本号/目录/提示)
|
|
81
87
|
if (!("showStartupBanner" in settings)) {
|
|
82
88
|
settings["showStartupBanner"] = true;
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,6 @@ import { registerLoopCommand } from "./loop.js";
|
|
|
8
8
|
import { ensureSearchTools } from "./search-tools.js";
|
|
9
9
|
import { ensureUsableShell } from "./shell-doctor.js";
|
|
10
10
|
import { applyBrandUi, setUpdateNotice } from "./style.js";
|
|
11
|
-
import { offerStarterTemplates } from "./templates.js";
|
|
12
11
|
import { fetchModels, loadCustomEndpoints } from "./api.js";
|
|
13
12
|
const PACKAGE_NAME = "u1s1-cli";
|
|
14
13
|
/** 启动时检测到的可自动安装的新版;TUI 退出后才装(见 installPendingUpdate)。 */
|
|
@@ -215,7 +214,6 @@ async function runAgent(cfg, args) {
|
|
|
215
214
|
name: "u1s1",
|
|
216
215
|
factory: (pi) => {
|
|
217
216
|
applyBrandUi(pi, VERSION);
|
|
218
|
-
offerStarterTemplates(pi);
|
|
219
217
|
// /loop: 会话级定时循环执行提示词(对标 Claude Code),实现见 loop.ts
|
|
220
218
|
registerLoopCommand(pi);
|
|
221
219
|
// 会话标题:拿用户第一条正经输入的前半截当名字(像 Claude Code 那样),
|
package/dist/loop.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* /loop 20m /review-pr 1234 循环里也能跑其他斜杠命令
|
|
9
9
|
* /loop stop 停止当前循环
|
|
10
10
|
*
|
|
11
|
-
* 语义与 Claude Code 对齐:任务只活在当前会话,/clear
|
|
12
|
-
*
|
|
11
|
+
* 语义与 Claude Code 对齐:任务只活在当前会话,/clear 或退出即消失;
|
|
12
|
+
* 启动时立刻跑第一轮,之后每隔一个间隔再跑。
|
|
13
|
+
* 实现核心是 pi.sendUserMessage():到点就"替用户打字"发一轮,
|
|
13
14
|
* deliverAs: "followUp" 保证上一轮没跑完时排队等它结束,不硬插打断;
|
|
14
15
|
* expandPromptTemplates: true 让 prompt 里的斜杠命令也能被展开执行。
|
|
15
16
|
*/
|
|
@@ -116,7 +117,7 @@ export function registerLoopCommand(pi) {
|
|
|
116
117
|
stopLoop(ctx.ui);
|
|
117
118
|
activePrompt = parsed.prompt;
|
|
118
119
|
activeIntervalMs = parsed.intervalMs;
|
|
119
|
-
|
|
120
|
+
const fire = () => {
|
|
120
121
|
try {
|
|
121
122
|
pi.sendUserMessage(activePrompt, {
|
|
122
123
|
deliverAs: "followUp",
|
|
@@ -126,9 +127,12 @@ export function registerLoopCommand(pi) {
|
|
|
126
127
|
catch (e) {
|
|
127
128
|
ctx.ui.notify(`loop 执行失败: ${e instanceof Error ? e.message : String(e)}`, "error");
|
|
128
129
|
}
|
|
129
|
-
}
|
|
130
|
+
};
|
|
131
|
+
// 第一轮立刻跑:用户写完命令就想看到结果,不该干等一个间隔
|
|
132
|
+
fire();
|
|
133
|
+
timer = setInterval(fire, activeIntervalMs);
|
|
130
134
|
ctx.ui.setStatus(STATUS_KEY, `🔁 每${formatInterval(activeIntervalMs)} · ${preview(activePrompt)}`);
|
|
131
|
-
ctx.ui.notify(`🔁
|
|
135
|
+
ctx.ui.notify(`🔁 已开始:现在先跑第一轮,之后每${formatInterval(activeIntervalMs)}一次「${preview(activePrompt)}」`, "info");
|
|
132
136
|
},
|
|
133
137
|
});
|
|
134
138
|
}
|
package/dist/tools.js
CHANGED
|
@@ -81,7 +81,7 @@ export function createSearchTool(cfg) {
|
|
|
81
81
|
name: "web_search",
|
|
82
82
|
label: "联网搜索",
|
|
83
83
|
description: "Search the web for current information. Use it for anything time-sensitive or outside the codebase: library docs, error messages, latest versions, recent events. Returns a short answer plus ranked results with URLs and snippets. Follow up with web_fetch to read a promising result in full.",
|
|
84
|
-
promptSnippet: "
|
|
84
|
+
promptSnippet: "Search the internet for up-to-date information (docs, errors, versions, current events)",
|
|
85
85
|
promptGuidelines: [
|
|
86
86
|
"Use web_search when the answer depends on information newer than your knowledge or not present in the repository, instead of guessing.",
|
|
87
87
|
"Prefer one focused web_search query over several near-duplicate queries; each search costs the user credits.",
|
|
@@ -160,9 +160,9 @@ export function createImageTool(cfg) {
|
|
|
160
160
|
"Saves the result as a local image file and returns its path. " +
|
|
161
161
|
"Pass local file paths or http(s) URLs in `images` to edit an image or use references (style transfer, adding elements, combining up to 10 images). " +
|
|
162
162
|
"Prompts work in Chinese or English; describe content, style, composition, and any text to render.",
|
|
163
|
-
promptSnippet: "AI
|
|
163
|
+
promptSnippet: "AI image generation/editing (text-to-image, editing, multi-image composition)",
|
|
164
164
|
promptGuidelines: [
|
|
165
|
-
"Use generate_image when the user wants a picture created or an existing image modified (logo, illustration, poster, placeholder art,
|
|
165
|
+
"Use generate_image when the user wants a picture created or an existing image modified (logo, illustration, poster, placeholder art, style change).",
|
|
166
166
|
"Each call generates one image and costs the user credits; refine the prompt first instead of regenerating repeatedly.",
|
|
167
167
|
"To edit an existing image, pass its path in `images` and describe only the change in `prompt`.",
|
|
168
168
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u1s1-cli",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.5",
|
|
4
4
|
"description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"build": "tsc",
|
|
27
27
|
"dev": "tsx src/index.ts",
|
|
28
28
|
"typecheck": "tsc --noEmit",
|
|
29
|
-
"prepublishOnly": "npm run build",
|
|
29
|
+
"prepublishOnly": "npm run build && npm test",
|
|
30
|
+
"test": "node scripts/render-regression.mjs",
|
|
30
31
|
"postinstall": "node scripts/patch-pi.js",
|
|
31
32
|
"package": "bash ../../scripts/package.sh",
|
|
32
33
|
"upload-release": "bash ../../scripts/upload-release.sh",
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 渲染回归测试 —— 防止「修好的空行问题在发版后又回来」。
|
|
3
|
+
*
|
|
4
|
+
* 背景:精简 UI 依赖两处修复才能保持每轮回复只占必要的行数:
|
|
5
|
+
* 1. pi 的 AssistantMessageComponent 补丁(patches/*.patch + postinstall patch-pi.js)
|
|
6
|
+
* —— 隐藏的思考块不算可见内容、空标签跳过、隐形思考块后不加间隔空行
|
|
7
|
+
* 2. 扩展给内置工具的 renderShell:"self" + 空 Text —— 工具行零行渲染
|
|
8
|
+
*
|
|
9
|
+
* 本脚本用 pi 真实组件模拟一轮对话并断言行数。pi 升级后补丁没跟上、
|
|
10
|
+
* 或扩展渲染被改动导致行数膨胀时,这里直接失败,挡在发布前。
|
|
11
|
+
*
|
|
12
|
+
* 运行:node scripts/render-regression.mjs(prepublishOnly 会自动跑)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
// pi 的 exports 不暴露 package.json 和 main,直接向上找 node_modules 里的包目录
|
|
21
|
+
// (npm 全局安装、pnpm workspace 都会把包放在搜索路径上的 node_modules 里)
|
|
22
|
+
function findPiRoot() {
|
|
23
|
+
let dir = dirname(dirname(fileURLToPath(import.meta.url))); // packages/cli
|
|
24
|
+
while (dir !== dirname(dir)) {
|
|
25
|
+
const candidate = join(dir, "node_modules", "@earendil-works", "pi-coding-agent");
|
|
26
|
+
if (existsSync(candidate)) return candidate;
|
|
27
|
+
dir = dirname(dir);
|
|
28
|
+
}
|
|
29
|
+
throw new Error("找不到 @earendil-works/pi-coding-agent 包目录");
|
|
30
|
+
}
|
|
31
|
+
const piRoot = findPiRoot();
|
|
32
|
+
|
|
33
|
+
const { initTheme } = await import(join(piRoot, "dist/modes/interactive/theme/theme.js"));
|
|
34
|
+
const { AssistantMessageComponent } = await import(
|
|
35
|
+
join(piRoot, "dist/modes/interactive/components/assistant-message.js")
|
|
36
|
+
);
|
|
37
|
+
const { ToolExecutionComponent } = await import(
|
|
38
|
+
join(piRoot, "dist/modes/interactive/components/tool-execution.js")
|
|
39
|
+
);
|
|
40
|
+
const { createBashTool } = await import(join(piRoot, "dist/core/tools/bash.js"));
|
|
41
|
+
|
|
42
|
+
initTheme("dark");
|
|
43
|
+
|
|
44
|
+
let failed = 0;
|
|
45
|
+
function check(name, actual, expected) {
|
|
46
|
+
const ok = actual === expected;
|
|
47
|
+
if (!ok) failed++;
|
|
48
|
+
console.log(`${ok ? "✓" : "✗"} ${name}: ${actual} 行(期望 ${expected})`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 与 u1s1-compact-ui 扩展一致的隐藏渲染器
|
|
52
|
+
function hiddenToolDef(orig) {
|
|
53
|
+
return {
|
|
54
|
+
name: "bash",
|
|
55
|
+
label: "bash",
|
|
56
|
+
description: orig.description,
|
|
57
|
+
parameters: orig.parameters,
|
|
58
|
+
renderShell: "self",
|
|
59
|
+
async execute() {},
|
|
60
|
+
renderCall() {
|
|
61
|
+
return new Text("", 0, 0);
|
|
62
|
+
},
|
|
63
|
+
renderResult(result, { expanded }, theme, context) {
|
|
64
|
+
if (context.isError) {
|
|
65
|
+
const c = result.content.find((x) => x.type === "text");
|
|
66
|
+
return new Text(theme.fg("warning", "✗ ") + theme.fg("muted", c?.text ?? "error"), 0, 0);
|
|
67
|
+
}
|
|
68
|
+
if (!expanded) return new Text("", 0, 0);
|
|
69
|
+
const c = result.content.find((x) => x.type === "text");
|
|
70
|
+
return new Text(c?.text ?? "", 0, 0);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---- 1. 助手消息(思考+正文,思考隐藏、标签为空):只应有 间隔行+正文 = 2 行 ----
|
|
76
|
+
// pi 补丁失效时这里会变成 4 行(思考块前后各多一个空行)
|
|
77
|
+
const msg = {
|
|
78
|
+
role: "assistant",
|
|
79
|
+
content: [
|
|
80
|
+
{ type: "thinking", thinking: "模型的一段思考" },
|
|
81
|
+
{ type: "text", text: "回复正文" },
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
const am = new AssistantMessageComponent(msg, true, undefined, "", 1, []);
|
|
85
|
+
const chat1 = new Container();
|
|
86
|
+
chat1.addChild(am);
|
|
87
|
+
check("助手消息(思考+正文,思考隐藏)", chat1.render(80).length, 2);
|
|
88
|
+
|
|
89
|
+
// ---- 2. 隐藏的工具行(带结果):0 行 ----
|
|
90
|
+
const origBash = createBashTool(process.cwd());
|
|
91
|
+
const comp = new ToolExecutionComponent(
|
|
92
|
+
"bash",
|
|
93
|
+
"t1",
|
|
94
|
+
{ command: "ls" },
|
|
95
|
+
{},
|
|
96
|
+
hiddenToolDef(origBash),
|
|
97
|
+
null,
|
|
98
|
+
process.cwd(),
|
|
99
|
+
);
|
|
100
|
+
comp.updateResult({ content: [{ type: "text", text: "file1\nfile2\nexit code: 0" }] });
|
|
101
|
+
const chat2 = new Container();
|
|
102
|
+
chat2.addChild(comp);
|
|
103
|
+
check("隐藏的工具行(已完成)", chat2.render(80).length, 0);
|
|
104
|
+
|
|
105
|
+
// ---- 3. 完整一轮:用户消息 + 思考正文回复 + 隐藏工具行 + 第二条回复 ----
|
|
106
|
+
// 期望:间隔 + 用户 + (间隔+正文) + 0 + (间隔+正文) = 6 行
|
|
107
|
+
const chat3 = new Container();
|
|
108
|
+
chat3.addChild(new Spacer(1));
|
|
109
|
+
chat3.addChild(new Text("用户提问", 1, 0));
|
|
110
|
+
chat3.addChild(new AssistantMessageComponent(mkMsg("思考1", "我先看看。"), true, undefined, "", 1, []));
|
|
111
|
+
chat3.addChild(comp);
|
|
112
|
+
chat3.addChild(new AssistantMessageComponent(mkMsg("思考2", "我去查一下。"), true, undefined, "", 1, []));
|
|
113
|
+
function mkMsg(thinking, text) {
|
|
114
|
+
return {
|
|
115
|
+
role: "assistant",
|
|
116
|
+
content: [
|
|
117
|
+
...(thinking ? [{ type: "thinking", thinking }] : []),
|
|
118
|
+
{ type: "text", text },
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
check("完整一轮对话", chat3.render(80).length, 6);
|
|
123
|
+
|
|
124
|
+
if (failed > 0) {
|
|
125
|
+
console.error(`\n渲染回归测试失败:${failed} 项。多半是 pi 升级后补丁没跟上,或扩展渲染被改动。`);
|
|
126
|
+
console.error("对照 patches/@earendil-works__pi-coding-agent*.patch 和 agent-setup.ts 里的 writeCompactUiExtension 检查。");
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
console.log("\n渲染回归测试全部通过 ✓");
|
package/dist/templates.d.ts
DELETED
package/dist/templates.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { readdirSync } from "node:fs";
|
|
2
|
-
/**
|
|
3
|
-
* 空目录首跑模板(roadmap-next #3):小白第一次在空文件夹里启动 u1s1,
|
|
4
|
-
* 面对空白输入框往往想不出第一句说什么。弹一个选择器给几个现成的小项目,
|
|
5
|
-
* 选中后把完整提示词放进输入框——不直接替用户发送,让他看到「原来就是
|
|
6
|
-
* 说这样一句话」,想改就改,回车才开跑。选「我自己说」或按 Esc 都不打扰。
|
|
7
|
-
*/
|
|
8
|
-
const TEMPLATES = [
|
|
9
|
-
{
|
|
10
|
-
label: "🏠 个人主页——介绍我自己的网页",
|
|
11
|
-
prompt: "帮我做一个个人主页网页,介绍我自己,放上我的爱好和联系方式,风格简洁好看,手机和电脑打开都正常。做完教我怎么在浏览器里预览。",
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
label: "📅 倒数日——距离某天还有多少天",
|
|
15
|
-
prompt: "做一个倒数日网页,可以输入日期和事件名,大字显示距离那天还有多少天,设计得可爱一点。做完教我怎么在浏览器里预览。",
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
label: "🖼️ 电子相册——可以翻页的照片墙",
|
|
19
|
-
prompt: "帮我做一个电子相册网页,照片可以左右翻页,先放几张占位图,并告诉我之后怎么换成自己的照片。做完教我怎么在浏览器里预览。",
|
|
20
|
-
},
|
|
21
|
-
];
|
|
22
|
-
const SELF_DESCRIBE = "✍️ 不用了,我自己说";
|
|
23
|
-
/** 只看可见条目:.git / .DS_Store 这类隐藏文件不算「有东西」。读不了就当非空,不打扰。 */
|
|
24
|
-
function isEmptyDir(dir) {
|
|
25
|
-
try {
|
|
26
|
-
return readdirSync(dir).filter((name) => !name.startsWith(".")).length === 0;
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
let offered = false;
|
|
33
|
-
export function offerStarterTemplates(pi) {
|
|
34
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
35
|
-
// /clear 等新会话也会触发 session_start,每次启动只弹一次
|
|
36
|
-
if (ctx.mode !== "tui" || offered)
|
|
37
|
-
return;
|
|
38
|
-
if (!isEmptyDir(process.cwd()))
|
|
39
|
-
return;
|
|
40
|
-
offered = true;
|
|
41
|
-
const choice = await ctx.ui.select("这个文件夹还是空的,挑一个现成的小项目直接开跑?", [...TEMPLATES.map((t) => t.label), SELF_DESCRIBE]);
|
|
42
|
-
const t = TEMPLATES.find((x) => x.label === choice);
|
|
43
|
-
if (!t)
|
|
44
|
-
return;
|
|
45
|
-
ctx.ui.setEditorText(t.prompt);
|
|
46
|
-
ctx.ui.notify("提示词放进输入框了:想改哪句直接改,按回车开跑", "info");
|
|
47
|
-
});
|
|
48
|
-
}
|