pi-piqo 0.0.1
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 +91 -0
- package/index.ts +383 -0
- package/package.json +9 -0
- package/test_dir/example.md +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Piqo — Pi Extension
|
|
2
|
+
|
|
3
|
+
A chat-less way to collaborate with your favorite LLM models (remote or local), directly from your files and from any editor. The output is stored automatically into your files.
|
|
4
|
+
|
|
5
|
+
It is a simple file-watcher extension for [pi](https://github.com/badlogic/pi-mono) triggered on-save, which monitors directories for `@piqo` markers and uses the LLM to generate content inline.
|
|
6
|
+
|
|
7
|
+
Might be useful for keeping notes, writing tasks, researching topics, etc.
|
|
8
|
+
|
|
9
|
+
Random Example:
|
|
10
|
+
If you write the following (highlighted line) and save the file:
|
|
11
|
+
<img width="719" height="160" alt="Image" src="https://github.com/user-attachments/assets/30eda1ca-34ad-468a-baef-52db15169573" />
|
|
12
|
+
|
|
13
|
+
You will get something like (using gpt-5.4-mini):
|
|
14
|
+
|
|
15
|
+
<img width="718" height="267" alt="Image" src="https://github.com/user-attachments/assets/8f0c9a27-c366-4022-8e27-e188a0e19188" />
|
|
16
|
+
|
|
17
|
+
## How It Works
|
|
18
|
+
|
|
19
|
+
1. You start pi with the piqo extension and specify directories to watch
|
|
20
|
+
2. Piqo recursively watches those directories for file changes
|
|
21
|
+
3. When a file contains one or more `@piqo <instruction>` markers, it reads the file, gathers context around all markers, and sends them to pi's LLM in one request
|
|
22
|
+
4. The LLM fulfills each prompt and removes the human prompt line/tag from the file
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
If you have [Pi](https://pi.dev/) installed, you are one command away from using it:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Load it directly from github
|
|
29
|
+
pi -e https://github.com/piqoni/piqo-extension --dir=/path/to/your/project
|
|
30
|
+
|
|
31
|
+
# Or if you want to reference it locally, git clone the repo and reference it directly
|
|
32
|
+
pi -e ./piqo-extension --dir /path/to/your/project
|
|
33
|
+
|
|
34
|
+
# Watch multiple directories
|
|
35
|
+
pi -e ./piqo-extension --dir /path/to/dir1,/path/to/dir2
|
|
36
|
+
|
|
37
|
+
# Headless mode (no TUI)
|
|
38
|
+
pi -e ./piqo-extension --dir /path/to/project -p "Start piqo watcher"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Marker Format
|
|
42
|
+
|
|
43
|
+
In any text file within the watched directories, add:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
@piqo <your instruction here>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The LLM will process it and replace/remove the prompt so the file becomes:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
... generated content ...
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Examples
|
|
56
|
+
|
|
57
|
+
**In a Python file:**
|
|
58
|
+
```python
|
|
59
|
+
# @piqo add a function to parse CSV files and return a list of dicts
|
|
60
|
+
|
|
61
|
+
# Becomes generated code with the @piqo prompt removed
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**In a Markdown file:**
|
|
65
|
+
```markdown
|
|
66
|
+
@piqo write a summary of REST API best practices
|
|
67
|
+
|
|
68
|
+
Becomes generated content with the @piqo prompt removed
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**In a config file:**
|
|
72
|
+
```yaml
|
|
73
|
+
# @piqo add sensible default nginx config for a Node.js app
|
|
74
|
+
|
|
75
|
+
# Becomes generated config with the @piqo prompt removed
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Behavior Details
|
|
79
|
+
|
|
80
|
+
- **Debounce**: File changes are debounced at 500ms per file to avoid duplicate processing
|
|
81
|
+
- **Initial scan**: On startup, piqo scans all watched directories for existing markers
|
|
82
|
+
- **Ignored paths**: Hidden files/dirs, `node_modules`, `.git` are automatically skipped
|
|
83
|
+
- **Text files only**: Only processes common text file extensions (.ts, .js, .py, .md, .txt, etc.)
|
|
84
|
+
|
|
85
|
+
## Installation
|
|
86
|
+
|
|
87
|
+
Place this extension in `~/.pi/agent/extensions/piqo/` for global access, or reference it directly:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pi -e /path/to/piqo-extension
|
|
91
|
+
```
|
package/index.ts
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Piqo Extension
|
|
3
|
+
*
|
|
4
|
+
* Watches directories for file changes. When a file contains one or more
|
|
5
|
+
* @piqo markers, it reads the file, focuses on the @piqo context, and sends
|
|
6
|
+
* it to pi's LLM to generate or modify content. The LLM must remove the
|
|
7
|
+
* human @piqo prompt line/tag as part of its edit, so no /piqo/ closing marker
|
|
8
|
+
* is needed.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* pi -e ./piqo-extension --dir /path/to/dir1,/path/to/dir2
|
|
12
|
+
*
|
|
13
|
+
* In headless (print) mode:
|
|
14
|
+
* pi -e ./piqo-extension --dir /path/to/dir1 -p "Start piqo watcher"
|
|
15
|
+
*
|
|
16
|
+
* Marker format:
|
|
17
|
+
* @piqo <instruction here>
|
|
18
|
+
* ... LLM replaces the prompt with generated content and removes @piqo ...
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as fs from "node:fs";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
24
|
+
|
|
25
|
+
// Track which files are currently being processed to avoid duplicate agent runs
|
|
26
|
+
interface PiqoMarker {
|
|
27
|
+
filePath: string;
|
|
28
|
+
lineNumber: number;
|
|
29
|
+
instruction: string;
|
|
30
|
+
lineText: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PIQO_PROMPT_SENTINEL = "[piqo-request]";
|
|
34
|
+
|
|
35
|
+
function getMessageText(message: any): string {
|
|
36
|
+
const content = message?.content;
|
|
37
|
+
if (typeof content === "string") return content;
|
|
38
|
+
if (!Array.isArray(content)) return "";
|
|
39
|
+
|
|
40
|
+
return content
|
|
41
|
+
.map((part) => {
|
|
42
|
+
if (typeof part === "string") return part;
|
|
43
|
+
if (part?.type === "text" && typeof part.text === "string") return part.text;
|
|
44
|
+
return "";
|
|
45
|
+
})
|
|
46
|
+
.join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default function (pi: ExtensionAPI) {
|
|
50
|
+
// Register the --dir flag
|
|
51
|
+
pi.registerFlag("dir", {
|
|
52
|
+
description: "Comma-separated directories to watch for @piqo markers",
|
|
53
|
+
type: "string",
|
|
54
|
+
default: "",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const processing = new Set<string>(); // file paths currently being processed
|
|
58
|
+
const debounceTimers = new Map<string, ReturnType<typeof setTimeout>>(); // per-file debounce
|
|
59
|
+
const watchers: fs.FSWatcher[] = [];
|
|
60
|
+
|
|
61
|
+
// Isolate Piqo LLM calls from prior chat/session history.
|
|
62
|
+
// We keep messages from the current Piqo request onward so tool-call loops
|
|
63
|
+
// still work, but older user/assistant turns cannot affect the request.
|
|
64
|
+
pi.on("context", async (event) => {
|
|
65
|
+
let piqoStartIdx = -1;
|
|
66
|
+
for (let i = event.messages.length - 1; i >= 0; i--) {
|
|
67
|
+
const message = event.messages[i];
|
|
68
|
+
if (message.role === "user" && getMessageText(message).includes(PIQO_PROMPT_SENTINEL)) {
|
|
69
|
+
piqoStartIdx = i;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (piqoStartIdx === -1) return;
|
|
75
|
+
return { messages: event.messages.slice(piqoStartIdx) };
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Scan a file for @piqo markers. Markers are considered pending until the
|
|
80
|
+
* agent removes the human prompt line/tag from the file.
|
|
81
|
+
*/
|
|
82
|
+
function findMarkers(filePath: string): PiqoMarker[] {
|
|
83
|
+
let content: string;
|
|
84
|
+
try {
|
|
85
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lines = content.split("\n");
|
|
91
|
+
const markers: PiqoMarker[] = [];
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
const line = lines[i];
|
|
95
|
+
const piqoMatch = line.match(/@piqo\b(.*)/);
|
|
96
|
+
if (!piqoMatch) continue;
|
|
97
|
+
|
|
98
|
+
markers.push({
|
|
99
|
+
filePath,
|
|
100
|
+
lineNumber: i + 1,
|
|
101
|
+
instruction: piqoMatch[1].trim(),
|
|
102
|
+
lineText: line,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return markers;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build context around a @piqo marker for the LLM.
|
|
111
|
+
* Includes surrounding lines for context.
|
|
112
|
+
*/
|
|
113
|
+
function buildContext(filePath: string, marker: PiqoMarker): string {
|
|
114
|
+
let content: string;
|
|
115
|
+
try {
|
|
116
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
117
|
+
} catch {
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const lines = content.split("\n");
|
|
122
|
+
const markerLineIdx = marker.lineNumber - 1;
|
|
123
|
+
|
|
124
|
+
// Get surrounding context (up to 30 lines before and 10 after)
|
|
125
|
+
const contextBefore = Math.max(0, markerLineIdx - 30);
|
|
126
|
+
const contextAfter = Math.min(lines.length, markerLineIdx + 11);
|
|
127
|
+
const surroundingLines = lines.slice(contextBefore, contextAfter);
|
|
128
|
+
|
|
129
|
+
const relativePath = filePath;
|
|
130
|
+
const ext = path.extname(filePath).slice(1) || "txt";
|
|
131
|
+
|
|
132
|
+
return [
|
|
133
|
+
`File: ${relativePath}`,
|
|
134
|
+
`Marker at line ${marker.lineNumber}`,
|
|
135
|
+
`Instruction: ${marker.instruction || "(no specific instruction — infer from context)"}`,
|
|
136
|
+
"",
|
|
137
|
+
`\`\`\`${ext}`,
|
|
138
|
+
...surroundingLines.map(
|
|
139
|
+
(line, idx) =>
|
|
140
|
+
`${contextBefore + idx + 1 === marker.lineNumber ? ">>> " : " "}${line}`
|
|
141
|
+
),
|
|
142
|
+
"```",
|
|
143
|
+
].join("\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Process all @piqo markers in a file in one agent run. This avoids line
|
|
148
|
+
* shifting and concurrent edit conflicts when a file has multiple markers.
|
|
149
|
+
*/
|
|
150
|
+
function processFileMarkers(filePath: string, markers: PiqoMarker[]): void {
|
|
151
|
+
if (processing.has(filePath)) return;
|
|
152
|
+
processing.add(filePath);
|
|
153
|
+
|
|
154
|
+
const contexts = markers.map((marker) => buildContext(filePath, marker)).filter(Boolean);
|
|
155
|
+
if (contexts.length === 0) {
|
|
156
|
+
processing.delete(filePath);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const markerList = markers
|
|
161
|
+
.map(
|
|
162
|
+
(marker, idx) =>
|
|
163
|
+
`${idx + 1}. Line ${marker.lineNumber}: ${marker.lineText.trim()}\n Instruction: ${marker.instruction || "(no specific instruction — infer from context)"}`
|
|
164
|
+
)
|
|
165
|
+
.join("\n");
|
|
166
|
+
|
|
167
|
+
const prompt = [
|
|
168
|
+
`${PIQO_PROMPT_SENTINEL} A file has one or more @piqo markers requesting AI assistance. Read the
|
|
169
|
+
file, understand each marker, and fulfill every request in one edit.`,
|
|
170
|
+
"",
|
|
171
|
+
"MARKERS TO PROCESS:",
|
|
172
|
+
markerList,
|
|
173
|
+
"",
|
|
174
|
+
"CONTEXT:",
|
|
175
|
+
contexts.join("\n\n---\n\n"),
|
|
176
|
+
"",
|
|
177
|
+
"INSTRUCTIONS:",
|
|
178
|
+
`1. Read the file "${filePath}" to get the full current content.`,
|
|
179
|
+
"2. For every @piqo marker listed above, generate or modify content as requested by the human prompt after @piqo.",
|
|
180
|
+
"3. CRITICAL: Always remove the human prompt from the file. If @piqo is on its own line or in a comment line, remove that whole prompt line and replace it with the generated content if appropriate.",
|
|
181
|
+
"4. CRITICAL The final file must contain no @piqo tags for the prompts you processed.",
|
|
182
|
+
"5. Keep unrelated content intact and preserve the file's style/formatting.",
|
|
183
|
+
"",
|
|
184
|
+
"Example transformation:",
|
|
185
|
+
" Before:",
|
|
186
|
+
" @piqo add a hello world function",
|
|
187
|
+
" After:",
|
|
188
|
+
" function helloWorld() {",
|
|
189
|
+
' console.log("Hello, World!");',
|
|
190
|
+
" }",
|
|
191
|
+
].join("\n");
|
|
192
|
+
|
|
193
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Handle a file change event: debounce, scan for markers, process.
|
|
198
|
+
*/
|
|
199
|
+
function onFileChange(filePath: string): void {
|
|
200
|
+
// Debounce per file: wait 500ms after last change before processing
|
|
201
|
+
const existing = debounceTimers.get(filePath);
|
|
202
|
+
if (existing) clearTimeout(existing);
|
|
203
|
+
|
|
204
|
+
debounceTimers.set(
|
|
205
|
+
filePath,
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
debounceTimers.delete(filePath);
|
|
208
|
+
|
|
209
|
+
const markers = findMarkers(filePath);
|
|
210
|
+
if (markers.length > 0) {
|
|
211
|
+
processFileMarkers(filePath, markers);
|
|
212
|
+
}
|
|
213
|
+
}, 500)
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Recursively watch a directory using fs.watch with recursive option.
|
|
219
|
+
*/
|
|
220
|
+
function watchDirectory(dirPath: string): void {
|
|
221
|
+
const resolvedDir = path.resolve(dirPath);
|
|
222
|
+
|
|
223
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
224
|
+
console.error(`[piqo] Directory does not exist: ${resolvedDir}`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const watcher = fs.watch(resolvedDir, { recursive: true }, (eventType, filename) => {
|
|
230
|
+
if (!filename) return;
|
|
231
|
+
|
|
232
|
+
const fullPath = path.join(resolvedDir, filename);
|
|
233
|
+
|
|
234
|
+
// Skip hidden files/dirs, node_modules, .git, etc.
|
|
235
|
+
if (
|
|
236
|
+
filename.startsWith(".") ||
|
|
237
|
+
filename.includes("node_modules") ||
|
|
238
|
+
filename.includes(".git") ||
|
|
239
|
+
filename.includes("/.")
|
|
240
|
+
) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Only process text-like files
|
|
245
|
+
const ext = path.extname(filename).toLowerCase();
|
|
246
|
+
const textExts = new Set([
|
|
247
|
+
".txt", ".md", ".js", ".ts", ".jsx", ".tsx", ".py", ".rb", ".rs",
|
|
248
|
+
".go", ".java", ".c", ".cpp", ".h", ".hpp", ".css", ".html", ".xml",
|
|
249
|
+
".json", ".yaml", ".yml", ".toml", ".sh", ".bash", ".zsh", ".fish",
|
|
250
|
+
".sql", ".r", ".swift", ".kt", ".scala", ".lua", ".vim", ".el",
|
|
251
|
+
".clj", ".hs", ".ml", ".ex", ".exs", ".erl", ".dart", ".cs",
|
|
252
|
+
".php", ".pl", ".pm", ".svelte", ".vue", ".astro", ".mdx",
|
|
253
|
+
]);
|
|
254
|
+
if (!textExts.has(ext)) return;
|
|
255
|
+
|
|
256
|
+
// Check file exists and is a regular file
|
|
257
|
+
try {
|
|
258
|
+
const stat = fs.statSync(fullPath);
|
|
259
|
+
if (!stat.isFile()) return;
|
|
260
|
+
} catch {
|
|
261
|
+
return; // File may have been deleted
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
onFileChange(fullPath);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
watchers.push(watcher);
|
|
268
|
+
console.log(`[piqo] Watching directory: ${resolvedDir}`);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.error(`[piqo] Failed to watch ${resolvedDir}:`, err);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Clean up on agent_end — remove processing keys for completed markers
|
|
275
|
+
pi.on("agent_end", async (_event, _ctx) => {
|
|
276
|
+
// After the agent finishes a turn, clear the processing set
|
|
277
|
+
// so markers that failed can be retried on next file change
|
|
278
|
+
processing.clear();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Start watching on session_start
|
|
282
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
283
|
+
const dirFlag = pi.getFlag("dir") as string;
|
|
284
|
+
if (!dirFlag) {
|
|
285
|
+
if (ctx.hasUI) {
|
|
286
|
+
ctx.ui.notify("[piqo] No --dir specified. Use --dir=path1,path2 to watch directories.", "warning");
|
|
287
|
+
}
|
|
288
|
+
console.log("[piqo] No --dir specified. Piqo is idle.");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const dirs = dirFlag
|
|
293
|
+
.split(",")
|
|
294
|
+
.map((d) => d.trim())
|
|
295
|
+
.filter(Boolean);
|
|
296
|
+
|
|
297
|
+
if (dirs.length === 0) {
|
|
298
|
+
console.log("[piqo] No valid directories specified.");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const dir of dirs) {
|
|
303
|
+
watchDirectory(dir);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (ctx.hasUI) {
|
|
307
|
+
ctx.ui.notify(`[piqo] Watching ${dirs.length} director${dirs.length === 1 ? "y" : "ies"} for @piqo markers`, "info");
|
|
308
|
+
ctx.ui.setStatus("piqo", `👁 Watching ${dirs.length} dir${dirs.length === 1 ? "" : "s"}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Do an initial scan of all watched directories
|
|
312
|
+
for (const dir of dirs) {
|
|
313
|
+
const resolvedDir = path.resolve(dir);
|
|
314
|
+
try {
|
|
315
|
+
scanDirectoryRecursive(resolvedDir);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
console.error(`[piqo] Initial scan failed for ${resolvedDir}:`, err);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Recursively scan a directory for files with @piqo markers.
|
|
324
|
+
*/
|
|
325
|
+
function scanDirectoryRecursive(dirPath: string): void {
|
|
326
|
+
let entries: fs.Dirent[];
|
|
327
|
+
try {
|
|
328
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
329
|
+
} catch {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
for (const entry of entries) {
|
|
334
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
335
|
+
|
|
336
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
337
|
+
if (entry.isDirectory()) {
|
|
338
|
+
scanDirectoryRecursive(fullPath);
|
|
339
|
+
} else if (entry.isFile()) {
|
|
340
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
341
|
+
const textExts = new Set([
|
|
342
|
+
".txt", ".md", ".js", ".ts", ".jsx", ".tsx", ".py", ".rb", ".rs",
|
|
343
|
+
".go", ".java", ".c", ".cpp", ".h", ".hpp", ".css", ".html", ".xml",
|
|
344
|
+
".json", ".yaml", ".yml", ".toml", ".sh", ".bash", ".zsh", ".fish",
|
|
345
|
+
".sql", ".r", ".swift", ".kt", ".scala", ".lua", ".vim", ".el",
|
|
346
|
+
".clj", ".hs", ".ml", ".ex", ".exs", ".erl", ".dart", ".cs",
|
|
347
|
+
".php", ".pl", ".pm", ".svelte", ".vue", ".astro", ".mdx",
|
|
348
|
+
]);
|
|
349
|
+
if (!textExts.has(ext)) continue;
|
|
350
|
+
|
|
351
|
+
// Quick check if file contains @piqo
|
|
352
|
+
try {
|
|
353
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
354
|
+
if (content.includes("@piqo")) {
|
|
355
|
+
onFileChange(fullPath);
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
// skip unreadable files
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Cleanup watchers on shutdown
|
|
365
|
+
pi.on("session_shutdown", async () => {
|
|
366
|
+
for (const watcher of watchers) {
|
|
367
|
+
try {
|
|
368
|
+
watcher.close();
|
|
369
|
+
} catch {
|
|
370
|
+
// ignore
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
watchers.length = 0;
|
|
374
|
+
|
|
375
|
+
for (const timer of debounceTimers.values()) {
|
|
376
|
+
clearTimeout(timer);
|
|
377
|
+
}
|
|
378
|
+
debounceTimers.clear();
|
|
379
|
+
processing.clear();
|
|
380
|
+
|
|
381
|
+
console.log("[piqo] Watchers closed.");
|
|
382
|
+
});
|
|
383
|
+
}
|
package/package.json
ADDED