@x-otto/shared 0.1.0-alpha.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 ADDED
@@ -0,0 +1,107 @@
1
+ # @otto/shared
2
+
3
+ > General infrastructure toolkit: error system, structured logging, event emitters, HTTP/SSE client, JSONC, file system utilities, OAuth primitives, fuzzy search, text truncation, and more.
4
+
5
+ `@otto/shared` provides the shared utilities consumed by most packages in the monorepo. It depends on `@otto/env` and adds pino for structured logging, undici for HTTP, and a comprehensive error hierarchy with 11 domain-specific error classes. It also exposes a browser-safe entry point (`@otto/shared/browser`) with event primitives only.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pnpm add @otto/shared
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { createLogger } from '@otto/shared'
17
+ import { CodedError, NetworkError } from '@otto/shared'
18
+ import { TypedEventEmitter } from '@otto/shared'
19
+ import { parseJsonc, safeStringify } from '@otto/shared'
20
+ import { request, stream } from '@otto/shared'
21
+
22
+ // Logging
23
+ const log = createLogger('my-module')
24
+ log.info('hello world')
25
+
26
+ // Error hierarchy
27
+ throw new NetworkError('Connection failed', { code: 'ECONNRESET' })
28
+
29
+ // Typed events
30
+ const bus = new TypedEventEmitter<{ data: (payload: string) => void }>()
31
+ bus.on('data', (msg) => console.log(msg))
32
+
33
+ // HTTP + SSE
34
+ const resp = await request('https://api.example.com/data')
35
+ for await (const event of stream('https://api.example.com/stream')) {
36
+ console.log(event)
37
+ }
38
+ ```
39
+
40
+ ## API
41
+
42
+ ### Error System
43
+ - `CodedError` — base class with `code` and optional `cause` chain
44
+ - 11 domain error classes: `ConfigError`, `NetworkError`, `ProviderError`, `OAuthError`, `StreamError`, `AgentError`, `ToolError`, `HookError`, `SessionError`, `ExtensionError`, `McpError`
45
+
46
+ ### Logging
47
+ - `createLogger(name)` — create named child logger (pino-based, lazy-init root)
48
+ - `setLogLevel(level)` — dynamic log level adjustment
49
+ - `attachLogListener(handler)` — subscribe to all log messages for monitoring
50
+ - `isTransportInitialized()` — check if logger has been initialized
51
+
52
+ ### Event System
53
+ - `TypedEventEmitter<T>` — type-safe event emitter with `on`/`off`/`once`/`emit`
54
+ - `EventBus<T>` — extended emitter with `publish()` and wildcard `*` subscription
55
+
56
+ ### Resource Disposal
57
+ - `DisposableStack` / `AsyncDisposableStack` — LIFO resource cleanup (implements `Symbol.dispose`)
58
+ - `createDisposable()` / `createAsyncDisposable()` — factory helpers
59
+
60
+ ### HTTP & SSE
61
+ - `request(options)` — Fetch API wrapper with timeout and proxy support
62
+ - `stream(url, options)` — SSE stream reader returning `AsyncIterable<SseEvent>`
63
+ - `setDefaultProxy()` / `getDefaultProxy()` / `createDispatcher()` — proxy configuration
64
+ - `probeProxy(url)` — proxy connectivity check
65
+
66
+ ### File System
67
+ - `mkdirp(path)` — recursive directory creation
68
+ - `exists(path)` / `isFile(path)` / `isDirectory(path)` — existence checks
69
+ - `mime(filename)` / `readFileCapped(path)` — MIME inference and capped file reads
70
+
71
+ ### JSONC & Serialization
72
+ - `parseJsonc<T>(text)` — parse JSON with comments and trailing commas
73
+ - `safeStringify(value)` — stable deterministic serialization
74
+
75
+ ### OAuth Primitives
76
+ - `createPkcePair()` / `generateVerifier()` / `generateChallenge()` / `generateState()` — PKCE helpers
77
+ - `startLoopbackServer(options)` — OAuth callback server
78
+
79
+ ### Text Utilities
80
+ - `truncateHead(text, maxBytes, options?)` / `truncateTail(text, maxBytes, options?)` — capped truncation
81
+ - `truncateLine(text, maxBytes)` — single-line truncation
82
+ - `formatBytes(bytes)` — human-readable byte formatting
83
+
84
+ ### Claude Compat
85
+ - `mapClaudeToolName(name)` / `ottoToClaudeToolName(name)` — tool name translation
86
+ - `CLAUDE_TO_OTTO_TOOL` — bidirectional mapping table
87
+
88
+ ### Fuzzy Search
89
+ - `matchSubsequence(query, text)` / `scoreSubsequence(result)` — subsequence matching
90
+ - `rankCandidates(query, items)` / `rankNamedItems(query, items)` — ranked search
91
+
92
+ ### Other
93
+ - `invariant(condition, message)` — type-narrowing assertion (merged from `@otto/invariant`, RFC-157 M3a)
94
+ - `splitFrontmatter(text)` / `parseFrontmatterFields(text)` — frontmatter parsing
95
+ - `probeImageDimensions(data)` — image dimension extraction
96
+ - `checkShellCommand(cmd)` / `buildAllowedEnv()` — command validation
97
+ - `markStartupPhase(name)` / `traceStartupPhase(name)` — startup tracing
98
+ - `headerGet(headers, key)` — case-insensitive header lookup
99
+
100
+ ## Dependencies
101
+
102
+ - Internal: `@otto/env`
103
+ - External: `pino`, `pino-pretty`, `eventsource-parser`, `jsonc-parser`, `mime-types`, `safe-stable-stringify`, `uuid`, `undici`
104
+
105
+ ## Related
106
+
107
+ - [Architecture](./ARCHITECTURE.md)
@@ -0,0 +1,2 @@
1
+ import { i as TypedEventEmitter, r as Events, t as EventBus } from "../event-bus-inioRzYz.js";
2
+ export { EventBus, type Events, TypedEventEmitter };
@@ -0,0 +1 @@
1
+ import{n as e,t}from"../event-bus-CABu09lC.js";export{t as EventBus,e as TypedEventEmitter};
@@ -0,0 +1 @@
1
+ var e=class{listeners={};on(e,t){let n=this.listeners[e];return n||(n=new Set,this.listeners[e]=n),n.add(t),()=>this.off(e,t)}off(e,t){this.listeners[e]?.delete(t)}emit(e,...t){let n=this.listeners[e];if(n)for(let e of[...n])e(...t)}once(e,t){let n=((...r)=>{this.off(e,n),t(...r)});return this.on(e,n)}removeAllListeners(e){if(e!==void 0)delete this.listeners[e];else for(let e of Object.keys(this.listeners))delete this.listeners[e]}listenerCount(e){return this.listeners[e]?.size??0}},t=class extends e{publish(e){let t=(e,...t)=>this.emit(e,...t);for(let[n,r]of Object.entries(e)){let e=r??[];t(n,...e),t(`*`,n,...e)}}subscribe(e,t,n=!1){return n?this.once(e,t):this.on(e,t)}subscribeAll(e){return this.subscribe(`*`,e)}};export{e as n,t};
@@ -0,0 +1,24 @@
1
+ //#region src/event-emitter.d.ts
2
+ type EventListener = (...args: never[]) => void;
3
+ type Events = Record<string, EventListener>;
4
+ declare class TypedEventEmitter<T extends Record<keyof T, EventListener>> {
5
+ private readonly listeners;
6
+ on<K extends keyof T>(event: K, listener: T[K]): () => void;
7
+ off<K extends keyof T>(event: K, listener: T[K]): void;
8
+ emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>): void;
9
+ once<K extends keyof T>(event: K, listener: T[K]): () => void;
10
+ removeAllListeners<K extends keyof T>(event?: K): void;
11
+ listenerCount<K extends keyof T>(event: K): number;
12
+ }
13
+ //#endregion
14
+ //#region src/event-bus.d.ts
15
+ type UnSubscribe = () => void;
16
+ type SubscribeEvent = Events;
17
+ declare class EventBus<T extends SubscribeEvent = SubscribeEvent> extends TypedEventEmitter<T> {
18
+ publish(event: Partial<{ [K in keyof T]: Parameters<T[K]> }>): void;
19
+ subscribe<K extends keyof T>(type: K, callback: T[K], once?: boolean): UnSubscribe;
20
+ subscribeAll(callback: (...args: unknown[]) => void): UnSubscribe;
21
+ }
22
+ //#endregion
23
+ export { TypedEventEmitter as i, EventListener as n, Events as r, EventBus as t };
24
+ //# sourceMappingURL=event-bus-inioRzYz.d.ts.map
@@ -0,0 +1,668 @@
1
+ import { i as TypedEventEmitter, n as EventListener, r as Events, t as EventBus } from "./event-bus-inioRzYz.js";
2
+ import pino, { Logger } from "pino";
3
+ import { ProxyAgent } from "undici";
4
+
5
+ //#region src/error.d.ts
6
+ declare class CodedError extends Error {
7
+ readonly code: string;
8
+ readonly cause?: Error;
9
+ /** HTTP 状态码(如有)——供 classifyStreamError 的 extractStatus 走状态分类(429/5xx→retryable)。 */
10
+ readonly status?: number;
11
+ /** 响应头(如有)——供 extractRetryAfterMs 读 Retry-After。 */
12
+ readonly headers?: unknown;
13
+ /**
14
+ * 供应商响应体自带的机器可读错误分类(如有)——与本类的 `code`(otto 内部错误码,如
15
+ * PROVIDER_AUTH_MISSING/PROVIDER_API_ERROR)是两个不同维度:`code` 分类"otto 如何处置这个
16
+ * 错误",`errorType` 记录"供应商认为错误的根因是什么"。典型例子:Anthropic 401 响应体
17
+ * `{"error":{"type":"authentication_error","message":"..."}}` 里的 `error.type`(SDK 解析
18
+ * 到 `APIError.type`),OpenAI 响应体 `{"error":{"type":"invalid_request_error","code":
19
+ * "invalid_api_key",...}}` 里的 `error.type`/`error.code`。此前这些字段被丢弃,只保留
20
+ * 拼接后的 message 文案——"记录登录失效原因"要求能区分"token 过期"(authentication_error)
21
+ * vs "权限不足"(permission_error) vs "账号被禁"等根因,而不只是重复 status/message。
22
+ */
23
+ readonly errorType?: string;
24
+ /**
25
+ * RFC-153 D6:错误来源标记(存 pluginId),`undefined` 表示非插件来源(内建能力)。
26
+ * 只服务"标记来源、供 UI 展示"——任何实现都不得据此引入新的重试/降级分支逻辑(规则 8)。
27
+ * 由插件到引擎的适配层(`toProviderFactory`/`toAgentTool`/`toOAuthProvider`,
28
+ * `packages/coding/src/plugin/module-wiring.ts`)在包装点补充。
29
+ */
30
+ readonly pluginSource?: string;
31
+ constructor(message: string, options: {
32
+ code: string;
33
+ cause?: Error;
34
+ status?: number;
35
+ headers?: unknown;
36
+ errorType?: string;
37
+ pluginSource?: string;
38
+ });
39
+ }
40
+ declare class NetworkError extends CodedError {}
41
+ declare class ProviderError extends CodedError {}
42
+ declare class OAuthError extends CodedError {}
43
+ declare class AgentError extends CodedError {}
44
+ declare class McpError extends CodedError {}
45
+ //#endregion
46
+ //#region src/invariant.d.ts
47
+ declare class InvariantError extends Error {
48
+ framesToPop: number;
49
+ name: string;
50
+ constructor(message?: string | number);
51
+ }
52
+ declare function invariant(condition: unknown, message?: string | number): asserts condition;
53
+ //#endregion
54
+ //#region src/types.d.ts
55
+ type DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P] };
56
+ type DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P] };
57
+ //#endregion
58
+ //#region src/logger.d.ts
59
+ type LogLevel = pino.Level;
60
+ interface LoggerOptions {
61
+ level: LogLevel;
62
+ destination: string;
63
+ }
64
+ declare function attachLogListener(callback: (log: unknown) => void): () => void;
65
+ declare function setLogLevel(level: LogLevel): void;
66
+ /**
67
+ * 根 logger 单例初始化。**`destination` 仅首次调用(root 未建)时生效**——后续调用复用已建 root,
68
+ * 传入的不同 destination 被忽略(单根多 child 架构,destination 是进程级而非 per-logger 选项)。
69
+ */
70
+ /**
71
+ * RFC-185 M1 顺序不变量探针:transport(pino.transport worker → stderr/文件 target)
72
+ * 是否已建。cli 的顺序测试用它断言"import cli 模块图不触发 transport 创建"。
73
+ *
74
+ * 为什么探 transport 而非 root logger:root 的创建无法推迟——本仓几十个模块级
75
+ * `const logger = createLogger(...)`(coding 包 plugin/* 等)在 import 阶段就触发
76
+ * `ensureLogger`,任何 runCli() 内的 env 写入都晚于它。因此把「读 env 决定 targets」
77
+ * 的时刻从 transport 建成时推迟到**首次日志字节写出时**(下方 lazy stream)——
78
+ * runCli() 设 `OTTO_LOG_STDERR` 恒早于首条日志(parseCLI/env 写入路径零日志),
79
+ * 顺序不变量由此成立。副收益:fatal 静默的 interactive 会话若全程零日志,
80
+ * transport worker 线程根本不会创建。
81
+ */
82
+ declare function isTransportInitialized(): boolean;
83
+ declare function createLogger(name: string, options?: LoggerOptions): pino.Logger;
84
+ //#endregion
85
+ //#region src/fs-extra.d.ts
86
+ /**
87
+ * 大文件字节阈值单一权威定义(RFC-189 D2,grill 已确认 10MB):文件"用户主动打开任意
88
+ * 文件系统路径查看"场景(`/open` 命令、`@file` sigil 预览)的读取层与渲染层
89
+ * (`@otto/tui file-content-views.ts` 的候选视图熔断)共用同一个数字,避免两处各自
90
+ * 定义产生不一致(如读取层限 10MB、渲染层限 8MB)。
91
+ */
92
+ declare const LARGE_FILE_BYTE_THRESHOLD: number;
93
+ declare function mime(path: string): Promise<string>;
94
+ declare function mkdirp(path: string): Promise<void>;
95
+ declare function exists(path: string): Promise<boolean>;
96
+ declare function isDirectory(path: string): Promise<boolean>;
97
+ declare function isFile(path: string): Promise<boolean>;
98
+ interface ReadCappedResult {
99
+ /** 实际读取到的内容(超限时只是文件的前缀,非全文)。 */
100
+ content: string;
101
+ /** 是否命中大小上限被截断(调用方据此决定是否展示"文件过大"提示)。 */
102
+ truncated: boolean;
103
+ /** 文件真实总字节数(stat 结果,即使被截断也返回真实大小,供调用方展示)。 */
104
+ totalBytes: number;
105
+ }
106
+ /**
107
+ * 按字节上限读取文件——超限时只读取文件前 `maxBytes` 字节,不做全量读取。
108
+ *
109
+ * 用于"用户主动打开任意文件系统路径查看"场景(`/open` 命令、`@file` sigil 预览):
110
+ * 修复此前"先 `readFile` 全量读入内存、再判断是否超限降级"的架构缺陷——对一个 2GB
111
+ * 的日志文件,那种写法会先尝试把整个 2GB 读进 Node 进程内存才轮到熔断逻辑生效,
112
+ * 熔断放错了层级(本函数把熔断前移到"读之前",用 `stat` 拿到真实大小后决定读取
113
+ * 字节数,读取本身是常数级开销,不随文件真实大小增长)。
114
+ *
115
+ * 只读前缀足够支撑"降级预览"场景(消费方进一步走 `resolveFileContentViews` 的
116
+ * 大文件熔断分支,本就只展示文件开头部分),不需要真正的流式渲染/分片加载——
117
+ * TUI 场景不支持编辑超大文件,没有 piece-tree 式增量结构的必要性。
118
+ */
119
+ declare function readFileCapped(path: string, maxBytes?: number): Promise<ReadCappedResult>;
120
+ //#endregion
121
+ //#region src/truncate.d.ts
122
+ interface TruncatedResult {
123
+ content: string;
124
+ truncated: boolean;
125
+ truncatedBy: 'lines' | 'bytes';
126
+ totalLines: number;
127
+ totalBytes: number;
128
+ outputLines: number;
129
+ outputBytes: number;
130
+ lastLinePartial: boolean;
131
+ firstLineExceedsLimit: boolean;
132
+ options: TruncateOptions;
133
+ }
134
+ /**
135
+ * 元数据视图(RFC-170 D2):与 `TruncatedResult` 同形但**不含 `content`**。
136
+ *
137
+ * 背景:`ToolResultMessage.details.truncation` 此前直接携带完整 `TruncatedResult`(含
138
+ * `content` 字段——工具输出截断后的全量文本副本,最大 60KB),与 `content[0].text`
139
+ * (已含同一份文本)重复驻留。全仓核实 `details.truncation` 的 `content` 字段零下游
140
+ * 消费方(TUI/CLI/trace/provider 序列化均只读 `content[0].text`),纯粹是内存 + DB
141
+ * 的双重浪费(RFC-170 事故会话实测 421 条 tool_result 重复驻留、1.2MB DB 浪费)。
142
+ * `details` 只需保留元数据(用于诊断"是否截断/截断方式/原始大小"),故拆出本类型
143
+ * 编译期禁止误把 `content` 塞回 details(`toTruncationMeta` 是唯一转换入口)。
144
+ */
145
+ type TruncationMeta = Omit<TruncatedResult, 'content'>;
146
+ /** `TruncatedResult` → `TruncationMeta`(剥离 content,供 details.truncation 使用)。 */
147
+ declare function toTruncationMeta(result: TruncatedResult): TruncationMeta;
148
+ declare function formatBytes(bytes: number): string;
149
+ interface TruncateOptions {
150
+ maxLines: number;
151
+ maxBytes: number;
152
+ }
153
+ declare function truncateHead(content: string, options: TruncateOptions): TruncatedResult;
154
+ declare function truncateTail(content: string, options: TruncateOptions): TruncatedResult;
155
+ declare function truncateLine(line: string, maxChars: number): {
156
+ text: string;
157
+ wasTruncated: boolean;
158
+ };
159
+ //#endregion
160
+ //#region ../env/dist/index.d.ts
161
+ declare function normalizePath(path: string): string;
162
+ /**
163
+ * 从 `startDir` 向上查找最近的 `pnpm-workspace.yaml`(monorepo root 标志文件),返回其所在目录。
164
+ * 找不到(如在非本仓的用户项目里运行)返回 `undefined`。
165
+ *
166
+ * RFC-130:单一真源实现——原先在 `@otto/coding`(`install-plugin.ts`,供 CLI `--repo` 消费)与
167
+ * `@otto/plugin`(`discovery.ts`,供仓库级插件发现消费)各自维护一份逐字相同的实现(copy-paste),
168
+ * 无法互相 import(`@otto/plugin` 是 `@otto/coding` 的依赖,反向 import 会成环)。收敛到零依赖的
169
+ * `@otto/env`(两者共同的底层依赖)消除维护漂移风险(终局审核 finding,2026-07-09)。
170
+ *
171
+ * 终止条件用 `dirname(dir) === dir`(到达文件系统根)而非固定层数上限——后者在深层嵌套路径下
172
+ * 会误截断(旧实现的 `i < 20` 上限已移除,审核 finding:该上限是冗余约束)。
173
+ */
174
+ //#endregion
175
+ //#region src/path.d.ts
176
+ declare function getProjectDir(): string;
177
+ declare function getHomeDir(): string;
178
+ declare function getThirdPartyToolDir(): string;
179
+ declare function getThirdPartyToolBinaryDir(toolName: string, version?: string): string;
180
+ declare function createTempLoggerPath(): string;
181
+ /**
182
+ * RFC-093 T4:bash 溢出日志的项目本地路径(`.otto/tool-results/bash/`)。
183
+ * 相比 /tmp:① read 工具项目根约束天然放行(模型可直接恢复全文)② 会话存活期内不被 1h TTL 清掉。
184
+ * 目录由调用方(bash.ts createWriteStream 前 mkdir)确保存在。
185
+ */
186
+ declare function createProjectOverflowLogPath(): string;
187
+ /**
188
+ * 清理过期的 bash 溢出临时日志(mtime 早于 now-ttlMs 的 `otto-coding-*.log`)。
189
+ * best-effort:单文件出错跳过、目录不可读返回 0;返回已删除数量。长驻进程(service)据此避免临时日志无界堆积。
190
+ * 当前运行刚创建的日志 mtime≈now 不会被清。`now`/`ttlMs` 由调用方传入(便于测试与确定性)。
191
+ */
192
+ declare function sweepStaleTempLogs(ttlMs: number, now: number): Promise<number>;
193
+ /**
194
+ * 清理过期的项目本地 bash 溢出日志(`.otto/tool-results/bash/otto-coding-*.log`)。
195
+ * 与 /tmp 的 1h TTL 不同:溢出日志设计上要在会话存活期内可被 read 工具恢复(见
196
+ * createProjectOverflowLogPath),因此用远长于会话生命周期的 TTL(调用方传入,建议 7 天)。
197
+ * 修复 RFC-093 T4 的清理盲区——此前该目录无任何回收路径,实测单文件堆到 1.2GB。
198
+ */
199
+ declare function sweepStaleProjectOverflowLogs(ttlMs: number, now: number): Promise<number>;
200
+ //#endregion
201
+ //#region src/jsonc.d.ts
202
+ declare function parseJsonc<T = unknown>(input: string): T;
203
+ declare function safeStringify(value: unknown, indent?: number): string;
204
+ //#endregion
205
+ //#region src/glob-match.d.ts
206
+ /**
207
+ * glob-match.ts —— 轻量 glob 匹配(RFC-187 D6)。
208
+ *
209
+ * 用于文件名/相对路径匹配场景(非路径 specifier 语义,与 `@otto/setting`
210
+ * `globToRegexSource` 的 `~` 展开/绝对路径锚定正交,各自独立实现——本函数不做
211
+ * `~` 展开,返回 boolean 而非 regex 字符串,且默认大小写不敏感)。
212
+ *
213
+ * 支持 `*`(不跨 `/`)、`**`(跨 `/`)、`?`(单字符)。不支持 brace expansion
214
+ * `{a,b}`、否定模式 `!(pattern)` 等重量级特性——文件类型 matcher 场景不需要
215
+ * (RFC-187 §2 非目标;R5:不得在本函数上功能蔓延,需要更强能力时评估引入专门库)。
216
+ *
217
+ * 已知边界(RFC-187 R8):`**` 是简单字符串替换为 `.*`,与相邻字面斜杠之间
218
+ * 不做"零或多层目录"的特殊语义——形如 "**" + 斜杠 + "*.proto" 的模式不匹配无前导
219
+ * 目录的 "c.proto",形如 "a" + 斜杠 + "**" + 斜杠 + "b" 的模式不匹配 "a/b"("**"
220
+ * 不能吸收相邻的字面斜杠)。这是与 `@otto/setting` `globToRegexSource` 同款的简单
221
+ * 转换限制,非 bug。插件作者/matcher 声明方需要"零或多层"语义时应显式声明不带
222
+ * 前导/尾随双星号斜杠的等价模式(如同时声明 `*.proto` 覆盖顶层),而非依赖 `**`
223
+ * 自动吸收空目录段。
224
+ */
225
+ declare function matchesGlobPattern(pattern: string, input: string): boolean;
226
+ //#endregion
227
+ //#region src/http.d.ts
228
+ /**
229
+ * 设置进程级默认 HTTP 代理。传空字符串或 falsy 值清除。
230
+ * CLI 层在 settings.load() 后及 settings.change 事件中调用。
231
+ */
232
+ declare function setDefaultProxy(url?: string): void;
233
+ /** 获取进程级默认代理 URL(供需要直接使用 ProxyAgent 的调用方,如 Anthropic SDK 的 httpAgent)。 */
234
+ declare function getDefaultProxy(): string | undefined;
235
+ /**
236
+ * 根据代理 URL 创建 undici ProxyAgent dispatcher。
237
+ * 优先用显式传入的 proxyUrl,其次用进程级默认代理。
238
+ * 无效 URL 返回 undefined(不走代理)。
239
+ */
240
+ declare function createDispatcher(proxyUrl?: string): ProxyAgent | undefined;
241
+ /**
242
+ * 安装/卸载**进程级全局**代理 dispatcher(RFC-288 D0b)。
243
+ *
244
+ * 与 `createDispatcher` 的分工:后者产出 per-request/per-client 的 dispatcher,只作用于
245
+ * 显式消费它的调用方(`request`/`stream`/Anthropic SDK);本函数改写 undici 的**全局**
246
+ * dispatcher,使进程内**所有** `fetch()` 自动经代理——包括各包直接裸调 `fetch` 的路径
247
+ * (OAuth token 交换、远端持久化、install/upgrade 检查、service 登录等)。
248
+ *
249
+ * 背景(RFC-288 §背景·第零类):此前代理只对 `@otto/shared` 的 `request()`/`stream()`
250
+ * 生效,全仓仅 1 个 import;Anthropic 走官方 SDK 未传 dispatcher,Node 22 内置 fetch 又
251
+ * 不读 `HTTP_PROXY` 环境变量——导致「面板显示已启用但 Claude 请求直连」的静默失败。
252
+ * 选择全局兜底而非逐个调用点接线,正是因为后者的漏接线成本已被这次事故证明。
253
+ *
254
+ * 用 undici 内建 `EnvHttpProxyAgent` 而非自研拦截器:它的 `httpProxy`/`httpsProxy` 显式
255
+ * 覆盖环境变量(不读也不写 `process.env`,故不污染子进程),`noProxy` 直接承载环回豁免。
256
+ *
257
+ * @param url 代理地址;传 falsy 值=卸载并还原到启动快照。非法 URL 静默忽略(不改变现状),
258
+ * 与 `createDispatcher` 的降级语义一致——启动期配置错误不应让进程崩溃。
259
+ */
260
+ declare function installGlobalProxyDispatcher(url?: string): void;
261
+ /** `probeProxy` 的失败归类——决定提示语该不该归咎代理(RFC-288 D4)。 */
262
+ type ProxyProbeErrorKind = /** 连不上代理本身(代理地址/端口错、代理软件没开)。 */'proxy-unreachable' /** 代理通了,但目标站点不可达(DNS 失败、目标宕机)——**不归咎代理**。 */ | 'target-unreachable' /** 超时,无法区分是代理慢还是目标慢。 */ | 'timeout' /** 其它未分类错误。 */ | 'unknown';
263
+ interface ProxyProbeResult {
264
+ ok: boolean;
265
+ /** 往返耗时(毫秒),失败时也记录(便于判断是"秒失败"还是"卡到超时")。 */
266
+ latencyMs: number;
267
+ /** HTTP 状态码;连接层失败时为 undefined。 */
268
+ status?: number;
269
+ errorKind?: ProxyProbeErrorKind;
270
+ /** 原始错误信息(展示给用户前由调用方决定截断/翻译)。 */
271
+ errorMessage?: string;
272
+ }
273
+ /**
274
+ * 探测「当前生效的代理能否连到目标」(RFC-288 D4)。
275
+ *
276
+ * **探测目标 = 调用方传入的真实 provider baseURL**,不是任何固定的第三方公开端点。
277
+ * 固定端点在墙内是双输:选境外端点(如 example.com)它自己可能被墙,代理是好的也报失败
278
+ * (假阴性);选国内可直连端点则不走代理也通,证明不了代理对境外有效(假阳性)。用户配
279
+ * 代理的唯一目的就是访问他正在用的那个模型端点,探测它即直接回答用户的真实问题。
280
+ *
281
+ * **401/403 视为成功**:探测不带凭据,故认证失败恰恰证明"传输层通了、请求到达了服务端"。
282
+ * 若把它算作失败,用户会把认证问题误读成代理问题。
283
+ *
284
+ * **走裸 `fetch` 而不显式传 dispatcher**——这不是疏漏,而是与生产路径保持一致的硬要求:
285
+ * 本仓代理由 `installGlobalProxyDispatcher` 经 undici 全局 dispatcher 接管,真实的 AI 请求
286
+ * 也是这样被代理的。若这里改用 per-request `dispatcher`,① 会撞上 Node 内置 fetch(undici 6.23)
287
+ * 与本包 undici(8.7) 的实例不兼容(抛 `UND_ERR_INVALID_ARG`);② 更根本的是,探测与真实
288
+ * 请求一旦走两条不同路径,"探测通过"就不再能代表"AI 请求也能通"——这正是探测功能最该避免
289
+ * 的假阳性。详见 `installGlobalProxyDispatcher` 与 RFC-288 重要事项规则 11。
290
+ *
291
+ * @param targetUrl 探测目标(当前模型的 baseURL)。
292
+ * @param timeoutMs 超时(默认 5s)。
293
+ */
294
+ declare function probeProxy(targetUrl: string, timeoutMs?: number): Promise<ProxyProbeResult>;
295
+ interface HttpRequestOptions {
296
+ url: string;
297
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
298
+ headers?: Record<string, string>;
299
+ body?: string | Record<string, unknown>;
300
+ timeout?: number;
301
+ /**
302
+ * SSE 流的**空闲**超时(毫秒,仅 `stream()`):每收到一个 chunk 就重置;连续 `idleTimeout` 内无任何
303
+ * 字节到达即 abort。区别于 `timeout`(整请求一次性墙钟上限)——空闲超时专治「连接活着但 provider
304
+ * 静默不再吐字节」的挂死(健康流远比此频繁吐 token,不受影响)。默认 120s。
305
+ */
306
+ idleTimeout?: number;
307
+ signal?: AbortSignal;
308
+ proxy?: string;
309
+ /**
310
+ * 成功响应回调(仅 stream(),仅 HTTP 200 时触发):在 SSE 迭代开始前同步调用,携带响应 Headers。
311
+ * 用于读取成功路径的 rate-limit/usage 头(如 anthropic-ratelimit-unified-*, x-ratelimit-* 等),
312
+ * 不必等到错误路径才挖 header(此时 response 对象仍在作用域内)。可选,不传=零开销。
313
+ */
314
+ onResponse?: (headers: Headers) => void;
315
+ }
316
+ interface HttpResponse {
317
+ status: number;
318
+ headers: Headers;
319
+ body: string;
320
+ ok: boolean;
321
+ }
322
+ interface SseEvent {
323
+ event?: string;
324
+ data: string;
325
+ id?: string;
326
+ }
327
+ declare function request(options: HttpRequestOptions): Promise<HttpResponse>;
328
+ declare function stream(options: HttpRequestOptions): AsyncIterable<SseEvent>;
329
+ //#endregion
330
+ //#region src/header-utils.d.ts
331
+ /**
332
+ * header-utils.ts — HTTP 头不区分大小写查询工具。
333
+ *
334
+ * 支持 `Headers` 对象(标准 Web API)与普通 `Record<string, string>` 两种形态,
335
+ * 也为 `unknown` 入参做防御(provider 错误链路中的 headers 可能来源不明)。
336
+ * 键匹配不区分大小写(HTTP/1.1 §4.2 头名大小写不敏感)。
337
+ */
338
+ declare function headerGet(headers: unknown, key: string): string | undefined;
339
+ //#endregion
340
+ //#region src/command.d.ts
341
+ /**
342
+ * 危险 shell 命令检测。
343
+ *
344
+ * 检测用户输入的 shell 命令是否匹配已知危险模式(递归删除、权限提升、
345
+ * 远程脚本执行等),拒绝在 TUI 交互式 `!`/`!!` 中直接执行,引导用户
346
+ * 走 bash tool(经 Zod 校验 + sandbox + 批准门禁)。
347
+ */
348
+ /** 安全检查结果 */
349
+ type CommandCheckResult = {
350
+ safe: true;
351
+ } | {
352
+ safe: false;
353
+ reason: string;
354
+ };
355
+ /**
356
+ * 检查 shell 命令是否危险。
357
+ * 返回 { safe: false, reason } 表示拒绝执行,{ safe: true } 表示可通过。
358
+ */
359
+ declare function checkShellCommand(command: string): CommandCheckResult;
360
+ /**
361
+ * 构建子进程安全环境变量。
362
+ *
363
+ * 白名单必要变量,strip 敏感凭据(API key、token、secret)。
364
+ * 用于 MCP server、LSP server、shell 子进程等第三方进程——它们不应
365
+ * 继承父进程的 API 凭据。
366
+ *
367
+ * 具体白名单以下方 allowedPrefixes / allowedNames 常量为单一真源,此处不复述。
368
+ *
369
+ * @param overrides 额外透传的变量(会覆盖白名单默认值)
370
+ */
371
+ declare function buildAllowedEnv(overrides?: Record<string, string | undefined>): Record<string, string>;
372
+ //#endregion
373
+ //#region src/claude-tool-map.d.ts
374
+ /**
375
+ * claude-tool-map.ts — RFC-032 §3.0:Claude Code 工具名 ↔ otto 工具名映射(承重前置).
376
+ *
377
+ * Claude 用 TitleCase(`Bash`/`Read`/`Edit`/`Task`…),otto 全小写(`bash`/`read`/`edit`/
378
+ * `task_delegate`…,真源 `packages/tools/src/tool-nodes.ts`)。permissions/hooks/commands/agents
379
+ * 四个面都按工具名匹配,故任何 `.claude` 来源的工具名都必须先经此表归一,否则永不命中。
380
+ *
381
+ * **fail-closed 铁律**:未知 Claude 工具名**不**回退成"全部工具"。
382
+ * - 严格用途(permissions/hooks 将来):`mapClaudeToolName` 返回 undefined,调用方丢弃。
383
+ * - agent `tools` 白名单:`mapClaudeAgentToolNames` 对未知名**保留原样**(otto 无此工具 →
384
+ * filterToolsByNames 自然排除),关键是**保持数组非空**,避免 resolveTools 落到
385
+ * "agentToolNames 为空 → 全部工具" 的越权分支(session-config-resolver.ts:163-167)。
386
+ */
387
+ /** Claude 工具名 → otto 工具名。只收录高置信映射;其余视为未知。 */
388
+ declare const CLAUDE_TO_OTTO_TOOL: Readonly<Record<string, string>>;
389
+ /** 严格映射:已知→otto 名,未知→undefined(调用方按 fail-closed 丢弃)。 */
390
+ declare function mapClaudeToolName(claudeName: string): string | undefined;
391
+ /** otto 工具名 → Claude 名(无对应则返回原 otto 名,便于 matcher 兜底匹配)。 */
392
+ declare function ottoToClaudeToolName(ottoName: string): string;
393
+ /**
394
+ * agent `tools` 白名单映射:已知→otto 名;未知→保留原样(不匹配任何 otto 工具,
395
+ * 等效排除)。**不去重去空**——保持非空以防 resolveTools 落入"全部工具"分支。
396
+ * 入参为空数组时原样返回空(调用方须自行区分"未声明 tools"=继承全部 vs "声明了但全未知")。
397
+ */
398
+ declare function mapClaudeAgentToolNames(claudeNames: readonly string[]): string[];
399
+ //#endregion
400
+ //#region src/oauth/pkce.d.ts
401
+ interface PkcePair {
402
+ /** code_verifier:token 交换时回传,向 server 证明授权码的持有者。 */
403
+ verifier: string;
404
+ /** code_challenge:S256(verifier),随 authorize 请求发出。 */
405
+ challenge: string;
406
+ /** challenge 方法,固定 'S256'。 */
407
+ method: 'S256';
408
+ }
409
+ /**
410
+ * 生成 code_verifier:32 字节随机熵 → base64url(43 字符,
411
+ * 落在 RFC 7636 要求的 43–128 字符区间内)。
412
+ */
413
+ declare function generateVerifier(): string;
414
+ declare function generateChallenge(verifier: string): string;
415
+ declare function createPkcePair(): PkcePair;
416
+ declare function generateState(): string;
417
+ //#endregion
418
+ //#region src/oauth/loopback-server.d.ts
419
+ interface LoopbackServerOptions {
420
+ ports?: number[];
421
+ callbackPath?: string;
422
+ expectedState?: string;
423
+ host?: string;
424
+ signal?: AbortSignal;
425
+ }
426
+ interface LoopbackCallback {
427
+ code: string;
428
+ state?: string;
429
+ }
430
+ interface LoopbackServer {
431
+ readonly port: number;
432
+ readonly redirectUri: string;
433
+ waitForCallback(): Promise<LoopbackCallback>;
434
+ close(): Promise<void>;
435
+ }
436
+ declare function startLoopbackServer(options?: LoopbackServerOptions): Promise<LoopbackServer>;
437
+ //#endregion
438
+ //#region src/oauth/browser.d.ts
439
+ /**
440
+ * 用操作系统默认浏览器打开 URL。
441
+ * 跨平台:macOS→`open`、Windows→`start`、Linux→`xdg-open`。
442
+ * 失败静默吞错(不抛),返回 boolean 表示是否成功启动。
443
+ */
444
+ declare function openBrowser(url: string): boolean;
445
+ //#endregion
446
+ //#region src/frontmatter.d.ts
447
+ interface ParsedFrontmatter {
448
+ fields: Map<string, string>;
449
+ body: string;
450
+ }
451
+ declare function splitFrontmatter(content: string): ParsedFrontmatter | null;
452
+ declare function parseFrontmatterFields(frontmatter: string): Map<string, string>;
453
+ declare function parseFrontmatterList(value: string | undefined): string[] | undefined;
454
+ //#endregion
455
+ //#region src/image-dimension-probe.d.ts
456
+ /**
457
+ * Anthropic 单图恒定硬上限 8000×8000px。
458
+ * 2000px 严格限制仅在单请求 >20 张图时生效,而 work-loop 的 capImagesPerRequest(D6)
459
+ * 已保证 ≤20 张,故接收端守卫按 8000px 拦截——拒绝仅限 API 必然拒绝的图,不误伤
460
+ * Retina 截图(2560px+)。
461
+ */
462
+ declare const API_IMAGE_DIMENSION_LIMIT_PX = 8000;
463
+ /**
464
+ * 内联进模型上下文的单图字节上限(5MB)——**跨包单源**(C 组收敛,2026-08-09)。
465
+ *
466
+ * 底层约束是 provider(Anthropic)对单图输入的字节上限。此前四处各自硬编码 5MB 并靠
467
+ * 注释「与 XX 对齐 / 变更时同步更新 XX」手动维护,是漂移反模式。收敛到本处(provider
468
+ * 图片约束常量的既有归属地,与上面的维度上限同域)。四处消费方:
469
+ * - `@otto/tools` fs/read.ts(读取本地图片文件的字节上限)
470
+ * - `@otto/mcp` mcp-tool-adapter.ts(MCP 工具产出图片的字节上限)
471
+ * - `@otto/tui` input/clipboard-image.ts(剪贴板图片的字节上限)
472
+ * - `@otto/agent` tool-result-clamp.ts(RFC-151 D2:image 块内联上限,超限走 spill)
473
+ *
474
+ * **不做 env 覆盖**(刻意):`@otto/agent` 是确定性纯引擎(RFC-192,零 env 读取以保证
475
+ * 重放一致性),若本常量经 env 求值,agent 消费的值会随环境漂移、破坏 record/replay 不
476
+ * 变式。故保持字面量;未来若需可调,应经注入传入 agent,而非改本常量为 env 读取。
477
+ *
478
+ * 语义:字节长度 > 本值才拒绝,恰好等于放行(各消费方统一此边界)。
479
+ */
480
+ declare const PROVIDER_IMAGE_INLINE_MAX_BYTES: number;
481
+ /**
482
+ * 从 base64 图片数据探测像素宽高。
483
+ *
484
+ * 按 mime 类型分派解析策略;mime 不明时退化到 magic bytes 嗅探。
485
+ * 任何解析异常(越界读取/未知格式/数据截断)捕获后返回 undefined,绝不抛出。
486
+ *
487
+ * 性能优化:PNG/GIF/WebP 仅解码 base64 前缀(128 字符 ≈ 96 字节),
488
+ * 足够所有定长 header + 魔数嗅探 + MIME 分派判断。仅 JPEG marker 扫描需全量解码。
489
+ */
490
+ declare function probeImageDimensions(base64: string, mime: string): {
491
+ width: number;
492
+ height: number;
493
+ } | undefined;
494
+ //#endregion
495
+ //#region src/startup-trace.d.ts
496
+ /**
497
+ * startup-trace.ts —— 启动相位打点(RFC-150 D7/M0)。
498
+ *
499
+ * 目的:给 otto 启动链路的关键相位(模块 import 完成 / App 构造 / app.start() 各子步 /
500
+ * TUI 首帧 / MCP 各 server 连接结果)打单行耗时到 stderr,作为 RFC-150 全部性能优化
501
+ * 里程碑的统一度量基准(“测量先行”,RFC-150 重要事项规则 4)。
502
+ *
503
+ * 设计要点:
504
+ * - **默认零开销**:只在 `OTTO_STARTUP_TRACE=1` 时启用;未启用时 `mark()`/`phase()` 是
505
+ * 一次 env 读取 + 短路返回,不产生任何字符串格式化或 stderr 写入。
506
+ * - **单调时钟**:用 `performance.now()`(不受系统时间调整影响),相对于模块加载时刻
507
+ * (近似进程启动时刻)计算相对毫秒数,跨包调用天然可比。
508
+ * - **无持久化/无面板**:只写 stderr,不落盘、不进 trace store——那是 `/perf` 面板
509
+ * (RFC-116)的职责,本模块只服务“肉眼实测启动耗时”这一诊断场景。
510
+ */
511
+ /**
512
+ * 打一个瞬时相位标记(如 "import:coding done")。
513
+ * 未启用时是一次 boolean 短路,不做任何计算。
514
+ */
515
+ declare function markStartupPhase(label: string): void;
516
+ /**
517
+ * 包裹一个异步函数,测量其耗时并打点("<label>: <ms>ms")。
518
+ * 未启用时直接透传调用,无额外开销(不包 try/finally、不调 performance.now)。
519
+ */
520
+ declare function traceStartupPhase<T>(label: string, fn: () => Promise<T>): Promise<T>;
521
+ /** 同步版本(用于 new App() 构造等同步耗时段)。 */
522
+ declare function traceStartupPhaseSync<T>(label: string, fn: () => T): T;
523
+ /** 供测试注入/重置的开关查询(不允许运行时切换 ENABLED——env 只在模块加载时读一次)。 */
524
+ declare function isStartupTraceEnabled(): boolean;
525
+ //#endregion
526
+ //#region src/fuzzy-search-types.d.ts
527
+ interface SearchableItem<T = unknown> {
528
+ text: string;
529
+ /** 额外参与评分但不单独展示的字段(group/detail/value) */
530
+ aliases?: string[];
531
+ payload: T;
532
+ /**
533
+ * 排序键(RFC-158 D5):等分 tiebreaker 优先用它而非 `text`,缺省回退 `text`
534
+ * (不影响其余消费方现状)。用于展示名与排序位置需解耦的场景——如 `/pin`⇄`/unpin`
535
+ * 动态展示名,排序始终固定用原始指令 id,避免展示名切换导致列表位置跳动。
536
+ * 仅影响等分排序,不参与模糊匹配评分(匹配仍对 `text` 计分)。
537
+ */
538
+ sortKey?: string;
539
+ }
540
+ interface HighlightRange {
541
+ start: number;
542
+ end: number;
543
+ }
544
+ interface RankedMatch<T = unknown> {
545
+ item: SearchableItem<T>;
546
+ score: number;
547
+ /** 对主 text 字段的匹配区间(用于高亮) */
548
+ highlightRanges: HighlightRange[];
549
+ }
550
+ interface FuzzySearchOptions {
551
+ maxResults?: number;
552
+ /** 空 query 时是否返回全量(默认 true) */
553
+ matchEmpty?: boolean;
554
+ }
555
+ //#endregion
556
+ //#region src/fuzzy-search.d.ts
557
+ interface SubsequenceResult {
558
+ score: number;
559
+ positions: number[];
560
+ }
561
+ /**
562
+ * 在 target 中贪心匹配 query 子序列,返回评分与命中下标。
563
+ * 无法完整匹配时返回 null。
564
+ */
565
+ declare function matchSubsequence(query: string, target: string): SubsequenceResult | null;
566
+ /** 对 query/target 评分;-1 表示无匹配。 */
567
+ declare function scoreSubsequence(query: string, target: string): number;
568
+ /** 将命中下标合并为连续 highlight 区间。 */
569
+ declare function positionsToHighlightRanges(positions: readonly number[]): HighlightRange[];
570
+ /**
571
+ * 对候选列表按模糊评分降序排序;空 query 时(默认)返回全量前 maxResults 条。
572
+ */
573
+ declare function rankCandidates<T>(items: readonly SearchableItem<T>[], query: string, options?: FuzzySearchOptions): RankedMatch<T>[];
574
+ /** 对带 name/description 的条目排序(slash 命令、参数补全)。 */
575
+ declare function rankNamedItems<T extends {
576
+ name: string;
577
+ description?: string;
578
+ sortKey?: string;
579
+ }>(items: readonly T[], query: string, options?: FuzzySearchOptions): T[];
580
+ /**
581
+ * SelectDialog searchable:对 searchText(未提供则回退 label)+ group/detail/value 联合评分。
582
+ *
583
+ * `label` 是渲染态文本,常含当前项标记(`●`)、对齐 padding、上下文窗口等展示后缀
584
+ * (如 `"● claude-sonnet-4 200k"`)——直接对其模糊评分会让短 query 命中装饰字符产生假阳性
585
+ * ("dsk" 因 "200k" 的 "k" 与 padding 空格误中不相关模型)。调用方提供纯语义 `searchText`
586
+ * (如干净的模型名/会话标题)时优先对它评分;未提供则回退 `label`(向后兼容)。
587
+ */
588
+ declare function rankLabeledItems<T extends {
589
+ label: string;
590
+ value: string;
591
+ group?: string;
592
+ detail?: string;
593
+ searchText?: string;
594
+ }>(items: readonly T[], query: string, options?: FuzzySearchOptions): T[];
595
+ //#endregion
596
+ //#region src/file-lock.d.ts
597
+ /**
598
+ * 陈旧锁清理阈值 与 等待释放超时——两者必须相等(RFC-137 D3/规则9):
599
+ * 若不相等会出现"锁持有者仍在正常刷新(未超过陈旧阈值)但等待者已判定超时失败"的
600
+ * 协调缝隙。默认 15 秒(保守值,覆盖绝大多数网络场景的往返时间);调用方可传参覆盖。
601
+ */
602
+ declare const DEFAULT_FILE_LOCK_TIMEOUT_MS = 15000;
603
+ /** 锁文件路径:`<dir>/.file-lock-<key>.lock`。调用方通过 dir/key 组合天然隔离不同锁域。
604
+ *
605
+ * RFC-303 D7:命名格式的调用方(`@otto/ai` 的 oauth-refresh-lock.ts)有滚动升级兼容
606
+ * 硬约束——历史锁文件名是 `.auth-refresh-<provider>.lock`,若本函数改变格式会导致
607
+ * 新旧进程各锁各的文件、互斥失效。故格式做成可参数化:`prefix` 缺省 `file-lock`
608
+ * (新调用方用默认值),历史调用方显式传入 `auth-refresh` 保持字节级不变。
609
+ */
610
+ declare function lockPath(dir: string, key: string, prefix?: string): string;
611
+ interface FileLockOptions {
612
+ /** 陈旧锁清理阈值(ms)。缺省 `DEFAULT_FILE_LOCK_TIMEOUT_MS`。 */
613
+ timeoutMs?: number;
614
+ /**
615
+ * 锁文件名前缀(`.{prefix}-{key}.lock`)。缺省 `'file-lock'`。
616
+ * 历史调用方(如 `@otto/ai` 的 oauth-refresh-lock.ts)需显式传入其历史前缀
617
+ * (`'auth-refresh'`)以保持锁文件名字节级不变——改变格式会破坏滚动升级期
618
+ * 新旧进程的互斥(各锁各的文件,等于没锁)。
619
+ */
620
+ prefix?: string;
621
+ }
622
+ /**
623
+ * 尝试获取跨进程文件锁。成功返回 true(调用方持有锁,后续必须调用 `releaseFileLock`
624
+ * 释放——建议用 try/finally 包裹);锁已被占用返回 false(调用方应转入
625
+ * `waitForFileLockRelease`)。
626
+ *
627
+ * 内部会先做一次陈旧锁清理尝试:若目标锁文件的 mtime 已超过 `timeoutMs`,视为持有者
628
+ * 已崩溃未清理,强制删除后重新尝试获取(TOCTOU 竞态由 `wx` 的原子性兜底——多个进程
629
+ * 同时清理只有一个能最终拿到锁)。
630
+ *
631
+ * 成功获取锁后自动启动存活心跳(按 `timeoutMs` 派生的间隔周期 touch mtime),使陈旧锁
632
+ * 判定真正反映"持有者是否还活着"而非"锁创建了多久"。
633
+ *
634
+ * @param dir 锁文件所在目录(自动创建,不存在时自愈)。
635
+ * @param key 锁标识(同一 dir 下不同 key 互不冲突)。
636
+ * @param options timeoutMs 必须与调用方后续 `waitForFileLockRelease` 使用的一致;
637
+ * prefix 必须与调用方后续 `releaseFileLock`/`waitForFileLockRelease` 使用的一致。
638
+ */
639
+ declare function acquireFileLock(dir: string, key: string, options?: FileLockOptions): Promise<boolean>;
640
+ /**
641
+ * 释放跨进程文件锁(幂等——文件不存在时静默忽略,不视为错误)。同时停止该锁的存活心跳。
642
+ *
643
+ * 所有权校验:release 前先读盘核对锁文件内容是否仍是本进程 acquire 时写入的 token——
644
+ * 不一致说明锁已在心跳失效期间被其他进程判定陈旧并偷走(所有权漂移),此时不能 unlink
645
+ * (会误删偷锁者的新锁),仅记录诊断日志。
646
+ */
647
+ declare function releaseFileLock(dir: string, key: string, options?: Pick<FileLockOptions, 'prefix'>): Promise<void>;
648
+ /**
649
+ * 等待另一进程持有的锁释放,超时返回 false(调用方应走自己的兜底路径,如重读磁盘或
650
+ * 降级为无锁执行)。超时阈值应与陈旧锁清理阈值相等,保证两者协调一致。
651
+ */
652
+ declare function waitForFileLockRelease(dir: string, key: string, options?: FileLockOptions): Promise<boolean>;
653
+ /**
654
+ * 同步获取跨进程文件锁。成功返回 true,失败(已被占用/异常)返回 false。
655
+ * 无心跳——陈旧锁判定纯看 mtime,适合短生命周期的读-改-写临界区。
656
+ */
657
+ declare function acquireFileLockSync(dir: string, key: string, options?: Pick<FileLockOptions, 'timeoutMs' | 'prefix'>): boolean;
658
+ /** 同步释放跨进程文件锁(幂等)。所有权校验同异步版本——不一致时跳过 unlink。 */
659
+ declare function releaseFileLockSync(dir: string, key: string, options?: Pick<FileLockOptions, 'prefix'>): void;
660
+ /**
661
+ * 同步等待另一进程持有的锁释放(仅供短超时场景使用,如 trust-store 的毫秒级临界区;
662
+ * 不适合长等待,那种场景应用异步版本)。用 `Atomics.wait` 做真正的同步阻塞睡眠
663
+ * (不占 CPU 自旋——底层走 futex,与忙等自旋循环完全不同)。
664
+ */
665
+ declare function waitForFileLockReleaseSync(dir: string, key: string, options?: Pick<FileLockOptions, 'timeoutMs' | 'prefix'>): boolean;
666
+ //#endregion
667
+ export { API_IMAGE_DIMENSION_LIMIT_PX, AgentError, CLAUDE_TO_OTTO_TOOL, CodedError, type CommandCheckResult, DEFAULT_FILE_LOCK_TIMEOUT_MS, type DeepPartial, type DeepReadonly, EventBus, type EventListener, type Events, type FileLockOptions, type FuzzySearchOptions, type HighlightRange, type HttpRequestOptions, type HttpResponse, InvariantError, LARGE_FILE_BYTE_THRESHOLD, type LogLevel, type Logger, type LoggerOptions, type LoopbackCallback, type LoopbackServer, type LoopbackServerOptions, McpError, NetworkError, OAuthError, PROVIDER_IMAGE_INLINE_MAX_BYTES, type ParsedFrontmatter, type PkcePair, ProviderError, type ProxyProbeErrorKind, type ProxyProbeResult, type RankedMatch, type ReadCappedResult, type SearchableItem, type SseEvent, type SubsequenceResult, type TruncateOptions, type TruncatedResult, type TruncationMeta, TypedEventEmitter, acquireFileLock, acquireFileLockSync, attachLogListener, buildAllowedEnv, checkShellCommand, createDispatcher, createLogger, createPkcePair, createProjectOverflowLogPath, createTempLoggerPath, exists, formatBytes, generateChallenge, generateState, generateVerifier, getDefaultProxy, getHomeDir, getProjectDir, getThirdPartyToolBinaryDir, getThirdPartyToolDir, headerGet, installGlobalProxyDispatcher, invariant, isDirectory, isFile, isStartupTraceEnabled, isTransportInitialized, lockPath, mapClaudeAgentToolNames, mapClaudeToolName, markStartupPhase, matchSubsequence, matchesGlobPattern, mime, mkdirp, normalizePath, openBrowser, ottoToClaudeToolName, parseFrontmatterFields, parseFrontmatterList, parseJsonc, positionsToHighlightRanges, probeImageDimensions, probeProxy, rankCandidates, rankLabeledItems, rankNamedItems, readFileCapped, releaseFileLock, releaseFileLockSync, request, safeStringify, scoreSubsequence, setDefaultProxy, setLogLevel, splitFrontmatter, startLoopbackServer, stream, sweepStaleProjectOverflowLogs, sweepStaleTempLogs, toTruncationMeta, traceStartupPhase, traceStartupPhaseSync, truncateHead, truncateLine, truncateTail, waitForFileLockRelease, waitForFileLockReleaseSync };
668
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,90 @@
1
+ import{n as e,t}from"./event-bus-CABu09lC.js";import{createRequire as n}from"node:module";import r from"pino";import{LOG_FILE as i,LOG_LEVEL as a,OTTO_HOME as o,OTTO_PROJECT_DIR as s,normalizePath as c}from"@otto/env";import{PassThrough as l}from"node:stream";import{chmod as u,mkdir as d,open as f,readFile as p,readdir as m,stat as h,unlink as g,utimes as ee}from"node:fs/promises";import{lookup as te}from"mime-types";import{join as _,resolve as ne}from"node:path";import{tmpdir as v}from"node:os";import{createHash as re,randomBytes as ie,randomUUID as y}from"node:crypto";import{parse as ae,printParseErrorCode as oe}from"jsonc-parser";import se from"safe-stable-stringify";import{createParser as ce}from"eventsource-parser";import{EnvHttpProxyAgent as le,ProxyAgent as ue,getGlobalDispatcher as de,setGlobalDispatcher as fe}from"undici";import{createServer as pe}from"node:http";import{spawn as me}from"node:child_process";import{chmodSync as he,closeSync as ge,mkdirSync as _e,openSync as ve,readFileSync as ye,statSync as be,unlinkSync as xe,writeSync as Se}from"node:fs";var b=class extends Error{code;cause;status;headers;errorType;pluginSource;constructor(e,t){super(e,{cause:t.cause}),this.name=new.target.name,this.code=t.code,this.cause=t.cause,this.status=t.status,this.headers=t.headers,this.errorType=t.errorType,this.pluginSource=t.pluginSource}},x=class extends b{},Ce=class extends b{},S=class extends b{},we=class extends b{},Te=class extends b{};const{setPrototypeOf:Ee=function(e,t){return e.__proto__=t,e}}=Object;var C=class e extends Error{framesToPop=1;name=`Invariant Violation`;constructor(t=`Invariant Violation`){super(typeof t==`number`?`Invariant Violation: ${t}`:t),Ee(this,e.prototype)}};function w(e,t){if(typeof e==`function`)w(e(),t);else if(!e)throw new C(t)}const De=new Set([`trace`,`debug`,`info`,`warn`,`error`,`fatal`]);function T(e){return De.has(e)?e:`info`}const Oe=(()=>{try{return n(import.meta.url)}catch{return}})();function ke(){if(!Oe)return!1;try{return Oe.resolve(`pino-pretty`),!0}catch{return!1}}function Ae(e){return e===`stdout`?1:e===`stderr`?2:e}function je(){return process.env.OTTO_LOG_STDERR===`0`}function Me(){return process.env.OTTO_LOG_FILE||`${process.env.HOME||process.env.USERPROFILE||`.`}/.otto/logs/otto.log`}function Ne(e,t){let n=ke(),r=je(),i={target:n?`pino-pretty`:`pino/file`,options:n?{colorize:!0,destination:2}:{destination:2},level:t};if(!e||e===`stdout`||e===`stderr`)return r?[{target:`pino/file`,options:{destination:Me(),mkdir:!0},level:t}]:[i];let a={target:`pino/file`,options:{destination:Ae(e),mkdir:!0},level:t};return r?[a]:[a,i]}const E=new l,D=new t;E.on(`data`,e=>{try{D.publish({log:[JSON.parse(e.toString())]})}catch(t){console.warn(`Failed to parse log chunk for listener:`,e.toString(),t)}});function Pe(e){return D.subscribe(`log`,e)}let O=null,k=T(a);const Fe=new Set;function Ie(e){if(De.has(e)){k=e,O&&(O.level=e);for(let t of Fe)t.level=e}}function Le(){return A!==null}let A=null;function Re(e,t){return{write(n){A??=r.transport({targets:Ne(e,t)}),A.write(n)}}}function ze(e,t){return O??=r({level:e},r.multistream([{level:e,stream:E},{level:e,stream:Re(t,e)}])),k=T(O.level),O}function Be(e,t){ze(t?.level??T(a),t?.destination??i),w(O,`Root logger is not initialized`);let n=O.child({name:e},{level:t?.level??k});return Fe.add(n),n}const j=10*1024*1024;async function Ve(e){return te(e)||`application/octet-stream`}async function He(e){await d(e,{recursive:!0})}async function Ue(e){try{return await h(e),!0}catch(e){if(e.code===`ENOENT`)return!1;throw e}}async function We(e){return(await h(e)).isDirectory()}async function Ge(e){return(await h(e)).isFile()}async function Ke(e,t=j){let n=await h(e);if(n.size<=t)return{content:await p(e,`utf-8`),truncated:!1,totalBytes:n.size};let r=await f(e,`r`);try{let e=Buffer.alloc(t),{bytesRead:i}=await r.read(e,0,t,0);return{content:e.subarray(0,i).toString(`utf-8`),truncated:!0,totalBytes:n.size}}finally{await r.close()}}function qe(e){let{content:t,...n}=e;return n}function Je(e){return e<1024?`${e}B`:e<1024*1024?`${(e/1024).toFixed(1)}KB`:`${(e/(1024*1024)).toFixed(1)}MB`}function M(e){let t=e.outputBytes??Buffer.byteLength(e.content,`utf-8`),n=e.outputLines??e.content.split(`
2
+ `).length;return{content:e.content,truncated:e.truncated,truncatedBy:e.truncatedBy,totalLines:e.totalLines,totalBytes:e.totalBytes,outputLines:n,outputBytes:t,lastLinePartial:e.lastLinePartial,firstLineExceedsLimit:e.firstLineExceedsLimit,options:e.options}}function Ye(e,t){let{maxLines:n,maxBytes:r}=t,i=Buffer.byteLength(e,`utf-8`),a=e.split(`
3
+ `),o=a.length;if(a.length<=n&&i<=r)return M({content:e,truncated:!1,truncatedBy:`lines`,totalLines:o,totalBytes:i,lastLinePartial:!1,firstLineExceedsLimit:!1,options:{maxLines:n,maxBytes:r},outputLines:o,outputBytes:i});if(n===0)return M({content:``,truncated:!0,truncatedBy:`lines`,totalLines:o,totalBytes:i,lastLinePartial:!1,firstLineExceedsLimit:!1,options:{maxLines:n,maxBytes:r},outputLines:0,outputBytes:0});if(Buffer.byteLength(a[0],`utf-8`)>r)return M({content:``,truncated:!0,truncatedBy:`bytes`,totalLines:o,totalBytes:i,lastLinePartial:!1,firstLineExceedsLimit:!0,options:{maxLines:n,maxBytes:r},outputLines:0,outputBytes:0});let s=[],c=0,l=`lines`;for(let e=0;e<a.length&&e<n;e++){let t=a[e],n=Buffer.byteLength(t,`utf-8`)+(s.length>0?1:0);if(c+n>r){l=`bytes`;break}s.push(t),c+=n}return s.length>=n&&c<=r&&(l=`lines`),M({content:s.join(`
4
+ `),truncated:!0,truncatedBy:l,totalLines:o,totalBytes:i,lastLinePartial:!1,firstLineExceedsLimit:!1,options:{maxLines:n,maxBytes:r},outputLines:s.length,outputBytes:c})}function Xe(e,t){let{maxLines:n,maxBytes:r}=t,i=Buffer.byteLength(e,`utf-8`),a=e.split(`
5
+ `),o=a.length;if(o<=n&&i<=r)return M({content:e,truncated:!1,truncatedBy:`lines`,totalLines:o,totalBytes:i,lastLinePartial:!1,firstLineExceedsLimit:!1,options:{maxLines:n,maxBytes:r},outputLines:o,outputBytes:i});if(n===0)return M({content:``,truncated:!0,truncatedBy:`lines`,totalLines:o,totalBytes:i,lastLinePartial:!1,firstLineExceedsLimit:!1,options:{maxLines:n,maxBytes:r},outputLines:0,outputBytes:0});let s=[],c=0,l=`lines`,u=!1;for(let e=a.length-1;e>=0&&s.length<n;e--){let t=a[e],n=Buffer.byteLength(t,`utf-8`)+(s.length>0?1:0);if(c+n>r){if(l=`bytes`,s.length===0){let e=Ze(t,r);e.length>0&&(s.unshift(e),c=Buffer.byteLength(e,`utf-8`),u=!0)}break}s.unshift(t),c+=n}return s.length>=n&&c<=r&&(l=`lines`),M({content:s.join(`
6
+ `),truncated:!0,truncatedBy:l,totalLines:o,totalBytes:i,lastLinePartial:u,firstLineExceedsLimit:!1,options:{maxLines:n,maxBytes:r},outputLines:s.length,outputBytes:c})}function Ze(e,t){let n=Buffer.from(e,`utf-8`);if(n.length<=t)return e;let r=n.length-t;for(;r<n.length;){let e=n[r];if(e===void 0||(e&192)!=128)break;r++}return n.subarray(r).toString(`utf-8`)}function Qe(e,t){return e.length<=t?{text:e,wasTruncated:!1}:{text:`${e.slice(0,t)}... [truncated]`,wasTruncated:!0}}const N=`otto-coding-`,P=`.log`;function F(){return s}function $e(){return o}function et(){return ne($e(),`tools`)}function tt(e,t=``){return ne(et(),...t?[e,t]:[e])}function nt(){let e=y();return _(v(),`${N}${e}${P}`)}function rt(){let e=y();return _(F(),`tool-results`,`bash`,`${N}${e}${P}`)}async function it(e,t){return ot(v(),e,t)}async function at(e,t){return ot(_(F(),`tool-results`,`bash`),e,t)}async function ot(e,t,n){let r;try{r=await m(e)}catch{return 0}let i=r.filter(e=>e.startsWith(N)&&e.endsWith(P)),a=0;return await Promise.all(i.map(async r=>{let i=_(e,r);try{n-(await h(i)).mtimeMs>t&&(await g(i),a++)}catch{}})),a}function st(e){let t=[],n=ae(e,t,{allowTrailingComma:!0,disallowComments:!1,allowEmptyContent:!1});if(t.length>0){let e=t[0],n=e?oe(e.error):`UnknownError`;throw Error(`Invalid JSONC: ${n}`)}return n}function ct(e,t){return se(e,void 0,t)??`null`}function lt(e,t){let n=``;for(let t=0;t<e.length;t++){let r=e[t];r===`*`?e[t+1]===`*`?(n+=`.*`,t++):n+=`[^/]*`:r===`?`?n+=`.`:n+=r.replace(/[.+^${}()|[\]\\]/g,`\\$&`)}return RegExp(`^${n}$`,`i`).test(t)}let I;function ut(e){I=e||void 0}function dt(){return I}function L(e){let t=e||I;if(t)try{return new ue({uri:t})}catch{return}}let R;function ft(e){if(R===void 0&&(R=de()),!e){fe(R);return}try{fe(new le({httpProxy:e,httpsProxy:e,noProxy:`localhost,127.0.0.1,[::1]`}))}catch{}}async function pt(e,t=5e3){let n=Date.now(),r=new AbortController,i=setTimeout(()=>r.abort(),t);try{let t=await fetch(e,{method:`GET`,signal:r.signal});return{ok:!0,latencyMs:Date.now()-n,status:t.status}}catch(e){let t=Date.now()-n;if(r.signal.aborted)return{ok:!1,latencyMs:t,errorKind:`timeout`,errorMessage:`timeout`};let i=e instanceof Error?e.message:String(e);return{ok:!1,latencyMs:t,errorKind:mt(e),errorMessage:i}}finally{clearTimeout(i)}}function mt(e){switch(e?.cause?.code??e?.code){case`ECONNREFUSED`:case`ECONNRESET`:case`EHOSTUNREACH`:case`ENETUNREACH`:return`proxy-unreachable`;case`ENOTFOUND`:case`EAI_AGAIN`:return`target-unreachable`;case`UND_ERR_CONNECT_TIMEOUT`:case`UND_ERR_HEADERS_TIMEOUT`:case`ETIMEDOUT`:return`timeout`;default:return`unknown`}}async function ht(e){let{url:t,method:n=`POST`,headers:r={},body:i,timeout:a=6e4,signal:o}=e,s=new AbortController,c=o?AbortSignal.any([o,s.signal]):s.signal,l=setTimeout(()=>s.abort(),a);try{let a=L(e.proxy),o={method:n,headers:{"content-type":`application/json`,...r},signal:c};i&&(o.body=typeof i==`string`?i:JSON.stringify(i));let s=await fetch(t,a?{...o,dispatcher:a}:o),l=await s.text();return{status:s.status,headers:s.headers,body:l,ok:s.ok}}catch(e){throw e instanceof DOMException&&e.name===`AbortError`?new x(`HTTP request aborted or timed out`,{code:`NETWORK_TIMEOUT`,cause:e}):new x(`HTTP request failed`,{code:`NETWORK_ERROR`,cause:e instanceof Error?e:Error(String(e))})}finally{clearTimeout(l)}}async function*gt(e){let{url:t,method:n=`POST`,headers:r={},body:i,timeout:a=3e5,idleTimeout:o=12e4,signal:s}=e,c=new AbortController,l=s?AbortSignal.any([s,c.signal]):c.signal,u=setTimeout(()=>c.abort(),a),d,f=()=>{d&&clearTimeout(d),d=setTimeout(()=>c.abort(),o)},p;try{let a=L(e.proxy),o={method:n,headers:{"content-type":`application/json`,accept:`text/event-stream`,...r},body:i?typeof i==`string`?i:JSON.stringify(i):void 0,signal:l},s=await fetch(t,a?{...o,dispatcher:a}:o);if(!s.ok){let e=await s.text(),t=`BAD_RESPONSE`;s.status===429?t=`RATE_LIMIT`:s.status===529?t=`OVERLOADED`:s.status>=500?t=`SERVER_ERROR`:s.status>=400&&(t=`REQUEST_ERROR`);let n;try{let t=JSON.parse(e)?.error;n=t?.code??t?.type}catch{}throw new x(`Http request failed ${s.status}: ${e}`,{code:t,status:s.status,headers:s.headers,errorType:n})}if(e.onResponse?.(s.headers),!s.body)throw new x(`Response body is empty`,{code:`BAD_RESPONSE`});p=s.body.getReader();let c=new TextDecoder,u=[],d=ce({onEvent(e){u.push({event:e.event||void 0,data:e.data,id:e.id||void 0})}});for(f();;){let{done:e,value:t}=await p.read();if(e)break;for(f(),d.feed(c.decode(t,{stream:!0}));u.length>0;){let e=u.shift();e&&(yield e)}}for(d.feed(c.decode());u.length>0;){let e=u.shift();e&&(yield e)}}catch(e){throw e instanceof x?e:e instanceof DOMException&&e.name===`AbortError`?new x(`Stream aborted or timed out`,{code:`TIMEOUT`,cause:e}):new x(`Stream request failed`,{code:`NETWORK_ERROR`,cause:e instanceof Error?e:Error(String(e))})}finally{clearTimeout(u),d&&clearTimeout(d),p&&p.cancel().catch(()=>{})}}function _t(e,t){if(e==null)return;if(typeof e.get==`function`)return e.get(t)??void 0;let n=e;for(let e of Object.keys(n))if(e.toLowerCase()===t){let t=n[e];return typeof t==`string`?t:void 0}}const vt=[{description:`递归强制删除`,substring:`rm -rf`},{description:`递归强制删除(长格式)`,substring:`rm -r -f`},{description:`递归强制删除(长格式 2)`,substring:`rm --recursive --force`},{description:`删除根目录`,substring:`rm -rf /`},{description:`权限提升`,substring:`sudo rm`},{description:`权限提升`,substring:`sudo `},{description:`chmod 777`,substring:`chmod 777`},{description:`chmod -R 777`,substring:`chmod -R 777`},{description:`chmod 777 递归`,substring:`chmod -R`},{description:`curl 管道 shell 执行`,regex:/curl\s+\S+\s*\|\s*(ba)?sh/},{description:`wget 管道 shell 执行`,regex:/wget\s+\S+\s*-\s*O\s*-\s*\|\s*(ba)?sh/},{description:`fork bomb`,regex:/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:?\s*&\s*\}\s*;?\s*:/},{description:`覆盖关键系统文件`,regex:/>\s*\/(etc|boot|sys|proc|dev)\//},{description:`mkfs 格式化`,substring:`mkfs.`},{description:`dd 覆写磁盘`,regex:/dd\s+if=.*of=\/dev\//}];function yt(e){let t=e.trim();if(!t)return{safe:!0};for(let e of vt)if(e.substring!==void 0&&t.includes(e.substring)||e.regex!==void 0&&e.regex.test(t))return{safe:!1,reason:e.description};return{safe:!0}}function bt(e){let t=new Map,n=[`LC_`,`OTTO_`,`npm_`],r=new Set([`PATH`,`HOME`,`USER`,`SHELL`,`TMPDIR`,`TEMP`,`TMP`,`LANG`,`NODE_PATH`,`NODE_ENV`,`TERM`,`COLORTERM`,`TERM_PROGRAM`,`SSH_AUTH_SOCK`,`DISPLAY`,`WAYLAND_DISPLAY`,`XDG_RUNTIME_DIR`,`XDG_SESSION_TYPE`]);for(let[e,i]of Object.entries(process.env)){if(!i)continue;if(r.has(e)){t.set(e,i);continue}let a=e.toLowerCase();a.includes(`api_key`)||a.includes(`token`)||a.includes(`secret`)||a.includes(`password`)||a.includes(`passwd`)||a.includes(`credential`)||a.includes(`auth`)||n.some(t=>e.startsWith(t))&&t.set(e,i)}if(e)for(let[n,r]of Object.entries(e))r===void 0?t.delete(n):t.set(n,r);return Object.fromEntries(t)}const z={Bash:`bash`,Read:`read`,Write:`write`,Edit:`edit`,MultiEdit:`edit`,Grep:`grep`,Glob:`find`,WebFetch:`web_fetch`,WebSearch:`web_search`,Task:`task_delegate`,TodoWrite:`write_todos`};function xt(e){return z[e]}const St=Object.fromEntries(Object.entries(z).reverse().map(([e,t])=>[t,e]));function Ct(e){return St[e]??e}function wt(e){return e.map(e=>z[e]??e)}function B(e){return e.toString(`base64url`)}function V(){return B(ie(32))}function H(e){return B(re(`sha256`).update(e).digest())}function Tt(){let e=V();return{verifier:e,challenge:H(e),method:`S256`}}function Et(){return B(ie(16))}const Dt=[1455,1457];function Ot(){return`<span class="otto-wordmark">OTTO</span>`}function kt(){return`<svg viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
7
+ <circle cx="26" cy="26" r="24" fill="var(--gray-3)" stroke="var(--gray-5)" stroke-width="2"/>
8
+ <path d="M15.5 27l7 6.5 14-15" stroke="var(--gray-12)" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/>
9
+ </svg>`}function At(){return`<svg viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
10
+ <circle cx="26" cy="26" r="24" fill="var(--gray-3)" stroke="var(--gray-5)" stroke-width="2"/>
11
+ <path d="M19 19l14 14M33 19l-14 14" stroke="var(--gray-9)" stroke-width="2.8" stroke-linecap="round"/>
12
+ </svg>`}function U(e){return e.replace(/[&<>"']/g,e=>({"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`})[e]??e)}function jt(e,t,n,r){let i=e===`success`?kt():At();return`<!doctype html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="utf-8">
16
+ <meta name="viewport" content="width=device-width,initial-scale=1">
17
+ <title>${U(t)}</title>
18
+ <style>
19
+ :root {
20
+ --gray-1:#FFFFFF;--gray-2:#F5F6FA;--gray-3:#EEF0F9;
21
+ --gray-5:#DBDDEB;--gray-7:#AEB2C8;--gray-9:#7A7EA0;
22
+ --gray-11:#565A78;--gray-12:#12152B;
23
+ --radius-xl:24px;
24
+ --shadow-card:0 1px 2px rgba(18,21,43,.04),0 8px 24px rgba(18,21,43,.06);
25
+ --hairline:.75px;
26
+ --pixel:"Silkscreen","Press Start 2P","Pixelify Sans","SF Mono",monospace;
27
+ }
28
+
29
+ @media (prefers-color-scheme:dark) {
30
+ :root:not([data-theme=light]) {
31
+ --gray-1:#191F39;--gray-2:#111529;--gray-3:rgba(255,255,255,.08);
32
+ --gray-5:rgba(255,255,255,.11);--gray-7:rgba(255,255,255,.18);
33
+ --gray-9:rgba(255,255,255,.34);--gray-11:#AAB2CC;--gray-12:#F4F6FF;
34
+ --shadow-card:0 1px 2px rgba(0,0,0,.14),0 8px 24px rgba(0,0,0,.22);
35
+ }
36
+ }
37
+
38
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
39
+ body{
40
+ min-height:100dvh;display:grid;place-items:center;padding:32px;
41
+ background:var(--gray-2);-webkit-font-smoothing:antialiased;
42
+ }
43
+
44
+ .page{width:min(440px,100%);text-align:center}
45
+
46
+ .otto-wordmark{font-family:var(--pixel);font-size:28px;font-weight:700;letter-spacing:.04em;color:var(--gray-12);margin-bottom:28px}
47
+
48
+ .card{
49
+ padding:36px 40px;border-radius:var(--radius-xl);
50
+ border:var(--hairline) solid var(--gray-3);background:var(--gray-1);
51
+ box-shadow:var(--shadow-card);
52
+ }
53
+
54
+ .status-icon{display:inline-block;margin-bottom:22px}
55
+ .status-icon svg{width:52px;height:52px}
56
+
57
+ .card *{font-family:var(--pixel)}
58
+ .kicker{
59
+ font-size:11px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;
60
+ color:var(--gray-7);margin-bottom:10px;
61
+ }
62
+ h1{font-size:22px;font-weight:700;line-height:1.3;letter-spacing:.02em;color:var(--gray-12)}
63
+ .desc{
64
+ margin-top:14px;font-size:13px;line-height:1.7;color:var(--gray-11);
65
+ max-width:300px;margin-left:auto;margin-right:auto;
66
+ }
67
+ .separator{
68
+ width:60px;height:2px;margin:24px auto 20px;background:var(--gray-3)
69
+ }
70
+ .footer{
71
+ font-size:11px;color:var(--gray-7);font-weight:600;letter-spacing:.06em;
72
+ text-transform:uppercase;
73
+ }
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <main class="page" aria-labelledby="title">
78
+ ${Ot()}
79
+ <section class="card">
80
+ <div class="status-icon">${i}</div>
81
+ <p class="kicker">${e===`success`?`Authentication`:`Error`}</p>
82
+ <h1 id="title">${U(t)}</h1>
83
+ <p class="desc">${U(n)}</p>
84
+ <div class="separator"></div>
85
+ <p class="footer">${U(r)}</p>
86
+ </section>
87
+ </main>
88
+ </body>
89
+ </html>`}const Mt=jt(`success`,`Login complete`,`You can close this window and return to the terminal.`,`Otto Auth`);function W(e){return jt(`error`,`Login failed`,e,`Otto Auth`)}function G(e,t,n){let r=Buffer.from(n,`utf-8`);e.writeHead(t,{"Content-Type":`text/html; charset=utf-8`,"Content-Length":String(r.length),Connection:`close`}),e.end(r)}function Nt(e,t,n){return new Promise((r,i)=>{if(n.length===0){i(new S(`No loopback port specified.`,{code:`OAUTH_LOOPBACK_PORT_UNAVAILABLE`}));return}let a=0,o=()=>{e.removeListener(`error`,l),e.removeListener(`listening`,c)},s=()=>{let r=n[a++];e.listen(r,t)},c=()=>{o();let t=e.address();r(typeof t==`object`&&t?t.port:n[a-1])},l=e=>{if(e.code===`EADDRINUSE`||e.code===`EACCES`){if(a<n.length){s();return}o(),i(new S(`No available loopback port among: ${n.join(`, `)}`,{code:`OAUTH_LOOPBACK_PORT_UNAVAILABLE`}));return}o(),i(e)};e.on(`error`,l),e.on(`listening`,c),s()})}function Pt(e,t){e.value||(e.value=!0,t())}async function Ft(e={}){let{ports:t=Dt,callbackPath:n=`/auth/callback`,expectedState:r,host:i=`127.0.0.1`,signal:a}=e;if(a?.aborted)throw new S(`Login cancelled`,{code:`OAUTH_CANCELLED`});let o,s,c={value:!1},l=new Promise((e,t)=>{o=t=>Pt(c,()=>e(t)),s=e=>Pt(c,()=>t(e))});l.catch(()=>{});let u=0,d=()=>`http://${i}:${u}`,f=pe((e,t)=>{let i;try{i=new URL(e.url??`/`,d())}catch{G(t,400,W(`Invalid request URL`)),s(new S(`Invalid callback URL`,{code:`OAUTH_INVALID_REQUEST`}));return}if(i.pathname!==n){G(t,404,W(`Not found`));return}let a=i.searchParams.get(`error`);if(a){let e=i.searchParams.get(`error_description`)??``;G(t,400,W(e||a)),s(new S(`Authorization failed: ${a}${e?`: ${e}`:``}`,{code:`OAUTH_AUTHORIZE_FAILED`}));return}let c=i.searchParams.get(`code`);if(!c){G(t,400,W(`Missing authorization code.`)),s(new S(`Callback missing authorization code.`,{code:`OAUTH_AUTHORIZE_FAILED`}));return}let l=i.searchParams.get(`state`)??void 0;if(r!==void 0&&l!==r){G(t,400,W(`State mismatch.`)),s(new S(`OAuth state mismatch (possible CSRF).`,{code:`OAUTH_STATE_MISMATCH`}));return}G(t,200,Mt),o({code:c,state:l})});u=await Nt(f,i,t);let p=!1,m=()=>new Promise(e=>{if(p){e();return}p=!0,s(new S(`Login cancelled (server closed)`,{code:`OAUTH_CANCELLED`})),f.close(()=>e()),f.closeAllConnections?.()});return a&&a.addEventListener(`abort`,()=>{s(new S(`Login cancelled`,{code:`OAUTH_CANCELLED`})),m()},{once:!0}),{port:u,redirectUri:`${d()}${n}`,waitForCallback:()=>l,close:m}}function It(e){try{new URL(e)}catch{return!1}try{let t=process.platform,n=t===`darwin`?[`open`,[e]]:t===`win32`?[`cmd`,[`/c`,`start`,``,e]]:[`xdg-open`,[e]];return me(n[0],[...n[1]],{detached:!0,stdio:`ignore`}).unref(),!0}catch{return!1}}const Lt=/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;function Rt(e){let t=Lt.exec(e);return t?{fields:zt(t[1]??``),body:(t[2]??``).trim()}:null}function zt(e){let t=new Map;for(let n of e.split(`
90
+ `)){let e=n.indexOf(`:`);if(e<=0)continue;let r=n.slice(0,e).trim().toLowerCase().replaceAll(`_`,``),i=n.slice(e+1).trim().replace(/^["']|["']$/g,``);r&&i&&t.set(r,i)}return t}function Bt(e){if(!e)return;let t=(e.startsWith(`[`)&&e.endsWith(`]`)?e.slice(1,-1):e).split(`,`).map(e=>e.trim().replace(/^["']|["']$/g,``)).filter(e=>e!==``);return t.length>0?t:void 0}const Vt=8e3,Ht=5*1024*1024,Ut=Buffer.from([137,80,78,71,13,10,26,10]),Wt=Buffer.from(`GIF8`,`ascii`);function Gt(e,t){if(!(!e||e.length===0)&&!(e.length>7340032))try{return Kt(Buffer.from(e.slice(0,128),`base64`),e,t)}catch{return}}function Kt(e,t,n){let r=n.toLowerCase();if(r.includes(`png`))return qt(e);if(r.includes(`jpeg`)||r.includes(`jpg`))return Jt(Buffer.from(t,`base64`));if(r.includes(`gif`))return Yt(e);if(r.includes(`webp`))return Xt(e);if(e.length>=8&&e.subarray(0,8).equals(Ut))return qt(e);if(e.length>=2&&e.readUInt16BE(0)===65496)return Jt(Buffer.from(t,`base64`));if(e.length>=4&&e.subarray(0,4).equals(Wt))return Yt(e);if(e.length>=12&&e.readUInt32BE(0)===1380533830&&e.readUInt32BE(8)===1464156752)return Xt(e)}function qt(e){if(!(e.length<24)&&e.subarray(0,8).equals(Ut))try{return e.toString(`ascii`,12,16)===`IHDR`?{width:e.readUInt32BE(16),height:e.readUInt32BE(20)}:void 0}catch{return}}function Jt(e){if(!(e.length<2))try{let t=2,n=0;for(;t<e.length-1;){if(n++,n>1e5)return;let r=e.readUInt16BE(t);if(r===65497||(r&65280)!=65280)return;let i=r&255;if(i>=192&&i<=195||i>=197&&i<=199||i>=201&&i<=203||i>=205&&i<=207){if(t+9>e.length||e.readUInt16BE(t+2)<8)return;let n=e.readUInt16BE(t+5);return{width:e.readUInt16BE(t+7),height:n}}if(i===216||i===217){t+=2;continue}if(t+4>e.length)return;let a=e.readUInt16BE(t+2);if(a<2)return;t+=2+a}return}catch{return}}function Yt(e){if(!(e.length<10)&&e.subarray(0,4).equals(Wt))try{return{width:e.readUInt16LE(6),height:e.readUInt16LE(8)}}catch{return}}function Xt(e){if(!(e.length<16)&&!(e.readUInt32BE(0)!==1380533830||e.readUInt32BE(8)!==1464156752))try{let t=e.toString(`ascii`,12,16);return t===`VP8X`?Zt(e):t===`VP8 `?Qt(e):t===`VP8L`?$t(e):void 0}catch{return}}function Zt(e){if(!(e.length<30))return{width:(e[24]|e[25]<<8|e[26]<<16)+1,height:(e[27]|e[28]<<8|e[29]<<16)+1}}function Qt(e){if(e.length<30)return;let t=e.readUInt16LE(26)&16383,n=e.readUInt16LE(28)&16383;if(!(t===0||n===0))return{width:t,height:n}}function $t(e){if(e.length<25||e[20]!==47)return;let t=e.readUInt32LE(21);return{width:(t&16383)+1,height:(t>>14&16383)+1}}const K=process.env.OTTO_STARTUP_TRACE===`1`,en=K?performance.now():0;function tn(e){if(!K)return;let t=performance.now()-en;process.stderr.write(`[startup] ${e}: ${t.toFixed(1)}ms\n`)}async function nn(e,t){if(!K)return t();let n=performance.now();try{return await t()}finally{let t=performance.now()-n;process.stderr.write(`[startup] ${e}: ${t.toFixed(1)}ms\n`)}}function rn(e,t){if(!K)return t();let n=performance.now();try{return t()}finally{let t=performance.now()-n;process.stderr.write(`[startup] ${e}: ${t.toFixed(1)}ms\n`)}}function an(){return K}const on=/[/\-_\s.]/;function sn(e){return e!==void 0&&on.test(e)}function q(e,t){if(e.length===0)return{score:0,positions:[]};if(t.length===0)return null;let n=e.toLowerCase(),r=t.toLowerCase(),i=0,a=0,o=[],s=-2;for(let e=0;e<r.length&&a<n.length;e++)r[e]===n[a]&&(s===e-1?i+=24:(i+=16,s>=0&&(i-=(e-s-1)*3)),e===0?i+=12:sn(r[e-1])&&(i+=8),o.push(e),s=e,a++);return a<n.length?null:(i-=Math.max(0,r.length-n.length)*.5,{score:i,positions:o})}function cn(e,t){return q(e,t)?.score??-1}function ln(e){if(e.length===0)return[];let t=[],n=e[0],r=n+1;for(let i=1;i<e.length;i++){let a=e[i];a===r?r++:(t.push({start:n,end:r}),n=a,r=a+1)}return t.push({start:n,end:r}),t}function un(e,t,n){let r=q(e,t);for(let t of n??[]){let n=q(e,t);n&&(!r||n.score>r.score)&&(r=n)}return r}function J(e,t,n={}){let r=n.maxResults??50,i=n.matchEmpty??!0,a=t.trim();if(a===``)return i?e.slice(0,r).map(e=>({item:e,score:0,highlightRanges:[]})):[];let o=[];for(let t of e){let e=un(a,t.text,t.aliases);if(!e)continue;let n=q(a,t.text);o.push({item:t,score:e.score,highlightRanges:ln(n?.positions??[])})}return o.sort((e,t)=>t.score-e.score||(e.item.sortKey??e.item.text).localeCompare(t.item.sortKey??t.item.text)),o.slice(0,r)}function dn(e,t,n){return J(e.map(e=>({text:e.name,aliases:e.description?[e.description]:void 0,payload:e,sortKey:e.sortKey})),t,n).map(e=>e.item.payload)}function fn(e,t,n){return J(e.map(e=>({text:e.searchText??e.label,aliases:[e.group,e.detail,e.value].filter(e=>!!e),payload:e})),t,{maxResults:n?.maxResults??e.length,matchEmpty:n?.matchEmpty}).map(e=>e.item.payload)}const Y=Be(`@otto/shared:file-lock`),pn=15e3;function mn(e){return Math.floor(e/3)}const X=new Map,Z=new Map;function Q(e,t,n=`file-lock`){return _(e,`.${n}-${t}.lock`)}function hn(e,t){gn(e);let n=setInterval(()=>{let t=new Date;ee(e,t,t).catch(()=>{})},mn(t));n.unref?.(),X.set(e,n)}function gn(e){let t=X.get(e);t&&(clearInterval(t),X.delete(e))}async function _n(e,t,n={}){let r=n.timeoutMs??15e3,i=Q(e,t,n.prefix),a=async(n=!1)=>{try{let e=await f(i,`wx`),t=y();await e.writeFile(t),await e.close(),Z.set(i,t);try{await u(i,384)}catch{}return hn(i,r),!0}catch(r){if(r.code===`EEXIST`)return!1;if(r.code===`ENOENT`&&!n){try{await d(e,{recursive:!0})}catch(e){return Y.warn(`Failed to create lock directory for ${t}: ${e.message}`),!1}return await a(!0)}return Y.warn(`Failed to acquire file lock for ${t}: ${r.message}`),!1}};if(await a())return!0;try{let e=await h(i);if(Date.now()-e.mtimeMs>r)return await g(i).catch(()=>{}),await a()}catch{return await a()}return!1}async function vn(e,t,n={}){let r=Q(e,t,n.prefix);gn(r);let i=Z.get(r);if(Z.delete(r),i!==void 0)try{if(await p(r,`utf-8`)!==i){Y.warn(`File lock ownership drifted for ${t} — another process stole this lock (likely stale-lock takeover during a heartbeat gap). Skipping unlink to avoid deleting the new owner's lock.`);return}}catch{}await g(r).catch(()=>{})}async function yn(e,t,n={}){let r=n.timeoutMs??15e3,i=Q(e,t,n.prefix),a=Date.now()+r;for(;Date.now()<a;){try{await h(i)}catch{return!0}await new Promise(e=>setTimeout(e,100))}return!1}const $=new Map;function bn(e,t,n={}){let r=n.timeoutMs??15e3,i=Q(e,t,n.prefix),a=(n=!1)=>{let r;try{r=ve(i,`wx`)}catch(r){let i=r.code;if(i===`EEXIST`)return!1;if(i===`ENOENT`&&!n){try{_e(e,{recursive:!0})}catch(e){return Y.warn(`Failed to create lock directory for ${t}: ${e.message}`),!1}return a(!0)}return Y.warn(`Failed to acquire file lock for ${t}: ${r.message}`),!1}let o=y();try{Se(r,o)}finally{ge(r)}$.set(i,o);try{he(i,384)}catch{}return!0};if(a())return!0;try{let e=be(i);if(Date.now()-e.mtimeMs>r){try{xe(i)}catch{}return a()}}catch{return a()}return!1}function xn(e,t,n={}){let r=Q(e,t,n.prefix),i=$.get(r);if($.delete(r),i!==void 0)try{if(ye(r,`utf-8`)!==i){Y.warn(`File lock ownership drifted for ${t} — another process stole this lock. Skipping unlink to avoid deleting the new owner's lock.`);return}}catch{}try{xe(r)}catch{}}function Sn(e,t,n={}){let r=n.timeoutMs??15e3,i=Q(e,t,n.prefix),a=Date.now()+r,o=new Int32Array(new SharedArrayBuffer(4));for(;Date.now()<a;){try{be(i)}catch{return!0}Atomics.wait(o,0,0,100)}return!1}export{Vt as API_IMAGE_DIMENSION_LIMIT_PX,we as AgentError,z as CLAUDE_TO_OTTO_TOOL,b as CodedError,pn as DEFAULT_FILE_LOCK_TIMEOUT_MS,t as EventBus,C as InvariantError,j as LARGE_FILE_BYTE_THRESHOLD,Te as McpError,x as NetworkError,S as OAuthError,Ht as PROVIDER_IMAGE_INLINE_MAX_BYTES,Ce as ProviderError,e as TypedEventEmitter,_n as acquireFileLock,bn as acquireFileLockSync,Pe as attachLogListener,bt as buildAllowedEnv,yt as checkShellCommand,L as createDispatcher,Be as createLogger,Tt as createPkcePair,rt as createProjectOverflowLogPath,nt as createTempLoggerPath,Ue as exists,Je as formatBytes,H as generateChallenge,Et as generateState,V as generateVerifier,dt as getDefaultProxy,$e as getHomeDir,F as getProjectDir,tt as getThirdPartyToolBinaryDir,et as getThirdPartyToolDir,_t as headerGet,ft as installGlobalProxyDispatcher,w as invariant,We as isDirectory,Ge as isFile,an as isStartupTraceEnabled,Le as isTransportInitialized,Q as lockPath,wt as mapClaudeAgentToolNames,xt as mapClaudeToolName,tn as markStartupPhase,q as matchSubsequence,lt as matchesGlobPattern,Ve as mime,He as mkdirp,c as normalizePath,It as openBrowser,Ct as ottoToClaudeToolName,zt as parseFrontmatterFields,Bt as parseFrontmatterList,st as parseJsonc,ln as positionsToHighlightRanges,Gt as probeImageDimensions,pt as probeProxy,J as rankCandidates,fn as rankLabeledItems,dn as rankNamedItems,Ke as readFileCapped,vn as releaseFileLock,xn as releaseFileLockSync,ht as request,ct as safeStringify,cn as scoreSubsequence,ut as setDefaultProxy,Ie as setLogLevel,Rt as splitFrontmatter,Ft as startLoopbackServer,gt as stream,at as sweepStaleProjectOverflowLogs,it as sweepStaleTempLogs,qe as toTruncationMeta,nn as traceStartupPhase,rn as traceStartupPhaseSync,Ye as truncateHead,Qe as truncateLine,Xe as truncateTail,yn as waitForFileLockRelease,Sn as waitForFileLockReleaseSync};
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@x-otto/shared",
3
+ "version": "0.1.0-alpha.0",
4
+ "files": [
5
+ "dist"
6
+ ],
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./browser": {
16
+ "types": "./dist/browser/index.d.ts",
17
+ "import": "./dist/browser/index.js"
18
+ }
19
+ },
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "registry": "https://registry.npmjs.org",
23
+ "tag": "alpha"
24
+ },
25
+ "scripts": {
26
+ "build": "tsdown && rm -f dist/*.map && sed -i '' '\\|//# sourceMappingURL=|d' dist/*.js dist/browser/index.js 2>/dev/null; true",
27
+ "typecheck:project": "tsc -p tsconfig.json --noEmit",
28
+ "typecheck:file": "tsc src/index.ts --noEmit --target ES2024 --module ESNext --moduleResolution bundler --esModuleInterop --skipLibCheck",
29
+ "typecheck": "tsc --noEmit",
30
+ "clean": "rm -rf dist"
31
+ },
32
+ "dependencies": {
33
+ "@x-otto/env": "workspace:*",
34
+ "eventsource-parser": "3.0.6",
35
+ "jsonc-parser": "3.3.1",
36
+ "mime-types": "3.0.2",
37
+ "pino": "10.3.1",
38
+ "pino-pretty": "13.1.3",
39
+ "safe-stable-stringify": "2.5.0",
40
+ "undici": "8.7.0",
41
+ "uuid": "13.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/mime-types": "3.0.1"
45
+ }
46
+ }