@dyyz1993/pi-coding-agent 0.74.24 → 0.74.25
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/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +3 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/extensions/agent-permissions/index.ts +235 -0
- package/dist/extensions/ask-tools/index.ts +115 -0
- package/dist/extensions/auto-memory/contract.d.ts +51 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -0
- package/dist/extensions/auto-memory/contract.js +2 -0
- package/dist/extensions/auto-memory/contract.js.map +1 -0
- package/dist/extensions/auto-memory/contract.ts +56 -0
- package/dist/extensions/auto-memory/index.ts +969 -0
- package/dist/extensions/auto-memory/prompts.ts +202 -0
- package/dist/extensions/auto-memory/skip-rules.ts +297 -0
- package/dist/extensions/auto-memory/utils.ts +208 -0
- package/dist/extensions/auto-session-title/index.ts +83 -0
- package/dist/extensions/bash-ext/contract.d.ts +79 -0
- package/dist/extensions/bash-ext/contract.d.ts.map +1 -0
- package/dist/extensions/bash-ext/contract.js +2 -0
- package/dist/extensions/bash-ext/contract.js.map +1 -0
- package/dist/extensions/bash-ext/contract.ts +69 -0
- package/dist/extensions/bash-ext/index.ts +858 -0
- package/dist/extensions/claude-hooks-compat/config-loader.ts +49 -0
- package/dist/extensions/claude-hooks-compat/handler-runner.ts +377 -0
- package/dist/extensions/claude-hooks-compat/if-parser.ts +53 -0
- package/dist/extensions/claude-hooks-compat/index.ts +178 -0
- package/dist/extensions/claude-hooks-compat/matcher.ts +17 -0
- package/dist/extensions/claude-hooks-compat/stdin-builder.ts +27 -0
- package/dist/extensions/claude-hooks-compat/types.ts +77 -0
- package/dist/extensions/compaction-manager/config.ts +47 -0
- package/dist/extensions/compaction-manager/context-fold.ts +63 -0
- package/dist/extensions/compaction-manager/index.ts +151 -0
- package/dist/extensions/compaction-manager/microcompact.ts +49 -0
- package/dist/extensions/compaction-manager/reactive.ts +9 -0
- package/dist/extensions/compaction-manager/session-memory.ts +48 -0
- package/dist/extensions/coordinator/INTEGRATION.md +376 -0
- package/dist/extensions/coordinator/handler.test.ts +277 -0
- package/dist/extensions/coordinator/handler.ts +189 -0
- package/dist/extensions/coordinator/index.ts +261 -0
- package/dist/extensions/coordinator/types.d.ts +100 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -0
- package/dist/extensions/coordinator/types.js +2 -0
- package/dist/extensions/coordinator/types.js.map +1 -0
- package/dist/extensions/coordinator/types.ts +72 -0
- package/dist/extensions/file-snapshot/index.ts +131 -0
- package/dist/extensions/file-time-guard/README.md +133 -0
- package/dist/extensions/file-time-guard/config.ts +13 -0
- package/dist/extensions/file-time-guard/index.ts +171 -0
- package/dist/extensions/hooks-engine/index.ts +117 -0
- package/dist/extensions/lsp/lsp/client/file-tracker.ts +70 -0
- package/dist/extensions/lsp/lsp/client/registry.ts +305 -0
- package/dist/extensions/lsp/lsp/client/runtime.ts +832 -0
- package/dist/extensions/lsp/lsp/config/resolver.ts +573 -0
- package/dist/extensions/lsp/lsp/contract.d.ts +101 -0
- package/dist/extensions/lsp/lsp/contract.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/contract.js +2 -0
- package/dist/extensions/lsp/lsp/contract.js.map +1 -0
- package/dist/extensions/lsp/lsp/contract.ts +103 -0
- package/dist/extensions/lsp/lsp/hooks/agent-end.ts +169 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts +10 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js +30 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.ts +41 -0
- package/dist/extensions/lsp/lsp/hooks/writethrough.ts +342 -0
- package/dist/extensions/lsp/lsp/index.ts +307 -0
- package/dist/extensions/lsp/lsp/lsp.test.ts +684 -0
- package/dist/extensions/lsp/lsp/monitoring/server-metrics.ts +176 -0
- package/dist/extensions/lsp/lsp/tools/lsp-tool.ts +402 -0
- package/dist/extensions/lsp/lsp/utils/dependency-resolver.ts +147 -0
- package/dist/extensions/lsp/lsp/utils/diagnostics-wait.ts +41 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts +20 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js +64 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.ts +76 -0
- package/dist/extensions/message-bridge/GUIDE.md +210 -0
- package/dist/extensions/message-bridge/index.ts +222 -0
- package/dist/extensions/output-guard/index.ts +384 -0
- package/dist/extensions/preview/index.ts +278 -0
- package/dist/extensions/rules-engine/MATCH_HISTORY_RECONCILIATION.md +111 -0
- package/dist/extensions/rules-engine/RULES-ENGINE-GUIDE.md +470 -0
- package/dist/extensions/rules-engine/cache.js +232 -0
- package/dist/extensions/rules-engine/cache.ts +38 -0
- package/dist/extensions/rules-engine/config.js +63 -0
- package/dist/extensions/rules-engine/config.ts +70 -0
- package/dist/extensions/rules-engine/index.js +1530 -0
- package/dist/extensions/rules-engine/index.ts +552 -0
- package/dist/extensions/rules-engine/injector.js +68 -0
- package/dist/extensions/rules-engine/injector.ts +74 -0
- package/dist/extensions/rules-engine/loader.js +179 -0
- package/dist/extensions/rules-engine/loader.ts +205 -0
- package/dist/extensions/rules-engine/matcher.js +534 -0
- package/dist/extensions/rules-engine/matcher.ts +52 -0
- package/dist/extensions/rules-engine/types.d.ts +156 -0
- package/dist/extensions/rules-engine/types.d.ts.map +1 -0
- package/dist/extensions/rules-engine/types.js +2 -0
- package/dist/extensions/rules-engine/types.js.map +1 -0
- package/dist/extensions/rules-engine/types.ts +169 -0
- package/dist/extensions/session-supervisor/checker.ts +116 -0
- package/dist/extensions/session-supervisor/config.ts +45 -0
- package/dist/extensions/session-supervisor/index.ts +726 -0
- package/dist/extensions/session-supervisor/prompts.ts +132 -0
- package/dist/extensions/session-supervisor/scheduler.ts +69 -0
- package/dist/extensions/session-supervisor/types.ts +215 -0
- package/dist/extensions/subagent/README.md +172 -0
- package/dist/extensions/subagent/agents/explorer.md +25 -0
- package/dist/extensions/subagent/agents/guide.md +27 -0
- package/dist/extensions/subagent/agents/planner.md +37 -0
- package/dist/extensions/subagent/agents/reviewer.md +35 -0
- package/dist/extensions/subagent/agents/scout.md +50 -0
- package/dist/extensions/subagent/agents/verification.md +35 -0
- package/dist/extensions/subagent/agents/worker.md +24 -0
- package/dist/extensions/subagent/agents.ts +25 -0
- package/dist/extensions/subagent/index.ts +987 -0
- package/dist/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/dist/extensions/subagent/prompts/implement.md +10 -0
- package/dist/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/dist/extensions/subagent-ext/contract.d.ts +2 -0
- package/dist/extensions/subagent-ext/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-ext/contract.js +2 -0
- package/dist/extensions/subagent-ext/contract.js.map +1 -0
- package/dist/extensions/subagent-ext/contract.ts +1 -0
- package/dist/extensions/subagent-ext/index.ts +347 -0
- package/dist/extensions/subagent-shared/contract.d.ts +25 -0
- package/dist/extensions/subagent-shared/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-shared/contract.js +2 -0
- package/dist/extensions/subagent-shared/contract.js.map +1 -0
- package/dist/extensions/subagent-shared/contract.ts +28 -0
- package/dist/extensions/subagent-shared/index.ts +4 -0
- package/dist/extensions/subagent-shared/render.ts +166 -0
- package/dist/extensions/subagent-shared/types.ts +35 -0
- package/dist/extensions/subagent-shared/utils.ts +112 -0
- package/dist/extensions/subagent-v2/contract.d.ts +2 -0
- package/dist/extensions/subagent-v2/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-v2/contract.js +2 -0
- package/dist/extensions/subagent-v2/contract.js.map +1 -0
- package/dist/extensions/subagent-v2/contract.ts +1 -0
- package/dist/extensions/subagent-v2/index.ts +599 -0
- package/dist/extensions/todo-ext/contract.d.ts +27 -0
- package/dist/extensions/todo-ext/contract.d.ts.map +1 -0
- package/dist/extensions/todo-ext/contract.js +2 -0
- package/dist/extensions/todo-ext/contract.js.map +1 -0
- package/dist/extensions/todo-ext/contract.ts +30 -0
- package/dist/extensions/todo-ext/index.ts +419 -0
- package/package.json +6 -5
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, SessionTreeEvent, TurnEndEvent } from "@dyyz1993/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export default function fileSnapshot(pi: ExtensionAPI) {
|
|
4
|
+
const channel = pi.registerChannel("file-snapshot");
|
|
5
|
+
|
|
6
|
+
channel.onReceive(async (msg) => {
|
|
7
|
+
const ctx = msg.context as ExtensionContext;
|
|
8
|
+
const mgr = ctx.fileSnapshotManager;
|
|
9
|
+
if (!mgr) {
|
|
10
|
+
return { error: "fileSnapshotManager not available" };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
switch (msg.method) {
|
|
14
|
+
case "snapshot.list": {
|
|
15
|
+
const snapshots = mgr.getModifiedFiles({});
|
|
16
|
+
return snapshots;
|
|
17
|
+
}
|
|
18
|
+
case "snapshot.rollback": {
|
|
19
|
+
const { snapshotId, files } = msg.params as {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
snapshotId: string;
|
|
22
|
+
files?: string[];
|
|
23
|
+
};
|
|
24
|
+
const result = await mgr.restoreFiles(ctx.cwd, {
|
|
25
|
+
targetEntryId: snapshotId,
|
|
26
|
+
files,
|
|
27
|
+
entries: ctx.sessionManager.getEntries() as import("@dyyz1993/pi-coding-agent").SessionEntry[],
|
|
28
|
+
appendEntry: (type: string, data: unknown) => {
|
|
29
|
+
return pi.appendEntry(type, data) ?? undefined;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
restoredFiles: [...result.restored, ...result.deleted],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
case "snapshot.unrevert": {
|
|
38
|
+
const { snapshotId } = msg.params as {
|
|
39
|
+
sessionId: string;
|
|
40
|
+
snapshotId: string;
|
|
41
|
+
};
|
|
42
|
+
const entries = ctx.sessionManager.getEntries() as import("@dyyz1993/pi-coding-agent").SessionEntry[];
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (entry.type !== "custom") continue;
|
|
45
|
+
const custom = entry as { customType: string; data: { rolledBackToLeaf: string; preRollbackTreeHash: string | null } };
|
|
46
|
+
if (custom.customType === "unrevert-point" && custom.data.rolledBackToLeaf === snapshotId) {
|
|
47
|
+
const restoreResult = await mgr.restoreFiles(ctx.cwd, {
|
|
48
|
+
snapshotHash: custom.data.preRollbackTreeHash ?? undefined,
|
|
49
|
+
entries,
|
|
50
|
+
appendEntry: (type: string, data: unknown) => {
|
|
51
|
+
return pi.appendEntry(type, data) ?? undefined;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
restoredFiles: [...restoreResult.restored, ...restoreResult.deleted],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { ok: false, error: "Unrevert point not found" };
|
|
61
|
+
}
|
|
62
|
+
case "snapshot.get": {
|
|
63
|
+
const { snapshotId } = msg.params as { sessionId: string; snapshotId: string };
|
|
64
|
+
const data = mgr.getSnapshotAtEntry(snapshotId);
|
|
65
|
+
if (!data) return null;
|
|
66
|
+
const diff = data.diff ?? { added: [], modified: [], deleted: [] };
|
|
67
|
+
return {
|
|
68
|
+
id: snapshotId,
|
|
69
|
+
stepIndex: data.turnIndex,
|
|
70
|
+
treeHash: data.snapshotTreeHash,
|
|
71
|
+
diff,
|
|
72
|
+
files: Object.fromEntries([
|
|
73
|
+
...diff.added.map((f) => [f, "added"]),
|
|
74
|
+
...diff.modified.map((f) => [f, "modified"]),
|
|
75
|
+
...diff.deleted.map((f) => [f, "deleted"]),
|
|
76
|
+
]),
|
|
77
|
+
rolledBack: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
case "snapshot.restoreByHash": {
|
|
81
|
+
const { snapshotTreeHash, files } = msg.params as {
|
|
82
|
+
snapshotTreeHash: string;
|
|
83
|
+
files?: string[];
|
|
84
|
+
};
|
|
85
|
+
const result = await mgr.restoreFiles(ctx.cwd, {
|
|
86
|
+
snapshotHash: snapshotTreeHash,
|
|
87
|
+
files,
|
|
88
|
+
entries: ctx.sessionManager.getEntries() as import("@dyyz1993/pi-coding-agent").SessionEntry[],
|
|
89
|
+
appendEntry: (type: string, data: unknown) => {
|
|
90
|
+
return pi.appendEntry(type, data) ?? undefined;
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
restored: [...result.restored, ...result.deleted],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
return { error: `Unknown method: ${msg.method}` };
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
|
|
103
|
+
const mgr = ctx.fileSnapshotManager;
|
|
104
|
+
if (!mgr) return;
|
|
105
|
+
await mgr.initialize(ctx.cwd);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
pi.on("turn_end", async (event: TurnEndEvent, ctx: ExtensionContext) => {
|
|
109
|
+
const mgr = ctx.fileSnapshotManager;
|
|
110
|
+
if (!mgr) return;
|
|
111
|
+
mgr.onTurnEnd(ctx.cwd, event.turnIndex, (type, data) => {
|
|
112
|
+
return pi.appendEntry(type, data, { display: false }) ?? undefined;
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
pi.on("session_tree", async (event: SessionTreeEvent, ctx: ExtensionContext) => {
|
|
117
|
+
if (event.skipFiles) return;
|
|
118
|
+
const mgr = ctx.fileSnapshotManager;
|
|
119
|
+
if (!mgr) return;
|
|
120
|
+
|
|
121
|
+
await mgr.restoreFiles(ctx.cwd, {
|
|
122
|
+
targetEntryId: event.newLeafId ?? undefined,
|
|
123
|
+
preview: event.preview,
|
|
124
|
+
currentLeafId: event.oldLeafId,
|
|
125
|
+
entries: ctx.sessionManager.getEntries() as import("@dyyz1993/pi-coding-agent").SessionEntry[],
|
|
126
|
+
appendEntry: (type: string, data: unknown) => {
|
|
127
|
+
return pi.appendEntry(type, data) ?? undefined;
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# File Time Guard
|
|
2
|
+
|
|
3
|
+
防止 AI 在文件被外部修改后覆盖用户更改的安全扩展。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- ✅ 记录文件读取时的元数据(mtime、ctime、size)
|
|
8
|
+
- ✅ 写入前检查文件是否被外部修改
|
|
9
|
+
- ✅ 三种检查模式:block(阻止)、warn(警告)、ignore(忽略)
|
|
10
|
+
- ✅ 支持忽略特定路径(如 `node_modules/`、`.git/`)
|
|
11
|
+
- ✅ 会话隔离,不同会话独立追踪
|
|
12
|
+
- ✅ 提供 `/file-time-status` 命令查看追踪状态
|
|
13
|
+
|
|
14
|
+
## 使用场景
|
|
15
|
+
|
|
16
|
+
### 场景 1:阻止覆盖外部修改
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# AI 读取文件
|
|
20
|
+
pi
|
|
21
|
+
|
|
22
|
+
# 在另一个终端打开编辑器修改文件
|
|
23
|
+
vim src/index.ts
|
|
24
|
+
|
|
25
|
+
# AI 尝试写入,被拦截并提示
|
|
26
|
+
ERROR: 文件已被外部修改: src/index.ts
|
|
27
|
+
请重新读取文件
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 场景 2:仅警告不阻止
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pi --file-time-check-mode warn
|
|
34
|
+
|
|
35
|
+
# AI 写入文件时显示警告,但允许操作
|
|
36
|
+
警告: 文件已被外部修改: src/index.ts
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 场景 3:完全禁用
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pi --disable-file-time-check
|
|
43
|
+
# 或
|
|
44
|
+
export OPENCODE_DISABLE_FILETIME_CHECK=true
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 配置选项
|
|
48
|
+
|
|
49
|
+
### 命令行 Flag
|
|
50
|
+
|
|
51
|
+
| Flag | 类型 | 默认值 | 说明 |
|
|
52
|
+
|------|------|---------|------|
|
|
53
|
+
| `--file-time-check-mode` | string | `block` | 检查模式:`block`/`warn`/`ignore` |
|
|
54
|
+
| `--disable-file-time-check` | boolean | `false` | 禁用文件时间戳检查 |
|
|
55
|
+
|
|
56
|
+
### 环境变量
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
export OPENCODE_DISABLE_FILETIME_CHECK=true
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 检查模式说明
|
|
63
|
+
|
|
64
|
+
- **block**: 阻止写入,显示错误提示
|
|
65
|
+
- **warn**: 允许写入,显示警告信息
|
|
66
|
+
- **ignore**: 不检查,直接写入
|
|
67
|
+
|
|
68
|
+
## 默认忽略路径
|
|
69
|
+
|
|
70
|
+
以下路径默认被忽略,不会被检查:
|
|
71
|
+
|
|
72
|
+
- `node_modules/**`
|
|
73
|
+
- `.git/**`
|
|
74
|
+
- `dist/**`
|
|
75
|
+
- `build/**`
|
|
76
|
+
|
|
77
|
+
## 命令
|
|
78
|
+
|
|
79
|
+
### `/file-time-status`
|
|
80
|
+
|
|
81
|
+
查看当前会话的文件追踪状态:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
文件时间戳检查: 启用
|
|
85
|
+
检查模式: block
|
|
86
|
+
已追踪文件: 5
|
|
87
|
+
|
|
88
|
+
已追踪文件:
|
|
89
|
+
/path/to/file1.ts
|
|
90
|
+
/path/to/file2.ts
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 工作原理
|
|
94
|
+
|
|
95
|
+
1. **读取文件**:记录文件元数据(mtime、ctime、size)和读取时间
|
|
96
|
+
2. **写入文件**:对比当前文件元数据与记录的值
|
|
97
|
+
3. **检测修改**:如果任意一项不匹配,认为文件被外部修改
|
|
98
|
+
4. **执行操作**:根据检查模式决定是阻止、警告还是忽略
|
|
99
|
+
|
|
100
|
+
### 检查规则
|
|
101
|
+
|
|
102
|
+
文件被认为"被修改"当且仅当满足以下任意一个条件:
|
|
103
|
+
|
|
104
|
+
- `mtime`(修改时间)改变
|
|
105
|
+
- `ctime`(元数据改变时间)改变
|
|
106
|
+
- `size`(文件大小)改变
|
|
107
|
+
|
|
108
|
+
## 技术实现
|
|
109
|
+
|
|
110
|
+
- 使用 `tool_call` 事件拦截 `read`/`write`/`edit` 工具
|
|
111
|
+
- 基于 `sessionID` 实现会话隔离
|
|
112
|
+
- 使用 `Map<SessionID, Map<FilePath, FileStamp>>` 存储追踪数据
|
|
113
|
+
- 依赖 `minimatch` 实现路径忽略模式匹配
|
|
114
|
+
- 利用现有的 `withFileMutationQueue()` 实现并发保护
|
|
115
|
+
|
|
116
|
+
## 兼容性
|
|
117
|
+
|
|
118
|
+
- ✅ 与其他扩展兼容(如 `file-lock-guard`、`protected-paths`)
|
|
119
|
+
- ✅ 不修改核心工具代码
|
|
120
|
+
- ✅ 可通过 flag 环境变量动态配置
|
|
121
|
+
|
|
122
|
+
## 注意事项
|
|
123
|
+
|
|
124
|
+
1. 仅追踪通过 `read` 工具读取的文件
|
|
125
|
+
2. 必须先读取文件才能写入(block 模式下)
|
|
126
|
+
3. 会话结束时自动清理追踪记录
|
|
127
|
+
4. 忽略路径不会被追踪或检查
|
|
128
|
+
|
|
129
|
+
## 相关扩展
|
|
130
|
+
|
|
131
|
+
- **file-lock-guard**: 文件并发写入保护
|
|
132
|
+
- **protected-paths**: 阻止写入敏感路径
|
|
133
|
+
- **file-snapshot**: 文件快照和恢复功能
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface FileTimeGuardConfig {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
checkMode: "block" | "warn" | "ignore";
|
|
4
|
+
ignorePatterns: string[];
|
|
5
|
+
sessionTimeout: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_CONFIG: FileTimeGuardConfig = {
|
|
9
|
+
enabled: true,
|
|
10
|
+
checkMode: "block",
|
|
11
|
+
ignorePatterns: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
12
|
+
sessionTimeout: 30 * 60 * 1000,
|
|
13
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@dyyz1993/pi-coding-agent";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { minimatch } from "minimatch";
|
|
5
|
+
import { DEFAULT_CONFIG, type FileTimeGuardConfig } from "./config.js";
|
|
6
|
+
|
|
7
|
+
interface FileStamp {
|
|
8
|
+
readTime: number;
|
|
9
|
+
mtime: number;
|
|
10
|
+
ctime: number;
|
|
11
|
+
size: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const fileRecords = new Map<string, Map<string, FileStamp>>();
|
|
15
|
+
|
|
16
|
+
export default function (pi: ExtensionAPI) {
|
|
17
|
+
const config: FileTimeGuardConfig = { ...DEFAULT_CONFIG };
|
|
18
|
+
|
|
19
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
20
|
+
if (!config.enabled) return;
|
|
21
|
+
|
|
22
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
23
|
+
if (!sessionId) return;
|
|
24
|
+
|
|
25
|
+
fileRecords.set(sessionId, new Map());
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
29
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
30
|
+
if (sessionId) {
|
|
31
|
+
fileRecords.delete(sessionId);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
36
|
+
if (!config.enabled) return;
|
|
37
|
+
|
|
38
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
39
|
+
if (!sessionId) return;
|
|
40
|
+
|
|
41
|
+
const records = fileRecords.get(sessionId);
|
|
42
|
+
if (!records) return;
|
|
43
|
+
|
|
44
|
+
if (event.toolName === "read") {
|
|
45
|
+
const relativePath = (event.input as { path: string }).path;
|
|
46
|
+
const absolutePath = resolve(ctx.cwd, relativePath);
|
|
47
|
+
|
|
48
|
+
if (shouldIgnorePath(absolutePath)) return;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const stats = await stat(absolutePath);
|
|
52
|
+
|
|
53
|
+
records.set(absolutePath, {
|
|
54
|
+
readTime: Date.now(),
|
|
55
|
+
mtime: stats.mtimeMs,
|
|
56
|
+
ctime: stats.ctimeMs,
|
|
57
|
+
size: stats.size,
|
|
58
|
+
});
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.debug("[file-time-guard] file stat failed:", err instanceof Error ? err.message : err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (event.toolName === "write" || event.toolName === "edit") {
|
|
65
|
+
const relativePath = (event.input as { path: string }).path;
|
|
66
|
+
const absolutePath = resolve(ctx.cwd, relativePath);
|
|
67
|
+
|
|
68
|
+
if (shouldIgnorePath(absolutePath)) return;
|
|
69
|
+
|
|
70
|
+
const record = records.get(absolutePath);
|
|
71
|
+
|
|
72
|
+
if (!record) {
|
|
73
|
+
if (config.checkMode === "block") {
|
|
74
|
+
ctx.ui.notify(
|
|
75
|
+
`文件未读取过: ${relativePath}\n请先读取文件再修改`,
|
|
76
|
+
"error",
|
|
77
|
+
);
|
|
78
|
+
return { block: true, reason: "文件未读取过" };
|
|
79
|
+
}
|
|
80
|
+
if (config.checkMode === "warn") {
|
|
81
|
+
ctx.ui.notify(
|
|
82
|
+
`警告: 文件未读取过: ${relativePath}`,
|
|
83
|
+
"warning",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const currentStats = await stat(absolutePath);
|
|
91
|
+
|
|
92
|
+
const isModified =
|
|
93
|
+
currentStats.mtimeMs !== record.mtime ||
|
|
94
|
+
currentStats.ctimeMs !== record.ctime ||
|
|
95
|
+
currentStats.size !== record.size;
|
|
96
|
+
|
|
97
|
+
if (isModified) {
|
|
98
|
+
if (config.checkMode === "block") {
|
|
99
|
+
ctx.ui.notify(
|
|
100
|
+
`文件已被外部修改: ${relativePath}\n请重新读取文件`,
|
|
101
|
+
"error",
|
|
102
|
+
);
|
|
103
|
+
return { block: true, reason: "文件已被外部修改" };
|
|
104
|
+
}
|
|
105
|
+
if (config.checkMode === "warn") {
|
|
106
|
+
ctx.ui.notify(
|
|
107
|
+
`警告: 文件已被外部修改: ${relativePath}`,
|
|
108
|
+
"warning",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.debug("[file-time-guard] current file stat failed:", err instanceof Error ? err.message : err);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
function shouldIgnorePath(path: string): boolean {
|
|
119
|
+
for (const pattern of config.ignorePatterns) {
|
|
120
|
+
if (minimatch(path, pattern)) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
pi.registerCommand("file-time-status", {
|
|
128
|
+
description: "查看文件时间戳检查状态",
|
|
129
|
+
handler: async (_args, ctx) => {
|
|
130
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
131
|
+
if (!sessionId) {
|
|
132
|
+
ctx.ui.notify("无活动会话", "info");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const records = fileRecords.get(sessionId);
|
|
137
|
+
if (!records) {
|
|
138
|
+
ctx.ui.notify("会话无文件记录", "info");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const count = records.size;
|
|
143
|
+
const lines = [
|
|
144
|
+
`文件时间戳检查: ${config.enabled ? "启用" : "禁用"}`,
|
|
145
|
+
`检查模式: ${config.checkMode}`,
|
|
146
|
+
`已追踪文件: ${count}`,
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
if (count > 0 && count <= 10) {
|
|
150
|
+
lines.push("\n已追踪文件:");
|
|
151
|
+
for (const [path] of Array.from(records.entries())) {
|
|
152
|
+
lines.push(` ${path}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
pi.registerFlag("file-time-check-mode", {
|
|
161
|
+
description: "文件时间戳检查模式 (block/warn/ignore)",
|
|
162
|
+
type: "string",
|
|
163
|
+
default: "block",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
pi.registerFlag("disable-file-time-check", {
|
|
167
|
+
description: "禁用文件时间戳检查",
|
|
168
|
+
type: "boolean",
|
|
169
|
+
default: false,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks Engine Extension
|
|
3
|
+
*
|
|
4
|
+
* Executes hooks defined in AgentConfig.hooks.
|
|
5
|
+
* Hooks are stored as JSON in event.variables["agentHooks"].
|
|
6
|
+
*
|
|
7
|
+
* Supported events: tool_call, tool_result, agent_start, agent_end
|
|
8
|
+
* Supported hook types: command (spawn process), prompt (inject text)
|
|
9
|
+
*
|
|
10
|
+
* Command hooks: exit code 2 = block operation, 0 = allow
|
|
11
|
+
* Prompt hooks: text injected into the conversation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ExtensionAPI, AgentHook, AgentHooks } from "@dyyz1993/pi-coding-agent";
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
|
|
17
|
+
const EVENT_MAP: Record<string, string> = {
|
|
18
|
+
tool_call: "on_tool_start",
|
|
19
|
+
tool_result: "on_tool_complete",
|
|
20
|
+
agent_start: "on_agent_start",
|
|
21
|
+
agent_end: "on_agent_complete",
|
|
22
|
+
session_start: "on_session_start",
|
|
23
|
+
session_shutdown: "on_session_end",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function parseHooks(raw: string | undefined): AgentHooks | null {
|
|
27
|
+
if (!raw) return null;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function matchesCondition(condition: string | undefined, event: Record<string, unknown>): boolean {
|
|
36
|
+
if (!condition) return true;
|
|
37
|
+
const toolName = (event.toolName as string) ?? "";
|
|
38
|
+
const parts = condition.split("|").map(s => s.trim());
|
|
39
|
+
return parts.includes(toolName);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function executeCommand(command: string, event: Record<string, unknown>, timeout = 5000): Promise<number> {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const toolName = (event.toolName as string) ?? "";
|
|
45
|
+
const input = event.input ?? {};
|
|
46
|
+
const env: Record<string, string> = {
|
|
47
|
+
...process.env as Record<string, string>,
|
|
48
|
+
PI_HOOK_TOOL: toolName,
|
|
49
|
+
PI_HOOK_EVENT: JSON.stringify(input),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const proc = spawn("sh", ["-c", command], {
|
|
53
|
+
env,
|
|
54
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
proc.kill("SIGTERM");
|
|
59
|
+
resolve(0);
|
|
60
|
+
}, timeout);
|
|
61
|
+
|
|
62
|
+
proc.on("close", (code) => {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
resolve(code ?? 0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
proc.on("error", () => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
resolve(0);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default function hooksEngine(pi: ExtensionAPI): void {
|
|
75
|
+
const subscribe = (eventName: string) => {
|
|
76
|
+
const hookKey = EVENT_MAP[eventName];
|
|
77
|
+
if (!hookKey) return;
|
|
78
|
+
|
|
79
|
+
pi.on(eventName, async (event: Record<string, unknown>) => {
|
|
80
|
+
const vars = event.variables as Record<string, string> | undefined;
|
|
81
|
+
if (!vars?.agentHooks) return undefined;
|
|
82
|
+
|
|
83
|
+
const hooks = parseHooks(vars.agentHooks);
|
|
84
|
+
if (!hooks) return undefined;
|
|
85
|
+
|
|
86
|
+
const eventHooks = hooks[hookKey] ?? hooks["*"] ?? [];
|
|
87
|
+
if (eventHooks.length === 0) return undefined;
|
|
88
|
+
|
|
89
|
+
const results: string[] = [];
|
|
90
|
+
|
|
91
|
+
for (const hook of eventHooks) {
|
|
92
|
+
if (!matchesCondition(hook.if, event)) continue;
|
|
93
|
+
|
|
94
|
+
if (hook.type === "command") {
|
|
95
|
+
const code = await executeCommand(hook.command, event);
|
|
96
|
+
if (code === 2) {
|
|
97
|
+
return {
|
|
98
|
+
block: true,
|
|
99
|
+
reason: `[hook] Operation blocked by hook: ${hook.command}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
} else if (hook.type === "prompt") {
|
|
103
|
+
results.push(hook.prompt);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (results.length > 0) {
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return undefined;
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
for (const eventName of Object.keys(EVENT_MAP)) {
|
|
115
|
+
subscribe(eventName);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface FileTrackerOptions {
|
|
2
|
+
maxOpenFiles?: number;
|
|
3
|
+
now?: () => number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface FileTracker {
|
|
7
|
+
open(filePath: string, onClose: (file: string) => void): void;
|
|
8
|
+
getOpenFiles(): string[];
|
|
9
|
+
getIdleFiles(idleMs: number): string[];
|
|
10
|
+
closeAll(onClose: (file: string) => void): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TrackedFile {
|
|
14
|
+
filePath: string;
|
|
15
|
+
lastAccess: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createFileTracker(options: FileTrackerOptions = {}): FileTracker {
|
|
19
|
+
const maxOpenFiles = options.maxOpenFiles ?? 30;
|
|
20
|
+
const now = options.now ?? (() => Date.now());
|
|
21
|
+
const files = new Map<string, TrackedFile>();
|
|
22
|
+
const order: string[] = [];
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
open(filePath: string, onClose: (file: string) => void): void {
|
|
26
|
+
if (files.has(filePath)) {
|
|
27
|
+
const entry = files.get(filePath)!;
|
|
28
|
+
entry.lastAccess = now();
|
|
29
|
+
const idx = order.indexOf(filePath);
|
|
30
|
+
if (idx !== -1) {
|
|
31
|
+
order.splice(idx, 1);
|
|
32
|
+
}
|
|
33
|
+
order.push(filePath);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
while (order.length >= maxOpenFiles) {
|
|
38
|
+
const evicted = order.shift()!;
|
|
39
|
+
files.delete(evicted);
|
|
40
|
+
onClose(evicted);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
files.set(filePath, { filePath, lastAccess: now() });
|
|
44
|
+
order.push(filePath);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
getOpenFiles(): string[] {
|
|
48
|
+
return [...order];
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
getIdleFiles(idleMs: number): string[] {
|
|
52
|
+
const threshold = now() - idleMs;
|
|
53
|
+
const idle: string[] = [];
|
|
54
|
+
for (const entry of files.values()) {
|
|
55
|
+
if (entry.lastAccess < threshold) {
|
|
56
|
+
idle.push(entry.filePath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return idle;
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
closeAll(onClose: (file: string) => void): void {
|
|
63
|
+
for (const filePath of order) {
|
|
64
|
+
onClose(filePath);
|
|
65
|
+
}
|
|
66
|
+
files.clear();
|
|
67
|
+
order.length = 0;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|