pi-patch-keeper 1.0.3 → 1.0.5
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.en.md +31 -29
- package/README.md +29 -23
- package/index.ts +219 -128
- package/package.json +8 -4
package/README.en.md
CHANGED
|
@@ -1,43 +1,45 @@
|
|
|
1
1
|
# pi-patch-keeper — extension load-order guard (pi extension)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Prevents "force-typed" extensions from silently swallowing every section written after them.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Why it exists
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- Statically scans npm extension sources for unconditional `return { systemPrompt }` / `forceSystemPrompt`.
|
|
9
|
-
- Moves known force-type packages to the end of `packages` (settings backed up first).
|
|
10
|
-
- Speaks only when something is wrong; `/patch-keeper` reports, `/patch-keeper fix` repairs.
|
|
11
|
-
- This release contains no machine-specific patch healing (source patches, PiDeck asar, etc.).
|
|
7
|
+
`before_agent_start` runs extensions in order. An extension that returns `{ systemPrompt }` (or sets `forceSystemPrompt`) performs a **full replacement**, so every later extension's `sections` write is discarded — with no error. The symptom looks like "this extension is installed but has no effect".
|
|
12
8
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
| Item | Default | Note |
|
|
16
|
-
| --- | --- | --- |
|
|
17
|
-
| Force-type packages | `npm:pi-rtk-optimizer`, `npm:pi-cache-optimizer` | override via `forceTail` |
|
|
18
|
-
| Auto repair | on | backup `settings.json.bak-force-order-*` |
|
|
19
|
-
| Startup notice | only on issue | silent when clean |
|
|
20
|
-
| Scope | `npm:` packages | local path packages are skipped |
|
|
9
|
+
## What it does
|
|
21
10
|
|
|
22
|
-
|
|
11
|
+
- On startup, checks the order of `packages` in `~/.pi/agent/settings.json`: known force-typed extensions must sit at the end.
|
|
12
|
+
- Statically scans installed npm extension entry files and separates:
|
|
13
|
+
- **unconditional force** (`return { systemPrompt }` / `forceSystemPrompt`) → must be last;
|
|
14
|
+
- **sections-first with legacy fallback** → report only, no move suggested.
|
|
15
|
+
- Records a fingerprint of key settings fields and warns when it changes (earlier conclusions may be stale).
|
|
16
|
+
- `codemode` single-point check: disabling `builtin:codemode` requires a replacement, and vice versa.
|
|
17
|
+
- Silent when everything is fine.
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
pi install npm:pi-patch-keeper@1.0.0
|
|
26
|
-
pi install ./pi-patch-keeper
|
|
27
|
-
```
|
|
19
|
+
## Commands
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
| Command | Effect |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `/patch-keeper` | Show order, suspected force packages, fallback packages, codemode status |
|
|
24
|
+
| `/patch-keeper fix` | Move known force-typed packages to the end (backup written first) |
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
{ "forceTail": ["npm:pi-rtk-optimizer", "npm:pi-cache-optimizer"] }
|
|
33
|
-
```
|
|
26
|
+
## Defaults
|
|
34
27
|
|
|
35
|
-
|
|
28
|
+
| Key | Default |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `forceTail` | `npm:pi-rtk-optimizer`, `npm:pi-cache-optimizer` |
|
|
31
|
+
| Auto-fix | on (order only, backup first) |
|
|
32
|
+
| Notify | only on problems |
|
|
33
|
+
| Scope | `npm:` packages |
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
- Reports when any package still follows a force-type package.
|
|
35
|
+
Optional config: `~/.pi/patch-keeper-state.json`
|
|
39
36
|
|
|
40
37
|
## Limits
|
|
41
38
|
|
|
42
|
-
-
|
|
43
|
-
-
|
|
39
|
+
- Detects static forms only; dynamic `systemPrompt` construction is not detected.
|
|
40
|
+
- Local-path packages are skipped.
|
|
41
|
+
- Unknown force-typed extensions must be added to `forceTail` manually.
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT.
|
package/README.md
CHANGED
|
@@ -4,44 +4,50 @@
|
|
|
4
4
|
|
|
5
5
|
## 来源与借鉴
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
原创,无上游实现。动机来自一个反复出现的症状:某个扩展明明装了、section 也写了,却没出现在 system prompt 里——根因是它前面有扩展 `return { systemPrompt }`,覆盖式替换把后续写入全丢了,而且不报错。
|
|
8
8
|
|
|
9
9
|
## 功能
|
|
10
10
|
|
|
11
|
-
- 启动时检查 `~/.pi/agent/settings.json` 的 `packages`
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
11
|
+
- 启动时检查 `~/.pi/agent/settings.json` 的 `packages` 顺序:已知 force 型必须在末尾。
|
|
12
|
+
- 静态扫描已装 npm 扩展的入口源码,区分两类:
|
|
13
|
+
- **无条件 force**(`return { systemPrompt }` 或 `forceSystemPrompt`)→ 必须排在末尾;
|
|
14
|
+
- **section 优先 + 旧宿主兜底**(先写 sections,旧宿主才 force)→ 只提示,不建议移动。
|
|
15
|
+
- 记录 settings 关键字段指纹(packages / defaultTools / enabledModels / extensions / codemode),变化时提示「此前的结论可能过期」。
|
|
16
|
+
- codemode 单点检查:禁用 `builtin:codemode` 时必须有替代实现,反之亦然(双实现或单点断裂都会报)。
|
|
17
|
+
- 只在发现问题时提示;命令随时查看状态。
|
|
18
|
+
|
|
19
|
+
## 命令
|
|
20
|
+
|
|
21
|
+
| 命令 | 作用 |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `/patch-keeper` | 查看顺序、疑似 force 包、兜底包、codemode 单点状态 |
|
|
24
|
+
| `/patch-keeper fix` | 把已知 force 型移到 `packages` 末尾(写前备份 `settings.json.bak-force-order-*`) |
|
|
16
25
|
|
|
17
26
|
## 默认配置
|
|
18
27
|
|
|
19
28
|
| 项 | 默认 | 说明 |
|
|
20
29
|
| --- | --- | --- |
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
30
|
+
| `forceTail` | `npm:pi-rtk-optimizer`, `npm:pi-cache-optimizer` | 已知无条件 force 型;触发概率升序 |
|
|
31
|
+
| 自动修复 | 开 | 仅调整顺序,写前备份 |
|
|
32
|
+
| 提示时机 | 仅异常 | 无问题零输出 |
|
|
24
33
|
| 检测范围 | `npm:` 包 | 本地路径包跳过 |
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
pi install npm:pi-patch-keeper
|
|
30
|
-
pi install ./pi-patch-keeper
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## 配置示例
|
|
35
|
+
配置文件(可选):`~/.pi/patch-keeper-state.json`
|
|
34
36
|
|
|
35
37
|
```json
|
|
36
38
|
{ "forceTail": ["npm:pi-rtk-optimizer", "npm:pi-cache-optimizer"] }
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
##
|
|
41
|
+
## 机制(为什么放在启动时)
|
|
42
|
+
|
|
43
|
+
pi 的 `before_agent_start` 依次调用各扩展;返回 `{ systemPrompt }` 等于 `forceSystemPrompt`——**覆盖式替换**,此后所有扩展对 sections 的修改都被丢弃。因此「谁在末尾」是有行为后果的,不是风格问题。本扩展只在 `session_start` 检查与提示,自身**绝不** return systemPrompt。
|
|
44
|
+
|
|
45
|
+
## 局限
|
|
40
46
|
|
|
41
|
-
-
|
|
42
|
-
-
|
|
47
|
+
- 只识别静态形态(`return { systemPrompt }` / `forceSystemPrompt`);动态拼接无法识别。
|
|
48
|
+
- 只处理 `npm:` 前缀包;本地路径包(`path:` / 相对路径)不扫描。
|
|
49
|
+
- 只能保证「已知 force 型在末尾」;未知的 force 型需要人工确认后加入 `forceTail`。
|
|
43
50
|
|
|
44
|
-
##
|
|
51
|
+
## 许可证
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
- 不修包内代码,只调整加载顺序。
|
|
53
|
+
MIT。
|
package/index.ts
CHANGED
|
@@ -1,161 +1,252 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
1
|
+
/**
|
|
2
|
+
* patch-keeper —— 扩展加载顺序守护(pi 扩展)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* one of them.
|
|
4
|
+
* 问题:pi 的 system prompt 由各扩展的 section 写入拼装。若某扩展在
|
|
5
|
+
* `before_agent_start` 里 `return { systemPrompt }`(或设置 forceSystemPrompt),
|
|
6
|
+
* 它会**静默丢弃其后所有扩展的 section 写入**——没有任何报错,表现为
|
|
7
|
+
* 「某个扩展配了却没效果」。
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* 本扩展做三件事(全部只读,唯一的写是带备份修 settings):
|
|
10
|
+
* 1. 检查 `settings.json` 的 packages 顺序:已知 force 型必须排在末尾。
|
|
11
|
+
* 2. 静态扫描已装 npm 扩展入口源码,区分「无条件 force」与「section 优先 + 旧宿主兜底」。
|
|
12
|
+
* 3. 记录 settings 关键字段指纹;变化时提示「环境已变,旧结论可能过期」。
|
|
13
|
+
*
|
|
14
|
+
* 纪律:绝不 return systemPrompt(否则自己就是问题);任何失败只提示、不抛错。
|
|
12
15
|
*/
|
|
16
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
13
17
|
import { existsSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
|
|
14
18
|
import * as os from "node:os";
|
|
15
19
|
import * as path from "node:path";
|
|
16
20
|
|
|
17
21
|
const HOME = os.homedir();
|
|
18
22
|
const SETTINGS = path.join(HOME, ".pi", "agent", "settings.json");
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
23
|
+
const STATE = path.join(HOME, ".pi", "patch-keeper-state.json");
|
|
24
|
+
const NPM_ROOT = path.join(HOME, ".pi", "agent", "npm", "node_modules");
|
|
25
|
+
|
|
26
|
+
/** 已知无条件 force 型包。触发概率升序:越靠后越应排在 packages 末尾。 */
|
|
27
|
+
const DEFAULT_FORCE_TAIL = ["npm:pi-rtk-optimizer", "npm:pi-cache-optimizer"];
|
|
28
|
+
|
|
29
|
+
/** 静态扫描:入口源码里出现这两个形态即判为 force。 */
|
|
30
|
+
const FORCE_RE = /return\s*\{\s*systemPrompt|forceSystemPrompt/;
|
|
31
|
+
const SECTION_RE = /sections\s*(\[|\.)/;
|
|
32
|
+
|
|
33
|
+
export type Result = { ok: boolean; detail: string };
|
|
22
34
|
|
|
23
35
|
function safeRead(p: string): string | null {
|
|
24
|
-
|
|
36
|
+
try {
|
|
37
|
+
return readFileSync(p, "utf8");
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
25
41
|
}
|
|
26
42
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
function readSettings(): Record<string, unknown> | null {
|
|
44
|
+
const txt = safeRead(SETTINGS);
|
|
45
|
+
if (txt === null) return null;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(txt) as Record<string, unknown>;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
31
51
|
}
|
|
32
52
|
|
|
53
|
+
/** 用户可在 ~/.pi/patch-keeper.json 用 forceTail 覆盖已知 force 型列表。 */
|
|
33
54
|
function forceTail(): string[] {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
const txt = safeRead(STATE);
|
|
56
|
+
if (!txt) return DEFAULT_FORCE_TAIL;
|
|
57
|
+
try {
|
|
58
|
+
const cfg = JSON.parse(txt) as { forceTail?: unknown };
|
|
59
|
+
return Array.isArray(cfg.forceTail) && cfg.forceTail.every((x) => typeof x === "string")
|
|
60
|
+
? (cfg.forceTail as string[])
|
|
61
|
+
: DEFAULT_FORCE_TAIL;
|
|
62
|
+
} catch {
|
|
63
|
+
return DEFAULT_FORCE_TAIL;
|
|
64
|
+
}
|
|
38
65
|
}
|
|
39
66
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
67
|
+
/** force 型必须整体位于末尾,且相对顺序按 tail 数组。 */
|
|
68
|
+
export function forceOrderIssues(packages: string[], tail: string[] = DEFAULT_FORCE_TAIL): string[] {
|
|
69
|
+
const out: string[] = [];
|
|
70
|
+
const idx = (n: string) => packages.indexOf(n);
|
|
71
|
+
const present = tail.filter((n) => idx(n) >= 0);
|
|
72
|
+
if (!present.length) return out;
|
|
73
|
+
const minForce = Math.min(...present.map(idx));
|
|
74
|
+
const later = packages.slice(minForce + 1).filter((n) => !tail.includes(n));
|
|
75
|
+
if (later.length) {
|
|
76
|
+
out.push(
|
|
77
|
+
`force 型之后仍有 ${later.length} 个包(${later.slice(0, 2).join(" / ")})—— 它们的 section 写入会被静默丢弃;把 ${tail.join(" / ")} 移到 packages 末尾`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const ordered = present.every((n, i) => i === 0 || idx(present[i - 1]) < idx(n));
|
|
81
|
+
if (!ordered) out.push(`force 型相对顺序应与 ${tail.join(" → ")} 一致(最可能触发者最后)`);
|
|
82
|
+
return out;
|
|
44
83
|
}
|
|
45
84
|
|
|
46
|
-
/**
|
|
47
|
-
export function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const later = packages.slice(minForce + 1).filter((n) => !tail.includes(n));
|
|
54
|
-
if (later.length) out.push(
|
|
55
|
-
"force-type package is followed by " + later.length + " package(s) (" + later.slice(0, 2).join(" / ") +
|
|
56
|
-
") — their sections are dropped; move " + tail.join(" / ") + " to the end");
|
|
57
|
-
if (tail.every((n) => packages.includes(n)) && packages.indexOf(tail[0]) > packages.indexOf(tail[1]))
|
|
58
|
-
out.push("force order should be " + tail.join(" -> ") + " (the most likely trigger goes last)");
|
|
59
|
-
return out;
|
|
85
|
+
/** 把 force 型移到末尾(其余相对顺序不变);已正确返回 null。 */
|
|
86
|
+
export function normalizeForceOrder(packages: string[], tail: string[] = DEFAULT_FORCE_TAIL): string[] | null {
|
|
87
|
+
const present = tail.filter((n) => packages.includes(n));
|
|
88
|
+
if (!present.length) return null;
|
|
89
|
+
const rest = packages.filter((n) => !tail.includes(n));
|
|
90
|
+
const next = [...rest, ...present];
|
|
91
|
+
return next.join("\n") === packages.join("\n") ? null : next;
|
|
60
92
|
}
|
|
61
93
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
/** 读扩展入口源码并分类:无条件 force / section 优先(旧宿主兜底)/ 无。 */
|
|
95
|
+
function classifyPackages(packages: string[], tail: string[]): { force: string[]; fallback: string[] } {
|
|
96
|
+
const force: string[] = [];
|
|
97
|
+
const fallback: string[] = [];
|
|
98
|
+
for (const name of packages) {
|
|
99
|
+
if (!name.startsWith("npm:") || tail.includes(name)) continue;
|
|
100
|
+
const dir = path.join(NPM_ROOT, name.slice(4));
|
|
101
|
+
if (!existsSync(dir)) continue;
|
|
102
|
+
let entries: string[] = [];
|
|
103
|
+
try {
|
|
104
|
+
const pj = JSON.parse(safeRead(path.join(dir, "package.json")) ?? "{}") as { pi?: { extensions?: unknown } };
|
|
105
|
+
const ex = pj.pi?.extensions;
|
|
106
|
+
entries = Array.isArray(ex) ? ex.filter((x): x is string => typeof x === "string") : [];
|
|
107
|
+
} catch {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
for (const rel of entries.slice(0, 3)) {
|
|
111
|
+
const src = safeRead(path.join(dir, rel));
|
|
112
|
+
if (src === null) continue;
|
|
113
|
+
const i = src.indexOf("before_agent_start");
|
|
114
|
+
if (i < 0) continue;
|
|
115
|
+
const body = src.slice(i, i + 4000);
|
|
116
|
+
const fi = body.search(FORCE_RE);
|
|
117
|
+
if (fi < 0) continue;
|
|
118
|
+
const si = body.search(SECTION_RE);
|
|
119
|
+
if (si >= 0 && si < fi) fallback.push(name);
|
|
120
|
+
else force.push(name);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { force, fallback };
|
|
69
125
|
}
|
|
70
126
|
|
|
71
|
-
/**
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
if (!s) return null;
|
|
75
|
-
const packages = Array.isArray(s.packages) ? (s.packages as string[]) : [];
|
|
76
|
-
const next = normalizeForceOrder(packages);
|
|
77
|
-
if (!next) return null;
|
|
78
|
-
try {
|
|
79
|
-
copyFileSync(SETTINGS, SETTINGS + ".bak-force-order-" + Date.now());
|
|
80
|
-
s.packages = next;
|
|
81
|
-
writeFileSync(SETTINGS, JSON.stringify(s, null, 2), "utf8");
|
|
82
|
-
} catch (e) { return "force order repair failed: " + (e as Error).message; }
|
|
83
|
-
return "force order repaired: " + forceTail().filter((n) => next.includes(n)).join(" / ") + " moved to the end";
|
|
127
|
+
/** 疑似无条件 force(会吞掉其后 section)。 */
|
|
128
|
+
export function scanForcePackages(packages: string[], tail: string[] = DEFAULT_FORCE_TAIL): string[] {
|
|
129
|
+
return classifyPackages(packages, tail).force;
|
|
84
130
|
}
|
|
85
131
|
|
|
86
|
-
|
|
87
|
-
|
|
132
|
+
/** 兼容兜底型(section 优先,仅旧宿主 force)。 */
|
|
133
|
+
export function scanForceFallbacks(packages: string[], tail: string[] = DEFAULT_FORCE_TAIL): string[] {
|
|
134
|
+
return classifyPackages(packages, tail).fallback;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** codemode 单点检查:禁用 builtin:codemode 时必须有替代实现,反之亦然。 */
|
|
138
|
+
export function consistencyCheck(extensions: string[], packages: string[]): string[] {
|
|
139
|
+
const disabled = extensions.includes("-builtin:codemode");
|
|
140
|
+
const guard = packages.includes("npm:pi-codemode-guard");
|
|
141
|
+
if (disabled && !guard) return ["codemode 单点断裂:已禁 builtin:codemode 但 packages 无替代实现 —— codemode 工具将不可用"];
|
|
142
|
+
if (!disabled && guard) return ["codemode 单点异常:替代实现已在,但 builtin:codemode 未禁用 —— 双实现风险"];
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** settings 关键字段指纹(顺序敏感)。 */
|
|
147
|
+
function fingerprint(s: Record<string, unknown>): Record<string, string[]> {
|
|
148
|
+
return {
|
|
149
|
+
packages: (s.packages as string[]) ?? [],
|
|
150
|
+
defaultTools: (s.defaultTools as string[]) ?? [],
|
|
151
|
+
enabledModels: (s.enabledModels as string[]) ?? [],
|
|
152
|
+
extensions: (s.extensions as string[]) ?? [],
|
|
153
|
+
codemode: [(s.codemode as { mode?: string })?.mode ?? "?"],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
88
156
|
|
|
89
|
-
/**
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return { force, fallback };
|
|
157
|
+
/** 与上次指纹对比并写回;返回变化摘要(无变化为空)。 */
|
|
158
|
+
function fingerprintDrift(cur: Record<string, string[]>): string[] {
|
|
159
|
+
let prev: Record<string, string[]> | null = null;
|
|
160
|
+
const raw = safeRead(STATE);
|
|
161
|
+
if (raw) {
|
|
162
|
+
try {
|
|
163
|
+
const j = JSON.parse(raw) as { fingerprint?: Record<string, string[]> };
|
|
164
|
+
prev = j.fingerprint ?? null;
|
|
165
|
+
} catch {
|
|
166
|
+
prev = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const cfg = raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
|
|
171
|
+
writeFileSync(STATE, JSON.stringify({ ...cfg, fingerprint: cur }, null, 1), "utf8");
|
|
172
|
+
} catch {
|
|
173
|
+
/* 静默 */
|
|
174
|
+
}
|
|
175
|
+
if (prev === null) return [];
|
|
176
|
+
const out: string[] = [];
|
|
177
|
+
for (const k of ["packages", "defaultTools", "enabledModels", "extensions", "codemode"] as const) {
|
|
178
|
+
const a = prev[k] ?? [];
|
|
179
|
+
const b = cur[k];
|
|
180
|
+
if (a.join("\n") !== b.join("\n")) out.push(k);
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
116
183
|
}
|
|
117
184
|
|
|
185
|
+
/** 修 settings 的 force 顺序(写前备份);无变化返回 null。 */
|
|
186
|
+
function fixForceOrder(): string | null {
|
|
187
|
+
const s = readSettings();
|
|
188
|
+
if (!s) return null;
|
|
189
|
+
const tail = forceTail();
|
|
190
|
+
const packages = (s.packages as string[] | undefined) ?? [];
|
|
191
|
+
const next = normalizeForceOrder(packages, tail);
|
|
192
|
+
if (!next) return null;
|
|
193
|
+
try {
|
|
194
|
+
copyFileSync(SETTINGS, `${SETTINGS}.bak-force-order-${Date.now()}`);
|
|
195
|
+
s.packages = next;
|
|
196
|
+
writeFileSync(SETTINGS, JSON.stringify(s, null, 2), "utf8");
|
|
197
|
+
} catch (e) {
|
|
198
|
+
return `force 顺序自动修复失败:${(e as Error).message}`;
|
|
199
|
+
}
|
|
200
|
+
return `force 顺序已自动修复:${tail.filter((n) => next.includes(n)).join(" / ")} 移到 packages 末尾`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 状态报告(命令与启动提示共用)。 */
|
|
118
204
|
function report(): string[] {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
205
|
+
const s = readSettings();
|
|
206
|
+
if (!s) return ["读不到 settings.json"];
|
|
207
|
+
const tail = forceTail();
|
|
208
|
+
const packages = (s.packages as string[]) ?? [];
|
|
209
|
+
const lines: string[] = [];
|
|
210
|
+
const fo = forceOrderIssues(packages, tail);
|
|
211
|
+
lines.push(`force顺序 : ${fo.length ? fo[0] : "OK"}`);
|
|
212
|
+
const fe = scanForcePackages(packages, tail);
|
|
213
|
+
lines.push(`疑似force: ${fe.length ? fe.join(" / ") : "无(静态扫描)"}`);
|
|
214
|
+
const fb = scanForceFallbacks(packages, tail);
|
|
215
|
+
lines.push(`兼容兜底 : ${fb.length ? fb.join(" / ") : "无"}`);
|
|
216
|
+
const cm = consistencyCheck((s.extensions as string[]) ?? [], packages);
|
|
217
|
+
lines.push(`codemode : ${cm.length ? cm[0] : "单点一致"}`);
|
|
218
|
+
return lines;
|
|
130
219
|
}
|
|
131
220
|
|
|
132
|
-
export default function (pi: ExtensionAPI) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
221
|
+
export default function patchKeeper(pi: ExtensionAPI): void {
|
|
222
|
+
pi.on("session_start", async (_event, ctx: ExtensionContext) => {
|
|
223
|
+
try {
|
|
224
|
+
const s = readSettings();
|
|
225
|
+
if (!s) return;
|
|
226
|
+
const msgs: string[] = [];
|
|
227
|
+
const fixed = fixForceOrder();
|
|
228
|
+
if (fixed) msgs.push(fixed);
|
|
229
|
+
else for (const x of forceOrderIssues((s.packages as string[]) ?? [], forceTail())) msgs.push(x);
|
|
230
|
+
const drift = fingerprintDrift(fingerprint(s));
|
|
231
|
+
if (drift.length) msgs.push(`环境指纹已变(${drift.join(" / ")})—— 此前关于工具/模型的结论可能过期`);
|
|
232
|
+
const cm = consistencyCheck((s.extensions as string[]) ?? [], (s.packages as string[]) ?? []);
|
|
233
|
+
for (const x of cm) msgs.push(x);
|
|
234
|
+
for (const m of msgs) ctx.ui.notify("patch-keeper: " + m, "warning");
|
|
235
|
+
} catch {
|
|
236
|
+
/* 静默:检测不该影响会话 */
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
pi.registerCommand("patch-keeper", {
|
|
241
|
+
description: "查看 / 修复扩展加载顺序(force 型必须在 packages 末尾)",
|
|
242
|
+
handler: async (args: string, ctx: ExtensionContext) => {
|
|
243
|
+
const arg = (args || "").trim().toLowerCase();
|
|
244
|
+
if (arg === "fix") {
|
|
245
|
+
const r = fixForceOrder();
|
|
246
|
+
ctx.ui.notify(r ? `patch-keeper: ${r}` : "patch-keeper: force 顺序已正确,无需修复", r ? "info" : "info");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
ctx.ui.notify(`扩展加载顺序\n${report().join("\n")}`, "info");
|
|
250
|
+
},
|
|
251
|
+
});
|
|
161
252
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-patch-keeper",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.5",
|
|
4
|
+
"description": "Detect and fix pi extension load-order hazards: force-typed extensions silently swallow later system-prompt sections.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
8
|
+
"load-order",
|
|
8
9
|
"pi",
|
|
10
|
+
"pi-coding-agent",
|
|
9
11
|
"pi-extension",
|
|
10
|
-
"pi-
|
|
12
|
+
"pi-package",
|
|
13
|
+
"system-prompt"
|
|
11
14
|
],
|
|
12
15
|
"pi": {
|
|
13
16
|
"extensions": [
|
|
@@ -22,5 +25,6 @@
|
|
|
22
25
|
],
|
|
23
26
|
"peerDependencies": {
|
|
24
27
|
"@earendil-works/pi-coding-agent": "*"
|
|
25
|
-
}
|
|
28
|
+
},
|
|
29
|
+
"author": "15066212pp"
|
|
26
30
|
}
|