u1s1-cli 0.10.0 → 0.11.1

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.
@@ -0,0 +1,32 @@
1
+ import { type CliConfig } from "./config.js";
2
+ /** pi auto-appends <agentDir>/APPEND_SYSTEM.md to its system prompt — our branding hook. */
3
+ export declare function ensureBrandPrompt(): void;
4
+ /** Defaults that don't overwrite values the user already set. */
5
+ export declare function ensureDefaultSettings(): void;
6
+ /** ≤0.4.0 wrote u1s1-dark/u1s1-light into the pi themes dir; remove them. */
7
+ export declare function cleanupBrandThemes(): void;
8
+ /**
9
+ * u1s1 provider for SDK-based frontends (`u1s1 web`): pi composes
10
+ * <agentDir>/models.json above registered providers, so this exposes the same
11
+ * models as the TUI's in-process extension without patching the frontend.
12
+ * apiKey stays an env reference — the launcher sets U1S1_API_KEY, the file
13
+ * itself holds no secret. Rewritten on every launch so baseUrl/model changes
14
+ * propagate; other providers a user may have added are preserved.
15
+ */
16
+ /**
17
+ * 联网工具经 <agentDir>/extensions 投影:pi 的资源加载器对 TUI 和 `u1s1 web`
18
+ * 都会扫描这个目录,两个前端从此共用同一份工具注册,不用各接一遍。
19
+ * 文件每次启动重写(baseUrl/服务端开关变化随之生效);apiKey 走 U1S1_API_KEY
20
+ * 环境变量,文件里不落密钥。U1S1_TOOLS_VIA_EXTENSION 守卫:旧版 CLI 仍在
21
+ * 进程内注册工具且不设该变量,残留的本文件在旧版下自动空转,避免双重注册。
22
+ */
23
+ export declare function writeWebToolsExtension(cfg: CliConfig, webSearchEnabled: boolean): void;
24
+ /**
25
+ * pi-web-ui 的就绪检测只认 <agentDir>/auth.json 里有没有凭据条目(models.json
26
+ * 的 apiKey 它不看),空 {} 会在网页里弹「未检测到 pi agent 配置」引导装 pi。
27
+ * 这里补一条 u1s1 的 api_key 记录 —— key 用 $U1S1_API_KEY 环境变量引用,
28
+ * pi 的 resolveConfigValue 读取时才解析,文件本身不落密钥,和 models.json 同款。
29
+ * 其他 provider 的凭据原样保留;文件损坏时不动它,交给 SDK 自己报错。
30
+ */
31
+ export declare function ensureAuthCredential(): void;
32
+ export declare function ensureProviderModels(cfg: CliConfig): void;
@@ -106,6 +106,33 @@ export function writeWebToolsExtension(cfg, webSearchEnabled) {
106
106
  ` pi.registerTool(tools.webFetchTool);\n` +
107
107
  `}\n`);
108
108
  }
