pi-doc-injector 0.1.4 → 0.2.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 +12 -2
- package/index.ts +70 -26
- 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
|
|
|
@@ -98,11 +101,18 @@ Injection is also skipped if the current context usage exceeds 80% of the token
|
|
|
98
101
|
|
|
99
102
|
The extension uses a per-session injection model:
|
|
100
103
|
|
|
101
|
-
- On `session_start`, the registry
|
|
104
|
+
- On `session_start`, the registry scans `docs/` and indexes all valid documents.
|
|
102
105
|
- Within a session, once a document is injected, it won't be re-injected automatically.
|
|
103
106
|
- Use `/doc-inject reset` to manually reset all flags and allow docs to be injected again.
|
|
104
107
|
- Use `/doc-inject list` to see which docs have been injected (✅) and which are pending (⬜).
|
|
105
108
|
|
|
109
|
+
### Injection Timing
|
|
110
|
+
|
|
111
|
+
- **User messages**: matched via the `input` event, injected before the assistant
|
|
112
|
+
responds — **same turn**, no delay.
|
|
113
|
+
- **Assistant streaming**: if the assistant mentions a NEW keyword mid-response,
|
|
114
|
+
generation is aborted and restarted with the doc injected immediately.
|
|
115
|
+
|
|
106
116
|
### System Prompt Lifecycle
|
|
107
117
|
|
|
108
118
|
Pi **reconstructs the system prompt from source files each turn** (verified against pi v0.70.6).
|
package/index.ts
CHANGED
|
@@ -66,9 +66,11 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
66
66
|
// ---- State ----
|
|
67
67
|
let config = loadConfig(process.cwd());
|
|
68
68
|
let registry: DocRegistry | null = null;
|
|
69
|
+
let initRegistryPromise: Promise<void> | null = null;
|
|
69
70
|
let enabled = true;
|
|
70
71
|
let textBuffer = "";
|
|
71
72
|
let pendingMatches = new Map<string, string[]>(); // filePath → matchedKeywords
|
|
73
|
+
let abortingForInjection = false; // guard against cascading aborts
|
|
72
74
|
|
|
73
75
|
// ---- Helpers ----
|
|
74
76
|
const getRegistry = () => registry;
|
|
@@ -98,8 +100,23 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
98
100
|
};
|
|
99
101
|
|
|
100
102
|
// ---- Event: session_start ----
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
// Pi emits session_start for startup, reload, and real session transitions.
|
|
104
|
+
// Skip the reload variant because resources_discover will rebuild docs right
|
|
105
|
+
// after it, and deduplicate any overlapping non-reload inits.
|
|
106
|
+
pi.on("session_start", async (event, ctx) => {
|
|
107
|
+
if (event.reason === "reload") return;
|
|
108
|
+
|
|
109
|
+
if (initRegistryPromise) {
|
|
110
|
+
await initRegistryPromise;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
initRegistryPromise = initRegistry(ctx.cwd);
|
|
115
|
+
try {
|
|
116
|
+
await initRegistryPromise;
|
|
117
|
+
} finally {
|
|
118
|
+
initRegistryPromise = null;
|
|
119
|
+
}
|
|
103
120
|
});
|
|
104
121
|
|
|
105
122
|
const reloadRegistry = async (): Promise<number> => {
|
|
@@ -115,54 +132,65 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
115
132
|
await reloadRegistry();
|
|
116
133
|
});
|
|
117
134
|
|
|
118
|
-
// ---- Event:
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
pi.on("
|
|
135
|
+
// ---- Event: input (user message matching) ----
|
|
136
|
+
// message_update only fires for assistant streaming messages, not user
|
|
137
|
+
// messages. We use the input event instead to populate pendingMatches
|
|
138
|
+
// BEFORE before_agent_start fires, so docs are injected in time for
|
|
139
|
+
// the assistant's immediate response.
|
|
140
|
+
pi.on("input", async (event, _ctx) => {
|
|
141
|
+
if (!enabled || !registry) return;
|
|
142
|
+
if (!event.text) return;
|
|
143
|
+
|
|
144
|
+
const matcher = buildMatcher();
|
|
145
|
+
if (!matcher) return;
|
|
146
|
+
|
|
147
|
+
const results = matcher.match(event.text);
|
|
148
|
+
for (const result of results) {
|
|
149
|
+
pendingMatches.set(result.entry.filePath, result.matchedKeywords);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---- Event: message_update (assistant streaming) ----
|
|
154
|
+
// For assistant streaming messages: if we detect NEW keyword matches for
|
|
155
|
+
// non-injected docs, abort the current generation and restart with the
|
|
156
|
+
// injected context — no waiting for the next turn.
|
|
157
|
+
pi.on("message_update", async (event, ctx) => {
|
|
124
158
|
if (!enabled || !registry) return;
|
|
125
159
|
|
|
126
|
-
// Only process assistant messages
|
|
127
160
|
const msg = event.message;
|
|
128
161
|
if (msg.role !== "assistant") return;
|
|
129
162
|
|
|
130
|
-
// Replace buffer with full message text (message_update contains full content)
|
|
131
163
|
const content = (msg as unknown as { content: unknown }).content;
|
|
132
164
|
textBuffer = extractText(content);
|
|
133
165
|
if (!textBuffer) return;
|
|
134
166
|
|
|
135
|
-
// Run matcher
|
|
136
167
|
const matcher = buildMatcher();
|
|
137
168
|
if (!matcher) return;
|
|
138
169
|
|
|
139
170
|
const results = matcher.match(textBuffer);
|
|
140
171
|
|
|
141
|
-
|
|
172
|
+
let hasNew = false;
|
|
142
173
|
for (const result of results) {
|
|
174
|
+
if (!pendingMatches.has(result.entry.filePath)) {
|
|
175
|
+
hasNew = true;
|
|
176
|
+
}
|
|
143
177
|
pendingMatches.set(result.entry.filePath, result.matchedKeywords);
|
|
144
178
|
}
|
|
179
|
+
|
|
180
|
+
if (hasNew && !ctx.isIdle() && !abortingForInjection) {
|
|
181
|
+
abortingForInjection = true;
|
|
182
|
+
ctx.abort();
|
|
183
|
+
}
|
|
145
184
|
});
|
|
146
185
|
|
|
147
186
|
// ---- Event: message_end (finalize matches) ----
|
|
148
|
-
|
|
187
|
+
// Notification moved to before_agent_start so it fires for both user-triggered
|
|
188
|
+
// and auto-abort-triggered injections. message_end now just resets state.
|
|
189
|
+
pi.on("message_end", async (event, _ctx) => {
|
|
149
190
|
if (!enabled || !registry) return;
|
|
150
|
-
|
|
151
191
|
const msg = event.message;
|
|
152
192
|
if (msg.role !== "assistant") return;
|
|
153
|
-
|
|
154
|
-
// Clear buffer
|
|
155
193
|
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
194
|
});
|
|
167
195
|
|
|
168
196
|
// ---- Event: before_agent_start (inject into system prompt) ----
|
|
@@ -194,6 +222,12 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
194
222
|
|
|
195
223
|
// Mark as injected only after confirming injection will happen
|
|
196
224
|
registry.markInjected(matchedEntries.map((e) => e.filePath));
|
|
225
|
+
|
|
226
|
+
// Notify user about injection (moved here from message_end so it fires
|
|
227
|
+
// even when matches come from user messages, which get cleared before
|
|
228
|
+
// the assistant's message_end)
|
|
229
|
+
notifyInjection(ctx.ui, matchedEntries, pendingMatches);
|
|
230
|
+
|
|
197
231
|
pendingMatches.clear();
|
|
198
232
|
|
|
199
233
|
return {
|
|
@@ -201,6 +235,16 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
201
235
|
};
|
|
202
236
|
});
|
|
203
237
|
|
|
238
|
+
// ---- Event: agent_end (restart after auto-abort) ----
|
|
239
|
+
pi.on("agent_end", async () => {
|
|
240
|
+
if (abortingForInjection) {
|
|
241
|
+
abortingForInjection = false;
|
|
242
|
+
// Send a follow-up message to restart the turn.
|
|
243
|
+
// before_agent_start will inject the matched docs into context.
|
|
244
|
+
pi.sendUserMessage("continue", { deliverAs: "followUp" });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
204
248
|
// ---- Commands ----
|
|
205
249
|
registerCommands(pi, {
|
|
206
250
|
getRegistry,
|