u1s1-cli 0.1.0 → 0.3.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.
- package/README.md +6 -4
- package/dist/brand.js +41 -0
- package/dist/config.js +75 -10
- package/dist/index.js +142 -14
- package/dist/login.js +2 -2
- package/dist/model.js +24 -0
- package/dist/style.js +299 -0
- package/dist/themes.js +175 -0
- package/dist/update.js +69 -0
- package/dist/usage.js +6 -6
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# u1s1 — 有一说一,最省心的 AI 编程搭子
|
|
2
2
|
|
|
3
|
-
在终端里用中文说需求,AI 帮你读文件、改代码、跑命令。不用买模型、不用配 API
|
|
3
|
+
在终端里用中文说需求,AI 帮你读文件、改代码、跑命令。不用买模型、不用配 API、不用懂那些名词,注册就送**$10 免费额度**(一次性,用完不补)。
|
|
4
4
|
|
|
5
5
|
## 三步开始
|
|
6
6
|
|
|
@@ -22,13 +22,15 @@ u1s1
|
|
|
22
22
|
|---|---|
|
|
23
23
|
| `u1s1` | 进入交互模式,直接开聊 |
|
|
24
24
|
| `u1s1 -p "把 README 里的错别字修一下"` | 一句话模式,干完就退出 |
|
|
25
|
-
| `u1s1 usage` |
|
|
25
|
+
| `u1s1 usage` | 看额度用了多少 |
|
|
26
|
+
| `u1s1 model` | 看/切默认模型:`u1s1 model grok` 切 Grok 4.6,`u1s1 model deepseek` 切回(对话里 `/model` 同样会记住) |
|
|
27
|
+
| `u1s1 update` | 升级 u1s1 到最新版 |
|
|
26
28
|
| `u1s1 login` / `u1s1 logout` | 登录 / 退出 |
|
|
27
29
|
|
|
28
30
|
## 有一说一
|
|
29
31
|
|
|
30
|
-
-
|
|
31
|
-
- **怎么收费**:额度按 API
|
|
32
|
+
- **背后模型**:默认 DeepSeek V4 Flash(1M 上下文,便宜大碗);难题可随时 `u1s1 model grok` 切 Grok 4.6(更强,但烧额度快约 20 倍)。对话里 `/model` 也会记住,下次启动还是这个。
|
|
33
|
+
- **怎么收费**:额度按 API 实际成本扣,不加价;新用户一次性送 $10,用完不补。
|
|
32
34
|
- **额度不够**:邀请朋友,你俩各得 $1 加量,永不过期 → [u1s1.io/dashboard](https://u1s1.io/dashboard)
|
|
33
35
|
- **内核**:基于开源的 [pi](https://pi.dev) coding agent,会话管理、斜杠命令、皮肤等能力全都有。
|
|
34
36
|
|
package/dist/brand.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
export const BRAND_NAME = "u1s1";
|
|
4
|
+
export const BRAND_CN = "有一说一";
|
|
5
|
+
export const BRAND_TAGLINE = "说人话的 AI 编程搭子";
|
|
6
|
+
export const DASHBOARD_URL = "https://u1s1.io/dashboard";
|
|
7
|
+
export function formatHomePath(path) {
|
|
8
|
+
const home = homedir();
|
|
9
|
+
const resolved = resolve(path);
|
|
10
|
+
if (resolved === home)
|
|
11
|
+
return "~";
|
|
12
|
+
if (resolved.startsWith(`${home}/`) || resolved.startsWith(`${home}\\`)) {
|
|
13
|
+
return `~${resolved.slice(home.length)}`;
|
|
14
|
+
}
|
|
15
|
+
return path;
|
|
16
|
+
}
|
|
17
|
+
/** Compact Claude-like startup header. */
|
|
18
|
+
export function renderBrandHeader(theme, version, cwd) {
|
|
19
|
+
const diamond = theme.fg("accent", "◆");
|
|
20
|
+
const name = theme.bold(theme.fg("text", BRAND_NAME));
|
|
21
|
+
const cn = theme.fg("muted", BRAND_CN);
|
|
22
|
+
const meta = theme.fg("dim", `${BRAND_TAGLINE} · v${version}`);
|
|
23
|
+
const dir = theme.fg("muted", formatHomePath(cwd));
|
|
24
|
+
const hints = [
|
|
25
|
+
theme.fg("accent", "/help"),
|
|
26
|
+
theme.fg("dim", "命令"),
|
|
27
|
+
theme.fg("dim", "·"),
|
|
28
|
+
theme.fg("accent", "Shift+Enter"),
|
|
29
|
+
theme.fg("dim", "换行"),
|
|
30
|
+
theme.fg("dim", "·"),
|
|
31
|
+
theme.fg("accent", "Esc"),
|
|
32
|
+
theme.fg("dim", "中断"),
|
|
33
|
+
].join(" ");
|
|
34
|
+
return ["", ` ${diamond} ${name} ${cn}`, ` ${meta}`, "", ` ${dir}`, ` ${hints}`, ""];
|
|
35
|
+
}
|
|
36
|
+
export function printConsoleBanner(version) {
|
|
37
|
+
console.log("");
|
|
38
|
+
console.log(` ◆ ${BRAND_NAME} ${BRAND_CN}`);
|
|
39
|
+
console.log(` ${BRAND_TAGLINE} · v${version}`);
|
|
40
|
+
console.log("");
|
|
41
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -2,25 +2,83 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "n
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
export const DEFAULT_BASE_URL = "https://api.u1s1.io/v1";
|
|
5
|
-
export const MODEL_ID = "deepseek/deepseek-v4-flash";
|
|
6
5
|
export const PROVIDER_ID = "u1s1";
|
|
6
|
+
export const MODELS = [
|
|
7
|
+
{
|
|
8
|
+
id: "deepseek/deepseek-v4-flash",
|
|
9
|
+
name: "DeepSeek V4 Flash (u1s1)",
|
|
10
|
+
aliases: ["deepseek", "flash", "v4-flash"],
|
|
11
|
+
reasoning: false,
|
|
12
|
+
contextWindow: 1_048_576,
|
|
13
|
+
maxTokens: 65_536,
|
|
14
|
+
cost: { input: 0.14, output: 0.28, cacheRead: 0, cacheWrite: 0 },
|
|
15
|
+
note: "默认 · 便宜大碗,日常写代码首选",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "x-ai/grok-4.6",
|
|
19
|
+
name: "Grok 4.6 (u1s1)",
|
|
20
|
+
aliases: ["grok", "grok4.6", "grok-4.6"],
|
|
21
|
+
reasoning: true,
|
|
22
|
+
contextWindow: 500_000,
|
|
23
|
+
maxTokens: 65_536,
|
|
24
|
+
cost: { input: 2, output: 6, cacheRead: 0, cacheWrite: 0 },
|
|
25
|
+
note: "更强 · 但烧额度快约 20 倍,难题再用",
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
export const DEFAULT_MODEL_ID = MODELS[0].id;
|
|
29
|
+
export function resolveModel(nameOrAlias) {
|
|
30
|
+
const q = nameOrAlias.trim().toLowerCase();
|
|
31
|
+
return MODELS.find((m) => m.id.toLowerCase() === q || m.aliases.includes(q));
|
|
32
|
+
}
|
|
7
33
|
export const u1s1Dir = join(homedir(), ".u1s1");
|
|
8
34
|
const configFile = join(u1s1Dir, "config.json");
|
|
9
35
|
/** pi keeps auth/models/settings/sessions under this dir — isolated from any real pi install. */
|
|
10
36
|
export const agentDir = join(u1s1Dir, "agent");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (existsSync(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
37
|
+
const agentSettingsFile = join(agentDir, "settings.json");
|
|
38
|
+
function readJsonFile(path) {
|
|
39
|
+
if (!existsSync(path))
|
|
40
|
+
return undefined;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
20
46
|
}
|
|
47
|
+
}
|
|
48
|
+
/** Model last chosen in-session via /model (pi writes this). */
|
|
49
|
+
export function readAgentDefaultModel() {
|
|
50
|
+
const settings = readJsonFile(agentSettingsFile);
|
|
51
|
+
if (!settings)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (settings["defaultProvider"] !== PROVIDER_ID)
|
|
54
|
+
return undefined;
|
|
55
|
+
const id = settings["defaultModel"];
|
|
56
|
+
return typeof id === "string" && resolveModel(id) ? id : undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Keep pi's settings.json in sync so /model and `u1s1 model` share one default. */
|
|
59
|
+
export function writeAgentDefaultModel(modelId) {
|
|
60
|
+
mkdirSync(agentDir, { recursive: true });
|
|
61
|
+
const settings = readJsonFile(agentSettingsFile) ?? {};
|
|
62
|
+
if (settings["defaultProvider"] === PROVIDER_ID && settings["defaultModel"] === modelId)
|
|
63
|
+
return;
|
|
64
|
+
settings["defaultProvider"] = PROVIDER_ID;
|
|
65
|
+
settings["defaultModel"] = modelId;
|
|
66
|
+
writeFileSync(agentSettingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* After this fix both stores stay in sync. If they still disagree (old installs),
|
|
70
|
+
* prefer the in-session /model value — that's the one users thought they had set.
|
|
71
|
+
*/
|
|
72
|
+
export function resolvePreferredModel(configModel) {
|
|
73
|
+
const agentModel = readAgentDefaultModel();
|
|
74
|
+
return agentModel ?? configModel ?? DEFAULT_MODEL_ID;
|
|
75
|
+
}
|
|
76
|
+
export function loadConfig() {
|
|
77
|
+
const file = (readJsonFile(configFile) ?? {});
|
|
21
78
|
return {
|
|
22
79
|
apiKey: process.env["U1S1_API_KEY"] || file.apiKey,
|
|
23
80
|
baseUrl: process.env["U1S1_BASE_URL"] || file.baseUrl || DEFAULT_BASE_URL,
|
|
81
|
+
model: file.model && resolveModel(file.model) ? file.model : undefined,
|
|
24
82
|
};
|
|
25
83
|
}
|
|
26
84
|
export function saveConfig(cfg) {
|
|
@@ -28,3 +86,10 @@ export function saveConfig(cfg) {
|
|
|
28
86
|
writeFileSync(configFile, JSON.stringify(cfg, null, 2) + "\n");
|
|
29
87
|
chmodSync(configFile, 0o600);
|
|
30
88
|
}
|
|
89
|
+
/** Persist the user's preferred model to both stores. */
|
|
90
|
+
export function persistPreferredModel(cfg, modelId) {
|
|
91
|
+
const next = { ...cfg, model: modelId };
|
|
92
|
+
saveConfig(next);
|
|
93
|
+
writeAgentDefaultModel(modelId);
|
|
94
|
+
return next;
|
|
95
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
|
-
import {
|
|
6
|
+
import { printConsoleBanner } from "./brand.js";
|
|
7
|
+
import { agentDir, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, resolvePreferredModel, } from "./config.js";
|
|
8
|
+
import { applyBrandUi } from "./style.js";
|
|
9
|
+
import { DEFAULT_THEME_SETTING, ensureBrandThemes } from "./themes.js";
|
|
6
10
|
const require = createRequire(import.meta.url);
|
|
7
11
|
const VERSION = require("../package.json").version;
|
|
8
12
|
const BRAND_APPEND = `## u1s1
|
|
@@ -21,38 +25,149 @@ function ensureBrandPrompt() {
|
|
|
21
25
|
writeFileSync(p, BRAND_APPEND);
|
|
22
26
|
}
|
|
23
27
|
}
|
|
28
|
+
/** Defaults that don't overwrite values the user already set. */
|
|
29
|
+
function ensureDefaultSettings() {
|
|
30
|
+
mkdirSync(agentDir, { recursive: true });
|
|
31
|
+
const p = join(agentDir, "settings.json");
|
|
32
|
+
let settings = {};
|
|
33
|
+
if (existsSync(p)) {
|
|
34
|
+
try {
|
|
35
|
+
settings = JSON.parse(readFileSync(p, "utf8"));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let changed = false;
|
|
42
|
+
if (!("hideThinkingBlock" in settings)) {
|
|
43
|
+
settings.hideThinkingBlock = true;
|
|
44
|
+
changed = true;
|
|
45
|
+
}
|
|
46
|
+
// Default / leftover Pi themes → u1s1 Claude-like pair. Custom names stay put.
|
|
47
|
+
const theme = settings.theme;
|
|
48
|
+
if (theme === undefined || theme === "dark" || theme === "light") {
|
|
49
|
+
settings.theme = DEFAULT_THEME_SETTING;
|
|
50
|
+
changed = true;
|
|
51
|
+
}
|
|
52
|
+
if (changed)
|
|
53
|
+
writeFileSync(p, JSON.stringify(settings, null, 2) + "\n");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* tmux 默认不转发键盘修饰键,会把 Shift+Enter 当成普通回车发出去,消息没写完就被发送。
|
|
57
|
+
* 只开 extended-keys 不够:默认 terminal-features 里 xterm* 没有 extkeys,tmux 根本不会
|
|
58
|
+
* 向外层终端要 modifyOtherKeys,S-Enter 绑定也就永远触发不了。
|
|
59
|
+
* 启动时自动做这些事(运行时生效,不写用户 tmux.conf;失败则静默忽略,仍可用 Ctrl+J 换行):
|
|
60
|
+
* 1. 开启 extended-keys,并给当前终端补上 extkeys;
|
|
61
|
+
* 2. tmux 3.5+ 再切到 csi-u(3.4 及以下没有这个选项,忽略即可);
|
|
62
|
+
* 3. 兜底把 S-Enter / C-Enter 原样转成 pi 认得的 CSI-u 序列。
|
|
63
|
+
*/
|
|
64
|
+
function ensureTmuxKeyboardProtocol() {
|
|
65
|
+
if (!process.env.TMUX)
|
|
66
|
+
return;
|
|
67
|
+
const run = (args) => spawnSync("tmux", args, { timeout: 1500, encoding: "utf8" });
|
|
68
|
+
if (run(["-V"]).status !== 0)
|
|
69
|
+
return; // tmux 不可用(如沙箱)则跳过
|
|
70
|
+
// 当前会话 + 全局默认都开,避免只改 -g 时已有会话仍是 off
|
|
71
|
+
run(["set-option", "-g", "extended-keys", "on"]);
|
|
72
|
+
run(["set-option", "extended-keys", "on"]);
|
|
73
|
+
// 3.5+; 3.4 会失败,忽略
|
|
74
|
+
run(["set-option", "-g", "extended-keys-format", "csi-u"]);
|
|
75
|
+
run(["set-option", "extended-keys-format", "csi-u"]);
|
|
76
|
+
const features = run(["show", "-gv", "terminal-features"]);
|
|
77
|
+
if (features.status === 0 && !/\bextkeys\b/.test(features.stdout ?? "")) {
|
|
78
|
+
run(["set-option", "-ga", "terminal-features", "xterm*:extkeys"]);
|
|
79
|
+
}
|
|
80
|
+
// 已挂上的客户端不会自动重读 terminal-features;直接往当前客户端 tty
|
|
81
|
+
// 发 modifyOtherKeys,让外层终端立刻开始区分 Shift+Enter。
|
|
82
|
+
const tty = run(["display-message", "-p", "#{client_tty}"]).stdout?.trim();
|
|
83
|
+
if (tty) {
|
|
84
|
+
try {
|
|
85
|
+
writeFileSync(tty, "\x1b[>4;2m");
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// 没权限或不是 tty 就忽略
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// -l: 按字面量发送,避免 send-keys 把转义序列拆成一串普通按键
|
|
92
|
+
run(["bind-key", "-n", "S-Enter", "send-keys", "-l", "\x1b[13;2u"]);
|
|
93
|
+
run(["bind-key", "-n", "C-Enter", "send-keys", "-l", "\x1b[13;5u"]);
|
|
94
|
+
}
|
|
24
95
|
async function runAgent(cfg, args) {
|
|
96
|
+
ensureBrandThemes();
|
|
25
97
|
ensureBrandPrompt();
|
|
98
|
+
ensureDefaultSettings();
|
|
99
|
+
ensureTmuxKeyboardProtocol();
|
|
26
100
|
// must be set before pi reads them (getAgentDir() reads at call time, env at import is fine too)
|
|
27
101
|
process.env["PI_CODING_AGENT_DIR"] = agentDir;
|
|
28
102
|
process.env["U1S1_API_KEY"] = cfg.apiKey;
|
|
29
|
-
|
|
103
|
+
// hide pi's own "Run pi update" banner; users should run `u1s1 update` instead
|
|
104
|
+
process.env["PI_SKIP_VERSION_CHECK"] = "1";
|
|
105
|
+
const { CustomEditor, main } = await import("@earendil-works/pi-coding-agent");
|
|
106
|
+
// 网页终端(如 Taikula)把 Shift+Enter 发成 ESC+CR(\x1b\r)。pi 在没开 Kitty
|
|
107
|
+
// 协议时把它当成 Alt+Enter:空闲就直接发送,忙碌才排队。先改成 CSI-u 的
|
|
108
|
+
// Shift+Enter,输入框就会换行,真·Alt+Enter(\x1b[13;3u)不受影响。
|
|
109
|
+
class ShiftEnterEditor extends CustomEditor {
|
|
110
|
+
handleInput(data) {
|
|
111
|
+
super.handleInput(data === "\x1b\r" ? "\x1b[13;2u" : data);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
30
114
|
const extension = [
|
|
31
115
|
{
|
|
32
116
|
name: "u1s1",
|
|
33
117
|
factory: (pi) => {
|
|
118
|
+
applyBrandUi(pi, VERSION);
|
|
119
|
+
// pi 内置只有 /quit;补一个更常见的 /exit,方便新手退出
|
|
120
|
+
pi.registerCommand("exit", {
|
|
121
|
+
description: "退出 u1s1",
|
|
122
|
+
handler: async (_args, ctx) => {
|
|
123
|
+
ctx.shutdown();
|
|
124
|
+
},
|
|
125
|
+
});
|
|
34
126
|
pi.registerProvider(PROVIDER_ID, {
|
|
35
127
|
name: "u1s1",
|
|
36
128
|
baseUrl: cfg.baseUrl,
|
|
37
129
|
api: "openai-completions",
|
|
38
130
|
apiKey: "$U1S1_API_KEY",
|
|
39
|
-
models:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
131
|
+
models: MODELS.map((m) => ({
|
|
132
|
+
id: m.id,
|
|
133
|
+
name: m.name,
|
|
134
|
+
reasoning: m.reasoning,
|
|
135
|
+
input: ["text"],
|
|
136
|
+
cost: m.cost,
|
|
137
|
+
contextWindow: m.contextWindow,
|
|
138
|
+
maxTokens: m.maxTokens,
|
|
139
|
+
})),
|
|
140
|
+
});
|
|
141
|
+
// /model and Ctrl+P already write pi settings; also keep ~/.u1s1/config.json in sync
|
|
142
|
+
pi.on("model_select", (event) => {
|
|
143
|
+
if (event.source === "restore")
|
|
144
|
+
return;
|
|
145
|
+
if (event.model.provider !== PROVIDER_ID)
|
|
146
|
+
return;
|
|
147
|
+
if (!MODELS.some((m) => m.id === event.model.id))
|
|
148
|
+
return;
|
|
149
|
+
persistPreferredModel(loadConfig(), event.model.id);
|
|
150
|
+
});
|
|
151
|
+
pi.on("session_start", (_event, ctx) => {
|
|
152
|
+
const previous = ctx.ui.getEditorComponent();
|
|
153
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
154
|
+
if (previous) {
|
|
155
|
+
const inner = previous(tui, theme, keybindings);
|
|
156
|
+
const orig = inner.handleInput.bind(inner);
|
|
157
|
+
inner.handleInput = (data) => orig(data === "\x1b\r" ? "\x1b[13;2u" : data);
|
|
158
|
+
return inner;
|
|
159
|
+
}
|
|
160
|
+
return new ShiftEnterEditor(tui, theme, keybindings);
|
|
161
|
+
});
|
|
50
162
|
});
|
|
51
163
|
},
|
|
52
164
|
},
|
|
53
165
|
];
|
|
54
166
|
const hasModelArg = args.some((a) => a === "--model" || a.startsWith("--model=") || a === "--provider");
|
|
55
|
-
const
|
|
167
|
+
const defaultModel = resolvePreferredModel(cfg.model);
|
|
168
|
+
if (cfg.model !== defaultModel)
|
|
169
|
+
persistPreferredModel(cfg, defaultModel);
|
|
170
|
+
const finalArgs = hasModelArg ? args : ["--model", `${PROVIDER_ID}/${defaultModel}`, ...args];
|
|
56
171
|
await main(finalArgs, { extensionFactories: extension });
|
|
57
172
|
}
|
|
58
173
|
async function run() {
|
|
@@ -62,6 +177,9 @@ async function run() {
|
|
|
62
177
|
console.log(`u1s1 v${VERSION}`);
|
|
63
178
|
return;
|
|
64
179
|
}
|
|
180
|
+
if (cmd === "--help" || cmd === "-h") {
|
|
181
|
+
printConsoleBanner(VERSION);
|
|
182
|
+
}
|
|
65
183
|
if (cmd === "login") {
|
|
66
184
|
const { login } = await import("./login.js");
|
|
67
185
|
await login(args[1]);
|
|
@@ -72,12 +190,22 @@ async function run() {
|
|
|
72
190
|
await usage();
|
|
73
191
|
return;
|
|
74
192
|
}
|
|
193
|
+
if (cmd === "model") {
|
|
194
|
+
const { modelCommand } = await import("./model.js");
|
|
195
|
+
await modelCommand(args[1]);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
75
198
|
if (cmd === "logout") {
|
|
76
199
|
const { saveConfig } = await import("./config.js");
|
|
77
200
|
saveConfig({ ...loadConfig(), apiKey: undefined });
|
|
78
201
|
console.log("已退出登录。");
|
|
79
202
|
return;
|
|
80
203
|
}
|
|
204
|
+
if (cmd === "update") {
|
|
205
|
+
const { update } = await import("./update.js");
|
|
206
|
+
await update();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
81
209
|
const { ensureAuth } = await import("./login.js");
|
|
82
210
|
const cfg = await ensureAuth();
|
|
83
211
|
await runAgent(cfg, args);
|
package/dist/login.js
CHANGED
|
@@ -16,7 +16,7 @@ export async function login(keyArg) {
|
|
|
16
16
|
if (!key) {
|
|
17
17
|
console.log("");
|
|
18
18
|
console.log(" u1s1 需要一把 API Key(免费):");
|
|
19
|
-
console.log(` 1. 打开 ${DASHBOARD_URL} 注册/登录(30
|
|
19
|
+
console.log(` 1. 打开 ${DASHBOARD_URL} 注册/登录(30 秒,送 $10 额度)`);
|
|
20
20
|
console.log(" 2. 复制你的 API Key,粘贴到下面");
|
|
21
21
|
console.log("");
|
|
22
22
|
tryOpenBrowser(DASHBOARD_URL);
|
|
@@ -34,7 +34,7 @@ export async function login(keyArg) {
|
|
|
34
34
|
process.exit(1);
|
|
35
35
|
});
|
|
36
36
|
saveConfig(next);
|
|
37
|
-
console.log(` ✓ 登录成功${me.email ? `(${me.email})` : ""}
|
|
37
|
+
console.log(` ✓ 登录成功${me.email ? `(${me.email})` : ""},还剩 $${me.remaining_usd} 额度。`);
|
|
38
38
|
return next;
|
|
39
39
|
}
|
|
40
40
|
/** Returns a config that definitely has an apiKey, prompting the user if needed. */
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { loadConfig, MODELS, persistPreferredModel, resolveModel, resolvePreferredModel } from "./config.js";
|
|
2
|
+
export async function modelCommand(nameOrAlias) {
|
|
3
|
+
const cfg = loadConfig();
|
|
4
|
+
const current = resolvePreferredModel(cfg.model);
|
|
5
|
+
if (!nameOrAlias) {
|
|
6
|
+
console.log("");
|
|
7
|
+
for (const m of MODELS) {
|
|
8
|
+
const mark = m.id === current ? "●" : " ";
|
|
9
|
+
console.log(` ${mark} ${m.aliases[0].padEnd(10)} ${m.name}`);
|
|
10
|
+
console.log(` ${m.note} · $${m.cost.input}/$${m.cost.output} 每百万 token`);
|
|
11
|
+
}
|
|
12
|
+
console.log("");
|
|
13
|
+
console.log(" 切换:u1s1 model grok / u1s1 model deepseek(对话里 /model 同样会记住)");
|
|
14
|
+
console.log("");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const m = resolveModel(nameOrAlias);
|
|
18
|
+
if (!m) {
|
|
19
|
+
console.error(` 没有叫「${nameOrAlias}」的模型,可选:${MODELS.map((x) => x.aliases[0]).join(" / ")}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
persistPreferredModel(cfg, m.id);
|
|
23
|
+
console.log(` ✓ 默认模型已切到 ${m.name}(${m.note})`);
|
|
24
|
+
}
|
package/dist/style.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { createBashTool, createEditTool, createFindTool, createGrepTool, createLsTool, createReadTool, createWriteTool, } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { renderBrandHeader } from "./brand.js";
|
|
5
|
+
function row(text) {
|
|
6
|
+
return {
|
|
7
|
+
render() {
|
|
8
|
+
return text.split("\n");
|
|
9
|
+
},
|
|
10
|
+
invalidate() { },
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function shortPath(path) {
|
|
14
|
+
if (typeof path !== "string" || !path)
|
|
15
|
+
return "";
|
|
16
|
+
const home = homedir();
|
|
17
|
+
if (path === home)
|
|
18
|
+
return "~";
|
|
19
|
+
if (path.startsWith(`${home}/`) || path.startsWith(`${home}\\`)) {
|
|
20
|
+
return `~${path.slice(home.length)}`;
|
|
21
|
+
}
|
|
22
|
+
const cwd = process.cwd();
|
|
23
|
+
if (path === cwd)
|
|
24
|
+
return ".";
|
|
25
|
+
if (path.startsWith(`${cwd}/`) || path.startsWith(`${cwd}\\`)) {
|
|
26
|
+
return path.slice(cwd.length + 1);
|
|
27
|
+
}
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
function asString(value) {
|
|
31
|
+
return typeof value === "string" ? value : "";
|
|
32
|
+
}
|
|
33
|
+
function clip(text, max = 72) {
|
|
34
|
+
if (text.length <= max)
|
|
35
|
+
return text;
|
|
36
|
+
return `${text.slice(0, max - 1)}…`;
|
|
37
|
+
}
|
|
38
|
+
function callLine(theme, name, detail) {
|
|
39
|
+
let text = `${theme.fg("accent", "⏺")} ${theme.bold(theme.fg("toolTitle", name))}`;
|
|
40
|
+
if (detail)
|
|
41
|
+
text += ` ${theme.fg("muted", clip(detail))}`;
|
|
42
|
+
return text;
|
|
43
|
+
}
|
|
44
|
+
function resultLine(theme, text, color = "dim") {
|
|
45
|
+
return ` ${theme.fg("dim", "⎿")} ${theme.fg(color, text)}`;
|
|
46
|
+
}
|
|
47
|
+
function indentOutput(theme, output, limit = 20) {
|
|
48
|
+
const all = output.split("\n");
|
|
49
|
+
const shown = all.slice(0, limit);
|
|
50
|
+
let text = shown.map((line) => ` ${theme.fg("dim", line)}`).join("\n");
|
|
51
|
+
if (all.length > limit) {
|
|
52
|
+
text += `\n ${theme.fg("muted", `… ${all.length - limit} 行`)}`;
|
|
53
|
+
}
|
|
54
|
+
return text;
|
|
55
|
+
}
|
|
56
|
+
const toolCache = new Map();
|
|
57
|
+
function createBuiltInTools(cwd) {
|
|
58
|
+
return {
|
|
59
|
+
read: createReadTool(cwd),
|
|
60
|
+
bash: createBashTool(cwd),
|
|
61
|
+
edit: createEditTool(cwd),
|
|
62
|
+
write: createWriteTool(cwd),
|
|
63
|
+
find: createFindTool(cwd),
|
|
64
|
+
grep: createGrepTool(cwd),
|
|
65
|
+
ls: createLsTool(cwd),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function toolsFor(cwd) {
|
|
69
|
+
let cached = toolCache.get(cwd);
|
|
70
|
+
if (!cached) {
|
|
71
|
+
cached = createBuiltInTools(cwd);
|
|
72
|
+
toolCache.set(cwd, cached);
|
|
73
|
+
}
|
|
74
|
+
return cached;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Claude Code-like chrome: brand header, pulsing dot, one-line tool rows.
|
|
78
|
+
* All of this is Pi's public extension API — no node_modules patch.
|
|
79
|
+
*/
|
|
80
|
+
export function applyBrandUi(pi, version) {
|
|
81
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
82
|
+
if (ctx.mode !== "tui")
|
|
83
|
+
return;
|
|
84
|
+
ctx.ui.setHeader((_tui, theme) => ({
|
|
85
|
+
render() {
|
|
86
|
+
return renderBrandHeader(theme, version, process.cwd());
|
|
87
|
+
},
|
|
88
|
+
invalidate() { },
|
|
89
|
+
}));
|
|
90
|
+
const theme = ctx.ui.theme;
|
|
91
|
+
ctx.ui.setWorkingMessage("思考中");
|
|
92
|
+
ctx.ui.setWorkingIndicator({
|
|
93
|
+
frames: [
|
|
94
|
+
theme.fg("dim", "·"),
|
|
95
|
+
theme.fg("muted", "•"),
|
|
96
|
+
theme.fg("accent", "●"),
|
|
97
|
+
theme.fg("muted", "•"),
|
|
98
|
+
],
|
|
99
|
+
intervalMs: 140,
|
|
100
|
+
});
|
|
101
|
+
ctx.ui.setHiddenThinkingLabel("思考中");
|
|
102
|
+
ctx.ui.setTitle(`u1s1 — ${basename(process.cwd())}`);
|
|
103
|
+
});
|
|
104
|
+
const cwd = process.cwd();
|
|
105
|
+
const originals = toolsFor(cwd);
|
|
106
|
+
pi.registerTool({
|
|
107
|
+
name: "read",
|
|
108
|
+
label: "read",
|
|
109
|
+
description: originals.read.description,
|
|
110
|
+
parameters: originals.read.parameters,
|
|
111
|
+
renderShell: "self",
|
|
112
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
113
|
+
return toolsFor(ctx.cwd).read.execute(toolCallId, params, signal, onUpdate);
|
|
114
|
+
},
|
|
115
|
+
renderCall(args, theme) {
|
|
116
|
+
return row(callLine(theme, "Read", shortPath(args.path)));
|
|
117
|
+
},
|
|
118
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
119
|
+
if (isPartial)
|
|
120
|
+
return row(resultLine(theme, "读取中…", "warning"));
|
|
121
|
+
const content = result.content[0];
|
|
122
|
+
if (content?.type === "image")
|
|
123
|
+
return row(resultLine(theme, "图片", "success"));
|
|
124
|
+
if (content?.type !== "text")
|
|
125
|
+
return row(resultLine(theme, "无内容", "error"));
|
|
126
|
+
const details = result.details;
|
|
127
|
+
const count = content.text.split("\n").length;
|
|
128
|
+
let text = resultLine(theme, `${count} 行`, "success");
|
|
129
|
+
if (details?.truncation?.truncated)
|
|
130
|
+
text += theme.fg("warning", " · 已截断");
|
|
131
|
+
if (expanded)
|
|
132
|
+
text += `\n${indentOutput(theme, content.text)}`;
|
|
133
|
+
return row(text);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
pi.registerTool({
|
|
137
|
+
name: "bash",
|
|
138
|
+
label: "bash",
|
|
139
|
+
description: originals.bash.description,
|
|
140
|
+
parameters: originals.bash.parameters,
|
|
141
|
+
renderShell: "self",
|
|
142
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
143
|
+
return toolsFor(ctx.cwd).bash.execute(toolCallId, params, signal, onUpdate);
|
|
144
|
+
},
|
|
145
|
+
renderCall(args, theme) {
|
|
146
|
+
return row(callLine(theme, "Bash", asString(args.command)));
|
|
147
|
+
},
|
|
148
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
149
|
+
if (isPartial)
|
|
150
|
+
return row(resultLine(theme, "执行中…", "warning"));
|
|
151
|
+
const content = result.content[0];
|
|
152
|
+
const output = content?.type === "text" ? content.text : "";
|
|
153
|
+
const exitMatch = output.match(/exit code: (\d+)/);
|
|
154
|
+
const exitCode = exitMatch ? Number(exitMatch[1]) : null;
|
|
155
|
+
const details = result.details;
|
|
156
|
+
const ok = exitCode === 0 || exitCode === null;
|
|
157
|
+
let text = resultLine(theme, ok ? "完成" : `退出码 ${exitCode}`, ok ? "success" : "error");
|
|
158
|
+
if (details?.truncation?.truncated)
|
|
159
|
+
text += theme.fg("warning", " · 已截断");
|
|
160
|
+
if (expanded && output)
|
|
161
|
+
text += `\n${indentOutput(theme, output)}`;
|
|
162
|
+
return row(text);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
pi.registerTool({
|
|
166
|
+
name: "edit",
|
|
167
|
+
label: "edit",
|
|
168
|
+
description: originals.edit.description,
|
|
169
|
+
parameters: originals.edit.parameters,
|
|
170
|
+
renderShell: "self",
|
|
171
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
172
|
+
return toolsFor(ctx.cwd).edit.execute(toolCallId, params, signal, onUpdate);
|
|
173
|
+
},
|
|
174
|
+
renderCall(args, theme) {
|
|
175
|
+
return row(callLine(theme, "Edit", shortPath(args.path)));
|
|
176
|
+
},
|
|
177
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
178
|
+
if (isPartial)
|
|
179
|
+
return row(resultLine(theme, "修改中…", "warning"));
|
|
180
|
+
const content = result.content[0];
|
|
181
|
+
if (content?.type === "text" && content.text.startsWith("Error")) {
|
|
182
|
+
return row(resultLine(theme, content.text.split("\n")[0] ?? "失败", "error"));
|
|
183
|
+
}
|
|
184
|
+
const details = result.details;
|
|
185
|
+
if (!details?.diff)
|
|
186
|
+
return row(resultLine(theme, "已写入", "success"));
|
|
187
|
+
let additions = 0;
|
|
188
|
+
let removals = 0;
|
|
189
|
+
for (const line of details.diff.split("\n")) {
|
|
190
|
+
if (line.startsWith("+") && !line.startsWith("+++"))
|
|
191
|
+
additions++;
|
|
192
|
+
if (line.startsWith("-") && !line.startsWith("---"))
|
|
193
|
+
removals++;
|
|
194
|
+
}
|
|
195
|
+
let text = resultLine(theme, `+${additions} / -${removals}`, "success");
|
|
196
|
+
if (expanded)
|
|
197
|
+
text += `\n${indentOutput(theme, details.diff, 30)}`;
|
|
198
|
+
return row(text);
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
pi.registerTool({
|
|
202
|
+
name: "write",
|
|
203
|
+
label: "write",
|
|
204
|
+
description: originals.write.description,
|
|
205
|
+
parameters: originals.write.parameters,
|
|
206
|
+
renderShell: "self",
|
|
207
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
208
|
+
return toolsFor(ctx.cwd).write.execute(toolCallId, params, signal, onUpdate);
|
|
209
|
+
},
|
|
210
|
+
renderCall(args, theme) {
|
|
211
|
+
const count = asString(args.content).split("\n").length;
|
|
212
|
+
const detail = `${shortPath(args.path)}${count ? ` (${count} 行)` : ""}`;
|
|
213
|
+
return row(callLine(theme, "Write", detail));
|
|
214
|
+
},
|
|
215
|
+
renderResult(result, { isPartial }, theme) {
|
|
216
|
+
if (isPartial)
|
|
217
|
+
return row(resultLine(theme, "写入中…", "warning"));
|
|
218
|
+
const content = result.content[0];
|
|
219
|
+
if (content?.type === "text" && content.text.startsWith("Error")) {
|
|
220
|
+
return row(resultLine(theme, content.text.split("\n")[0] ?? "失败", "error"));
|
|
221
|
+
}
|
|
222
|
+
return row(resultLine(theme, "已写入", "success"));
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
pi.registerTool({
|
|
226
|
+
name: "grep",
|
|
227
|
+
label: "grep",
|
|
228
|
+
description: originals.grep.description,
|
|
229
|
+
parameters: originals.grep.parameters,
|
|
230
|
+
renderShell: "self",
|
|
231
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
232
|
+
return toolsFor(ctx.cwd).grep.execute(toolCallId, params, signal, onUpdate);
|
|
233
|
+
},
|
|
234
|
+
renderCall(args, theme) {
|
|
235
|
+
const where = shortPath(args.path);
|
|
236
|
+
const detail = where ? `${asString(args.pattern)} ${where}` : asString(args.pattern);
|
|
237
|
+
return row(callLine(theme, "Grep", detail));
|
|
238
|
+
},
|
|
239
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
240
|
+
if (isPartial)
|
|
241
|
+
return row(resultLine(theme, "搜索中…", "warning"));
|
|
242
|
+
const content = result.content[0];
|
|
243
|
+
const output = content?.type === "text" ? content.text : "";
|
|
244
|
+
const hits = output ? output.split("\n").filter(Boolean).length : 0;
|
|
245
|
+
let text = resultLine(theme, hits ? `${hits} 处` : "无匹配", hits ? "success" : "dim");
|
|
246
|
+
if (expanded && output)
|
|
247
|
+
text += `\n${indentOutput(theme, output)}`;
|
|
248
|
+
return row(text);
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
pi.registerTool({
|
|
252
|
+
name: "find",
|
|
253
|
+
label: "find",
|
|
254
|
+
description: originals.find.description,
|
|
255
|
+
parameters: originals.find.parameters,
|
|
256
|
+
renderShell: "self",
|
|
257
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
258
|
+
return toolsFor(ctx.cwd).find.execute(toolCallId, params, signal, onUpdate);
|
|
259
|
+
},
|
|
260
|
+
renderCall(args, theme) {
|
|
261
|
+
return row(callLine(theme, "Find", asString(args.pattern)));
|
|
262
|
+
},
|
|
263
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
264
|
+
if (isPartial)
|
|
265
|
+
return row(resultLine(theme, "查找中…", "warning"));
|
|
266
|
+
const content = result.content[0];
|
|
267
|
+
const output = content?.type === "text" ? content.text : "";
|
|
268
|
+
const hits = output ? output.split("\n").filter(Boolean).length : 0;
|
|
269
|
+
let text = resultLine(theme, hits ? `${hits} 个文件` : "没找到", hits ? "success" : "dim");
|
|
270
|
+
if (expanded && output)
|
|
271
|
+
text += `\n${indentOutput(theme, output)}`;
|
|
272
|
+
return row(text);
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
pi.registerTool({
|
|
276
|
+
name: "ls",
|
|
277
|
+
label: "ls",
|
|
278
|
+
description: originals.ls.description,
|
|
279
|
+
parameters: originals.ls.parameters,
|
|
280
|
+
renderShell: "self",
|
|
281
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
282
|
+
return toolsFor(ctx.cwd).ls.execute(toolCallId, params, signal, onUpdate);
|
|
283
|
+
},
|
|
284
|
+
renderCall(args, theme) {
|
|
285
|
+
return row(callLine(theme, "List", shortPath(args.path) || "."));
|
|
286
|
+
},
|
|
287
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
288
|
+
if (isPartial)
|
|
289
|
+
return row(resultLine(theme, "列出中…", "warning"));
|
|
290
|
+
const content = result.content[0];
|
|
291
|
+
const output = content?.type === "text" ? content.text : "";
|
|
292
|
+
const hits = output ? output.split("\n").filter(Boolean).length : 0;
|
|
293
|
+
let text = resultLine(theme, `${hits} 项`, "success");
|
|
294
|
+
if (expanded && output)
|
|
295
|
+
text += `\n${indentOutput(theme, output)}`;
|
|
296
|
+
return row(text);
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
package/dist/themes.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { agentDir } from "./config.js";
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code-like: warm terracotta accent, no gray message/tool boxes,
|
|
6
|
+
* text sits on the terminal background instead of Codex-style cards.
|
|
7
|
+
*/
|
|
8
|
+
export const DARK_THEME = {
|
|
9
|
+
name: "u1s1-dark",
|
|
10
|
+
vars: {
|
|
11
|
+
orange: "#d97757",
|
|
12
|
+
orangeSoft: "#e8a87c",
|
|
13
|
+
text: "#e8e4db",
|
|
14
|
+
muted: "#9a9488",
|
|
15
|
+
dim: "#6b665e",
|
|
16
|
+
green: "#8fbc8f",
|
|
17
|
+
red: "#e07a7a",
|
|
18
|
+
yellow: "#d4a574",
|
|
19
|
+
blue: "#89b4c4",
|
|
20
|
+
selectedBg: "#3a342e",
|
|
21
|
+
},
|
|
22
|
+
colors: {
|
|
23
|
+
accent: "orange",
|
|
24
|
+
border: "orangeSoft",
|
|
25
|
+
borderAccent: "orange",
|
|
26
|
+
borderMuted: "dim",
|
|
27
|
+
success: "green",
|
|
28
|
+
error: "red",
|
|
29
|
+
warning: "yellow",
|
|
30
|
+
muted: "muted",
|
|
31
|
+
dim: "dim",
|
|
32
|
+
text: "text",
|
|
33
|
+
thinkingText: "muted",
|
|
34
|
+
selectedBg: "selectedBg",
|
|
35
|
+
scrollbarThumb: "selectedBg",
|
|
36
|
+
searchMatchBg: "selectedBg",
|
|
37
|
+
searchMatchText: "text",
|
|
38
|
+
userMessageBg: "",
|
|
39
|
+
userMessageText: "text",
|
|
40
|
+
customMessageBg: "",
|
|
41
|
+
customMessageText: "text",
|
|
42
|
+
customMessageLabel: "orangeSoft",
|
|
43
|
+
toolPendingBg: "",
|
|
44
|
+
toolSuccessBg: "",
|
|
45
|
+
toolErrorBg: "",
|
|
46
|
+
toolTitle: "text",
|
|
47
|
+
toolOutput: "muted",
|
|
48
|
+
mdHeading: "orangeSoft",
|
|
49
|
+
mdLink: "blue",
|
|
50
|
+
mdLinkUrl: "dim",
|
|
51
|
+
mdCode: "orangeSoft",
|
|
52
|
+
mdCodeBlock: "text",
|
|
53
|
+
mdCodeBlockBorder: "dim",
|
|
54
|
+
mdQuote: "muted",
|
|
55
|
+
mdQuoteBorder: "orange",
|
|
56
|
+
mdHr: "dim",
|
|
57
|
+
mdListBullet: "orange",
|
|
58
|
+
toolDiffAdded: "green",
|
|
59
|
+
toolDiffRemoved: "red",
|
|
60
|
+
toolDiffContext: "muted",
|
|
61
|
+
syntaxComment: "muted",
|
|
62
|
+
syntaxKeyword: "orangeSoft",
|
|
63
|
+
syntaxFunction: "orange",
|
|
64
|
+
syntaxVariable: "text",
|
|
65
|
+
syntaxString: "green",
|
|
66
|
+
syntaxNumber: "yellow",
|
|
67
|
+
syntaxType: "blue",
|
|
68
|
+
syntaxOperator: "muted",
|
|
69
|
+
syntaxPunctuation: "dim",
|
|
70
|
+
thinkingOff: "dim",
|
|
71
|
+
thinkingMinimal: "muted",
|
|
72
|
+
thinkingLow: "blue",
|
|
73
|
+
thinkingMedium: "orangeSoft",
|
|
74
|
+
thinkingHigh: "orange",
|
|
75
|
+
thinkingXhigh: "yellow",
|
|
76
|
+
thinkingMax: "red",
|
|
77
|
+
bashMode: "green",
|
|
78
|
+
},
|
|
79
|
+
export: {
|
|
80
|
+
pageBg: "#1c1917",
|
|
81
|
+
cardBg: "#292524",
|
|
82
|
+
infoBg: "#3d3429",
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
export const LIGHT_THEME = {
|
|
86
|
+
name: "u1s1-light",
|
|
87
|
+
vars: {
|
|
88
|
+
orange: "#c45c38",
|
|
89
|
+
orangeSoft: "#b86a45",
|
|
90
|
+
text: "#2c2825",
|
|
91
|
+
muted: "#6f6a63",
|
|
92
|
+
dim: "#9a9488",
|
|
93
|
+
green: "#3d7a4a",
|
|
94
|
+
red: "#c45c5c",
|
|
95
|
+
yellow: "#9a6b2f",
|
|
96
|
+
blue: "#3d6f8a",
|
|
97
|
+
selectedBg: "#efe8e0",
|
|
98
|
+
},
|
|
99
|
+
colors: {
|
|
100
|
+
accent: "orange",
|
|
101
|
+
border: "orangeSoft",
|
|
102
|
+
borderAccent: "orange",
|
|
103
|
+
borderMuted: "dim",
|
|
104
|
+
success: "green",
|
|
105
|
+
error: "red",
|
|
106
|
+
warning: "yellow",
|
|
107
|
+
muted: "muted",
|
|
108
|
+
dim: "dim",
|
|
109
|
+
text: "text",
|
|
110
|
+
thinkingText: "muted",
|
|
111
|
+
selectedBg: "selectedBg",
|
|
112
|
+
scrollbarThumb: "selectedBg",
|
|
113
|
+
searchMatchBg: "selectedBg",
|
|
114
|
+
searchMatchText: "text",
|
|
115
|
+
userMessageBg: "",
|
|
116
|
+
userMessageText: "text",
|
|
117
|
+
customMessageBg: "",
|
|
118
|
+
customMessageText: "text",
|
|
119
|
+
customMessageLabel: "orangeSoft",
|
|
120
|
+
toolPendingBg: "",
|
|
121
|
+
toolSuccessBg: "",
|
|
122
|
+
toolErrorBg: "",
|
|
123
|
+
toolTitle: "text",
|
|
124
|
+
toolOutput: "muted",
|
|
125
|
+
mdHeading: "orange",
|
|
126
|
+
mdLink: "blue",
|
|
127
|
+
mdLinkUrl: "dim",
|
|
128
|
+
mdCode: "orangeSoft",
|
|
129
|
+
mdCodeBlock: "text",
|
|
130
|
+
mdCodeBlockBorder: "dim",
|
|
131
|
+
mdQuote: "muted",
|
|
132
|
+
mdQuoteBorder: "orange",
|
|
133
|
+
mdHr: "dim",
|
|
134
|
+
mdListBullet: "orange",
|
|
135
|
+
toolDiffAdded: "green",
|
|
136
|
+
toolDiffRemoved: "red",
|
|
137
|
+
toolDiffContext: "muted",
|
|
138
|
+
syntaxComment: "muted",
|
|
139
|
+
syntaxKeyword: "orange",
|
|
140
|
+
syntaxFunction: "orangeSoft",
|
|
141
|
+
syntaxVariable: "text",
|
|
142
|
+
syntaxString: "green",
|
|
143
|
+
syntaxNumber: "yellow",
|
|
144
|
+
syntaxType: "blue",
|
|
145
|
+
syntaxOperator: "muted",
|
|
146
|
+
syntaxPunctuation: "dim",
|
|
147
|
+
thinkingOff: "dim",
|
|
148
|
+
thinkingMinimal: "muted",
|
|
149
|
+
thinkingLow: "blue",
|
|
150
|
+
thinkingMedium: "orangeSoft",
|
|
151
|
+
thinkingHigh: "orange",
|
|
152
|
+
thinkingXhigh: "yellow",
|
|
153
|
+
thinkingMax: "red",
|
|
154
|
+
bashMode: "green",
|
|
155
|
+
},
|
|
156
|
+
export: {
|
|
157
|
+
pageBg: "#faf7f2",
|
|
158
|
+
cardBg: "#ffffff",
|
|
159
|
+
infoBg: "#fff4e8",
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
const THEMES = [DARK_THEME, LIGHT_THEME];
|
|
163
|
+
export const DEFAULT_THEME_SETTING = "u1s1-light/u1s1-dark";
|
|
164
|
+
/** Keep ~/.u1s1/agent/themes in sync so Pi picks them up as normal theme files. */
|
|
165
|
+
export function ensureBrandThemes() {
|
|
166
|
+
const dir = join(agentDir, "themes");
|
|
167
|
+
mkdirSync(dir, { recursive: true });
|
|
168
|
+
for (const theme of THEMES) {
|
|
169
|
+
const path = join(dir, `${theme.name}.json`);
|
|
170
|
+
const body = `${JSON.stringify(theme, null, 2)}\n`;
|
|
171
|
+
if (!existsSync(path) || readFileSync(path, "utf8") !== body) {
|
|
172
|
+
writeFileSync(path, body);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const VERSION = require("../package.json").version;
|
|
5
|
+
const PACKAGE_NAME = "u1s1-cli";
|
|
6
|
+
/** Detect the package manager that installed u1s1. */
|
|
7
|
+
function detectPackageManager() {
|
|
8
|
+
// Check common global install locations for clues
|
|
9
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
10
|
+
if (userAgent.startsWith("pnpm"))
|
|
11
|
+
return "pnpm";
|
|
12
|
+
if (userAgent.startsWith("yarn"))
|
|
13
|
+
return "yarn";
|
|
14
|
+
if (userAgent.startsWith("bun"))
|
|
15
|
+
return "bun";
|
|
16
|
+
return "npm";
|
|
17
|
+
}
|
|
18
|
+
/** Fetch the latest published version from npm registry. */
|
|
19
|
+
async function getLatestVersion() {
|
|
20
|
+
const url = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(url, {
|
|
23
|
+
headers: { Accept: "application/json" },
|
|
24
|
+
signal: AbortSignal.timeout(10_000),
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok)
|
|
27
|
+
return undefined;
|
|
28
|
+
const body = (await res.json());
|
|
29
|
+
return body.version;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function compareVersions(a, b) {
|
|
36
|
+
const pa = a.split(".").map(Number);
|
|
37
|
+
const pb = b.split(".").map(Number);
|
|
38
|
+
for (let i = 0; i < 3; i++) {
|
|
39
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
40
|
+
if (diff !== 0)
|
|
41
|
+
return diff;
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
export async function update() {
|
|
46
|
+
console.log("正在检查更新…");
|
|
47
|
+
const latest = await getLatestVersion();
|
|
48
|
+
if (!latest) {
|
|
49
|
+
console.error("无法获取最新版本。请检查网络连接后重试。");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
if (compareVersions(latest, VERSION) <= 0) {
|
|
53
|
+
console.log(`当前 v${VERSION} 已是最新版本 ✓`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const pm = detectPackageManager();
|
|
57
|
+
console.log(`发现新版本 v${latest} (当前 v${VERSION})`);
|
|
58
|
+
console.log(`正在用 ${pm} 更新 ${PACKAGE_NAME}…`);
|
|
59
|
+
try {
|
|
60
|
+
const installCmd = pm === "npm" ? `npm install -g ${PACKAGE_NAME}@latest` : `${pm} add -g ${PACKAGE_NAME}@latest`;
|
|
61
|
+
execSync(installCmd, { stdio: "inherit" });
|
|
62
|
+
console.log(`\n✅ 已更新到 v${latest},重启 u1s1 后生效。`);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
console.error("\n自动更新失败。请手动运行:");
|
|
66
|
+
console.error(` ${pm === "npm" ? "npm install -g" : `${pm} add -g`} ${PACKAGE_NAME}@latest`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/usage.js
CHANGED
|
@@ -14,14 +14,14 @@ export async function usage() {
|
|
|
14
14
|
console.error(e.message);
|
|
15
15
|
process.exit(1);
|
|
16
16
|
});
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const remain = me.remaining_usd;
|
|
18
|
+
const spent = me.mtd_usd;
|
|
19
|
+
const denom = remain + spent;
|
|
20
|
+
const remainRatio = denom > 0 ? remain / denom : 0;
|
|
19
21
|
console.log("");
|
|
20
22
|
console.log(` 账号 ${me.email ?? "(未绑定邮箱)"}`);
|
|
21
|
-
console.log(` 本月已用 $${
|
|
22
|
-
console.log(` 剩余额度 $${
|
|
23
|
-
if (me.bonus_balance_usd > 0)
|
|
24
|
-
console.log(` 其中加量 $${me.bonus_balance_usd}(邀请所得,不过期)`);
|
|
23
|
+
console.log(` 本月已用 $${spent}`);
|
|
24
|
+
console.log(` 剩余额度 $${remain} ${bar(remainRatio)}`);
|
|
25
25
|
console.log("");
|
|
26
26
|
console.log(" 邀请朋友双方各得加量 → https://u1s1.io/dashboard");
|
|
27
27
|
console.log("");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u1s1-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"dev": "tsx src/index.ts",
|
|
18
|
-
"typecheck": "tsc --noEmit"
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
19
20
|
},
|
|
20
21
|
"keywords": [
|
|
21
22
|
"ai",
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
"license": "MIT",
|
|
28
29
|
"homepage": "https://u1s1.io",
|
|
29
30
|
"dependencies": {
|
|
30
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
31
|
+
"@earendil-works/pi-coding-agent": "0.84.2"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"typescript": "^5.9.2",
|