109
+ /**
110
+ * pi-web-ui 的就绪检测只认 <agentDir>/auth.json 里有没有凭据条目(models.json
111
+ * 的 apiKey 它不看),空 {} 会在网页里弹「未检测到 pi agent 配置」引导装 pi。
112
+ * 这里补一条 u1s1 的 api_key 记录 —— key 用 $U1S1_API_KEY 环境变量引用,
113
+ * pi 的 resolveConfigValue 读取时才解析,文件本身不落密钥,和 models.json 同款。
114
+ * 其他 provider 的凭据原样保留;文件损坏时不动它,交给 SDK 自己报错。
115
+ */
116
+ export function ensureAuthCredential() {
117
+ mkdirSync(agentDir, { recursive: true });
118
+ const p = join(agentDir, "auth.json");
119
+ let root = {};
120
+ if (existsSync(p)) {
121
+ try {
122
+ root = JSON.parse(readFileSync(p, "utf8"));
123
+ }
124
+ catch {
125
+ return;
126
+ }
127
+ if (typeof root !== "object" || root === null || Array.isArray(root))
128
+ return;
129
+ }
130
+ const entry = root[PROVIDER_ID];
131
+ if (entry?.["type"] === "api_key" && entry["key"] === "$U1S1_API_KEY")
132
+ return;
133
+ root[PROVIDER_ID] = { type: "api_key", key: "$U1S1_API_KEY" };
134
+ writeFileSync(p, JSON.stringify(root, null, 2) + "\n", { mode: 0o600 });
135
+ }
109
136
  export function ensureProviderModels(cfg) {
110
137
  mkdirSync(agentDir, { recursive: true });
111
138
  const p = join(agentDir, "models.json");
package/dist/api.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { type CliConfig } from "./config.js";
2
+ export interface MeResponse {
3
+ email: string | null;
4
+ signup_credit_usd?: number;
5
+ monthly_free_usd?: number;
6
+ daily_free_usd: number;
7
+ daily_free_used_usd: number;
8
+ daily_free_remaining_usd: number;
9
+ daily_free_resets_at: string;
10
+ daily_free_model: string;
11
+ mtd_usd: number;
12
+ balance_spent_usd: number;
13
+ bonus_balance_usd: number;
14
+ remaining_usd: number;
15
+ /** USD→默认模型 Token 折算率;老网关没有该字段,展示时需兜底回 $ */
16
+ tokens_per_usd?: number;
17
+ }
18
+ export interface ApiModel {
19
+ id: string;
20
+ name: string;
21
+ reasoning: boolean;
22
+ context_length: number;
23
+ max_tokens: number;
24
+ price: {
25
+ input: number;
26
+ output: number;
27
+ cache_read: number | null;
28
+ };
29
+ }
30
+ /** 服务端能力开关;字段缺失(老网关)时按开启处理,保持现状。 */
31
+ export interface ApiFeatures {
32
+ web_search?: boolean;
33
+ }
34
+ export interface ModelsResponse {
35
+ models: ApiModel[];
36
+ features: ApiFeatures;
37
+ }
38
+ export declare function fetchModels(cfg: CliConfig): Promise<ModelsResponse>;
39
+ export interface SearchResult {
40
+ title: string;
41
+ url: string;
42
+ snippet: string;
43
+ }
44
+ export interface SearchResponse {
45
+ query: string;
46
+ answer: string | null;
47
+ results: SearchResult[];
48
+ }
49
+ /** 联网搜索走网关代理(上游 key 只在服务端)。maxResults 不传时由服务端决定默认值。 */
50
+ export declare function searchWeb(cfg: Pick<CliConfig, "baseUrl" | "apiKey">, query: string, maxResults?: number, signal?: AbortSignal): Promise<SearchResponse>;
51
+ export declare function fetchMe(cfg: CliConfig): Promise<MeResponse>;
@@ -0,0 +1,13 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ export declare const BRAND_NAME = "u1s1";
3
+ export declare const BRAND_CN = "\u6709\u4E00\u8BF4\u4E00";
4
+ export declare const BRAND_TAGLINE = "\u8BF4\u4EBA\u8BDD\u7684 AI \u7F16\u7A0B\u642D\u5B50";
5
+ export declare const DASHBOARD_URL = "https://u1s1.io/dashboard";
6
+ export declare function formatHomePath(path: string): string;
7
+ export declare const HERO_ART: string[];
8
+ /**
9
+ * Startup hero, responsive to terminal width:
10
+ * wide → wordmark with info column beside it; medium → stacked; narrow → one-liner.
11
+ */
12
+ export declare function renderBrandHeader(theme: Theme, version: string, cwd: string, width: number, notice?: string): string[];
13
+ export declare function printConsoleBanner(version: string): void;
@@ -0,0 +1,77 @@
1
+ export declare const VERSION: string;
2
+ /**
3
+ * 便携包安装(install.sh / install.ps1):包根旁边带自己的 node 运行时,
4
+ * npm 更新碰不到这份拷贝,升级只能整包重装。npm 全局安装的包在 node_modules
5
+ * 下,按父目录名先排除,避免撞上恰好叫 node 的目录误判。
6
+ */
7
+ export declare function isPortableInstall(): boolean;
8
+ export declare const DEFAULT_BASE_URL = "https://api.u1s1.io/v1";
9
+ export declare const PROVIDER_ID = "u1s1";
10
+ /**
11
+ * Convert an API model response to our internal ModelDef.
12
+ * Aliases are derived from the short id; note is left empty (filled by /model command).
13
+ */
14
+ export declare function apiModelToDef(m: {
15
+ id: string;
16
+ name: string;
17
+ reasoning: boolean;
18
+ context_length: number;
19
+ max_tokens: number;
20
+ price: {
21
+ input: number;
22
+ output: number;
23
+ cache_read: number | null;
24
+ };
25
+ }): ModelDef;
26
+ export interface ModelDef {
27
+ id: string;
28
+ name: string;
29
+ /** short aliases accepted by `u1s1 model <name>` */
30
+ aliases: string[];
31
+ reasoning: boolean;
32
+ contextWindow: number;
33
+ maxTokens: number;
34
+ /** USD per million tokens — display/estimation only; billing is server-side */
35
+ cost: {
36
+ input: number;
37
+ output: number;
38
+ cacheRead: number;
39
+ cacheWrite: number;
40
+ };
41
+ note: string;
42
+ }
43
+ export declare const MODELS: ModelDef[];
44
+ /** Replace MODELS with a fresh list fetched from server (e.g. at startup). */
45
+ export declare function setModelsFromApi(apiModels: ModelDef[]): void;
46
+ export declare const DEFAULT_MODEL_ID: string;
47
+ export declare function resolveModel(nameOrAlias: string): ModelDef | undefined;
48
+ export interface CliConfig {
49
+ apiKey?: string;
50
+ baseUrl: string;
51
+ /** preferred model id; must be one of MODELS */
52
+ model?: string;
53
+ }
54
+ export declare const u1s1Dir: string;
55
+ /** pi keeps auth/models/settings/sessions under this dir — isolated from any real pi install. */
56
+ export declare const agentDir: string;
57
+ export declare const agentSettingsFile: string;
58
+ export interface AgentSettings {
59
+ showStartupBanner?: boolean;
60
+ autoUpdate?: boolean;
61
+ [key: string]: unknown;
62
+ }
63
+ /** Read u1s1 agent settings (settings.json). Returns empty object if missing or invalid. */
64
+ export declare function readSettings(): AgentSettings;
65
+ /** Model last chosen in-session via /model (pi writes this). */
66
+ export declare function readAgentDefaultModel(): string | undefined;
67
+ /** Keep pi's settings.json in sync so /model and `u1s1 model` share one default. */
68
+ export declare function writeAgentDefaultModel(modelId: string): void;
69
+ /**
70
+ * After this fix both stores stay in sync. If they still disagree (old installs),
71
+ * prefer the in-session /model value — that's the one users thought they had set.
72
+ */
73
+ export declare function resolvePreferredModel(configModel: string | undefined): string;
74
+ export declare function loadConfig(): CliConfig;
75
+ export declare function saveConfig(cfg: CliConfig): void;
76
+ /** Persist the user's preferred model to both stores. */
77
+ export declare function persistPreferredModel(cfg: CliConfig, modelId: string): CliConfig;
@@ -0,0 +1,2 @@
1
+ import { type CliConfig } from "./config.js";
2
+ export declare function deployCommand(cfg: CliConfig, args: string[]): Promise<void>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 桌面版 App(packages/app)的嵌入 API,对外路径 "u1s1-cli/embed"(见
3
+ * package.json exports)。App 与 CLI 共享 ~/.u1s1 的登录态和 agentDir 配置,
4
+ * 这里只做转发,不放实现 —— 保证两个入口永远走同一份逻辑。
5
+ */
6
+ export { agentDir, loadConfig, saveConfig, u1s1Dir, VERSION, type CliConfig, } from "./config.js";
7
+ export { fetchMe, fetchModels } from "./api.js";
8
+ export { apiOrigin, pollDeviceLogin, startDeviceLogin, type DeviceStart } from "./login.js";
9
+ export { prepareWebEnv } from "./web.js";
10
+ export { applyWebUiBranding, applyWebUiFrontend } from "./webui-brand.js";
11
+ export { DASHBOARD_URL } from "./brand.js";
package/dist/embed.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 桌面版 App(packages/app)的嵌入 API,对外路径 "u1s1-cli/embed"(见
3
+ * package.json exports)。App 与 CLI 共享 ~/.u1s1 的登录态和 agentDir 配置,
4
+ * 这里只做转发,不放实现 —— 保证两个入口永远走同一份逻辑。
5
+ */
6
+ export { agentDir, loadConfig, saveConfig, u1s1Dir, VERSION, } from "./config.js";
7
+ export { fetchMe, fetchModels } from "./api.js";
8
+ export { apiOrigin, pollDeviceLogin, startDeviceLogin } from "./login.js";
9
+ export { prepareWebEnv } from "./web.js";
10
+ export { applyWebUiBranding, applyWebUiFrontend } from "./webui-brand.js";
11
+ export { DASHBOARD_URL } from "./brand.js";
@@ -0,0 +1,3 @@
1
+ import type { SourceAdapter, SourceSession } from "./types.js";
2
+ export declare function hydrateClaudeSession(session: SourceSession): Promise<SourceSession>;
3
+ export declare const claudeAdapter: SourceAdapter;
@@ -0,0 +1,3 @@
1
+ import type { SourceAdapter, SourceSession } from "./types.js";
2
+ export declare function hydrateCodexSession(session: SourceSession): Promise<SourceSession>;
3
+ export declare const codexAdapter: SourceAdapter;
@@ -0,0 +1 @@
1
+ export declare function importCommand(args: string[]): Promise<void>;
@@ -0,0 +1,104 @@
1
+ export type ImportSourceId = "claude" | "codex";
2
+ export interface SourceSession {
3
+ source: ImportSourceId;
4
+ /** Stable id from the original tool (session uuid). */
5
+ sourceId: string;
6
+ sourcePath: string;
7
+ cwd: string;
8
+ title?: string;
9
+ startedAt?: number;
10
+ mtimeMs: number;
11
+ }
12
+ export interface DiscoverOpts {
13
+ /** If set, only sessions that belong to this working directory. */
14
+ cwd?: string;
15
+ }
16
+ export type ImportedContent = {
17
+ type: "text";
18
+ text: string;
19
+ } | {
20
+ type: "thinking";
21
+ thinking: string;
22
+ } | {
23
+ type: "toolCall";
24
+ id: string;
25
+ name: string;
26
+ arguments: Record<string, unknown>;
27
+ };
28
+ export interface ImportedUser {
29
+ role: "user";
30
+ text: string;
31
+ timestamp: number;
32
+ }
33
+ export interface ImportedAssistant {
34
+ role: "assistant";
35
+ content: ImportedContent[];
36
+ provider: string;
37
+ model: string;
38
+ api: string;
39
+ stopReason: "stop" | "length" | "toolUse" | "error" | "aborted";
40
+ usage?: {
41
+ input: number;
42
+ output: number;
43
+ cacheRead: number;
44
+ cacheWrite: number;
45
+ };
46
+ timestamp: number;
47
+ }
48
+ export interface ImportedToolResult {
49
+ role: "toolResult";
50
+ toolCallId: string;
51
+ toolName: string;
52
+ text: string;
53
+ isError: boolean;
54
+ timestamp: number;
55
+ }
56
+ export type ImportedMessage = ImportedUser | ImportedAssistant | ImportedToolResult;
57
+ export interface ConvertedSession {
58
+ cwd: string;
59
+ title?: string;
60
+ messages: ImportedMessage[];
61
+ }
62
+ export interface SourceAdapter {
63
+ id: ImportSourceId;
64
+ label: string;
65
+ discover(opts: DiscoverOpts): SourceSession[];
66
+ convert(session: SourceSession): ConvertedSession;
67
+ }
68
+ export interface ImportRecord {
69
+ source: ImportSourceId;
70
+ sourceId: string;
71
+ sourcePath: string;
72
+ destPath: string;
73
+ cwd: string;
74
+ title?: string;
75
+ importedAt: string;
76
+ }
77
+ export interface ImportIndex {
78
+ version: 1;
79
+ items: Record<string, ImportRecord>;
80
+ }
81
+ export interface ImportOptions {
82
+ sources: ImportSourceId[];
83
+ /** Only this project. Omit to import every discovered session. */
84
+ cwd?: string;
85
+ dryRun: boolean;
86
+ force: boolean;
87
+ limit?: number;
88
+ }
89
+ export interface ImportResultItem {
90
+ source: ImportSourceId;
91
+ sourceId: string;
92
+ title: string;
93
+ cwd: string;
94
+ destPath?: string;
95
+ status: "imported" | "skipped" | "empty" | "error";
96
+ detail?: string;
97
+ }
98
+ export interface ImportSummary {
99
+ items: ImportResultItem[];
100
+ imported: number;
101
+ skipped: number;
102
+ empty: number;
103
+ errors: number;
104
+ }
@@ -0,0 +1,26 @@
1
+ export declare const MAX_TOOL_RESULT_CHARS = 80000;
2
+ export declare const MAX_TEXT_CHARS = 200000;
3
+ export declare const PREVIEW_TITLE_CHARS = 48;
4
+ export declare function resolveExistingDir(path: string): string | undefined;
5
+ export declare function listHomeClaudeDirs(): string[];
6
+ export declare function uniqueExistingDirs(paths: Array<string | undefined>): string[];
7
+ export declare function encodeClaudeProjectDir(cwd: string): string;
8
+ export declare function samePath(a: string, b: string): boolean;
9
+ /** Nearest git root at or above cwd. Stops at home so `~/` is never treated as a mega-project. */
10
+ export declare function projectRoot(cwd: string): string | undefined;
11
+ /** cwd + ancestors up to the git root (or just cwd when there is no repo). */
12
+ export declare function projectAncestors(cwd: string): string[];
13
+ /** Same folder, anywhere inside this git repo, or the repo root when you're in a subfolder. */
14
+ export declare function sessionBelongsToCwd(sessionCwd: string, wanted: string): boolean;
15
+ export declare function parseJsonLine(line: string): Record<string, unknown> | undefined;
16
+ export declare function asRecord(value: unknown): Record<string, unknown> | undefined;
17
+ export declare function asString(value: unknown): string | undefined;
18
+ export declare function parseTime(value: unknown): number | undefined;
19
+ export declare function truncateText(text: string, max: number): string;
20
+ export declare function formatBytes(n: number): string;
21
+ export declare function oneLine(text: string, max?: number): string;
22
+ export declare function firstMeaningfulLine(text: string): string;
23
+ export declare function parseArgsJson(raw: string): Record<string, unknown>;
24
+ export declare function fileMtimeMs(path: string): number;
25
+ export declare function isProbablyInjection(text: string): boolean;
26
+ export declare function readFirstJsonObject(path: string): Record<string, unknown> | undefined;
@@ -0,0 +1,4 @@
1
+ import type { ConvertedSession } from "./types.js";
2
+ export declare function writeConvertedSession(converted: ConvertedSession, destCwd?: string): string;
3
+ export declare function readJsonIfExists(path: string): unknown;
4
+ export declare function writeJson(path: string, value: unknown): void;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js CHANGED
@@ -4,7 +4,9 @@ import { writeFileSync } from "node:fs";
4
4
  import { cleanupBrandThemes, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, writeWebToolsExtension, } from "./agent-setup.js";
5
5
  import { printConsoleBanner } from "./brand.js";
6
6
  import { agentDir, apiModelToDef, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
7
+ import { ensureSearchTools } from "./search-tools.js";
7
8
  import { applyBrandUi, setUpdateNotice } from "./style.js";
9
+ import { offerStarterTemplates } from "./templates.js";
8
10
  import { fetchModels } from "./api.js";
9
11
  const PACKAGE_NAME = "u1s1-cli";
10
12
  /**
@@ -116,6 +118,9 @@ async function runAgent(cfg, args) {
116
118
  cleanupBrandThemes();
117
119
  ensureBrandPrompt();
118
120
  ensureDefaultSettings();
121
+ // 预 seed fd/rg(国内直连 GitHub 不通,pi 自己下不动);与取模型列表并行,
122
+ // 但必须在进 pi 之前就位,否则 pi 会自己去 GitHub 下载
123
+ const searchToolsReady = ensureSearchTools(cfg);
119
124
  // Fetch model list from server; fall back to built-in MODELS on error.
120
125
  // 服务端没开搜索(或老网关没有 features 字段)时不注册 web_search,模型就不会白调
121
126
  let webSearchEnabled = true;
@@ -137,6 +142,7 @@ async function runAgent(cfg, args) {
137
142
  process.env["U1S1_TOOLS_VIA_EXTENSION"] = "1";
138
143
  // hide pi's own "Run pi update" banner; users should run `u1s1 update` instead
139
144
  process.env["PI_SKIP_VERSION_CHECK"] = "1";
145
+ await searchToolsReady;
140
146
  const { CustomEditor, main } = await import("@earendil-works/pi-coding-agent");
141
147
  // 网页终端(如 Taikula)把 Shift+Enter 发成 ESC+CR(\x1b\r)。pi 在没开 Kitty
142
148
  // 协议时把它当成 Alt+Enter:空闲就直接发送,忙碌才排队。先改成 CSI-u 的
@@ -167,6 +173,7 @@ async function runAgent(cfg, args) {
167
173
  name: "u1s1",
168
174
  factory: (pi) => {
169
175
  applyBrandUi(pi, VERSION);
176
+ offerStarterTemplates(pi);
170
177
  // pi 内置只有 /quit;补一个更常见的 /exit,方便新手退出
171
178
  pi.registerCommand("exit", {
172
179
  description: "退出 u1s1",
@@ -0,0 +1,16 @@
1
+ import { type CliConfig } from "./config.js";
2
+ /** baseUrl 形如 https://api.u1s1.io/v1;auth 路由挂在同一域名的根路径。 */
3
+ export declare function apiOrigin(cfg: CliConfig): string;
4
+ export interface DeviceStart {
5
+ verify_url: string;
6
+ poll_secret: string;
7
+ interval: number;
8
+ expires_in: number;
9
+ }
10
+ /** 发起浏览器登录;网关太老或连不上时返回 null,退回手动粘贴。 */
11
+ export declare function startDeviceLogin(origin: string): Promise<DeviceStart | null>;
12
+ /** 轮询等浏览器那边批准;拿到 key 返回,过期返回 null。 */
13
+ export declare function pollDeviceLogin(origin: string, start: DeviceStart): Promise<string | null>;
14
+ export declare function login(keyArg?: string): Promise<CliConfig>;
15
+ /** Returns a config that definitely has an apiKey, prompting the user if needed. */
16
+ export declare function ensureAuth(): Promise<CliConfig>;
package/dist/login.js CHANGED
@@ -18,11 +18,11 @@ function tryOpenBrowser(url) {
18
18
  }
19
19
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
20
20
  /** baseUrl 形如 https://api.u1s1.io/v1;auth 路由挂在同一域名的根路径。 */
21
- function apiOrigin(cfg) {
21
+ export function apiOrigin(cfg) {
22
22
  return cfg.baseUrl.replace(/\/v1\/?$/, "");
23
23
  }
24
24
  /** 发起浏览器登录;网关太老或连不上时返回 null,退回手动粘贴。 */
25
- async function startDeviceLogin(origin) {
25
+ export async function startDeviceLogin(origin) {
26
26
  try {
27
27
  const resp = await fetch(`${origin}/auth/device/start`, { method: "POST" });
28
28
  if (!resp.ok)
@@ -42,7 +42,7 @@ async function startDeviceLogin(origin) {
42
42
  }
43
43
  }
44
44
  /** 轮询等浏览器那边批准;拿到 key 返回,过期返回 null。 */
45
- async function pollDeviceLogin(origin, start) {
45
+ export async function pollDeviceLogin(origin, start) {
46
46
  const deadline = Date.now() + start.expires_in * 1000;
47
47
  while (Date.now() < deadline) {
48
48
  await sleep(start.interval * 1000);
@@ -0,0 +1 @@
1
+ export declare function modelCommand(nameOrAlias?: string): Promise<void>;
@@ -0,0 +1,3 @@
1
+ import { type CliConfig } from "./config.js";
2
+ /** 缺哪个装哪个;都在(本地目录或 PATH)则秒退。失败只提示,不阻塞启动。 */
3
+ export declare function ensureSearchTools(cfg: Pick<CliConfig, "baseUrl">): Promise<void>;
@@ -0,0 +1,153 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
3
+ import { arch, platform } from "node:os";
4
+ import { join } from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { pipeline } from "node:stream/promises";
7
+ import { agentDir } from "./config.js";
8
+ /**
9
+ * 预 seed pi 的搜索组件(fd / ripgrep)到 ~/.u1s1/agent/bin。
10
+ *
11
+ * pi 首启发现缺 fd/rg 会从 GitHub 现下,国内直连基本不通:启动时连弹
12
+ * "Failed to download ... fetch failed",更糟的是 grep/find 工具运行时直接
13
+ * 报错,没有降级路径。这里在进 pi 之前从自家网关(/tools/<asset>,CF 边缘
14
+ * 代理 GitHub release,见 gateway/src/index.ts)把二进制放进 pi 的探测目录
15
+ * (探测顺序:agentDir/bin → PATH);探到本地文件后 pi 不再碰 GitHub。
16
+ * 任何失败都不阻塞启动——pi 自己的 GitHub 下载仍是兜底(有代理的用户能通)。
17
+ *
18
+ * 版本钉死而不是查 latest:免去对 GitHub API 的依赖,且 fd 10.4+ 不再发
19
+ * x86_64 macOS 产物,统一钉 10.3.0(与 pi 对 darwin-x64 的钉版一致)。
20
+ */
21
+ const TOOLS = [
22
+ { bin: "fd", version: "10.3.0", pathNames: ["fd", "fdfind"], asset: fdAsset },
23
+ { bin: "rg", version: "15.2.0", pathNames: ["rg"], asset: rgAsset },
24
+ ];
25
+ const binDir = join(agentDir, "bin");
26
+ const DOWNLOAD_TIMEOUT_MS = 60_000;
27
+ function archStr() {
28
+ const a = arch();
29
+ return a === "arm64" ? "aarch64" : a === "x64" ? "x86_64" : null;
30
+ }
31
+ function fdAsset(version) {
32
+ const a = archStr();
33
+ if (!a)
34
+ return null;
35
+ const p = platform();
36
+ if (p === "darwin")
37
+ return `fd-v${version}-${a}-apple-darwin.tar.gz`;
38
+ if (p === "linux")
39
+ return `fd-v${version}-${a}-unknown-linux-gnu.tar.gz`;
40
+ if (p === "win32")
41
+ return `fd-v${version}-${a}-pc-windows-msvc.zip`;
42
+ return null;
43
+ }
44
+ function rgAsset(version) {
45
+ const a = archStr();
46
+ if (!a)
47
+ return null;
48
+ const p = platform();
49
+ if (p === "darwin")
50
+ return `ripgrep-${version}-${a}-apple-darwin.tar.gz`;
51
+ // linux 上 arm64 用 gnu、x64 用 musl:与 pi 的资产选择保持一致
52
+ if (p === "linux") {
53
+ return a === "aarch64"
54
+ ? `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`
55
+ : `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;
56
+ }
57
+ if (p === "win32")
58
+ return `ripgrep-${version}-${a}-pc-windows-msvc.zip`;
59
+ return null;
60
+ }
61
+ /** 与 pi 的 commandExists 同语义:能 spawn 就算在 PATH 里(fd 也认 Debian 名 fdfind)。 */
62
+ function inPath(names) {
63
+ return names.some((name) => {
64
+ const r = spawnSync(name, ["--version"], { stdio: "pipe" });
65
+ return r.error === undefined || r.error === null;
66
+ });
67
+ }
68
+ function run(command, cmdArgs) {
69
+ const r = spawnSync(command, cmdArgs, { stdio: "pipe" });
70
+ return !r.error && r.status === 0;
71
+ }
72
+ function extract(archive, dir) {
73
+ if (archive.endsWith(".zip")) {
74
+ // 只有 win32 会拿到 zip。System32 的 tar 是 bsdtar,认 zip;Git Bash 的
75
+ // GNU tar 不认——优先绝对路径,失败再退 powershell Expand-Archive。
76
+ const sysRoot = process.env["SystemRoot"] ?? process.env["WINDIR"];
77
+ const sysTar = sysRoot ? join(sysRoot, "System32", "tar.exe") : "tar.exe";
78
+ if (run(existsSync(sysTar) ? sysTar : "tar.exe", ["xf", archive, "-C", dir]))
79
+ return true;
80
+ return run("powershell.exe", [
81
+ "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command",
82
+ "& { param($a,$d) $ErrorActionPreference='Stop'; Expand-Archive -LiteralPath $a -DestinationPath $d -Force }",
83
+ archive, dir,
84
+ ]);
85
+ }
86
+ return run("tar", ["xzf", archive, "-C", dir]);
87
+ }
88
+ /** 资产里的二进制可能嵌在版本号目录下(fd 是,rg 也是),递归找。 */
89
+ function findBinary(root, name) {
90
+ const stack = [root];
91
+ while (stack.length > 0) {
92
+ const dir = stack.pop();
93
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
94
+ const full = join(dir, entry.name);
95
+ if (entry.isFile() && entry.name === name)
96
+ return full;
97
+ if (entry.isDirectory())
98
+ stack.push(full);
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ async function install(tool, baseUrl, binName) {
104
+ const asset = tool.asset(tool.version);
105
+ if (!asset)
106
+ throw new Error(`不支持的平台 ${platform()}/${arch()}`);
107
+ mkdirSync(binDir, { recursive: true });
108
+ const res = await fetch(new URL(`/tools/${asset}`, baseUrl), {
109
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
110
+ });
111
+ if (!res.ok || !res.body)
112
+ throw new Error(`HTTP ${res.status}`);
113
+ const archive = join(binDir, asset);
114
+ // fd/rg 并行安装,解压目录按工具名+pid 隔离,避免互踩
115
+ const extractDir = join(binDir, `preseed_${tool.bin}_${process.pid}`);
116
+ try {
117
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(archive));
118
+ mkdirSync(extractDir, { recursive: true });
119
+ if (!extract(archive, extractDir))
120
+ throw new Error(`解压失败 ${asset}`);
121
+ const found = findBinary(extractDir, binName);
122
+ if (!found)
123
+ throw new Error(`包内没有 ${binName}`);
124
+ renameSync(found, join(binDir, binName));
125
+ if (platform() !== "win32")
126
+ chmodSync(join(binDir, binName), 0o755);
127
+ }
128
+ finally {
129
+ rmSync(archive, { force: true });
130
+ rmSync(extractDir, { recursive: true, force: true });
131
+ }
132
+ }
133
+ /** 缺哪个装哪个;都在(本地目录或 PATH)则秒退。失败只提示,不阻塞启动。 */
134
+ export async function ensureSearchTools(cfg) {
135
+ try {
136
+ const ext = platform() === "win32" ? ".exe" : "";
137
+ const missing = TOOLS.filter((t) => !existsSync(join(binDir, t.bin + ext)) && !inPath(t.pathNames));
138
+ if (missing.length === 0)
139
+ return;
140
+ console.log(` 正在安装搜索组件(${missing.map((t) => t.bin).join("/")})…`);
141
+ const results = await Promise.allSettled(missing.map((t) => install(t, cfg.baseUrl, t.bin + ext)));
142
+ for (let i = 0; i < results.length; i++) {
143
+ const r = results[i];
144
+ if (r.status === "rejected") {
145
+ const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
146
+ console.error(` ${missing[i].bin} 安装失败(${msg}),稍后将尝试 GitHub 直连`);
147
+ }
148
+ }
149
+ }
150
+ catch {
151
+ // 预 seed 只是加速,任何意外都不该挡住启动
152
+ }
153
+ }
@@ -0,0 +1 @@
1
+ export declare function createWebShortcut(): void;
@@ -0,0 +1,7 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export declare function setUpdateNotice(notice: string): void;
3
+ /**
4
+ * Brand chrome is just the startup hero + window title; everything else
5
+ * (tool rows, thinking blocks, spinner) stays Pi's default UI.
6
+ */
7
+ export declare function applyBrandUi(pi: ExtensionAPI, version: string): void;
@@ -0,0 +1,2 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export declare function offerStarterTemplates(pi: ExtensionAPI): void;