@trim21/personal-pi-extensions 0.0.245 → 0.0.247

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.245",
3
+ "version": "0.0.247",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -12,19 +12,16 @@ import {
12
12
  sep,
13
13
  } from "node:path";
14
14
 
15
- import { Type } from "typebox";
15
+ import { type Static, Type } from "typebox";
16
16
  import { Value } from "typebox/value";
17
17
 
18
- export interface FileSnapshot {
19
- digest: string;
20
- textEditable: boolean;
21
- }
22
-
23
18
  const fileSnapshotSchema = Type.Object({
24
19
  digest: Type.String(),
25
20
  textEditable: Type.Boolean(),
26
21
  });
27
22
 
23
+ export type FileSnapshot = Static<typeof fileSnapshotSchema>;
24
+
28
25
  export interface ClaudeCodeState {
29
26
  readonly reads: Map<string, FileSnapshot>;
30
27
  }
@@ -187,6 +187,7 @@ export function registerFileTools(pi: ExtensionAPI, state: ClaudeCodeState): voi
187
187
  label: "Read",
188
188
  description: [
189
189
  "Reads a file from the local filesystem. You can access any file directly using this tool.",
190
+ "Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.",
190
191
  "The file_path parameter must be an absolute path. By default, it reads the entire file; files over 256 KB or 25K tokens require offset and limit.",
191
192
  "Results use cat -n style line numbers starting at 1. Images are returned visually.",
192
193
  "This tool reads files, not directories.",
@@ -294,7 +295,9 @@ export function registerFileTools(pi: ExtensionAPI, state: ClaudeCodeState): voi
294
295
  {
295
296
  file_path: Type.String({ description: "The absolute path to the file to modify" }),
296
297
  old_string: Type.String({ description: "The text to replace" }),
297
- new_string: Type.String({ description: "The text to replace it with" }),
298
+ new_string: Type.String({
299
+ description: "The text to replace it with (must be different from old_string)",
300
+ }),
298
301
  replace_all: Type.Optional(
299
302
  Type.Boolean({ description: "Replace all occurrences of old_string", default: false }),
300
303
  ),
@@ -118,7 +118,7 @@ export function registerGlobTool(pi: ExtensionAPI): void {
118
118
  path: Type.Optional(
119
119
  Type.String({
120
120
  description:
121
- "The directory to search in. If omitted, the current working directory is used.",
121
+ 'The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.',
122
122
  }),
123
123
  ),
124
124
  },
@@ -15,14 +15,12 @@ import { fileURLToPath } from "node:url";
15
15
 
16
16
  import { StringEnum } from "@earendil-works/pi-ai";
17
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
- import { Type } from "typebox";
18
+ import { type Static, Type } from "typebox";
19
19
 
20
20
  import { searchRoot, suggestPathUnderCwd, throwIfAborted, toRelativePath } from "./common.js";
21
21
 
22
22
  const GREP_OUTPUT_MODES = ["content", "files_with_matches", "count"] as const;
23
23
 
24
- type GrepOutputMode = (typeof GREP_OUTPUT_MODES)[number];
25
-
26
24
  /** Tool guidance, kept in markdown so it reads like documentation. */
27
25
  const GREP_PROMPT = readFileSync(fileURLToPath(new URL("grep.md", import.meta.url)), "utf8").trim();
28
26
 
@@ -41,22 +39,49 @@ function truncateOutput(output: string, maxCharacters = 30_000): string {
41
39
  return `${output.slice(0, maxCharacters)}\n\n[Output truncated at ${maxCharacters} characters]`;
42
40
  }
43
41
 
44
- interface GrepParameters {
45
- pattern: string;
46
- path?: string;
47
- glob?: string;
48
- output_mode?: GrepOutputMode;
49
- "-B"?: number;
50
- "-A"?: number;
51
- "-C"?: number;
52
- context?: number;
53
- "-n"?: boolean;
54
- "-i"?: boolean;
55
- type?: string;
56
- head_limit?: number;
57
- offset?: number;
58
- multiline?: boolean;
59
- }
42
+ const grepParametersSchema = Type.Object(
43
+ {
44
+ pattern: Type.String({ description: "The regular expression pattern to search for" }),
45
+ path: Type.Optional(
46
+ Type.String({
47
+ description: "File or directory to search. Defaults to the current directory.",
48
+ }),
49
+ ),
50
+ glob: Type.Optional(Type.String({ description: 'Glob filter such as "*.js" or "*.{ts,tsx}"' })),
51
+ output_mode: Type.Optional(
52
+ StringEnum(GREP_OUTPUT_MODES, {
53
+ description: "Output mode. Defaults to files_with_matches.",
54
+ }),
55
+ ),
56
+ "-B": Type.Optional(
57
+ Type.Number({ description: "Lines to show before each match in content mode" }),
58
+ ),
59
+ "-A": Type.Optional(
60
+ Type.Number({ description: "Lines to show after each match in content mode" }),
61
+ ),
62
+ "-C": Type.Optional(Type.Number({ description: "Lines to show before and after each match" })),
63
+ context: Type.Optional(
64
+ Type.Number({ description: "Lines to show before and after each match" }),
65
+ ),
66
+ "-n": Type.Optional(
67
+ Type.Boolean({ description: "Show line numbers in content mode; defaults true" }),
68
+ ),
69
+ "-i": Type.Optional(Type.Boolean({ description: "Case-insensitive search" })),
70
+ type: Type.Optional(
71
+ Type.String({ description: "ripgrep file type such as js, py, rust, or go" }),
72
+ ),
73
+ head_limit: Type.Optional(
74
+ Type.Number({ description: "Limit output to the first N entries after offset" }),
75
+ ),
76
+ offset: Type.Optional(Type.Number({ description: "Skip the first N output entries" })),
77
+ multiline: Type.Optional(
78
+ Type.Boolean({ description: "Allow patterns to span multiple lines" }),
79
+ ),
80
+ },
81
+ { additionalProperties: false },
82
+ );
83
+
84
+ type GrepParameters = Static<typeof grepParametersSchema>;
60
85
 
61
86
  export function buildGrepArguments(params: GrepParameters, cwd: string): string[] {
62
87
  const mode = params.output_mode ?? "files_with_matches";
@@ -217,51 +242,7 @@ export function registerGrepTool(pi: ExtensionAPI): void {
217
242
  ].join("\n"),
218
243
  promptSnippet: "Search file contents with regular expressions",
219
244
  promptGuidelines: [`- -\n${GREP_PROMPT}`],
220
- parameters: Type.Object(
221
- {
222
- pattern: Type.String({ description: "The regular expression pattern to search for" }),
223
- path: Type.Optional(
224
- Type.String({
225
- description: "File or directory to search. Defaults to the current directory.",
226
- }),
227
- ),
228
- glob: Type.Optional(
229
- Type.String({ description: 'Glob filter such as "*.js" or "*.{ts,tsx}"' }),
230
- ),
231
- output_mode: Type.Optional(
232
- StringEnum(GREP_OUTPUT_MODES, {
233
- description: "Output mode. Defaults to files_with_matches.",
234
- }),
235
- ),
236
- "-B": Type.Optional(
237
- Type.Number({ description: "Lines to show before each match in content mode" }),
238
- ),
239
- "-A": Type.Optional(
240
- Type.Number({ description: "Lines to show after each match in content mode" }),
241
- ),
242
- "-C": Type.Optional(
243
- Type.Number({ description: "Lines to show before and after each match" }),
244
- ),
245
- context: Type.Optional(
246
- Type.Number({ description: "Lines to show before and after each match" }),
247
- ),
248
- "-n": Type.Optional(
249
- Type.Boolean({ description: "Show line numbers in content mode; defaults true" }),
250
- ),
251
- "-i": Type.Optional(Type.Boolean({ description: "Case-insensitive search" })),
252
- type: Type.Optional(
253
- Type.String({ description: "ripgrep file type such as js, py, rust, or go" }),
254
- ),
255
- head_limit: Type.Optional(
256
- Type.Number({ description: "Limit output to the first N entries after offset" }),
257
- ),
258
- offset: Type.Optional(Type.Number({ description: "Skip the first N output entries" })),
259
- multiline: Type.Optional(
260
- Type.Boolean({ description: "Allow patterns to span multiple lines" }),
261
- ),
262
- },
263
- { additionalProperties: false },
264
- ),
245
+ parameters: grepParametersSchema,
265
246
  async execute(_id, params, signal, _onUpdate, ctx) {
266
247
  if (
267
248
  params.offset !== undefined &&
@@ -1,6 +1,6 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { Type } from "typebox";
3
+ import { type Static, Type } from "typebox";
4
4
  import { Value } from "typebox/value";
5
5
 
6
6
  import { selectWithOptionalInput } from "../lib/ui.js";
@@ -18,23 +18,48 @@ const ASK_PROMPT = [
18
18
 
19
19
  type TodoStatus = (typeof TODO_STATUSES)[number];
20
20
 
21
- export interface ClaudeCodeTodo {
22
- content: string;
23
- status: TodoStatus;
24
- activeForm: string;
25
- }
21
+ const todoItemSchema = Type.Object(
22
+ {
23
+ content: Type.String({ minLength: 1 }),
24
+ status: StringEnum(TODO_STATUSES),
25
+ activeForm: Type.String({ minLength: 1 }),
26
+ },
27
+ { additionalProperties: false },
28
+ );
29
+ const todoSchema = Type.Object(
30
+ {
31
+ todos: Type.Array(todoItemSchema, { description: "The updated todo list" }),
32
+ },
33
+ { additionalProperties: false },
34
+ );
26
35
 
27
- interface QuestionOption {
28
- label: string;
29
- description: string;
30
- }
36
+ export type ClaudeCodeTodo = Static<typeof todoItemSchema>;
31
37
 
32
- interface QuestionInput {
33
- question: string;
34
- header: string;
35
- options: QuestionOption[];
36
- multiSelect: boolean;
37
- }
38
+ const optionSchema = Type.Object(
39
+ {
40
+ label: Type.String({ description: "Concise display text for the option" }),
41
+ description: Type.String({ description: "Explanation of the option" }),
42
+ },
43
+ { additionalProperties: false },
44
+ );
45
+ const questionSchema = Type.Object(
46
+ {
47
+ question: Type.String({ description: "The complete question to ask" }),
48
+ header: Type.String({ description: "Very short label displayed with the question" }),
49
+ options: Type.Array(optionSchema, {
50
+ minItems: 2,
51
+ maxItems: 4,
52
+ description: "The available choices; do not include an Other option",
53
+ }),
54
+ multiSelect: Type.Boolean({
55
+ default: false,
56
+ description: "Allow the user to select multiple options",
57
+ }),
58
+ },
59
+ { additionalProperties: false },
60
+ );
61
+
62
+ type QuestionInput = Static<typeof questionSchema>;
38
63
 
39
64
  function formatTodos(todos: readonly ClaudeCodeTodo[]): string[] | undefined {
40
65
  if (todos.length === 0) return undefined;
@@ -103,21 +128,6 @@ async function askMultiple(
103
128
  }
104
129
 
105
130
  export function registerSessionTools(pi: ExtensionAPI): void {
106
- const todoItemSchema = Type.Object(
107
- {
108
- content: Type.String({ minLength: 1 }),
109
- status: StringEnum(TODO_STATUSES),
110
- activeForm: Type.String({ minLength: 1 }),
111
- },
112
- { additionalProperties: false },
113
- );
114
- const todoSchema = Type.Object(
115
- {
116
- todos: Type.Array(todoItemSchema, { description: "The updated todo list" }),
117
- },
118
- { additionalProperties: false },
119
- );
120
-
121
131
  // TodoWrite 的列表随工具结果 details 持久化(跟随会话分支),但 widget 是
122
132
  // 纯 TUI 状态,进程重启后丢失。session 恢复时从当前分支取最后一个 TodoWrite
123
133
  // 的列表重新渲染(完整列表替换语义,后出现的覆盖前面的)。
@@ -167,30 +177,6 @@ export function registerSessionTools(pi: ExtensionAPI): void {
167
177
  },
168
178
  });
169
179
 
170
- const optionSchema = Type.Object(
171
- {
172
- label: Type.String({ description: "Concise display text for the option" }),
173
- description: Type.String({ description: "Explanation of the option" }),
174
- },
175
- { additionalProperties: false },
176
- );
177
- const questionSchema = Type.Object(
178
- {
179
- question: Type.String({ description: "The complete question to ask" }),
180
- header: Type.String({ description: "Very short label displayed with the question" }),
181
- options: Type.Array(optionSchema, {
182
- minItems: 2,
183
- maxItems: 4,
184
- description: "The available choices; do not include an Other option",
185
- }),
186
- multiSelect: Type.Boolean({
187
- default: false,
188
- description: "Allow the user to select multiple options",
189
- }),
190
- },
191
- { additionalProperties: false },
192
- );
193
-
194
180
  pi.registerTool({
195
181
  name: "AskUserQuestion",
196
182
  label: "Ask User Question",
@@ -21,7 +21,7 @@
21
21
  */
22
22
 
23
23
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
24
- import { Type } from "typebox";
24
+ import { type Static, Type } from "typebox";
25
25
 
26
26
  import { selectWithOptionalInput } from "../lib/ui.js";
27
27
 
@@ -47,34 +47,6 @@ export const QUESTION_DESCRIPTION = [
47
47
  '- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label',
48
48
  ].join("\n");
49
49
 
50
- // ── types ────────────────────────────────────────────────────────────────────
51
-
52
- export interface QuestionOption {
53
- label: string;
54
- description: string;
55
- }
56
-
57
- /** 与 schema 对齐的输入形状(multiple 可选) */
58
- export interface QuestionInput {
59
- question: string;
60
- header: string;
61
- options: QuestionOption[];
62
- multiple?: boolean;
63
- }
64
-
65
- /** 规整后的问题(multiple 已默认化为 boolean) */
66
- export interface Question extends Omit<QuestionInput, "multiple"> {
67
- multiple: boolean;
68
- }
69
-
70
- /** 每个问题的答案:选中的 label 数组(自定义输入就是单元素的 [text]) */
71
- export type Answer = string[];
72
-
73
- export interface QuestionDetails {
74
- questions: Question[];
75
- answers: Answer[];
76
- }
77
-
78
50
  // ── schema(与 opencode 的 Question.Prompt 一致)──────────────────────────────
79
51
 
80
52
  const optionSchema = Type.Object({
@@ -93,6 +65,26 @@ export const questionParamsSchema = Type.Object({
93
65
  questions: Type.Array(questionSchema, { description: "Questions to ask" }),
94
66
  });
95
67
 
68
+ // ── types ────────────────────────────────────────────────────────────────────
69
+
70
+ export type QuestionOption = Static<typeof optionSchema>;
71
+
72
+ /** 与 schema 对齐的输入形状(multiple 可选) */
73
+ export type QuestionInput = Static<typeof questionSchema>;
74
+
75
+ /** 规整后的问题(multiple 已默认化为 boolean) */
76
+ export interface Question extends Omit<QuestionInput, "multiple"> {
77
+ multiple: boolean;
78
+ }
79
+
80
+ /** 每个问题的答案:选中的 label 数组(自定义输入就是单元素的 [text]) */
81
+ export type Answer = string[];
82
+
83
+ export interface QuestionDetails {
84
+ questions: Question[];
85
+ answers: Answer[];
86
+ }
87
+
96
88
  // ── 纯函数(可测试)──────────────────────────────────────────────────────────
97
89
 
98
90
  /** trim 各字段并把可选的 multiple 默认化为 false */
@@ -24,7 +24,7 @@
24
24
 
25
25
  import { StringEnum } from "@earendil-works/pi-ai";
26
26
  import { type ExtensionAPI, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
27
- import { Type } from "typebox";
27
+ import { type Static, Type } from "typebox";
28
28
 
29
29
  import { type ToolPendant } from "../lib/pendant.js";
30
30
 
@@ -74,19 +74,6 @@ export const TODOWRITE_DESCRIPTION = [
74
74
  "- Items should be specific and actionable; break large work into smaller steps",
75
75
  ].join("\n");
76
76
 
77
- // ── types ────────────────────────────────────────────────────────────────────
78
-
79
- export interface TodoInfo {
80
- content: string;
81
- status: TodoStatus;
82
- priority: TodoPriority;
83
- }
84
-
85
- export interface TodoDetails {
86
- todos: TodoInfo[];
87
- pendant?: ToolPendant;
88
- }
89
-
90
77
  // ── schema(与 opencode 的 Parameters 一致)──────────────────────────────────
91
78
 
92
79
  const todoInfoSchema = Type.Object({
@@ -103,6 +90,15 @@ export const todowriteSchema = Type.Object({
103
90
  todos: Type.Array(todoInfoSchema, { description: "The updated todo list" }),
104
91
  });
105
92
 
93
+ // ── types ────────────────────────────────────────────────────────────────────
94
+
95
+ export type TodoInfo = Static<typeof todoInfoSchema>;
96
+
97
+ export interface TodoDetails {
98
+ todos: TodoInfo[];
99
+ pendant?: ToolPendant;
100
+ }
101
+
106
102
  // ── 纯函数(可测试)──────────────────────────────────────────────────────────
107
103
 
108
104
  /** trim content 并返回干净副本;字段类型已由 typebox schema 推断,无需运行时校验 */