@x-otto/env 0.1.0-alpha.0 → 0.1.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
- # @otto/env
1
+ # @x-otto/env
2
2
 
3
3
  > Environment variable standardization, path constants, and resilience configuration. Zero internal dependencies — the foundational layer of the monorepo.
4
4
 
5
- `@otto/env` provides the canonical path constants (dot-otto root, auth path, session dirs, etc.), environment variable normalization helpers, Claude compatibility toggles, process enumeration/liveness probes, config-layer resolution, and the unified resilience config contract. Every other package in the monorepo depends on it — it is the leaf of the dependency graph.
5
+ `@x-otto/env` provides the canonical path constants (dot-otto root, auth path, session dirs, etc.), environment variable normalization helpers, Claude compatibility toggles, process enumeration/liveness probes, config-layer resolution, and the unified resilience config contract. Every other package in the monorepo depends on it — it is the leaf of the dependency graph.
6
6
 
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
- pnpm add @otto/env
10
+ pnpm add @x-otto/env
11
11
  ```
12
12
 
13
13
  ## Usage
@@ -23,7 +23,7 @@ import {
23
23
  resolveConfigLayers,
24
24
  listOttoProcesses,
25
25
  isProcessAlive,
26
- } from '@otto/env'
26
+ } from '@x-otto/env'
27
27
 
28
28
  // Canonical paths
29
29
  console.log(OTTO_HOME) // ~/.otto (overridable via OTTO_HOME env)
@@ -78,7 +78,7 @@ console.log(processes.map(p => `${p.pid}: ${p.command} (${p.etimeMs}ms)`))
78
78
  ### Config Layers
79
79
  - `resolveConfigLayers(options)` — enumerate `.otto`/`.claude` × project/user layers
80
80
 
81
- ### Process Management (RFC-196)
81
+ ### Process Management
82
82
  - `listOttoProcesses(pattern?)` — enumerate otto main processes via `ps`
83
83
  - `parseOttoProcessTable(stdout)` — parse `ps` output for testing
84
84
  - `isProcessAlive(pid)` — probe process via `process.kill(pid, 0)`
@@ -86,11 +86,9 @@ console.log(processes.map(p => `${p.pid}: ${p.command} (${p.etimeMs}ms)`))
86
86
  - `currentHostname()` — local hostname
87
87
  - `isContainerEnvironment()` — detect Docker/k8s
88
88
 
89
- ### Resilience Config (RFC-230)
89
+ ### Resilience Config
90
90
  - `DEFAULT_RESILIENCE_CONFIG` — defaults for all 6 subsystems (stream, network, mcp, schedule, error-retry, oauth)
91
- - `resolveResilienceConfig(explicit?, fromSettings?, warn?)` — merge 4 config layers
92
- - `parseResilienceEnv(raw, warn?)` — parse `OTTO_RESILIENCE_CONFIG` env
93
- - `validateResilienceOverride(input, warn)` — validate partial override shape
91
+ - `mergeResilienceConfig(...layers)` — pure merge of already-validated override layers (right wins). Validation moved to `@x-otto/setting` (`validateResilienceOverride`/`parseResilienceEnvOverride`, zod single source, RFC-412 M3)
94
92
 
95
93
  ## Dependencies
96
94
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
+ //#region src/path-utils.d.ts
2
+ /** 归一化路径并统一为正斜杠分隔(跨平台一致)。 */
3
+ declare function normalizePath(path: string): string;
4
+ //#endregion
1
5
  //#region src/build-id.d.ts
2
- declare const BUILD_ID = 4512;
6
+ declare const BUILD_ID = 5515;
3
7
  //#endregion
4
8
  //#region src/config-layers.d.ts
5
9
  /**
@@ -10,7 +14,7 @@ declare const BUILD_ID = 4512;
10
14
  * 不含信任门。纯函数、确定性、无副作用。
11
15
  *
12
16
  * 现状(RFC-044 review S1 + RFC-081 终局复审定档):真实职责 = 派生插件根
13
- * (`@otto/coding discoverPlugins` 取 otto 层得 `<.otto>/plugins`)。**「四轴收编中央源」经复审判定不迁**
17
+ * (`@x-otto/coding discoverPlugins` 取 otto 层得 `<.otto>/plugins`)。**「四轴收编中央源」经复审判定不迁**
14
18
  * (不是待办):command/agent 两个目录扫描轴的 precedence 已完全一致
15
19
  * (`plugin < user/claude < user/otto < project/claude < project/otto`,各有 RFC-032 测试守护漂移),
16
20
  * 强迁会把清晰的本地 layer 数组换成 `resolveConfigLayers().reverse().filter().map()` 徒增耦合;
@@ -125,20 +129,26 @@ declare function isContainerEnvironment(): boolean;
125
129
  //#endregion
126
130
  //#region src/resilience.d.ts
127
131
  /**
128
- * 统一重试/超时/退避配置契约(RFC-230)。
132
+ * 统一重试/超时/退避配置契约 + 纯合并(RFC-230 / RFC-412 M3)。
129
133
  *
130
- * 归口在零依赖叶包 `@otto/env`(不新建独立包,量级参照 RFC-157 已判定的 `@otto/invariant`
131
- * 29 行不该独立成包先例)。四层配置源优先级(右覆盖左):
134
+ * 归口在零依赖叶包 `@x-otto/env`。四层配置源优先级(右覆盖左):
132
135
  *
133
136
  * 硬编码默认值 → env(`OTTO_RESILIENCE_CONFIG`,JSON 字符串)→ settings(`.otto/config.json`
134
137
  * 的 `resilience` 字段)→ 程序化注入(`AppOptions.resilience`)
135
138
  *
136
- * 本文件只负责契约类型 + 默认值 + 纯函数解析/合并逻辑,不认识 `App`/`SettingsManager` 等
137
- * 任何上层概念——消费方(`@otto/setting`/`@otto/coding` 等)各自在正确时机调用
138
- * `resolveResilienceConfig()`,不由本文件决定何时求值(见 RFC-230 §3 D5 的"延迟冻结"设计,
139
- * 那是消费方 `App` 的职责,不是本包的职责)。
139
+ * **RFC-412 M3 职责收缩**:本文件只负责**契约类型 + 默认值 + 纯合并逻辑**。校验(JSON 解析、
140
+ * 值域约束、字段剔除)已上移到调用方——`@x-otto/setting` 提供唯一的 zod 校验真源
141
+ * (`validateResilienceOverride`/`parseResilienceEnvOverride`),`@x-otto/coding` `App` 在装配
142
+ * 时读 env、经 setting 校验后连同 settings/explicit 三层传入 `mergeResilienceConfig`。
143
+ *
144
+ * 为什么删掉 env 侧的手写校验:此前 env 手写递归校验器与 setting 的 zod schema 是**同一套规则
145
+ * 的两处实现**(stream positive、mcp/schedule nonnegative、oauth min 1000 逐字对应),
146
+ * 注释曾书面自认"不能假设两者永远同步"——双实现漂移风险。收敛到 setting zod 单一真源后,
147
+ * env 回归"默认值 + 合并层"的纯粹职责,零外部依赖特性完整保留(不引入 zod)。
140
148
  *
141
- * 不引入 zod(`@otto/env` 当前零外部依赖,见 package.json),字段级容错走手写递归校验。
149
+ * 行为变化(有意收敛):字段级非法值此前 env 层会 `console.warn`、settings 层静默 catch——
150
+ * 两层不一致。上移后统一走 setting zod 的静默剔除(`lenient = optional().catch(undefined)`),
151
+ * 消除该不一致;`OTTO_RESILIENCE_CONFIG` 的**整体 JSON 解析失败**仍由 coding 侧 warn(重要诊断)。
142
152
  */
143
153
  /** 统一重试/超时/退避配置的完整形状(六个子系统,全部字段均为必填——`DEFAULT_RESILIENCE_CONFIG` 保证完整性)。 */
144
154
  interface ResilienceConfig {
@@ -171,15 +181,17 @@ interface ResilienceConfig {
171
181
  };
172
182
  /** OAuth 跨进程刷新锁(陈旧锁清理阈值 = 等待超时 = 心跳间隔×3,三者必须同步派生,见 RFC-137 规则9)。 */
173
183
  oauth: {
174
- /** 刷新锁超时(ms)。默认 15_000,下限 1_000(心跳间隔 = timeoutMs/3,过小值会导致 setInterval 忙循环,见 RFC-230 二轮评审 P1)。 */refreshLockTimeoutMs: number;
184
+ /** 刷新锁超时(ms)。默认 15_000,下限 1_000(心跳间隔 = timeoutMs/3,过小值会导致 setInterval 忙循环,见 RFC-230 二轮评审 P1)。校验在 setting zod(`min(1000)`)。 */refreshLockTimeoutMs: number;
175
185
  };
176
186
  }
177
187
  declare const DEFAULT_RESILIENCE_CONFIG: ResilienceConfig;
178
188
  /**
179
189
  * `ResilienceConfig` 的深度可选覆盖形状。
180
190
  *
181
- * 手写局部类型,不引入 `@otto/shared` 的 `DeepPartial`——`@otto/env` 是比 `@otto/shared`
191
+ * 手写局部类型,不引入 `@x-otto/shared` 的 `DeepPartial`——`@x-otto/env` 是比 `@x-otto/shared`
182
192
  * 更底层的依赖叶子(`shared` 依赖 `env`,反向引入会成环),见 RFC-230 §3 D2 依赖方向核实。
193
+ * `@x-otto/setting` 的 zod `resilienceConfigSchema` 经 `satisfies z.ZodType<ResilienceConfigOverride>`
194
+ * 钉死与本类型同构(字段增减/改名 → 编译报错),是校验真源。
183
195
  */
184
196
  interface ResilienceConfigOverride {
185
197
  stream?: Partial<ResilienceConfig['stream']>;
@@ -190,32 +202,21 @@ interface ResilienceConfigOverride {
190
202
  oauth?: Partial<ResilienceConfig['oauth']>;
191
203
  }
192
204
  /**
193
- * 手写递归校验:把任意 `unknown`(通常来自 `JSON.parse` 的结果)收窄为合法的
194
- * `ResilienceConfigOverride` 子集,非法部分被剔除并通过 `warn` 回调上报(不抛异常)。
205
+ * 纯合并:把默认值与若干**已校验**的覆盖层按参数顺序(右覆盖左)深合并为完整的
206
+ * `ResilienceConfig`(永远齐全,不含 undefined 字段)。
195
207
  *
196
- * 校验规则(见 RFC-230 §3 D3):
197
- * - 顶层不是 object,或某个顶层 key 的值不是 object → 该顶层 key 整体剔除。
198
- * - 顶层 key 不在已知的 6 个 key 之内 → 该未知 key 整体剔除(防止拼写错误静默生效误导用户)。
199
- * - 子字段存在但不是"合法数值"(见 `isValidFieldValue`)→ 单字段剔除,兄弟字段不受影响。
200
- * - 子字段名不在已知字段名集合内 → 该未知子字段剔除。
201
- */
202
- declare function validateResilienceOverride(input: unknown, warn: (message: string) => void): ResilienceConfigOverride | undefined;
203
- /** 解析 `OTTO_RESILIENCE_CONFIG` 环境变量(JSON 字符串),非法值整体回退(无法字段级容错,因为连结构都没解析出来)。 */
204
- declare function parseResilienceEnv(raw: string | undefined, warn?: (message: string) => void): ResilienceConfigOverride | undefined;
205
- /**
206
- * 合并四层配置源,返回完整的 `ResilienceConfig`(永远齐全,不含 undefined 字段)。
208
+ * 调用约定(RFC-412 M3):本函数**信任输入已校验**(越界/非法字段应在传入前由调用方经
209
+ * `@x-otto/setting` zod 校验剔除),不做任何值域检查——校验单一真源在 setting。
207
210
  *
208
- * 合并顺序(右覆盖左):默认值 → env → settings → 程序化注入。
209
- * 消费方(`@otto/coding` `App`)负责在正确时机(settings 已加载后)传入 `fromSettings`,
210
- * 本函数是纯合并逻辑,不关心调用时机(见 RFC-230 §3 D5)。
211
+ * 典型调用(`@x-otto/coding` App,右覆盖左):
212
+ * `mergeResilienceConfig(fromEnv, fromSettings, explicit)`
213
+ * 其中 fromEnv 来自 `OTTO_RESILIENCE_CONFIG`(经 setting 校验)、fromSettings 来自 settings
214
+ * (加载时已过 setting zod)、explicit 来自程序化注入(经 setting 校验)。
211
215
  *
212
- * 三层输入(env/fromSettings/explicit)**全部**经 `validateResilienceOverride` 校验,不只校验
213
- * env 层——`fromSettings`/`explicit` 虽然编译期类型是 `ResilienceConfigOverride`,但运行时仍可能
214
- * 携带越界数值(如 settings 层理论上应已被 `@otto/setting` 的 zod schema 拦下,但那是另一个包
215
- * 的独立校验实现,不能假设它与本包的规则永远同步;`explicit` 是程序化注入,TS 类型只保证是
216
- * `number` 不保证范围)。统一在合并入口做一次权威校验,避免"同一条规则两处实现、日后漂移"。
216
+ * 实现:层维度循环化——消除此前"每层手写 6 域 `mergeSubObject` 展开"的重复(RFC-412 M3)。
217
+ * 每个覆盖层的每个存在子对象 `Object.assign` 进结果对应域,兄弟字段不丢失。
217
218
  */
218
- declare function resolveResilienceConfig(explicit?: ResilienceConfigOverride, fromSettings?: ResilienceConfigOverride, warn?: (message: string) => void): ResilienceConfig;
219
+ declare function mergeResilienceConfig(...layers: readonly (ResilienceConfigOverride | undefined)[]): ResilienceConfig;
219
220
  //#endregion
220
221
  //#region src/index.d.ts
221
222
  /** 清空队列并逐条调 sink。调用方应包一层 `(msg) => process.stderr.write(msg)` 绑定 this。 */
@@ -229,21 +230,20 @@ declare const normalizeEnv: (value: string | undefined, defaultValue: number) =>
229
230
  * 到 (0,1](比例语义:0 无意义、>1 超出窗口无意义),越界或非数回退默认值(不静默接受错值)。
230
231
  */
231
232
  declare const normalizeEnvFraction: (value: string | undefined, defaultValue: number) => number;
232
- declare function normalizePath(path: string): string;
233
233
  /**
234
234
  * 从 `startDir` 向上查找最近的 `pnpm-workspace.yaml`(monorepo root 标志文件),返回其所在目录。
235
235
  * 找不到(如在非本仓的用户项目里运行)返回 `undefined`。
236
236
  *
237
- * RFC-130:单一真源实现——原先在 `@otto/coding`(`install-plugin.ts`,供 CLI `--repo` 消费)与
238
- * `@otto/plugin`(`discovery.ts`,供仓库级插件发现消费)各自维护一份逐字相同的实现(copy-paste),
239
- * 无法互相 import(`@otto/plugin` 是 `@otto/coding` 的依赖,反向 import 会成环)。收敛到零依赖的
240
- * `@otto/env`(两者共同的底层依赖)消除维护漂移风险(终局审核 finding,2026-07-09)。
237
+ * RFC-130:单一真源实现——原先在 `@x-otto/coding`(`install-plugin.ts`,供 CLI `--repo` 消费)与
238
+ * `@x-otto/plugin`(`discovery.ts`,供仓库级插件发现消费)各自维护一份逐字相同的实现(copy-paste),
239
+ * 无法互相 import(`@x-otto/plugin` 是 `@x-otto/coding` 的依赖,反向 import 会成环)。收敛到零依赖的
240
+ * `@x-otto/env`(两者共同的底层依赖)消除维护漂移风险(终局审核 finding,2026-07-09)。
241
241
  *
242
242
  * 终止条件用 `dirname(dir) === dir`(到达文件系统根)而非固定层数上限——后者在深层嵌套路径下
243
243
  * 会误截断(旧实现的 `i < 20` 上限已移除,审核 finding:该上限是冗余约束)。
244
244
  */
245
245
  declare function findWorkspaceRoot(startDir: string): string | undefined;
246
- /** SDK 版本号(`@otto/env` package.json version),会话创建时戳入 metadata。 */
246
+ /** SDK 版本号(`@x-otto/env` package.json version),会话创建时戳入 metadata。 */
247
247
  declare const SDK_VERSION: string;
248
248
  declare const OTTO_USER_AGENT: string;
249
249
  declare const OTTO_ROOT = ".otto";
@@ -315,6 +315,30 @@ declare const SERVICE_URL: string;
315
315
  * 当前 env,既 testable(测试可运行时改 env)又始终反映真实值。
316
316
  */
317
317
  declare const resolveNpmSniffRegistries: () => readonly string[];
318
+ /**
319
+ * RFC-341 B6/D9 同模式:内网可达性探测目标 URL(逗号分隔候选,缺省空)。
320
+ *
321
+ * 用途(两处消费方,同一份判断结果):① Setup Wizard 是否展示企业内网 provider 选项
322
+ * (此前只查"用户是否已手动 /registry add 内网源",新用户从未配过就永远看不到);
323
+ * ② 走企业内网网关的 provider 模型可用性判定——即使凭据配置正确,脱离内网时物理不可达,
324
+ * 此前只判凭据不判网络,用户要等真正发请求失败才发现模型选错了。
325
+ *
326
+ * 公网源码缺省空 = 零内网痕迹(D1 隔离红线,同 `resolveNpmSniffRegistries` doc)——本文件
327
+ * 及其消费方是公网发布包,不硬编码任何具体内网域名/凭据环境变量名(含本 doc 注释本身,
328
+ * 敏感词扫描 `scan-public-sensitive.mjs` 会扫描含注释的 `.d.ts` 产物)。内网构建/用户
329
+ * 通过环境变量注入真实探测地址。call-time 解析,消费方 `intranet-probe.ts` 每次查表都
330
+ * 读当前 env。
331
+ *
332
+ * 2026-08-14 举一反三修复:env 注入不再是唯一候选——消费方可以把「目标资源自身地址」
333
+ * (provider 插件 manifest 声明的 baseUrl)经 `isIntranetReachable(force, extraUrls)`
334
+ * 并入候选。真实内网用户通常不配置本 env,而目标地址就写在插件声明里,探测它才是
335
+ * 可达性最直接的证据(URL 运行时供给,公网发布包仍零内网硬编码)。
336
+ *
337
+ * 探测语义(同日用户指令「ping 通即可」):候选 URL 只取其 host:port 做 TCP 握手,
338
+ * 不要求目标提供 HTTP 服务——探测的是网络层通不通,与应用层无关(见
339
+ * intranet-probe.ts 文件头)。因此候选写任意内网可达的 host 即可,路径无意义。
340
+ */
341
+ declare const resolveIntranetProbeUrls: () => readonly string[];
318
342
  declare const LOG_LEVEL: "info" | "warn" | "error" | "debug" | "trace" | "fatal";
319
343
  declare const SETTING_OFFLINE_MODE_ENABLED: boolean;
320
344
  /**
@@ -322,7 +346,7 @@ declare const SETTING_OFFLINE_MODE_ENABLED: boolean;
322
346
  * 而非 offline 的模块级 const)。
323
347
  *
324
348
  * 为什么必须函数式而非 const:`OTTO_SHADOW` 由 `packages/cli/src/cli.ts` 在**运行时**
325
- * 解析 `--shadow` flag 后写入 `process.env`,而 `@otto/env` 在 cli.ts 顶部就被 import——
349
+ * 解析 `--shadow` flag 后写入 `process.env`,而 `@x-otto/env` 在 cli.ts 顶部就被 import——
326
350
  * 若用模块级 const,常量在 flag 写入前就已冻结为 false。调用时求值规避此时序陷阱,
327
351
  * 也便于测试注入。(offline 模式可用 const 是因为 `OTTO_OFFLINE` 由外部 shell 预设、
328
352
  * node 启动前就存在——两者注入时机不同,范式不能照搬。)
@@ -330,7 +354,7 @@ declare const SETTING_OFFLINE_MODE_ENABLED: boolean;
330
354
  * 语义:为真时该进程及其全部子进程/worker 在整个生命周期内不向磁盘写入任何 otto 自有
331
355
  * 状态(凭据/会话/记忆/配置/trace/日志),既有磁盘状态照常只读继承(overlay 语义)。
332
356
  *
333
- * 传染面:`OTTO_SHADOW` 走 `OTTO_` 前缀,由 `@otto/shared` 的 `buildAllowedEnv` 白名单
357
+ * 传染面:`OTTO_SHADOW` 走 `OTTO_` 前缀,由 `@x-otto/shared` 的 `buildAllowedEnv` 白名单
334
358
  * 自动透传给全部 spawn 子进程;worker_threads 共享 `process.env` 天然继承——无需额外接线。
335
359
  *
336
360
  * 判断该开关的位置受 RFC-345 §D1 白名单约束(架构门禁强制),不得在业务层散落 `if (shadow)`。
@@ -349,5 +373,5 @@ interface UserIdentity {
349
373
  /** 从 identity.json 加载 UserIdentity。文件不存在/解析失败返回 null。 */
350
374
  declare function loadUserIdentity(path?: string): Promise<UserIdentity | null>;
351
375
  //#endregion
352
- export { AUTH_PATH, BUILD_ID, CLAUDE_HOME, CLAUDE_ROOT, type ConfigLayer, type ConfigLayerKind, type ConfigLayerScope, DEFAULT_RESILIENCE_CONFIG, type DecodedLeaseToken, LOG_FILE, LOG_LEVEL, OTTO_CONFIG_FILENAME, OTTO_DAEMON_DIR, OTTO_HOME, OTTO_IDENTITY_PATH, OTTO_PROJECT_CONFIG_RELPATH, OTTO_PROJECT_DIR, OTTO_ROOT, OTTO_SESSIONS_DIR, OTTO_SESSION_LOCKS_DIR, OTTO_USER_AGENT, OTTO_USER_CONFIG_DIR, type OttoProcessInfo, type ResilienceConfig, type ResilienceConfigOverride, type ResolveConfigLayersOptions, SDK_VERSION, SERVICE_PORT, SERVICE_URL, SETTING_OFFLINE_MODE_ENABLED, UserIdentity, currentHostname, decodeLeaseToken, drainEnvWarnings, encodeLeaseToken, findWorkspaceRoot, flushEnvWarnings, isClaudeCompatEnabled, isClaudeHooksCompatEnabled, isClaudeSettingsCompatEnabled, isContainerEnvironment, isProcessAlive, isShadowModeEnabled, listOttoProcesses, loadUserIdentity, normalizeEnv, normalizeEnvFraction, normalizePath, parseEtimeMs, parseOttoProcessTable, parseResilienceEnv, resolveConfigLayers, resolveNpmSniffRegistries, resolveResilienceConfig, validateResilienceOverride };
376
+ export { AUTH_PATH, BUILD_ID, CLAUDE_HOME, CLAUDE_ROOT, type ConfigLayer, type ConfigLayerKind, type ConfigLayerScope, DEFAULT_RESILIENCE_CONFIG, type DecodedLeaseToken, LOG_FILE, LOG_LEVEL, OTTO_CONFIG_FILENAME, OTTO_DAEMON_DIR, OTTO_HOME, OTTO_IDENTITY_PATH, OTTO_PROJECT_CONFIG_RELPATH, OTTO_PROJECT_DIR, OTTO_ROOT, OTTO_SESSIONS_DIR, OTTO_SESSION_LOCKS_DIR, OTTO_USER_AGENT, OTTO_USER_CONFIG_DIR, type OttoProcessInfo, type ResilienceConfig, type ResilienceConfigOverride, type ResolveConfigLayersOptions, SDK_VERSION, SERVICE_PORT, SERVICE_URL, SETTING_OFFLINE_MODE_ENABLED, UserIdentity, currentHostname, decodeLeaseToken, drainEnvWarnings, encodeLeaseToken, findWorkspaceRoot, flushEnvWarnings, isClaudeCompatEnabled, isClaudeHooksCompatEnabled, isClaudeSettingsCompatEnabled, isContainerEnvironment, isProcessAlive, isShadowModeEnabled, listOttoProcesses, loadUserIdentity, mergeResilienceConfig, normalizeEnv, normalizeEnvFraction, normalizePath, parseEtimeMs, parseOttoProcessTable, resolveConfigLayers, resolveIntranetProbeUrls, resolveNpmSniffRegistries };
353
377
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import{homedir as e,hostname as t}from"node:os";import{existsSync as n,readFileSync as r}from"node:fs";import{dirname as i,join as a,normalize as o,sep as s}from"node:path";import{spawnSync as c}from"node:child_process";const l=4512;function u(e){let{cwd:t,homedir:n,workspaceRoot:r}=e,i=e.claudeCompat??q(),o=[];return o.push({kind:`otto`,scope:`project`,dir:j(a(t,`.otto`))}),r&&j(r)!==j(t)&&o.push({kind:`otto`,scope:`repository`,dir:j(a(r,`.otto`))}),i&&o.push({kind:`claude`,scope:`project`,dir:j(a(t,`.claude`))}),n&&o.push({kind:`otto`,scope:`user`,dir:j(a(n,`.otto`))}),i&&n&&o.push({kind:`claude`,scope:`user`,dir:j(a(n,`.claude`))}),o}function d(e){try{return process.kill(e,0),!0}catch{return!1}}function f(){return t()}function p(e,t,n){return`${e}:${t}:${n}`}function m(e){let t=e.indexOf(`:`);if(t===-1)return{token:e};let n=e.indexOf(`:`,t+1);if(n===-1)return{token:e};let r=e.slice(0,t),i=e.slice(t+1,n),a=e.slice(n+1),o=Number(i),s=Number.isFinite(o)&&o>0?o:void 0;return s!==void 0&&a.length>0?{token:r,pid:s,host:a}:{token:r}}function h(e){let t=e.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/);if(!t)return 0;let[,n,r,i,a]=t;return(Number(n??0)*86400+Number(r??0)*3600+Number(i)*60+Number(a))*1e3}function ee(e){return e.match(/--continue\s+([0-9a-f-]{36})/)?.[1]}function te(e){let t=e.match(/--restart-generation=(\d+)/);return t?Number(t[1]):void 0}function g(e=`bin/otto.js`){let t=c(`ps`,[`-axo`,`pid=,ppid=,rss=,tty=,etime=,args=`],{encoding:`utf8`,timeout:1e4,maxBuffer:16*1024*1024});return t.status!==0||!t.stdout?[]:_(t.stdout,e)}function _(e,t=`bin/otto.js`){let n=[];for(let r of e.split(`
2
- `)){if(!r.includes(t))continue;let e=r.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/);if(!e)continue;let[,i,a,o,s,c,l]=e;l.includes(`spawn-helper`)||n.push({pid:Number(i),ppid:Number(a),rssKb:Number(o),tty:s,etimeMs:h(c),command:l,sessionId:ee(l),generation:te(l)})}return n}function v(){if(process.platform===`darwin`)return!1;try{if(n(`/.dockerenv`))return!0;if(n(`/proc/1/cgroup`)){let e=r(`/proc/1/cgroup`,`utf8`);return/docker|kubepods|containerd/.test(e)}}catch{}return!1}const y={stream:{maxRetries:5,initialDelayMs:500,backoffFactor:2,maxDelayMs:6e4},networkDisconnect:{maxRetries:5,intervalMs:3e4},mcp:{maxReconnectAttempts:3,reconnectDelayMs:500,requestTimeoutMs:3e4},schedule:{retryBackoffMs:0},errorRetry:{maxAttempts:5,intervalMs:3e4},oauth:{refreshLockTimeoutMs:15e3}},b={stream:[`maxRetries`,`initialDelayMs`,`backoffFactor`,`maxDelayMs`],networkDisconnect:[`maxRetries`,`intervalMs`],mcp:[`maxReconnectAttempts`,`reconnectDelayMs`,`requestTimeoutMs`],schedule:[`retryBackoffMs`],errorRetry:[`maxAttempts`,`intervalMs`],oauth:[`refreshLockTimeoutMs`]},x=Object.keys(b),S=new Set([`mcp.maxReconnectAttempts`,`schedule.retryBackoffMs`]);function ne(e,t,n){return typeof n!=`number`||!Number.isFinite(n)||(S.has(`${e}.${t}`)?n<0:n<=0)?!1:e===`oauth`&&t===`refreshLockTimeoutMs`?n>=1e3:!0}function C(e,t){if(typeof e!=`object`||!e||Array.isArray(e)){t(`resilience config must be an object, got: ${typeof e}`);return}let n={},r=e;for(let e of Object.keys(r)){if(!x.includes(e)){t(`resilience config has unknown top-level key "${e}", ignored`);continue}let i=e,a=r[e];if(typeof a!=`object`||!a||Array.isArray(a)){t(`resilience.${i} must be an object, got: ${typeof a}, ignored`);continue}let o=a,s={};for(let e of Object.keys(o)){if(!b[i].includes(e)){t(`resilience.${i}.${e} is not a recognized field, ignored`);continue}let n=o[e];if(!ne(i,e,n)){t(`resilience.${i}.${e} has invalid value (${JSON.stringify(n)}), ignored`);continue}s[e]=n}Object.keys(s).length>0&&(n[i]=s)}return n}function w(e,t=e=>console.warn(`[resilience-config] ${e}`)){if(!e)return;let n;try{n=JSON.parse(e)}catch(e){t(`OTTO_RESILIENCE_CONFIG is not valid JSON, ignoring entirely: ${e instanceof Error?e.message:String(e)}`);return}return C(n,t)}function T(e,t){return t?{...e,...t}:e}function re(e,t,n=e=>console.warn(`[resilience-config] ${e}`)){let r=[w(process.env.OTTO_RESILIENCE_CONFIG,n),t?C(t,n):void 0,e?C(e,n):void 0],i={stream:{...y.stream},networkDisconnect:{...y.networkDisconnect},mcp:{...y.mcp},schedule:{...y.schedule},errorRetry:{...y.errorRetry},oauth:{...y.oauth}};for(let e of r)e&&(i={stream:T(i.stream,e.stream),networkDisconnect:T(i.networkDisconnect,e.networkDisconnect),mcp:T(i.mcp,e.mcp),schedule:T(i.schedule,e.schedule),errorRetry:T(i.errorRetry,e.errorRetry),oauth:T(i.oauth,e.oauth)});return i}const E=[];function D(e){let t=E.splice(0);for(let n of t)e(n)}function O(){return E.splice(0)}const k=(e,t)=>{if(e===void 0)return t;let n=parseInt(e,10);return isNaN(n)||n<=0?(E.push(`Invalid environment variable value: ${e}. Using default: ${t}`),t):n},A=(e,t)=>{if(e===void 0)return t;let n=parseFloat(e);return isNaN(n)||n<=0||n>1?(E.push(`Invalid fraction environment variable value: ${e}. Using default: ${t}`),t):n};function j(e){return o(e).replaceAll(s===`\\`?`\\`:s,`/`)}function M(e){let t=e;for(;;){if(n(a(t,`pnpm-workspace.yaml`)))return t;let e=i(t);if(e===t)return;t=e}}function N(){try{let e=j(`${j(new URL(`.`,import.meta.url).pathname)}/../package.json`);return JSON.parse(r(e,`utf-8`)).version||`unknown`}catch(e){return console.warn(`Failed to read package version:`,e),`unknown`}}const P=N(),F=`otto/${P}`,I=`.otto`,L=process.env.OTTO_CONFIG_FILENAME||`config.json`,R=`${I}/${L}`,z=j(process.env.OTTO_HOME||`${e()}/.otto`),B=`${z}/sessions`,V=`${B}/locks`,H=`${z}/daemon`,U=j(e()+`/.config/otto`),W=j(`${process.cwd()}/${I}`),G=`.claude`,K=j(`${e()}/${G}`);function q(){let e=process.env.OTTO_CLAUDE_COMPAT?.toLowerCase();return!(e===`0`||e===`false`||e===`no`||e===`off`)}function J(){if(!q())return!1;let e=process.env.OTTO_CLAUDE_COMPAT_SETTINGS?.toLowerCase();return e===`1`||e===`true`||e===`yes`||e===`on`}function Y(){if(!q())return!1;let e=process.env.OTTO_CLAUDE_COMPAT_HOOKS?.toLowerCase();return e===`1`||e===`true`||e===`yes`||e===`on`}const X=process.env.OTTO_LOG_FILE?j(process.env.OTTO_LOG_FILE):``,Z=k(process.env.OTTO_SERVICE_PORT,8417),ie=process.env.OTTO_SERVICE_URL??`http://127.0.0.1:${Z}`,ae=()=>(process.env.OTTO_NPM_SNIFF_REGISTRIES??``).split(`,`).map(e=>e.trim().replace(/\/+$/,``)).filter(e=>e.length>0),oe=process.env.OTTO_LOG_LEVEL||(process.env.NODE_ENV===`production`?`info`:`trace`),se=process.env.OTTO_OFFLINE===`1`||process.env.OTTO_OFFLINE?.toLowerCase()===`true`||process.env.OTTO_OFFLINE?.toLowerCase()===`yes`;function Q(){let e=process.env.OTTO_SHADOW?.toLowerCase();return e===`1`||e===`true`||e===`yes`||e===`on`}const ce=j(process.env.OTTO_AUTH_PATH||`${U}/auth.json`),$=j(process.env.OTTO_IDENTITY_PATH||`${z}/identity.json`);async function le(e){let t=e??$;try{let{readFile:e}=await import(`node:fs/promises`),n=await e(t,`utf-8`),r=JSON.parse(n);return typeof r.userId==`string`&&typeof r.token==`string`&&typeof r.serviceUrl==`string`&&typeof r.loginAt==`number`?r:null}catch{return null}}export{ce as AUTH_PATH,l as BUILD_ID,K as CLAUDE_HOME,G as CLAUDE_ROOT,y as DEFAULT_RESILIENCE_CONFIG,X as LOG_FILE,oe as LOG_LEVEL,L as OTTO_CONFIG_FILENAME,H as OTTO_DAEMON_DIR,z as OTTO_HOME,$ as OTTO_IDENTITY_PATH,R as OTTO_PROJECT_CONFIG_RELPATH,W as OTTO_PROJECT_DIR,I as OTTO_ROOT,B as OTTO_SESSIONS_DIR,V as OTTO_SESSION_LOCKS_DIR,F as OTTO_USER_AGENT,U as OTTO_USER_CONFIG_DIR,P as SDK_VERSION,Z as SERVICE_PORT,ie as SERVICE_URL,se as SETTING_OFFLINE_MODE_ENABLED,f as currentHostname,m as decodeLeaseToken,O as drainEnvWarnings,p as encodeLeaseToken,M as findWorkspaceRoot,D as flushEnvWarnings,q as isClaudeCompatEnabled,Y as isClaudeHooksCompatEnabled,J as isClaudeSettingsCompatEnabled,v as isContainerEnvironment,d as isProcessAlive,Q as isShadowModeEnabled,g as listOttoProcesses,le as loadUserIdentity,k as normalizeEnv,A as normalizeEnvFraction,j as normalizePath,h as parseEtimeMs,_ as parseOttoProcessTable,w as parseResilienceEnv,u as resolveConfigLayers,ae as resolveNpmSniffRegistries,re as resolveResilienceConfig,C as validateResilienceOverride};
1
+ import{homedir as e,hostname as t}from"node:os";import{existsSync as n,readFileSync as r}from"node:fs";import{dirname as i,join as a,normalize as o,sep as s}from"node:path";import{spawnSync as c}from"node:child_process";function l(e){return o(e).replaceAll(s===`\\`?`\\`:s,`/`)}const u=5515;function d(e){let{cwd:t,homedir:n,workspaceRoot:r}=e,i=e.claudeCompat??U(),o=[];return o.push({kind:`otto`,scope:`project`,dir:l(a(t,`.otto`))}),r&&l(r)!==l(t)&&o.push({kind:`otto`,scope:`repository`,dir:l(a(r,`.otto`))}),i&&o.push({kind:`claude`,scope:`project`,dir:l(a(t,`.claude`))}),n&&o.push({kind:`otto`,scope:`user`,dir:l(a(n,`.otto`))}),i&&n&&o.push({kind:`claude`,scope:`user`,dir:l(a(n,`.claude`))}),o}function f(e){try{return process.kill(e,0),!0}catch{return!1}}function p(){return t()}function m(e,t,n){return`${e}:${t}:${n}`}function h(e){let t=e.indexOf(`:`);if(t===-1)return{token:e};let n=e.indexOf(`:`,t+1);if(n===-1)return{token:e};let r=e.slice(0,t),i=e.slice(t+1,n),a=e.slice(n+1),o=Number(i),s=Number.isFinite(o)&&o>0?o:void 0;return s!==void 0&&a.length>0?{token:r,pid:s,host:a}:{token:r}}function g(e){let t=e.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/);if(!t)return 0;let[,n,r,i,a]=t;return(Number(n??0)*86400+Number(r??0)*3600+Number(i)*60+Number(a))*1e3}function _(e){return e.match(/--continue\s+([0-9a-f-]{36})/)?.[1]}function v(e){let t=e.match(/--restart-generation=(\d+)/);return t?Number(t[1]):void 0}function y(e=`bin/otto.js`){let t=c(`ps`,[`-axo`,`pid=,ppid=,rss=,tty=,etime=,args=`],{encoding:`utf8`,timeout:1e4,maxBuffer:16*1024*1024});return t.status!==0||!t.stdout?[]:b(t.stdout,e)}function b(e,t=`bin/otto.js`){let n=[];for(let r of e.split(`
2
+ `)){if(!r.includes(t))continue;let e=r.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/);if(!e)continue;let[,i,a,o,s,c,l]=e;l.includes(`spawn-helper`)||n.push({pid:Number(i),ppid:Number(a),rssKb:Number(o),tty:s,etimeMs:g(c),command:l,sessionId:_(l),generation:v(l)})}return n}function x(){if(process.platform===`darwin`)return!1;try{if(n(`/.dockerenv`))return!0;if(n(`/proc/1/cgroup`)){let e=r(`/proc/1/cgroup`,`utf8`);return/docker|kubepods|containerd/.test(e)}}catch{}return!1}const S={stream:{maxRetries:5,initialDelayMs:500,backoffFactor:2,maxDelayMs:6e4},networkDisconnect:{maxRetries:5,intervalMs:3e4},mcp:{maxReconnectAttempts:3,reconnectDelayMs:500,requestTimeoutMs:3e4},schedule:{retryBackoffMs:0},errorRetry:{maxAttempts:5,intervalMs:3e4},oauth:{refreshLockTimeoutMs:15e3}};function C(...e){let t={stream:{...S.stream},networkDisconnect:{...S.networkDisconnect},mcp:{...S.mcp},schedule:{...S.schedule},errorRetry:{...S.errorRetry},oauth:{...S.oauth}};for(let n of e)if(n)for(let e of Object.keys(t)){let r=n[e];r&&Object.assign(t[e],r)}return t}const w=[];function ee(e){let t=w.splice(0);for(let n of t)e(n)}function T(){return w.splice(0)}const E=(e,t)=>{if(e===void 0)return t;let n=parseInt(e,10);return isNaN(n)||n<=0?(w.push(`Invalid environment variable value: ${e}. Using default: ${t}`),t):n},D=(e,t)=>{if(e===void 0)return t;let n=parseFloat(e);return isNaN(n)||n<=0||n>1?(w.push(`Invalid fraction environment variable value: ${e}. Using default: ${t}`),t):n};function O(e){let t=e;for(;;){if(n(a(t,`pnpm-workspace.yaml`)))return t;let e=i(t);if(e===t)return;t=e}}function k(){try{let e=l(`${l(new URL(`.`,import.meta.url).pathname)}/../package.json`);return JSON.parse(r(e,`utf-8`)).version||`unknown`}catch{return`unknown`}}const A=k(),j=`otto/${A}`,M=`.otto`,N=process.env.OTTO_CONFIG_FILENAME||`config.json`,P=`${M}/${N}`,F=l(process.env.OTTO_HOME||`${e()}/.otto`),I=`${F}/sessions`,L=`${I}/locks`,R=`${F}/daemon`,z=l(e()+`/.config/otto`),B=l(`${process.cwd()}/${M}`),V=`.claude`,H=l(`${e()}/${V}`);function U(){let e=process.env.OTTO_CLAUDE_COMPAT?.toLowerCase();return!(e===`0`||e===`false`||e===`no`||e===`off`)}function W(){if(!U())return!1;let e=process.env.OTTO_CLAUDE_COMPAT_SETTINGS?.toLowerCase();return e===`1`||e===`true`||e===`yes`||e===`on`}function G(){if(!U())return!1;let e=process.env.OTTO_CLAUDE_COMPAT_HOOKS?.toLowerCase();return e===`1`||e===`true`||e===`yes`||e===`on`}const K=process.env.OTTO_LOG_FILE?l(process.env.OTTO_LOG_FILE):``,q=E(process.env.OTTO_SERVICE_PORT,8417),J=process.env.OTTO_SERVICE_URL??`http://127.0.0.1:${q}`,Y=()=>(process.env.OTTO_NPM_SNIFF_REGISTRIES??``).split(`,`).map(e=>e.trim().replace(/\/+$/,``)).filter(e=>e.length>0),X=()=>(process.env.OTTO_INTRANET_PROBE_URLS??``).split(`,`).map(e=>e.trim().replace(/\/+$/,``)).filter(e=>e.length>0),Z=process.env.OTTO_LOG_LEVEL||(process.env.NODE_ENV===`production`?`info`:`trace`),Q=process.env.OTTO_OFFLINE===`1`||process.env.OTTO_OFFLINE?.toLowerCase()===`true`||process.env.OTTO_OFFLINE?.toLowerCase()===`yes`;function te(){let e=process.env.OTTO_SHADOW?.toLowerCase();return e===`1`||e===`true`||e===`yes`||e===`on`}const ne=l(process.env.OTTO_AUTH_PATH||`${z}/auth.json`),$=l(process.env.OTTO_IDENTITY_PATH||`${F}/identity.json`);async function re(e){let t=e??$;try{let{readFile:e}=await import(`node:fs/promises`),n=await e(t,`utf-8`),r=JSON.parse(n);return typeof r.userId==`string`&&typeof r.token==`string`&&typeof r.serviceUrl==`string`&&typeof r.loginAt==`number`?r:null}catch{return null}}export{ne as AUTH_PATH,u as BUILD_ID,H as CLAUDE_HOME,V as CLAUDE_ROOT,S as DEFAULT_RESILIENCE_CONFIG,K as LOG_FILE,Z as LOG_LEVEL,N as OTTO_CONFIG_FILENAME,R as OTTO_DAEMON_DIR,F as OTTO_HOME,$ as OTTO_IDENTITY_PATH,P as OTTO_PROJECT_CONFIG_RELPATH,B as OTTO_PROJECT_DIR,M as OTTO_ROOT,I as OTTO_SESSIONS_DIR,L as OTTO_SESSION_LOCKS_DIR,j as OTTO_USER_AGENT,z as OTTO_USER_CONFIG_DIR,A as SDK_VERSION,q as SERVICE_PORT,J as SERVICE_URL,Q as SETTING_OFFLINE_MODE_ENABLED,p as currentHostname,h as decodeLeaseToken,T as drainEnvWarnings,m as encodeLeaseToken,O as findWorkspaceRoot,ee as flushEnvWarnings,U as isClaudeCompatEnabled,G as isClaudeHooksCompatEnabled,W as isClaudeSettingsCompatEnabled,x as isContainerEnvironment,f as isProcessAlive,te as isShadowModeEnabled,y as listOttoProcesses,re as loadUserIdentity,C as mergeResilienceConfig,E as normalizeEnv,D as normalizeEnvFraction,l as normalizePath,g as parseEtimeMs,b as parseOttoProcessTable,d as resolveConfigLayers,X as resolveIntranetProbeUrls,Y as resolveNpmSniffRegistries};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x-otto/env",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.10",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -19,10 +19,10 @@
19
19
  "tag": "alpha"
20
20
  },
21
21
  "scripts": {
22
- "build": "tsdown && rm -f dist/*.map && sed -i '' '\\|//# sourceMappingURL=|d' dist/*.js dist/browser/index.js 2>/dev/null; true",
22
+ "build": "node ../../scripts/gen-build-id.mjs && tsdown && rm -f dist/*.map && sed -i '' '\\|//# sourceMappingURL=|d' dist/*.js 2>/dev/null; true",
23
23
  "typecheck:project": "tsc -p tsconfig.json --noEmit",
24
24
  "typecheck:file": "tsc src/index.ts --noEmit --target ES2024 --module ESNext --moduleResolution bundler --esModuleInterop --skipLibCheck",
25
25
  "typecheck": "tsc --noEmit",
26
26
  "clean": "rm -rf dist"
27
27
  }
28
- }
28
+ }