@trim21/personal-pi-extensions 0.1.697 → 0.1.699

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
@@ -330,7 +330,7 @@ bwrap 已集成进 bash 工具实现(opencode 风格 `bash` 位于 `src/openco
330
330
 
331
331
  命令失败(`gh` 未登录 / 不在 PATH)时该服务器启动失败并报错。
332
332
 
333
- 启动时才能算出的值(例如项目把 `typescript` alias 成 `@typescript/typescript6` 时,要去 pnpm store 里找真实的 `tsserver.js`)用 `initializationOptionsCommand`,避免把绝对路径写死在配置里:
333
+ 启动时才能算出的值(例如项目把 `typescript` alias 成 `@typescript/typescript6` 时,要去 pnpm store 或 `@typescript/*` 里找真实的 `tsserver.js`)用 `initializationOptionsCommand`,避免把绝对路径写死在配置里。这类现算脚本随包提供了一个:`bin/typescript-options.mjs`(复制到 `~/.pi/agent/lsp/` 后由下面这行调用,标准 typescript 依赖的项目输出空对象、不做覆盖):
334
334
 
335
335
  ```jsonc
336
336
  {
@@ -340,7 +340,7 @@ bwrap 已集成进 bash 工具实现(opencode 风格 `bash` 位于 `src/openco
340
340
  "args": ["--stdio"],
341
341
  "initializationOptions": { "tsserver": { "logVerbosity": "verbose" } },
342
342
  // 脚本 stdout 的 JSON 对象与上面的静态值深合并(命令优先)
343
- "initializationOptionsCommand": ["node", "{root}/.pi/lsp/ts-options.mjs"],
343
+ "initializationOptionsCommand": ["node", "/home/<user>/.pi/agent/lsp/typescript-options.mjs"],
344
344
  },
345
345
  },
346
346
  }
@@ -0,0 +1,146 @@
1
+ // 给 typescript-language-server 计算 initializationOptions.tsserver.path。
2
+ //
3
+ // 用法:把这个文件复制到 ~/.pi/agent/lsp/,在 lsp.json 的 typescript server 上配
4
+ // "initializationOptionsCommand": ["node", "/home/<user>/.pi/agent/lsp/typescript-options.mjs"]
5
+ // 引擎在 spawn 服务器前执行它(cwd = 服务器 root),stdout 的 JSON 对象与静态
6
+ // initializationOptions 深合并,且用户显式给出的值优先。
7
+ //
8
+ // 只在 workspace 里的 typescript 不可用时才需要干预:
9
+ // - 项目把 typescript alias 成 @typescript/typescript6 这类 shim 包时,
10
+ // node_modules/typescript/lib 下只有转发 stub、没有 tsserver.js,
11
+ // typescript-language-server 的 workspace 探测会失败并直接报错退出;
12
+ // 真实 typescript 在哪取决于包管理器与链接方式:
13
+ // npm 提升/pnpm hoisted:node_modules/@typescript/<包>/lib/tsserver.js
14
+ // npm 嵌套:node_modules/typescript/node_modules/@typescript/<包>/lib/tsserver.js
15
+ // pnpm store:node_modules/.pnpm/<typescript@版本 或 @typescript+包@版本>/
16
+ // node_modules/{typescript,@typescript/<包>}/lib/tsserver.js
17
+ // - 项目里是标准 typescript 依赖时 workspace 探测本来就能命中,脚本输出空对象,
18
+ // 不做任何覆盖,保持服务器自己的解析结果(版本随项目走)。
19
+ //
20
+ // 沿 node_modules 链向上找(与语言服务器的 workspace 探测一致),每级先看标准位置,
21
+ // 再看上面几种落法,取带 tsserver.js 的最高版本(typescript@7 是 native 包,
22
+ // 没有 tsserver.js,会被过滤掉)。
23
+ //
24
+ // 独立可执行脚本:只用 node 内置模块、不依赖扩展运行时,因此用同步 fs API。
25
+
26
+ import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
27
+ import { dirname, join } from "node:path";
28
+ import process from "node:process";
29
+
30
+ /** cwd 到根的所有 node_modules 目录(含自身)。 */
31
+ function nodeModulesChain(dir) {
32
+ const chain = [];
33
+ let current = dir;
34
+ for (;;) {
35
+ chain.push(join(current, "node_modules"));
36
+ const parent = dirname(current);
37
+ if (parent === current) {
38
+ return chain;
39
+ }
40
+ current = parent;
41
+ }
42
+ }
43
+
44
+ /** 版本段转数字;非数字段(如 `0-beta`)当 0。 */
45
+ function versionPart(segment = "0") {
46
+ const parsed = Number(segment);
47
+ return Number.isNaN(parsed) ? 0 : parsed;
48
+ }
49
+
50
+ /** 版本号降序比较;只认前导数字段,足够比对 `typescript@<版本>` 这类目录名。 */
51
+ function compareVersions(a, b) {
52
+ const left = a.split(".");
53
+ const right = b.split(".");
54
+ for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
55
+ const na = versionPart(left[i]);
56
+ const nb = versionPart(right[i]);
57
+ if (na !== nb) {
58
+ return na - nb;
59
+ }
60
+ }
61
+ return 0;
62
+ }
63
+
64
+ /** 目录下的子目录名;不存在或读不了给空数组。 */
65
+ function listDirs(dir) {
66
+ try {
67
+ return readdirSync(dir, { withFileTypes: true })
68
+ .filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
69
+ .map((entry) => entry.name);
70
+ } catch {
71
+ return [];
72
+ }
73
+ }
74
+
75
+ /** package.json 的 version;读不到或不是字符串时给 "0"(只用于排序,不影响可用性)。 */
76
+ function packageVersion(dir) {
77
+ try {
78
+ const version = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).version;
79
+ return typeof version === "string" ? version : "0";
80
+ } catch {
81
+ return "0";
82
+ }
83
+ }
84
+
85
+ /** 包目录(typescript 或 @typescript/<包>)带 tsserver.js 时的候选。 */
86
+ function candidate(packageDir) {
87
+ const path = join(packageDir, "lib/tsserver.js");
88
+ return existsSync(path) ? { version: packageVersion(packageDir), path } : undefined;
89
+ }
90
+
91
+ /** dir/@typescript/<包> 下的候选(顶层与嵌套层共用)。 */
92
+ function scopedCandidates(dir) {
93
+ return listDirs(join(dir, "@typescript")).map((name) =>
94
+ candidate(join(dir, "@typescript", name)),
95
+ );
96
+ }
97
+
98
+ /** 该级 node_modules 下所有带 tsserver.js 的候选:npm(提升/嵌套)与 pnpm(store/hoisted)都覆盖。 */
99
+ function levelCandidates(nodeModules) {
100
+ const candidates = [
101
+ candidate(join(nodeModules, "typescript")),
102
+ ...scopedCandidates(nodeModules),
103
+ // npm 把 shim 自己的依赖嵌在它下面(顶层放不下时)
104
+ candidate(join(nodeModules, "typescript", "node_modules", "typescript")),
105
+ ...scopedCandidates(join(nodeModules, "typescript", "node_modules")),
106
+ ];
107
+ // pnpm store:虚拟 store 里真实 typescript 与 alias 出来的 shim 兄弟包
108
+ for (const name of listDirs(join(nodeModules, ".pnpm"))) {
109
+ if (!name.startsWith("typescript@") && !name.startsWith("@typescript+")) {
110
+ continue;
111
+ }
112
+ const store = join(nodeModules, ".pnpm", name, "node_modules");
113
+ candidates.push(candidate(join(store, "typescript")), ...scopedCandidates(store));
114
+ }
115
+ return candidates.filter((value) => value !== undefined);
116
+ }
117
+
118
+ /** 该级 node_modules 下带 tsserver.js 的最高版本 tsserver 路径;没有则 undefined。 */
119
+ function levelTsserver(nodeModules) {
120
+ return levelCandidates(nodeModules).toSorted((a, b) => compareVersions(b.version, a.version))[0]
121
+ ?.path;
122
+ }
123
+
124
+ /** 解析该 workspace 该用的 tsserver 路径;标准依赖可由服务器自己解析时不返回路径。 */
125
+ function resolveTsserver(cwd) {
126
+ for (const nodeModules of nodeModulesChain(cwd)) {
127
+ // 标准依赖:语言服务器自己就能解析到,别覆盖(否则会把项目锁到别的 TS 版本)
128
+ try {
129
+ const real = realpathSync(join(nodeModules, "typescript"));
130
+ if (existsSync(join(real, "lib/tsserver.js"))) {
131
+ return;
132
+ }
133
+ } catch {
134
+ // 没有这个包,继续
135
+ }
136
+ const found = levelTsserver(nodeModules);
137
+ if (found) {
138
+ return found;
139
+ }
140
+ }
141
+ }
142
+
143
+ const tsserverPath = resolveTsserver(process.cwd());
144
+ process.stdout.write(
145
+ `${JSON.stringify(tsserverPath ? { tsserver: { path: tsserverPath } } : {})}\n`,
146
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.697",
3
+ "version": "0.1.699",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "files": [
15
15
  "src",
16
+ "bin/typescript-options.mjs",
16
17
  "README.md"
17
18
  ],
18
19
  "publishConfig": {
@@ -107,9 +107,12 @@ function buildDescription(tools: readonly CallableTool[]): string {
107
107
  "- Call tools with `await call(name, args)`. Arguments and results make a JSON round trip,",
108
108
  " and a failing tool rejects with a `CallFailedError` you can catch.",
109
109
  "- Only what the script passes to `text(value)` / `console.log(...)` and its `return` value enter",
110
- " this conversation; nested calls and their results stay out of it.",
111
- "- Pass only necessary content to `text(value)`: print the content the next step needs, not whole",
112
- " tool results or data you will not use.",
110
+ " this conversation; nested calls and their results stay out of it. Print what the next step needs,",
111
+ " nothing else.",
112
+ "- Every `call` overload below declares that tool's return shape: read it, then project just the",
113
+ " fields you need into `text(value)` / `return` — e.g. `text(result.items.map((item) => item.title))`.",
114
+ "- Do not stringify whole results (`JSON.stringify(result)`) or print every field the tool happened to",
115
+ " return: urls, ids and metadata you will not use cost context and buy nothing.",
113
116
  "- `store.set(key, value)`, `store.get(key)` and `store.list()` are a small key/value store that",
114
117
  " persists across codemode calls in this session; `store.set(key, undefined)` removes a key.",
115
118
  "- The sandbox has no file system, network, timers or Node APIs: calling tools is its only",
@@ -163,9 +163,30 @@ typescript-language-server **不内置 TypeScript**(零依赖)。启动时
163
163
 
164
164
  `tsserver.path` 必须指向 **`typescript/lib/tsserver.js` 或 `typescript/lib/` 目录的绝对路径**(或 PATH 可执行名);因为初始化字符串不支持 `{root}` 模板,无法用相对 workspace 的表达式。
165
165
 
166
- **alias 依赖坑**:当项目把 typescript 写成 `npm:@typescript/typescript6`(alias stub)时,`node_modules/typescript/lib/` 下只有转发 stub(`typescript.js`/`tsserverlibrary.js`/`tsc.js`),**没有 `tsserver.js`** → workspace 探测失效;实体 tsserver 在 pnpm 虚拟 store:`node_modules/.pnpm/typescript@<真实版本>/node_modules/typescript/lib/tsserver.js`(目录名无 peer 后缀、相对稳定;**升级 typescript 版本后要同步更新路径**)。依赖为标准 `typescript` 包时 `node_modules/typescript` 直接是完整包,workspace 探测即命中、无需配 path。
166
+ **alias 依赖坑**:当项目把 typescript 写成 `npm:@typescript/typescript6`(alias stub)时,`node_modules/typescript/lib/` 下只有转发 stub(`typescript.js`/`tsserverlibrary.js`/`tsc.js`),**没有 `tsserver.js`** → workspace 探测失效。真实 tsserver 在哪取决于包管理器与链接方式:
167
167
 
168
- 这种项目里把路径写死会随 typescript 升级失效,用 `initializationOptionsCommand` 让脚本现算更稳。因为 servers 按 id 整条覆盖,本地 `.pi/lsp.json` 要写完整条目:
168
+ | 布局 | 真实 tsserver 位置 |
169
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
170
+ | 标准 typescript 依赖 | `<root>/node_modules/typescript/lib/tsserver.js`(workspace 探测即命中,不需要任何配置) |
171
+ | npm 提升 / pnpm hoisted | `node_modules/@typescript/<包>/lib/tsserver.js`(如 `@typescript/old`,是 shim 自己的依赖被提到顶层) |
172
+ | npm 嵌套 | `node_modules/typescript/node_modules/@typescript/<包>/lib/tsserver.js`(顶层放不下时 shim 的依赖嵌在它下面) |
173
+ | pnpm store | `node_modules/.pnpm/{typescript@<版本>,@typescript+<包>@<版本>}/node_modules/{typescript,@typescript/<包>}/lib/tsserver.js`(含带 peer 后缀的目录名) |
174
+
175
+ `@typescript/native`(typescript@7)是 native 包、**没有 `tsserver.js`**,不能当候选;TS6 项目要的是 `@typescript/old` 里的那份。
176
+
177
+ 不用自己写脚本:扩展随包提供 `bin/typescript-options.mjs`(纯 node 内置模块、无依赖、无需构建),它按上表的布局现算路径,标准依赖命中时输出 `{}`(不覆盖,版本随项目走),找不到也输出 `{}`。把它复制到本机固定位置,然后在 server 上配一行:
178
+
179
+ ```bash
180
+ cp <package>/bin/typescript-options.mjs ~/.pi/agent/lsp/typescript-options.mjs
181
+ ```
182
+
183
+ ```json
184
+ "initializationOptionsCommand": ["node", "/home/<user>/.pi/agent/lsp/typescript-options.mjs"]
185
+ ```
186
+
187
+ 命令的 argv 只支持 `{root}` / `{cwd}` 模板与 `${VAR}` 插值,没有指向包内路径的模板,所以这里写绝对路径;脚本升级后重新复制一次。
188
+
189
+ 因为 servers 按 id 整条覆盖,本地 `.pi/lsp.json` 里写 `typescript` 要写完整条目(否则全局那条被整体替换):
169
190
 
170
191
  ```json
171
192
  {
@@ -180,54 +201,12 @@ typescript-language-server **不内置 TypeScript**(零依赖)。启动时
180
201
  ".js": "javascript",
181
202
  ".jsx": "javascriptreact"
182
203
  },
183
- "initializationOptionsCommand": ["node", "{root}/.pi/lsp/ts-options.mjs"]
204
+ "initializationOptionsCommand": ["node", "/home/<user>/.pi/agent/lsp/typescript-options.mjs"]
184
205
  }
185
206
  }
186
207
  }
187
208
  ```
188
209
 
189
- ```js
190
- // .pi/lsp/ts-options.mjs:项目能解析到 typescript/lib/tsserver.js(标准依赖 / hoisted)时输出 {},
191
- // 让服务器按 workspace 解析、版本随项目走;只有 alias shim(解析不到)才去 pnpm store 里找真实 tsserver。
192
- import { existsSync, readdirSync } from "node:fs";
193
- import { createRequire } from "node:module";
194
- import { join } from "node:path";
195
-
196
- const requireFromProject = createRequire(join(process.cwd(), "package.json"));
197
-
198
- // workspace 里可用的 tsserver:解析得到就不干预(hoisted 的依赖也能解析到)
199
- function workspaceHasTsserver() {
200
- try {
201
- requireFromProject.resolve("typescript/lib/tsserver.js");
202
- return true;
203
- } catch {
204
- return false;
205
- }
206
- }
207
-
208
- // store 里带 tsserver.js 的最高版本:typescript@7 是 native 包、没有 tsserver.js,会被筛掉
209
- function storeTsserver() {
210
- const store = join(process.cwd(), "node_modules/.pnpm");
211
- let entries;
212
- try {
213
- entries = readdirSync(store);
214
- } catch {
215
- return undefined;
216
- }
217
- return entries
218
- .map((name) => /^typescript@(\d[^/]*)$/.exec(name)?.[1])
219
- .filter(Boolean)
220
- .sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
221
- .map((version) =>
222
- join(store, `typescript@${version}`, "node_modules/typescript/lib/tsserver.js"),
223
- )
224
- .find((path) => existsSync(path));
225
- }
226
-
227
- const path = workspaceHasTsserver() ? undefined : storeTsserver();
228
- console.log(JSON.stringify(path ? { tsserver: { path } } : {}));
229
- ```
230
-
231
210
  只在某台机器上临时用、路径不常变时,也可以直接写静态值(同样要整条覆盖):
232
211
 
233
212
  ```json
@@ -249,10 +228,10 @@ console.log(JSON.stringify(path ? { tsserver: { path } } : {}));
249
228
 
250
229
  ## 常见排查
251
230
 
252
- | 现象 | 原因与修法 |
253
- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
254
- | notify `failed to start …: binary not found` | `bin` 不在 PATH(测试场景二进制缺失时整组跳过) |
255
- | `… provides no tsserver.js. No other valid TypeScript installation was found` | workspace 的 `node_modules/typescript` 是 alias stub:用 `initializationOptionsCommand` 算 `.pnpm` 里的实体路径、写静态 `initializationOptions.tsserver.path`,或换标准 typescript 依赖 |
256
- | notify `failed to start …: initializationOptions command …` | 脚本非零退出 / 空输出 / stdout 不是 JSON 对象(报错里带具体命令、退出码与 stderr) |
257
- | 诊断一直为空且无任何报错 | 服务器没匹配到文件(`include`/扩展名)、在 broken 冷却中、或文件在调用 cwd 之外(LSP 只在工作目录内启用) |
258
- | 读取配置直接抛错 | typebox 严格校验拒绝:字段类型不符 / 非法时长格式(未知字段只是 warning,不拒绝) |
231
+ | 现象 | 原因与修法 |
232
+ | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
233
+ | notify `failed to start …: binary not found` | `bin` 不在 PATH(测试场景二进制缺失时整组跳过) |
234
+ | `… provides no tsserver.js. No other valid TypeScript installation was found` | workspace 的 `node_modules/typescript` 是 alias stub:复制随包脚本 `bin/typescript-options.mjs` 到 `~/.pi/agent/lsp/` 并在该 server 上配 `initializationOptionsCommand`,或直接写静态 `initializationOptions.tsserver.path` |
235
+ | notify `failed to start …: initializationOptions command …` | 脚本非零退出 / 空输出 / stdout 不是 JSON 对象(报错里带具体命令、退出码与 stderr) |
236
+ | 诊断一直为空且无任何报错 | 服务器没匹配到文件(`include`/扩展名)、在 broken 冷却中、或文件在调用 cwd 之外(LSP 只在工作目录内启用) |
237
+ | 读取配置直接抛错 | typebox 严格校验拒绝:字段类型不符 / 非法时长格式(未知字段只是 warning,不拒绝) |