pi-doc-injector 0.1.4 → 0.2.0
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 +4 -1
- package/index.ts +60 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,7 +54,10 @@ keywords:
|
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
3. Start Pi. The extension scans `docs/` on session start.
|
|
57
|
-
4. When the
|
|
57
|
+
4. When the user mentions a keyword, the matching doc is injected into the
|
|
58
|
+
system prompt **before the assistant responds** — no one-turn delay.
|
|
59
|
+
5. If the assistant mentions a NEW keyword mid-response, generation is
|
|
60
|
+
automatically aborted and restarted with the doc injected immediately.
|
|
58
61
|
|
|
59
62
|
## Configuration
|
|
60
63
|
|
package/index.ts
CHANGED
|
@@ -69,6 +69,7 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
69
69
|
let enabled = true;
|
|
70
70
|
let textBuffer = "";
|
|
71
71
|
let pendingMatches = new Map<string, string[]>(); // filePath → matchedKeywords
|
|
72
|
+
let abortingForInjection = false; // guard against cascading aborts
|
|
72
73
|
|
|
73
74
|
// ---- Helpers ----
|
|
74
75
|
const getRegistry = () => registry;
|
|
@@ -97,8 +98,16 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
97
98
|
);
|
|
98
99
|
};
|
|
99
100
|
|
|
101
|
+
let lastInitTime = 0;
|
|
102
|
+
|
|
100
103
|
// ---- Event: session_start ----
|
|
104
|
+
// Pi fires session_start twice on startup (both with reason "startup").
|
|
105
|
+
// Use a 2-second dedup window to skip the duplicate. Real session changes
|
|
106
|
+
// (/new, /resume, /fork) happen well outside this window.
|
|
101
107
|
pi.on("session_start", async (_event, ctx) => {
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
if (now - lastInitTime < 100) return;
|
|
110
|
+
lastInitTime = now;
|
|
102
111
|
await initRegistry(ctx.cwd);
|
|
103
112
|
});
|
|
104
113
|
|
|
@@ -115,54 +124,65 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
115
124
|
await reloadRegistry();
|
|
116
125
|
});
|
|
117
126
|
|
|
118
|
-
// ---- Event:
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
pi.on("
|
|
127
|
+
// ---- Event: input (user message matching) ----
|
|
128
|
+
// message_update only fires for assistant streaming messages, not user
|
|
129
|
+
// messages. We use the input event instead to populate pendingMatches
|
|
130
|
+
// BEFORE before_agent_start fires, so docs are injected in time for
|
|
131
|
+
// the assistant's immediate response.
|
|
132
|
+
pi.on("input", async (event, _ctx) => {
|
|
133
|
+
if (!enabled || !registry) return;
|
|
134
|
+
if (!event.text) return;
|
|
135
|
+
|
|
136
|
+
const matcher = buildMatcher();
|
|
137
|
+
if (!matcher) return;
|
|
138
|
+
|
|
139
|
+
const results = matcher.match(event.text);
|
|
140
|
+
for (const result of results) {
|
|
141
|
+
pendingMatches.set(result.entry.filePath, result.matchedKeywords);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ---- Event: message_update (assistant streaming) ----
|
|
146
|
+
// For assistant streaming messages: if we detect NEW keyword matches for
|
|
147
|
+
// non-injected docs, abort the current generation and restart with the
|
|
148
|
+
// injected context — no waiting for the next turn.
|
|
149
|
+
pi.on("message_update", async (event, ctx) => {
|
|
124
150
|
if (!enabled || !registry) return;
|
|
125
151
|
|
|
126
|
-
// Only process assistant messages
|
|
127
152
|
const msg = event.message;
|
|
128
153
|
if (msg.role !== "assistant") return;
|
|
129
154
|
|
|
130
|
-
// Replace buffer with full message text (message_update contains full content)
|
|
131
155
|
const content = (msg as unknown as { content: unknown }).content;
|
|
132
156
|
textBuffer = extractText(content);
|
|
133
157
|
if (!textBuffer) return;
|
|
134
158
|
|
|
135
|
-
// Run matcher
|
|
136
159
|
const matcher = buildMatcher();
|
|
137
160
|
if (!matcher) return;
|
|
138
161
|
|
|
139
162
|
const results = matcher.match(textBuffer);
|
|
140
163
|
|
|
141
|
-
|
|
164
|
+
let hasNew = false;
|
|
142
165
|
for (const result of results) {
|
|
166
|
+
if (!pendingMatches.has(result.entry.filePath)) {
|
|
167
|
+
hasNew = true;
|
|
168
|
+
}
|
|
143
169
|
pendingMatches.set(result.entry.filePath, result.matchedKeywords);
|
|
144
170
|
}
|
|
171
|
+
|
|
172
|
+
if (hasNew && !ctx.isIdle() && !abortingForInjection) {
|
|
173
|
+
abortingForInjection = true;
|
|
174
|
+
ctx.abort();
|
|
175
|
+
}
|
|
145
176
|
});
|
|
146
177
|
|
|
147
178
|
// ---- Event: message_end (finalize matches) ----
|
|
148
|
-
|
|
179
|
+
// Notification moved to before_agent_start so it fires for both user-triggered
|
|
180
|
+
// and auto-abort-triggered injections. message_end now just resets state.
|
|
181
|
+
pi.on("message_end", async (event, _ctx) => {
|
|
149
182
|
if (!enabled || !registry) return;
|
|
150
|
-
|
|
151
183
|
const msg = event.message;
|
|
152
184
|
if (msg.role !== "assistant") return;
|
|
153
|
-
|
|
154
|
-
// Clear buffer
|
|
155
185
|
textBuffer = "";
|
|
156
|
-
|
|
157
|
-
// Notify user about pending injections
|
|
158
|
-
if (pendingMatches.size > 0) {
|
|
159
|
-
const matchedEntries: DocEntry[] = [];
|
|
160
|
-
for (const [filePath] of pendingMatches) {
|
|
161
|
-
const entry = registry.getEntries().find((e) => e.filePath === filePath);
|
|
162
|
-
if (entry) matchedEntries.push(entry);
|
|
163
|
-
}
|
|
164
|
-
notifyInjection(ctx.ui, matchedEntries, pendingMatches);
|
|
165
|
-
}
|
|
166
186
|
});
|
|
167
187
|
|
|
168
188
|
// ---- Event: before_agent_start (inject into system prompt) ----
|
|
@@ -194,6 +214,12 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
194
214
|
|
|
195
215
|
// Mark as injected only after confirming injection will happen
|
|
196
216
|
registry.markInjected(matchedEntries.map((e) => e.filePath));
|
|
217
|
+
|
|
218
|
+
// Notify user about injection (moved here from message_end so it fires
|
|
219
|
+
// even when matches come from user messages, which get cleared before
|
|
220
|
+
// the assistant's message_end)
|
|
221
|
+
notifyInjection(ctx.ui, matchedEntries, pendingMatches);
|
|
222
|
+
|
|
197
223
|
pendingMatches.clear();
|
|
198
224
|
|
|
199
225
|
return {
|
|
@@ -201,6 +227,16 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
201
227
|
};
|
|
202
228
|
});
|
|
203
229
|
|
|
230
|
+
// ---- Event: agent_end (restart after auto-abort) ----
|
|
231
|
+
pi.on("agent_end", async () => {
|
|
232
|
+
if (abortingForInjection) {
|
|
233
|
+
abortingForInjection = false;
|
|
234
|
+
// Send a follow-up message to restart the turn.
|
|
235
|
+
// before_agent_start will inject the matched docs into context.
|
|
236
|
+
pi.sendUserMessage("continue", { deliverAs: "followUp" });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
204
240
|
// ---- Commands ----
|
|
205
241
|
registerCommands(pi, {
|
|
206
242
|
getRegistry,
|