u1s1-cli 0.2.0 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # u1s1 — 有一说一,最省心的 AI 编程搭子
2
2
 
3
- 在终端里用中文说需求,AI 帮你读文件、改代码、跑命令。不用买模型、不用配 API、不用懂那些名词,注册就送**每月 $2 免费额度**(普通人根本用不完)。
3
+ 在终端里用中文说需求,AI 帮你读文件、改代码、跑命令。不用买模型、不用配 API、不用懂那些名词,注册就送**$10 免费额度**(一次性,用完不补)。
4
4
 
5
5
  ## 三步开始
6
6
 
@@ -22,14 +22,16 @@ u1s1
22
22
  |---|---|
23
23
  | `u1s1` | 进入交互模式,直接开聊 |
24
24
  | `u1s1 -p "把 README 里的错别字修一下"` | 一句话模式,干完就退出 |
25
- | `u1s1 usage` | 看本月额度用了多少 |
26
- | `u1s1 model` | 看/切模型:`u1s1 model grok` 切 Grok 4.6,`u1s1 model deepseek` 切回 |
25
+ | `u1s1 usage` | 看额度用了多少 |
26
+ | `u1s1 model` | 看/切默认模型:`u1s1 model grok` 切 Grok 4.6,`u1s1 model deepseek` 切回(对话里 `/model` 同样会记住) |
27
+ | `u1s1 update` | 升级 u1s1 到最新版 |
28
+ | `u1s1 import` | 从 Claude Code / Codex 导入历史对话,之后 `/resume` 就能接着聊 |
27
29
  | `u1s1 login` / `u1s1 logout` | 登录 / 退出 |
28
30
 
29
31
  ## 有一说一
30
32
 
