@alasano/pi-forcefeed 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 +70 -0
- package/assets/pi-forcefeed.png +0 -0
- package/assets/screenshot.png +0 -0
- package/extensions/index.ts +406 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# pi-forcefeed
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/pi-forcefeed.png" alt="pi-forcefeed" width="600" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="assets/screenshot.png" alt="pi-forcefeed screenshot" width="816" />
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
Force-feed complete files into [pi](https://pi.dev) conversation context without using the built-in `read` tool's truncation.
|
|
12
|
+
|
|
13
|
+
Use this when you intentionally want to put one or more whole files into the model context and are willing to own the provider/model context-window risk.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pi install npm:@alasano/pi-forcefeed
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
| Command | Description |
|
|
24
|
+
| ----------------------- | ---------------------------------------------------- |
|
|
25
|
+
| `/forcefeed <path...>` | Inject one or more complete files into context |
|
|
26
|
+
| `/forcefeed @<path...>` | Same command, with Pi's built-in `@` path completion |
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
/forcefeed README.md
|
|
32
|
+
/forcefeed @README.md @examples/harness/README.md
|
|
33
|
+
/forcefeed ./docs/notes.md /absolute/path/to/file.ts
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Behavior
|
|
37
|
+
|
|
38
|
+
- Accepts relative paths from the current Pi working directory.
|
|
39
|
+
- Accepts absolute paths.
|
|
40
|
+
- Accepts an optional leading `@` per path.
|
|
41
|
+
- Accepts quoted paths with spaces.
|
|
42
|
+
- Reads files as UTF-8 text.
|
|
43
|
+
- Sends one custom message containing all successfully read files.
|
|
44
|
+
- Renders one compact `[forcefeed]` line per file in the UI.
|
|
45
|
+
- Does not impose a file-size limit; provider/model context limits still apply.
|
|
46
|
+
|
|
47
|
+
The model receives start/end markers around every file:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
<<<PI_FORCEFEED_FILE_CONTENT_START path/to/file>>>
|
|
51
|
+
...
|
|
52
|
+
<<<PI_FORCEFEED_FILE_CONTENT_END path/to/file>>>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
If one file fails, `pi-forcefeed` still injects the files it could read and shows an error notification for the failed paths.
|
|
56
|
+
|
|
57
|
+
## Autocomplete
|
|
58
|
+
|
|
59
|
+
- `@` paths use Pi's built-in file autocomplete.
|
|
60
|
+
- Bare command arguments also provide simple path completion for `/forcefeed <path>`.
|
|
61
|
+
- Multi-path bare completion preserves earlier paths while completing the current path token.
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- Pi with extension support.
|
|
66
|
+
- Node.js 22 or newer.
|
|
67
|
+
|
|
68
|
+
## Limitations
|
|
69
|
+
|
|
70
|
+
`pi-forcefeed` bypasses Pi's `read` truncation, not model/provider context limits. Very large files or many files can still make a request too large, slow, or expensive.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
5
|
+
import type { AutocompleteItem } from '@mariozechner/pi-tui';
|
|
6
|
+
import { Text } from '@mariozechner/pi-tui';
|
|
7
|
+
|
|
8
|
+
const CUSTOM_TYPE = 'pi-forcefeed';
|
|
9
|
+
const MAX_COMPLETIONS = 50;
|
|
10
|
+
|
|
11
|
+
type ForcefeedFileDetails = {
|
|
12
|
+
path: string;
|
|
13
|
+
absolutePath: string;
|
|
14
|
+
bytes: number;
|
|
15
|
+
lines: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ForcefeedBatchDetails = {
|
|
19
|
+
files: ForcefeedFileDetails[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ForcefedFile = {
|
|
23
|
+
details: ForcefeedFileDetails;
|
|
24
|
+
content: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function stripOptionalAt(input: string): string {
|
|
28
|
+
const trimmed = input.trim();
|
|
29
|
+
return trimmed.startsWith('@') ? trimmed.slice(1).trimStart() : trimmed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function stripMatchingQuotes(input: string): string {
|
|
33
|
+
const trimmed = input.trim();
|
|
34
|
+
if (trimmed.length < 2) return trimmed;
|
|
35
|
+
|
|
36
|
+
const first = trimmed[0];
|
|
37
|
+
const last = trimmed[trimmed.length - 1];
|
|
38
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
39
|
+
return trimmed.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
return trimmed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizePathArg(args: string): string {
|
|
45
|
+
return stripMatchingQuotes(stripOptionalAt(args));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parsePathArgs(args: string): string[] {
|
|
49
|
+
const paths: string[] = [];
|
|
50
|
+
let current = '';
|
|
51
|
+
let quote: '"' | "'" | undefined;
|
|
52
|
+
let escaping = false;
|
|
53
|
+
|
|
54
|
+
for (const char of args) {
|
|
55
|
+
if (escaping) {
|
|
56
|
+
current += char;
|
|
57
|
+
escaping = false;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (char === '\\') {
|
|
62
|
+
escaping = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (quote) {
|
|
67
|
+
if (char === quote) {
|
|
68
|
+
quote = undefined;
|
|
69
|
+
} else {
|
|
70
|
+
current += char;
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (char === '"' || char === "'") {
|
|
76
|
+
quote = char;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (/\s/.test(char)) {
|
|
81
|
+
const normalized = normalizePathArg(current);
|
|
82
|
+
if (normalized) paths.push(normalized);
|
|
83
|
+
current = '';
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
current += char;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (escaping) current += '\\';
|
|
91
|
+
|
|
92
|
+
const normalized = normalizePathArg(current);
|
|
93
|
+
if (normalized) paths.push(normalized);
|
|
94
|
+
|
|
95
|
+
return paths;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function expandHomePath(path: string): string {
|
|
99
|
+
if (path === '~') return homedir();
|
|
100
|
+
if (path.startsWith('~/')) return join(homedir(), path.slice(2));
|
|
101
|
+
return path;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveInputPath(inputPath: string, cwd: string): string {
|
|
105
|
+
const expanded = expandHomePath(inputPath);
|
|
106
|
+
return isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toDisplayPath(path: string): string {
|
|
110
|
+
return path.split(sep).join('/');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function displayPath(absolutePath: string, cwd: string): string {
|
|
114
|
+
const rel = relative(cwd, absolutePath);
|
|
115
|
+
if (rel && !rel.startsWith('..') && !isAbsolute(rel)) {
|
|
116
|
+
return toDisplayPath(rel);
|
|
117
|
+
}
|
|
118
|
+
return toDisplayPath(absolutePath);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function completionDisplayPath(absolutePath: string, cwd: string, rawPrefix: string): string {
|
|
122
|
+
const home = homedir();
|
|
123
|
+
|
|
124
|
+
if (rawPrefix.startsWith('/')) {
|
|
125
|
+
return toDisplayPath(absolutePath);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (rawPrefix.startsWith('~')) {
|
|
129
|
+
const relHome = relative(home, absolutePath);
|
|
130
|
+
if (!relHome.startsWith('..') && !isAbsolute(relHome)) {
|
|
131
|
+
return relHome ? `~/${toDisplayPath(relHome)}` : '~';
|
|
132
|
+
}
|
|
133
|
+
return toDisplayPath(absolutePath);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return displayPath(absolutePath, cwd);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function countLines(content: string): number {
|
|
140
|
+
if (content.length === 0) return 0;
|
|
141
|
+
return content.split(/\r\n|\r|\n/).length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function formatByteSize(bytes: number): string {
|
|
145
|
+
if (bytes < 1024) {
|
|
146
|
+
return `${bytes.toLocaleString()} ${bytes === 1 ? 'byte' : 'bytes'}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const units = ['KB', 'MB', 'GB', 'TB'] as const;
|
|
150
|
+
let value = bytes / 1024;
|
|
151
|
+
let unitIndex = 0;
|
|
152
|
+
|
|
153
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
154
|
+
value /= 1024;
|
|
155
|
+
unitIndex += 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const displayValue = (value >= 100 ? value.toFixed(0) : value.toFixed(1)).replace(/\.0$/, '');
|
|
159
|
+
return `${displayValue} ${units[unitIndex]}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatLineCount(lines: number): string {
|
|
163
|
+
return `${lines.toLocaleString()} ${lines === 1 ? 'line' : 'lines'}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function formatFileStats(details: Pick<ForcefeedFileDetails, 'bytes' | 'lines'>): string {
|
|
167
|
+
return `${formatByteSize(details.bytes)}, ${formatLineCount(details.lines)}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function quoteCompletionValue(value: string): string {
|
|
171
|
+
if (!/[\s"']/.test(value)) return value;
|
|
172
|
+
return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseCompletionPrefix(argumentPrefix: string): string {
|
|
176
|
+
return stripOptionalAt(argumentPrefix.trimStart()).replace(/^['"]/, '');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function splitCompletionArgument(argumentPrefix: string): {
|
|
180
|
+
leading: string;
|
|
181
|
+
token: string;
|
|
182
|
+
} {
|
|
183
|
+
let quote: '"' | "'" | undefined;
|
|
184
|
+
let escaping = false;
|
|
185
|
+
let tokenStart = 0;
|
|
186
|
+
|
|
187
|
+
for (let i = 0; i < argumentPrefix.length; i += 1) {
|
|
188
|
+
const char = argumentPrefix[i];
|
|
189
|
+
if (char === undefined) continue;
|
|
190
|
+
|
|
191
|
+
if (escaping) {
|
|
192
|
+
escaping = false;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (char === '\\') {
|
|
197
|
+
escaping = true;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (quote) {
|
|
202
|
+
if (char === quote) quote = undefined;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (char === '"' || char === "'") {
|
|
207
|
+
quote = char;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (/\s/.test(char)) {
|
|
212
|
+
tokenStart = i + 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
leading: argumentPrefix.slice(0, tokenStart),
|
|
218
|
+
token: argumentPrefix.slice(tokenStart),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function getPathCompletions(
|
|
223
|
+
argumentPrefix: string,
|
|
224
|
+
cwd: string,
|
|
225
|
+
): Promise<AutocompleteItem[] | null> {
|
|
226
|
+
const { leading, token } = splitCompletionArgument(argumentPrefix);
|
|
227
|
+
const rawPrefix = parseCompletionPrefix(token);
|
|
228
|
+
const expandedPrefix = expandHomePath(rawPrefix);
|
|
229
|
+
|
|
230
|
+
const searchDir =
|
|
231
|
+
rawPrefix.endsWith('/') || rawPrefix === '' ? expandedPrefix : dirname(expandedPrefix);
|
|
232
|
+
const searchName = rawPrefix.endsWith('/') ? '' : basename(expandedPrefix);
|
|
233
|
+
const absoluteSearchDir = isAbsolute(searchDir) ? searchDir : resolve(cwd, searchDir || '.');
|
|
234
|
+
|
|
235
|
+
let entries;
|
|
236
|
+
try {
|
|
237
|
+
entries = await readdir(absoluteSearchDir, { withFileTypes: true });
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const items: AutocompleteItem[] = [];
|
|
243
|
+
const lowerSearchName = searchName.toLowerCase();
|
|
244
|
+
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
if (!entry.name.toLowerCase().startsWith(lowerSearchName)) continue;
|
|
247
|
+
|
|
248
|
+
const absoluteEntryPath = join(absoluteSearchDir, entry.name);
|
|
249
|
+
let isDirectory = entry.isDirectory();
|
|
250
|
+
if (!isDirectory && entry.isSymbolicLink()) {
|
|
251
|
+
try {
|
|
252
|
+
isDirectory = (await stat(absoluteEntryPath)).isDirectory();
|
|
253
|
+
} catch {
|
|
254
|
+
isDirectory = false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const completionPath =
|
|
259
|
+
completionDisplayPath(absoluteEntryPath, cwd, rawPrefix) + (isDirectory ? '/' : '');
|
|
260
|
+
items.push({
|
|
261
|
+
value: leading + quoteCompletionValue(completionPath),
|
|
262
|
+
label: `${entry.name}${isDirectory ? '/' : ''}`,
|
|
263
|
+
description: completionPath,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
items.sort((a, b) => {
|
|
268
|
+
const aDir = a.label.endsWith('/');
|
|
269
|
+
const bDir = b.label.endsWith('/');
|
|
270
|
+
if (aDir && !bDir) return -1;
|
|
271
|
+
if (!aDir && bDir) return 1;
|
|
272
|
+
return a.label.localeCompare(b.label);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return items.slice(0, MAX_COMPLETIONS);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildForcefeedContent(files: ForcefedFile[]): string {
|
|
279
|
+
const header =
|
|
280
|
+
files.length === 1
|
|
281
|
+
? '[pi-forcefeed] The user force-fed this complete file into context.'
|
|
282
|
+
: `[pi-forcefeed] The user force-fed these ${files.length} complete files into context.`;
|
|
283
|
+
|
|
284
|
+
return [
|
|
285
|
+
header,
|
|
286
|
+
'Treat the text below as the exact full file contents for any subsequent task.',
|
|
287
|
+
...files.flatMap((file, index) => [
|
|
288
|
+
'',
|
|
289
|
+
`File ${index + 1} of ${files.length}`,
|
|
290
|
+
`Path: ${file.details.path}`,
|
|
291
|
+
`Absolute path: ${file.details.absolutePath}`,
|
|
292
|
+
`Size: ${formatFileStats(file.details)}`,
|
|
293
|
+
'',
|
|
294
|
+
`<<<PI_FORCEFEED_FILE_CONTENT_START ${file.details.path}>>>`,
|
|
295
|
+
file.content,
|
|
296
|
+
`<<<PI_FORCEFEED_FILE_CONTENT_END ${file.details.path}>>>`,
|
|
297
|
+
]),
|
|
298
|
+
].join('\n');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getRenderedFiles(details: unknown): ForcefeedFileDetails[] {
|
|
302
|
+
if (!details || typeof details !== 'object') return [];
|
|
303
|
+
|
|
304
|
+
const maybeBatch = details as Partial<ForcefeedBatchDetails>;
|
|
305
|
+
if (Array.isArray(maybeBatch.files)) return maybeBatch.files;
|
|
306
|
+
|
|
307
|
+
const maybeFile = details as Partial<ForcefeedFileDetails>;
|
|
308
|
+
if (
|
|
309
|
+
typeof maybeFile.path === 'string' &&
|
|
310
|
+
typeof maybeFile.absolutePath === 'string' &&
|
|
311
|
+
typeof maybeFile.bytes === 'number' &&
|
|
312
|
+
typeof maybeFile.lines === 'number'
|
|
313
|
+
) {
|
|
314
|
+
return [maybeFile as ForcefeedFileDetails];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export default function forcefeed(pi: ExtensionAPI) {
|
|
321
|
+
let currentCwd = process.cwd();
|
|
322
|
+
|
|
323
|
+
pi.on('session_start', (_event, ctx) => {
|
|
324
|
+
currentCwd = ctx.cwd;
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
pi.registerMessageRenderer(CUSTOM_TYPE, (message, _options, theme) => {
|
|
328
|
+
const files = getRenderedFiles(message.details);
|
|
329
|
+
const content =
|
|
330
|
+
files
|
|
331
|
+
.map((details) => {
|
|
332
|
+
const size = formatFileStats(details);
|
|
333
|
+
return `${theme.fg('customMessageLabel', theme.bold('[forcefeed] '))}${theme.fg(
|
|
334
|
+
'customMessageText',
|
|
335
|
+
details.path,
|
|
336
|
+
)} ${theme.fg('dim', `(${size}; full content injected into context)`)}`;
|
|
337
|
+
})
|
|
338
|
+
.join('\n') ||
|
|
339
|
+
`${theme.fg('customMessageLabel', theme.bold('[forcefeed] '))}${theme.fg(
|
|
340
|
+
'customMessageText',
|
|
341
|
+
'unknown file',
|
|
342
|
+
)} ${theme.fg('dim', '(unknown size; full content injected into context)')}`;
|
|
343
|
+
|
|
344
|
+
return new Text(content, 0, 0);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
pi.registerCommand('forcefeed', {
|
|
348
|
+
description:
|
|
349
|
+
'Inject one or more complete files into conversation context without read-tool truncation',
|
|
350
|
+
getArgumentCompletions: (argumentPrefix) => getPathCompletions(argumentPrefix, currentCwd),
|
|
351
|
+
handler: async (args, ctx) => {
|
|
352
|
+
const inputPaths = parsePathArgs(args);
|
|
353
|
+
if (inputPaths.length === 0) {
|
|
354
|
+
ctx.ui.notify(
|
|
355
|
+
'Usage: /forcefeed <path> [more paths...] or /forcefeed @<path> @<other-path>',
|
|
356
|
+
'warning',
|
|
357
|
+
);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const failures: string[] = [];
|
|
362
|
+
const files: ForcefedFile[] = [];
|
|
363
|
+
|
|
364
|
+
for (const inputPath of inputPaths) {
|
|
365
|
+
const absolutePath = resolveInputPath(inputPath, ctx.cwd);
|
|
366
|
+
let buffer: Buffer;
|
|
367
|
+
try {
|
|
368
|
+
buffer = await readFile(absolutePath);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
371
|
+
failures.push(`${inputPath}: ${message}`);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const content = buffer.toString('utf8');
|
|
376
|
+
const details: ForcefeedFileDetails = {
|
|
377
|
+
path: displayPath(absolutePath, ctx.cwd),
|
|
378
|
+
absolutePath: toDisplayPath(absolutePath),
|
|
379
|
+
bytes: buffer.byteLength,
|
|
380
|
+
lines: countLines(content),
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
files.push({ details, content });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (files.length > 0) {
|
|
387
|
+
pi.sendMessage(
|
|
388
|
+
{
|
|
389
|
+
customType: CUSTOM_TYPE,
|
|
390
|
+
content: buildForcefeedContent(files),
|
|
391
|
+
display: true,
|
|
392
|
+
details: { files: files.map((file) => file.details) },
|
|
393
|
+
},
|
|
394
|
+
{ deliverAs: 'steer' },
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (failures.length > 0) {
|
|
399
|
+
ctx.ui.notify(
|
|
400
|
+
`forcefeed failed for ${failures.length} file(s):\n${failures.join('\n')}`,
|
|
401
|
+
'error',
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alasano/pi-forcefeed",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Force-feed complete files into pi conversation context without read-tool truncation",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package"
|
|
7
|
+
],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/alasano/house-of-pi",
|
|
12
|
+
"directory": "packages/pi-forcefeed"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
20
|
+
},
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./extensions"
|
|
24
|
+
],
|
|
25
|
+
"image": "https://raw.githubusercontent.com/alasano/house-of-pi/master/packages/pi-forcefeed/assets/pi-forcefeed.png"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"extensions",
|
|
29
|
+
"assets",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
34
|
+
"@mariozechner/pi-tui": "*"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@mariozechner/pi-coding-agent": "^0.73.0",
|
|
38
|
+
"@mariozechner/pi-tui": "^0.73.0"
|
|
39
|
+
}
|
|
40
|
+
}
|