@trim21/personal-pi-extensions 0.0.323 → 0.0.329

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.323",
3
+ "version": "0.0.329",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -41,7 +41,7 @@
41
41
  "@vitest/coverage-v8": "^4.1.10",
42
42
  "eslint": "^10.8.1",
43
43
  "eslint-config-prettier": "10.1.8",
44
- "eslint-plugin-erasable-syntax-only": "0.4.2",
44
+ "eslint-plugin-erasable-syntax-only": "0.6.0",
45
45
  "eslint-plugin-import-x": "^4.17.1",
46
46
  "eslint-plugin-promise": "7.3.0",
47
47
  "eslint-plugin-simple-import-sort": "14.0.0",
package/src/aft/tools.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { homedir } from "node:os";
9
- import { isAbsolute, join, resolve } from "node:path";
9
+ import { isAbsolute, join, relative, resolve } from "node:path";
10
10
 
11
11
  import {
12
12
  type AftProjectTransport,
@@ -32,6 +32,19 @@ export function resolvePathArg(cwd: string, input: string): string {
32
32
  return isAbsolute(input) ? input : resolve(cwd, input);
33
33
  }
34
34
 
35
+ /** 人类可读的显示路径:cwd 内用 `./…`,home 内用 `~/…`,否则原样绝对路径。 */
36
+ export function formatDisplayPath(cwd: string, filePath: string): string {
37
+ const relToCwd = relative(cwd, filePath);
38
+ if (relToCwd !== "" && !relToCwd.startsWith("..") && !isAbsolute(relToCwd)) {
39
+ return `./${relToCwd}`;
40
+ }
41
+ const relToHome = relative(homedir(), filePath);
42
+ if (relToHome !== "" && !relToHome.startsWith("..") && !isAbsolute(relToHome)) {
43
+ return `~/${relToHome}`;
44
+ }
45
+ return filePath;
46
+ }
47
+
35
48
  export interface AftToolContext {
36
49
  cwd: string;
37
50
  pool: AftTransportPool;
@@ -222,6 +235,8 @@ export function registerZoomTool(pi: ExtensionAPI, ctx: AftToolContext): void {
222
235
  if (contextLines !== undefined) rawArgs.contextLines = contextLines;
223
236
  if (coerceBoolean(params.callgraph)) rawArgs.callgraph = true;
224
237
 
238
+ const subtitle = buildZoomSubtitle(extCtx.cwd, params);
239
+
225
240
  const { text, response } = await callAftTool(bridgeFor(ctx), "zoom", rawArgs, extCtx);
226
241
  const truncated = response.truncated === true;
227
242
  return {
@@ -229,6 +244,8 @@ export function registerZoomTool(pi: ExtensionAPI, ctx: AftToolContext): void {
229
244
  details: {
230
245
  truncated,
231
246
  pendant: {
247
+ title: "aft_zoom",
248
+ subtitle,
232
249
  markdown: buildPendantMarkdown({
233
250
  title: "aft_zoom",
234
251
  input: params,
@@ -243,6 +260,37 @@ export function registerZoomTool(pi: ExtensionAPI, ctx: AftToolContext): void {
243
260
  });
244
261
  }
245
262
 
263
+ /** 构建 aft_zoom pendant 的 subtitle:`path="…" symbol="…"`,url 模式为 `url="…"`。 */
264
+ export function buildZoomSubtitle(
265
+ cwd: string,
266
+ params: Type.Static<typeof ZoomParams>,
267
+ ): string | undefined {
268
+ const isEmpty = (v: unknown): boolean =>
269
+ v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
270
+
271
+ const parts: string[] = [];
272
+ const targets = params.targets;
273
+ if (Array.isArray(targets)) {
274
+ for (const t of targets) {
275
+ parts.push(
276
+ `path="${formatDisplayPath(cwd, resolvePathArg(cwd, t.path))}" symbol="${t.symbol}"`,
277
+ );
278
+ }
279
+ } else if (targets) {
280
+ parts.push(
281
+ `path="${formatDisplayPath(cwd, resolvePathArg(cwd, targets.path))}" symbol="${targets.symbol}"`,
282
+ );
283
+ } else if (!isEmpty(params.url)) {
284
+ parts.push(`url="${params.url}"`);
285
+ } else if (params.path) {
286
+ const symbols = params.symbols;
287
+ const symbolStr = Array.isArray(symbols) ? symbols.join(", ") : symbols;
288
+ const pathPart = `path="${formatDisplayPath(cwd, resolvePathArg(cwd, params.path))}"`;
289
+ parts.push(symbolStr ? `${pathPart} symbol="${symbolStr}"` : pathPart);
290
+ }
291
+ return parts.length > 0 ? parts.join(" ") : undefined;
292
+ }
293
+
246
294
  const CALLGRAPH_OPS = [
247
295
  "call_tree",
248
296
  "callers",
@@ -209,21 +209,23 @@ export async function commandPatternsFor(command: string): Promise<string[]> {
209
209
  }
210
210
 
211
211
  /**
212
- * 对命令(含所有嵌套命令)求值:任一命令命中规则即生效,规则后写优先
213
- * (findLast,对齐 opencode PermissionV2)。返回 allow/deny;无规则命中
214
- * 返回 undefined(交给人审)。
212
+ * 对命令(含所有嵌套命令)求值:
213
+ * - deny 优先:任一命令命中 deny 规则即整体拒绝
214
+ * - allow 需全量:所有命令都命中 allow 规则才整体放行,否则返回
215
+ * undefined(有命令未命中规则,交给人审),避免未允许的命令被同链放行带过。
216
+ * 规则内后写优先(findLast,对齐 opencode PermissionV2)。
215
217
  */
216
- export function evaluateBashApproval(
218
+ export async function evaluateBashApproval(
217
219
  command: string,
218
220
  rules: readonly ApprovalRule[],
219
221
  ): Promise<ApprovalAction | undefined> {
220
- return commandPatternsFor(command).then((patterns) => {
221
- if (patterns.length === 0) return;
222
- // 规则后写优先(对齐 opencode PermissionV2 的 findLast)
223
- for (const pattern of patterns) {
224
- const rule = rules.findLast((r) => matchRule(pattern, r.pattern));
225
- if (rule) return rule.action;
226
- }
227
- return;
228
- });
222
+ const patterns = await commandPatternsFor(command);
223
+ if (patterns.length === 0) return;
224
+ let allowed = 0;
225
+ for (const pattern of patterns) {
226
+ const rule = rules.findLast((r) => matchRule(pattern, r.pattern));
227
+ if (rule?.action === "deny") return "deny";
228
+ if (rule?.action === "allow") allowed++;
229
+ }
230
+ return allowed === patterns.length ? "allow" : undefined;
229
231
  }
@@ -8,6 +8,14 @@
8
8
  * 列表等常驻信息用 false
9
9
  */
10
10
  export interface ToolPendant {
11
+ /**
12
+ * 面板标题
13
+ */
14
+ title?: string;
15
+ /**
16
+ * 面板副标题
17
+ */
18
+ subtitle?: string;
11
19
  markdown: string;
12
20
  /**
13
21
  * @default false