31
- - **背后模型**:默认 DeepSeek V4 Flash(1M 上下文,便宜大碗);难题可随时 `u1s1 model grok` 切 Grok 4.6(更强,但烧额度快约 20 倍)。对话中用 `/model` 可临时切换。
32
- - **怎么收费**:额度按 API 实际成本扣,不加价;每月 1 号自动重置。
33
+ - **背后模型**:默认 DeepSeek V4 Flash(1M 上下文,便宜大碗);难题可随时 `u1s1 model grok` 切 Grok 4.6(更强,但烧额度快约 20 倍)。对话里 `/model` 也会记住,下次启动还是这个。
34
+ - **怎么收费**:额度按 API 实际成本扣,不加价;新用户一次性送 $10,用完不补。
33
35
  - **额度不够**:邀请朋友,你俩各得 $1 加量,永不过期 → [u1s1.io/dashboard](https://u1s1.io/dashboard)
34
36
  - **内核**:基于开源的 [pi](https://pi.dev) coding agent,会话管理、斜杠命令、皮肤等能力全都有。
35
37
 
package/dist/brand.js ADDED
@@ -0,0 +1,77 @@
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
+ // "u1s1" in FIGlet ANSI Shadow. Rows are joined per glyph so widths stay aligned.
18
+ const GLYPH_U = ["██╗ ██╗", "██║ ██║", "██║ ██║", "██║ ██║", "╚██████╔╝", " ╚═════╝ "];
19
+ const GLYPH_1 = [" ██╗", "███║", "╚██║", " ██║", " ██║", " ╚═╝"];
20
+ const GLYPH_S = ["███████╗", "██╔════╝", "███████╗", "╚════██║", "███████║", "╚══════╝"];
21
+ export const HERO_ART = GLYPH_U.map((_, r) => [GLYPH_U[r], GLYPH_1[r], GLYPH_S[r], GLYPH_1[r]].join(" "));
22
+ const ART_WIDTH = HERO_ART[0].length;
23
+ /** Two-tone wordmark: solid blocks in accent, box-drawing "shadow" in dim. */
24
+ function paintArt(theme, line) {
25
+ let out = "";
26
+ let run = "";
27
+ let runIsBlock = null;
28
+ const flush = () => {
29
+ if (!run)
30
+ return;
31
+ out += runIsBlock === null ? run : theme.fg(runIsBlock ? "accent" : "dim", run);
32
+ run = "";
33
+ };
34
+ for (const ch of line) {
35
+ const kind = ch === " " ? null : ch === "█" ? true : false;
36
+ if (kind !== runIsBlock) {
37
+ flush();
38
+ runIsBlock = kind;
39
+ }
40
+ run += ch;
41
+ }
42
+ flush();
43
+ return out;
44
+ }
45
+ /**
46
+ * Startup hero, responsive to terminal width:
47
+ * wide → wordmark with info column beside it; medium → stacked; narrow → one-liner.
48
+ */
49
+ export function renderBrandHeader(theme, version, cwd, width) {
50
+ const name = theme.bold(theme.fg("text", `${BRAND_NAME} v${version}`));
51
+ const brand = theme.fg("muted", `${BRAND_CN} · ${BRAND_TAGLINE}`);
52
+ const dir = theme.fg("dim", `cwd: ${formatHomePath(cwd)}`);
53
+ const hints = theme.fg("dim", "/help 看命令 · Shift+Enter 换行 · Esc 中断");
54
+ if (width < ART_WIDTH + 4) {
55
+ return ["", ` ${theme.fg("accent", "✻")} ${name} ${theme.fg("muted", BRAND_CN)}`, ` ${dir}`, ""];
56
+ }
57
+ const art = HERO_ART.map((line) => ` ${paintArt(theme, line)}`);
58
+ // widest info row (hints) needs 46 cols beside the 28-col wordmark
59
+ if (width < ART_WIDTH + 46) {
60
+ return ["", ...art, "", ` ${name} ${brand}`, ` ${dir}`, ""];
61
+ }
62
+ const rows = [...art];
63
+ const gap = " ";
64
+ rows[1] += `${gap}${name}`;
65
+ rows[2] += `${gap}${brand}`;
66
+ rows[3] += `${gap}${dir}`;
67
+ rows[4] += `${gap}${hints}`;
68
+ return ["", ...rows, ""];
69
+ }
70
+ export function printConsoleBanner(version) {
71
+ console.log("");
72
+ for (const line of HERO_ART)
73
+ console.log(` ${line}`);
74
+ console.log("");
75
+ console.log(` ${BRAND_NAME} v${version} — ${BRAND_CN},${BRAND_TAGLINE}`);
76
+ console.log("");
77
+ }
package/dist/config.js CHANGED
@@ -34,16 +34,47 @@ export const u1s1Dir = join(homedir(), ".u1s1");
34
34
  const configFile = join(u1s1Dir, "config.json");
35
35
  /** pi keeps auth/models/settings/sessions under this dir — isolated from any real pi install. */
36
36
  export const agentDir = join(u1s1Dir, "agent");
37
- export function loadConfig() {
38
- let file = {};
39
- if (existsSync(configFile)) {
40
- try {
41
- file = JSON.parse(readFileSync(configFile, "utf8"));
42
- }
43
- catch {
44
- // corrupted config falls back to defaults; login rewrites it
45
- }
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;
46
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) ?? {});
47
78
  return {
48
79
  apiKey: process.env["U1S1_API_KEY"] || file.apiKey,
49
80
  baseUrl: process.env["U1S1_BASE_URL"] || file.baseUrl || DEFAULT_BASE_URL,
@@ -55,3 +86,10 @@ export function saveConfig(cfg) {
55
86
  writeFileSync(configFile, JSON.stringify(cfg, null, 2) + "\n");
56
87
  chmodSync(configFile, 0o600);
57
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
+ }
@@ -0,0 +1,372 @@
1
+ import { createReadStream, existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, join } from "node:path";
4
+ import { createInterface } from "node:readline";
5
+ import { asRecord, asString, encodeClaudeProjectDir, fileMtimeMs, firstMeaningfulLine, listHomeClaudeDirs, MAX_TEXT_CHARS, MAX_TOOL_RESULT_CHARS, oneLine, parseJsonLine, parseTime, projectAncestors, projectRoot, truncateText, uniqueExistingDirs, } from "./util.js";
6
+ const CLAUDE_TOOL_MAP = {
7
+ bash: "bash",
8
+ read: "read",
9
+ edit: "edit",
10
+ write: "write",
11
+ glob: "find",
12
+ grep: "grep",
13
+ ls: "ls",
14
+ };
15
+ function claudeHomes() {
16
+ return uniqueExistingDirs([
17
+ process.env["CLAUDE_CONFIG_DIR"],
18
+ ...listHomeClaudeDirs(),
19
+ join(homedir(), ".claude"),
20
+ ]);
21
+ }
22
+ function decodeProjectDirName(name) {
23
+ if (!name.startsWith("-"))
24
+ return undefined;
25
+ return `/${name.slice(1).replace(/-/g, "/")}`;
26
+ }
27
+ function claudeDirMatchesCwd(dirName, cwd) {
28
+ const encoded = new Set(projectAncestors(cwd).map(encodeClaudeProjectDir));
29
+ if (encoded.has(dirName))
30
+ return true;
31
+ const root = projectRoot(cwd);
32
+ if (!root)
33
+ return false;
34
+ // sessions that happened in a subfolder of this repo
35
+ return dirName.startsWith(`${encodeClaudeProjectDir(root)}-`);
36
+ }
37
+ function extractCwd(obj) {
38
+ return asString(obj["cwd"]);
39
+ }
40
+ function extractTitle(obj) {
41
+ return asString(obj["aiTitle"]) ?? asString(obj["title"]);
42
+ }
43
+ function isToolResultContent(content) {
44
+ if (!Array.isArray(content))
45
+ return false;
46
+ return content.some((block) => asRecord(block)?.["type"] === "tool_result");
47
+ }
48
+ function flattenClaudeContent(content) {
49
+ if (typeof content === "string")
50
+ return content;
51
+ if (!Array.isArray(content))
52
+ return "";
53
+ const parts = [];
54
+ for (const raw of content) {
55
+ const block = asRecord(raw);
56
+ if (!block)
57
+ continue;
58
+ const type = asString(block["type"]);
59
+ if (type === "text" || type === "input_text" || type === "output_text") {
60
+ const text = asString(block["text"]);
61
+ if (text)
62
+ parts.push(text);
63
+ }
64
+ }
65
+ return parts.join("\n");
66
+ }
67
+ function flattenToolResult(content) {
68
+ if (typeof content === "string")
69
+ return content;
70
+ if (!Array.isArray(content)) {
71
+ if (content == null)
72
+ return "";
73
+ try {
74
+ return JSON.stringify(content);
75
+ }
76
+ catch {
77
+ return String(content);
78
+ }
79
+ }
80
+ const parts = [];
81
+ for (const raw of content) {
82
+ if (typeof raw === "string") {
83
+ parts.push(raw);
84
+ continue;
85
+ }
86
+ const block = asRecord(raw);
87
+ if (!block)
88
+ continue;
89
+ const text = asString(block["text"]) ?? asString(block["content"]);
90
+ if (text)
91
+ parts.push(text);
92
+ }
93
+ return parts.join("\n");
94
+ }
95
+ function mapToolName(name) {
96
+ return CLAUDE_TOOL_MAP[name.toLowerCase()] ?? name;
97
+ }
98
+ function omit(obj, keys) {
99
+ const skip = new Set(keys);
100
+ const out = {};
101
+ for (const [k, v] of Object.entries(obj)) {
102
+ if (!skip.has(k))
103
+ out[k] = v;
104
+ }
105
+ return out;
106
+ }
107
+ function remapToolArgs(name, input) {
108
+ const mapped = mapToolName(name);
109
+ if (mapped === "read") {
110
+ const path = asString(input["file_path"]) ?? asString(input["path"]);
111
+ return path ? { path, ...omit(input, ["file_path", "path"]) } : input;
112
+ }
113
+ if (mapped === "write") {
114
+ const path = asString(input["file_path"]) ?? asString(input["path"]);
115
+ const content = asString(input["content"]) ?? asString(input["contents"]);
116
+ return {
117
+ ...(path ? { path } : {}),
118
+ ...(content !== undefined ? { content } : {}),
119
+ ...omit(input, ["file_path", "path", "content", "contents"]),
120
+ };
121
+ }
122
+ if (mapped === "edit") {
123
+ const path = asString(input["file_path"]) ?? asString(input["path"]);
124
+ return path ? { path, ...omit(input, ["file_path", "path"]) } : input;
125
+ }
126
+ if (mapped === "find") {
127
+ const pattern = asString(input["pattern"]) ?? asString(input["glob"]);
128
+ return pattern ? { pattern, ...omit(input, ["pattern", "glob"]) } : input;
129
+ }
130
+ return input;
131
+ }
132
+ function usageFromClaude(raw) {
133
+ const usage = asRecord(raw);
134
+ if (!usage)
135
+ return undefined;
136
+ const input = Number(usage["input_tokens"] ?? 0);
137
+ const output = Number(usage["output_tokens"] ?? 0);
138
+ const cacheRead = Number(usage["cache_read_input_tokens"] ?? 0);
139
+ const cacheWrite = Number(usage["cache_creation_input_tokens"] ?? 0);
140
+ if (![input, output, cacheRead, cacheWrite].some((n) => n > 0))
141
+ return undefined;
142
+ return { input, output, cacheRead, cacheWrite };
143
+ }
144
+ function firstUserPreview(obj) {
145
+ if (obj["type"] !== "user" || obj["isSidechain"] === true)
146
+ return undefined;
147
+ const msg = asRecord(obj["message"]);
148
+ if (!msg || isToolResultContent(msg["content"]))
149
+ return undefined;
150
+ const text = flattenClaudeContent(msg["content"]).trim();
151
+ if (!text)
152
+ return undefined;
153
+ return oneLine(firstMeaningfulLine(text) || text);
154
+ }
155
+ export async function hydrateClaudeSession(session) {
156
+ const stream = createReadStream(session.sourcePath, { encoding: "utf8" });
157
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
158
+ let cwd = "";
159
+ let title = session.title;
160
+ let sourceId = session.sourceId;
161
+ let startedAt = session.startedAt;
162
+ let preview;
163
+ let lines = 0;
164
+ try {
165
+ for await (const line of rl) {
166
+ lines += 1;
167
+ const obj = parseJsonLine(line);
168
+ if (!obj)
169
+ continue;
170
+ if (!sourceId)
171
+ sourceId = asString(obj["sessionId"]) ?? sourceId;
172
+ if (!cwd)
173
+ cwd = extractCwd(obj) ?? "";
174
+ if (!title)
175
+ title = extractTitle(obj) ?? title;
176
+ if (!startedAt)
177
+ startedAt = parseTime(obj["timestamp"]);
178
+ if (!preview)
179
+ preview = firstUserPreview(obj);
180
+ if (cwd && (title || preview) && sourceId && lines > 80)
181
+ break;
182
+ if (lines > 400)
183
+ break;
184
+ }
185
+ }
186
+ finally {
187
+ rl.close();
188
+ stream.destroy();
189
+ }
190
+ return {
191
+ ...session,
192
+ sourceId: sourceId || session.sourceId,
193
+ cwd: cwd || session.cwd,
194
+ title: title || preview || session.title,
195
+ startedAt: startedAt ?? session.startedAt,
196
+ };
197
+ }
198
+ export const claudeAdapter = {
199
+ id: "claude",
200
+ label: "Claude Code",
201
+ discover(opts) {
202
+ const wanted = opts.cwd;
203
+ const seen = new Set();
204
+ const found = [];
205
+ for (const home of claudeHomes()) {
206
+ const projectsRoot = join(home, "projects");
207
+ if (!existsSync(projectsRoot))
208
+ continue;
209
+ let projectDirs = [];
210
+ try {
211
+ projectDirs = readdirSync(projectsRoot)
212
+ .map((name) => join(projectsRoot, name))
213
+ .filter((dir) => {
214
+ try {
215
+ return statSync(dir).isDirectory();
216
+ }
217
+ catch {
218
+ return false;
219
+ }
220
+ });
221
+ }
222
+ catch {
223
+ continue;
224
+ }
225
+ for (const projectDir of projectDirs) {
226
+ const dirName = basename(projectDir);
227
+ if (wanted && !claudeDirMatchesCwd(dirName, wanted))
228
+ continue;
229
+ let files = [];
230
+ try {
231
+ files = readdirSync(projectDir)
232
+ .filter((name) => name.endsWith(".jsonl"))
233
+ .map((name) => join(projectDir, name));
234
+ }
235
+ catch {
236
+ continue;
237
+ }
238
+ for (const file of files) {
239
+ let real = file;
240
+ try {
241
+ real = realpathSync(file);
242
+ }
243
+ catch {
244
+ // keep original
245
+ }
246
+ if (seen.has(real))
247
+ continue;
248
+ seen.add(real);
249
+ found.push({
250
+ source: "claude",
251
+ sourceId: basename(file, ".jsonl"),
252
+ sourcePath: real,
253
+ cwd: wanted ?? decodeProjectDirName(dirName) ?? "",
254
+ mtimeMs: fileMtimeMs(real),
255
+ });
256
+ }
257
+ }
258
+ }
259
+ return found;
260
+ },
261
+ convert(session) {
262
+ let text = "";
263
+ try {
264
+ // Large Claude transcripts can be tens of MB; still fine as a one-shot import.
265
+ text = readFileSync(session.sourcePath, "utf8");
266
+ }
267
+ catch {
268
+ return { cwd: session.cwd || process.cwd(), title: session.title, messages: [] };
269
+ }
270
+ const messages = [];
271
+ let cwd = "";
272
+ let title = session.title;
273
+ const pendingTools = new Map();
274
+ for (const line of text.split(/\r?\n/)) {
275
+ if (line.length > 8_000_000)
276
+ continue;
277
+ const obj = parseJsonLine(line);
278
+ if (!obj)
279
+ continue;
280
+ const type = asString(obj["type"]);
281
+ if (!cwd)
282
+ cwd = extractCwd(obj) ?? cwd;
283
+ if (!title && type === "ai-title")
284
+ title = extractTitle(obj) ?? title;
285
+ if (obj["isSidechain"] === true)
286
+ continue;
287
+ if (type === "user") {
288
+ const msg = asRecord(obj["message"]);
289
+ if (!msg)
290
+ continue;
291
+ const content = msg["content"];
292
+ const ts = parseTime(obj["timestamp"]) ?? Date.now();
293
+ if (isToolResultContent(content) && Array.isArray(content)) {
294
+ for (const raw of content) {
295
+ const block = asRecord(raw);
296
+ if (!block || block["type"] !== "tool_result")
297
+ continue;
298
+ const callId = asString(block["tool_use_id"]) ?? "";
299
+ if (!callId)
300
+ continue;
301
+ messages.push({
302
+ role: "toolResult",
303
+ toolCallId: callId,
304
+ toolName: pendingTools.get(callId) ?? "unknown",
305
+ text: truncateText(flattenToolResult(block["content"]), MAX_TOOL_RESULT_CHARS),
306
+ isError: block["is_error"] === true,
307
+ timestamp: ts,
308
+ });
309
+ }
310
+ continue;
311
+ }
312
+ const userText = truncateText(flattenClaudeContent(content), MAX_TEXT_CHARS).trim();
313
+ if (!userText)
314
+ continue;
315
+ if (!title)
316
+ title = oneLine(firstMeaningfulLine(userText) || userText);
317
+ messages.push({ role: "user", text: userText, timestamp: ts });
318
+ continue;
319
+ }
320
+ if (type !== "assistant")
321
+ continue;
322
+ const msg = asRecord(obj["message"]);
323
+ if (!msg)
324
+ continue;
325
+ const ts = parseTime(obj["timestamp"]) ?? Date.now();
326
+ const blocks = Array.isArray(msg["content"]) ? msg["content"] : [];
327
+ const content = [];
328
+ let sawTool = false;
329
+ for (const raw of blocks) {
330
+ const block = asRecord(raw);
331
+ if (!block)
332
+ continue;
333
+ const btype = asString(block["type"]);
334
+ if (btype === "thinking") {
335
+ const thinking = asString(block["thinking"]) ?? "";
336
+ if (thinking.trim())
337
+ content.push({ type: "thinking", thinking: truncateText(thinking, MAX_TEXT_CHARS) });
338
+ }
339
+ else if (btype === "text") {
340
+ const textBlock = asString(block["text"]) ?? "";
341
+ if (textBlock.trim())
342
+ content.push({ type: "text", text: truncateText(textBlock, MAX_TEXT_CHARS) });
343
+ }
344
+ else if (btype === "tool_use") {
345
+ const id = asString(block["id"]) ?? "";
346
+ const name = asString(block["name"]) ?? "unknown";
347
+ if (!id)
348
+ continue;
349
+ const input = asRecord(block["input"]) ?? {};
350
+ const mapped = mapToolName(name);
351
+ pendingTools.set(id, mapped);
352
+ content.push({ type: "toolCall", id, name: mapped, arguments: remapToolArgs(name, input) });
353
+ sawTool = true;
354
+ }
355
+ }
356
+ if (content.length === 0)
357
+ continue;
358
+ const stop = asString(msg["stop_reason"]);
359
+ messages.push({
360
+ role: "assistant",
361
+ content,
362
+ provider: "anthropic",
363
+ model: asString(msg["model"]) ?? "claude",
364
+ api: "anthropic-messages",
365
+ stopReason: sawTool || stop === "tool_use" ? "toolUse" : stop === "max_tokens" ? "length" : "stop",
366
+ usage: usageFromClaude(msg["usage"]),
367
+ timestamp: ts,
368
+ });
369
+ }
370
+ return { cwd: cwd || session.cwd || process.cwd(), title, messages };
371
+ },
372
+ };