pi-doc-injector 0.1.0 → 0.1.2
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 +30 -16
- package/commands.ts +27 -34
- package/config.ts +17 -1
- package/docs/bun.md +32 -0
- package/docs/publish.md +80 -0
- package/index.ts +59 -19
- package/injector.ts +11 -3
- package/package.json +23 -4
- package/registry.ts +74 -14
- package/types.ts +5 -0
- package/docs/publish-md.md +0 -55
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ pi install npm:pi-doc-injector
|
|
|
13
13
|
### Via git
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
pi install git:github.com/
|
|
16
|
+
pi install git:github.com/lmn451/pi-doc-injector
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
### Manual
|
|
@@ -36,6 +36,7 @@ keywords: [test, testing, jest, vitest]
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
38
|
# Testing Workflow
|
|
39
|
+
|
|
39
40
|
Your documentation here...
|
|
40
41
|
```
|
|
41
42
|
|
|
@@ -57,19 +58,23 @@ keywords:
|
|
|
57
58
|
|
|
58
59
|
## Configuration
|
|
59
60
|
|
|
60
|
-
Create `.pi/doc-injector.json` to customize behavior:
|
|
61
|
+
Create `.pi/doc-injector.json` in your project root to customize behavior:
|
|
61
62
|
|
|
62
63
|
```json
|
|
63
64
|
{
|
|
64
65
|
"docsPath": "./docs",
|
|
65
|
-
"matchThreshold": 2
|
|
66
|
+
"matchThreshold": 2,
|
|
67
|
+
"contextThreshold": 80,
|
|
68
|
+
"recursive": true
|
|
66
69
|
}
|
|
67
70
|
```
|
|
68
71
|
|
|
69
|
-
| Option
|
|
70
|
-
|
|
71
|
-
| `docsPath`
|
|
72
|
-
| `matchThreshold`
|
|
72
|
+
| Option | Default | Description |
|
|
73
|
+
| ------------------ | ---------- | -------------------------------------------------------- |
|
|
74
|
+
| `docsPath` | `"./docs"` | Path to docs folder (relative to project root) |
|
|
75
|
+
| `matchThreshold` | `2` | Minimum keyword matches required to inject a doc |
|
|
76
|
+
| `contextThreshold` | `80` | Skip injection when context usage exceeds this % (0–100) |
|
|
77
|
+
| `recursive` | `true` | Scan docs subdirectories recursively |
|
|
73
78
|
|
|
74
79
|
### Keyword Matching
|
|
75
80
|
|
|
@@ -79,15 +84,24 @@ Injection is also skipped if the current context usage exceeds 80% of the token
|
|
|
79
84
|
|
|
80
85
|
## Commands
|
|
81
86
|
|
|
82
|
-
| Command
|
|
83
|
-
|
|
84
|
-
| `/doc-inject on`
|
|
85
|
-
| `/doc-inject off`
|
|
86
|
-
| `/doc-inject toggle` | Toggle on/off
|
|
87
|
-
| `/doc-inject
|
|
88
|
-
| `/doc-inject
|
|
89
|
-
| `/doc-inject
|
|
90
|
-
| `/doc-reload`
|
|
87
|
+
| Command | Description |
|
|
88
|
+
| -------------------- | ---------------------------------------------------- |
|
|
89
|
+
| `/doc-inject on` | Enable doc injection |
|
|
90
|
+
| `/doc-inject off` | Disable doc injection |
|
|
91
|
+
| `/doc-inject toggle` | Toggle doc injection on/off |
|
|
92
|
+
| `/doc-inject list` | List all registered docs and their injection status |
|
|
93
|
+
| `/doc-inject reset` | Reset all injected flags (docs become re-injectable) |
|
|
94
|
+
| `/doc-inject status` | Show current injection status and config |
|
|
95
|
+
| `/doc-reload` | Re-scan docs folder and rebuild registry |
|
|
96
|
+
|
|
97
|
+
## Injection Lifecycle
|
|
98
|
+
|
|
99
|
+
The extension uses a per-session injection model:
|
|
100
|
+
|
|
101
|
+
- On `session_start`, the registry is rebuilt from scratch, resetting all `injected` flags.
|
|
102
|
+
- Within a session, once a document is injected, it won't be re-injected automatically.
|
|
103
|
+
- Use `/doc-inject reset` to manually reset all flags and allow docs to be injected again.
|
|
104
|
+
- Use `/doc-inject list` to see which docs have been injected (✅) and which are pending (⬜).
|
|
91
105
|
|
|
92
106
|
## Development
|
|
93
107
|
|
package/commands.ts
CHANGED
|
@@ -4,38 +4,40 @@
|
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
5
|
import type { DocRegistry } from "./registry";
|
|
6
6
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
export interface CommandDeps {
|
|
8
|
+
getRegistry: () => DocRegistry | null;
|
|
9
|
+
getEnabled: () => boolean;
|
|
10
|
+
setEnabled: (v: boolean) => void;
|
|
11
|
+
reloadRegistry: () => Promise<number>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function registerCommands(pi: ExtensionAPI, deps: CommandDeps): void {
|
|
15
|
+
const cmd = (name: string, desc: string, handler: (args: string, ctx: ExtensionContext) => Promise<void>) => {
|
|
14
16
|
pi.registerCommand(name, { description: desc, handler });
|
|
15
17
|
};
|
|
16
18
|
|
|
17
|
-
cmd("doc-inject", "Doc injector: on|off|toggle|list|reset|status", (args, ctx) => {
|
|
19
|
+
cmd("doc-inject", "Doc injector: on|off|toggle|list|reset|status", async (args, ctx) => {
|
|
18
20
|
const a = args.trim().toLowerCase();
|
|
19
21
|
if (a === "on") {
|
|
20
|
-
setEnabled(true);
|
|
21
|
-
ctx.ui.notify("📄 Doc injection enabled", "
|
|
22
|
+
deps.setEnabled(true);
|
|
23
|
+
ctx.ui.notify("📄 Doc injection enabled", "info");
|
|
22
24
|
} else if (a === "off") {
|
|
23
|
-
setEnabled(false);
|
|
25
|
+
deps.setEnabled(false);
|
|
24
26
|
ctx.ui.notify("📄 Doc injection disabled", "warning");
|
|
25
27
|
} else if (a === "toggle") {
|
|
26
|
-
const next = !getEnabled();
|
|
27
|
-
setEnabled(next);
|
|
28
|
+
const next = !deps.getEnabled();
|
|
29
|
+
deps.setEnabled(next);
|
|
28
30
|
ctx.ui.notify(`📄 Doc injection ${next ? "enabled" : "disabled"}`, "info");
|
|
29
31
|
} else if (a === "reset") {
|
|
30
|
-
const reg = getRegistry();
|
|
32
|
+
const reg = deps.getRegistry();
|
|
31
33
|
if (reg) {
|
|
32
34
|
reg.reset();
|
|
33
|
-
ctx.ui.notify("📄 Injection state reset", "
|
|
35
|
+
ctx.ui.notify("📄 Injection state reset", "info");
|
|
34
36
|
} else {
|
|
35
37
|
ctx.ui.notify("📄 No registry loaded", "warning");
|
|
36
38
|
}
|
|
37
39
|
} else if (a === "list") {
|
|
38
|
-
const reg = getRegistry();
|
|
40
|
+
const reg = deps.getRegistry();
|
|
39
41
|
if (!reg) {
|
|
40
42
|
ctx.ui.notify("📄 No docs loaded", "warning");
|
|
41
43
|
return;
|
|
@@ -47,12 +49,12 @@ export function registerCommands(
|
|
|
47
49
|
}
|
|
48
50
|
const lines = entries.map((e) => {
|
|
49
51
|
const status = e.injected ? "✅" : "⬜";
|
|
50
|
-
return `${status} ${e.
|
|
52
|
+
return `${status} ${e.relativePath}: "${e.title}" — keywords: [${e.keywords.join(", ")}]`;
|
|
51
53
|
});
|
|
52
54
|
ctx.ui.notify(`📄 Registered docs:\n${lines.join("\n")}`, "info");
|
|
53
55
|
} else {
|
|
54
56
|
// status (default)
|
|
55
|
-
const reg = getRegistry();
|
|
57
|
+
const reg = deps.getRegistry();
|
|
56
58
|
if (!reg) {
|
|
57
59
|
ctx.ui.notify("📄 Status: No registry loaded", "warning");
|
|
58
60
|
return;
|
|
@@ -62,7 +64,7 @@ export function registerCommands(
|
|
|
62
64
|
const kwCount = entries.reduce((sum, e) => sum + e.keywords.length, 0);
|
|
63
65
|
ctx.ui.notify(
|
|
64
66
|
`📄 Doc Injector Status:\n` +
|
|
65
|
-
` Enabled: ${getEnabled() ? "✅" : "❌"}\n` +
|
|
67
|
+
` Enabled: ${deps.getEnabled() ? "✅" : "❌"}\n` +
|
|
66
68
|
` Docs: ${entries.length}\n` +
|
|
67
69
|
` Keywords: ${kwCount}\n` +
|
|
68
70
|
` Injected: ${injected}`,
|
|
@@ -71,21 +73,12 @@ export function registerCommands(
|
|
|
71
73
|
}
|
|
72
74
|
});
|
|
73
75
|
|
|
74
|
-
cmd("doc-reload", "Re-scan docs folder and rebuild registry", (_args, ctx) => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
ctx.ui.notify(
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
// We can't call rebuild() async from a command handler easily,
|
|
81
|
-
// so we notify and trigger via the event system
|
|
82
|
-
ctx.ui.notify("📄 Triggering docs reload…", "info");
|
|
83
|
-
// Trigger a resources_discover-like reload by rebuilding directly
|
|
84
|
-
reg.rebuild().then(() => {
|
|
85
|
-
const count = reg.getEntries().length;
|
|
86
|
-
ctx.ui.notify(`📄 Reloaded: ${count} documents found`, "success");
|
|
87
|
-
}).catch((err) => {
|
|
76
|
+
cmd("doc-reload", "Re-scan docs folder and rebuild registry", async (_args, ctx) => {
|
|
77
|
+
try {
|
|
78
|
+
const count = await deps.reloadRegistry();
|
|
79
|
+
ctx.ui.notify(`📄 Reloaded: ${count} documents found`, "info");
|
|
80
|
+
} catch (err) {
|
|
88
81
|
ctx.ui.notify(`📄 Reload failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
89
|
-
}
|
|
82
|
+
}
|
|
90
83
|
});
|
|
91
84
|
}
|
package/config.ts
CHANGED
|
@@ -21,9 +21,25 @@ export function loadConfig(cwd: string): DocInjectorConfig {
|
|
|
21
21
|
const raw = readFileSync(configPath, "utf-8");
|
|
22
22
|
const parsed = JSON.parse(raw) as Partial<DocInjectorConfig>;
|
|
23
23
|
|
|
24
|
+
// Clamp contextThreshold to 0-100 range
|
|
25
|
+
let contextThreshold = parsed.contextThreshold ?? DEFAULT_CONFIG.contextThreshold;
|
|
26
|
+
if (typeof contextThreshold === "number" && (contextThreshold < 0 || contextThreshold > 100)) {
|
|
27
|
+
console.warn(`[doc-injector] contextThreshold must be 0-100, got ${contextThreshold}. Clamping.`);
|
|
28
|
+
contextThreshold = Math.max(0, Math.min(100, contextThreshold));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Clamp matchThreshold to positive integers
|
|
32
|
+
let matchThreshold = parsed.matchThreshold ?? DEFAULT_CONFIG.matchThreshold;
|
|
33
|
+
if (typeof matchThreshold === "number" && matchThreshold < 1) {
|
|
34
|
+
console.warn(`[doc-injector] matchThreshold must be >= 1, got ${matchThreshold}. Using 1.`);
|
|
35
|
+
matchThreshold = 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
return {
|
|
25
39
|
docsPath: parsed.docsPath ?? DEFAULT_CONFIG.docsPath,
|
|
26
|
-
matchThreshold
|
|
40
|
+
matchThreshold,
|
|
41
|
+
contextThreshold,
|
|
42
|
+
recursive: parsed.recursive ?? DEFAULT_CONFIG.recursive,
|
|
27
43
|
};
|
|
28
44
|
} catch (err) {
|
|
29
45
|
console.warn(
|
package/docs/bun.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "JavaScript Runtime & Package Manager"
|
|
3
|
+
keywords: [bun, package manager, runtime, install, test, build]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# JavaScript Runtime & Package Manager
|
|
7
|
+
|
|
8
|
+
This project uses **Bun** as its JavaScript runtime and package manager.
|
|
9
|
+
|
|
10
|
+
## Why Bun?
|
|
11
|
+
|
|
12
|
+
- Fast native TypeScript support (no separate tsc step needed)
|
|
13
|
+
- All-in-one runtime, bundler, and test runner
|
|
14
|
+
- Drop-in replacement for Node.js and npm/yarn/pnpm
|
|
15
|
+
- Significantly faster installs and test executions
|
|
16
|
+
|
|
17
|
+
## Common Commands
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Install dependencies
|
|
21
|
+
bun install
|
|
22
|
+
|
|
23
|
+
# Run tests
|
|
24
|
+
bun test
|
|
25
|
+
|
|
26
|
+
# Run tests in watch mode (recommended for development)
|
|
27
|
+
bun test --watch
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Version
|
|
31
|
+
|
|
32
|
+
This project requires Bun >= 1.3.0. The lockfile (`bun.lock`) ensures reproducible installs.
|
package/docs/publish.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Publishing Workflow"
|
|
3
|
+
keywords: [publish, release, npm, version, tag, semantic versioning]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Publishing Workflow
|
|
7
|
+
|
|
8
|
+
## Versioning
|
|
9
|
+
|
|
10
|
+
We follow [Semantic Versioning](https://semver.org/):
|
|
11
|
+
- **MAJOR** — incompatible API changes
|
|
12
|
+
- **MINOR** — backwards-compatible functionality additions
|
|
13
|
+
- **PATCH** — backwards-compatible bug fixes
|
|
14
|
+
|
|
15
|
+
## Publishing a New Version
|
|
16
|
+
|
|
17
|
+
### 1. Bump the version
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm version patch # 0.1.1 → 0.1.2
|
|
21
|
+
npm version minor # 0.1.1 → 0.2.0
|
|
22
|
+
npm version major # 0.1.1 → 1.0.0
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This updates `package.json` and creates a local tag.
|
|
26
|
+
|
|
27
|
+
### 2. Push the tag to trigger the workflow
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git push origin v0.1.1
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or push all tags:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git push origin --tags
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Important:** The publish workflow triggers on tags pushed to remote, not just locally created tags. The tag must match the pattern `v*` (e.g., `v0.1.1`, `v0.2.0`).
|
|
40
|
+
|
|
41
|
+
### 3. GitHub Actions Publish workflow
|
|
42
|
+
|
|
43
|
+
Once the tag is pushed, the `Publish` workflow automatically:
|
|
44
|
+
- Runs tests
|
|
45
|
+
- Publishes to npm registry
|
|
46
|
+
|
|
47
|
+
Monitor the workflow at: `https://github.com/lmn451/pi-docs/actions`
|
|
48
|
+
|
|
49
|
+
## Verify the Publish
|
|
50
|
+
|
|
51
|
+
Check if the package was published:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm view pi-doc-injector
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Manual Publish
|
|
58
|
+
|
|
59
|
+
If needed, you can publish manually:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm publish
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Setup
|
|
66
|
+
|
|
67
|
+
Ensure your npm token is configured as a GitHub secret:
|
|
68
|
+
- Go to repository Settings → Secrets and variables → Actions
|
|
69
|
+
- Add a new secret named `NPM_TOKEN` with your npm access token
|
|
70
|
+
|
|
71
|
+
## Troubleshooting
|
|
72
|
+
|
|
73
|
+
**Workflow didn't run?**
|
|
74
|
+
- Verify the tag exists remotely: `git ls-remote origin refs/tags/v0.1.1`
|
|
75
|
+
- Check that the tag matches the pattern `v*`
|
|
76
|
+
- Check GitHub Actions runs at: `https://github.com/lmn451/pi-docs/actions`
|
|
77
|
+
|
|
78
|
+
**npm publish failed?**
|
|
79
|
+
- Ensure `NPM_TOKEN` secret is set
|
|
80
|
+
- Verify the version hasn't already been published
|
package/index.ts
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Automatically injects relevant project documentation into the LLM context
|
|
5
5
|
* by monitoring streaming output for keyword matches.
|
|
6
|
+
*
|
|
7
|
+
* ## Streaming Model
|
|
8
|
+
*
|
|
9
|
+
* This extension relies on Pi's streaming event contract:
|
|
10
|
+
* - `message_update`: Fires with the FULL accumulated assistant content on each
|
|
11
|
+
* streaming chunk. The extension replaces (not appends to) its text buffer
|
|
12
|
+
* on each update.
|
|
13
|
+
* - `message_end`: Fires once when the assistant's response is complete.
|
|
14
|
+
* The extension finalizes matches and notifies the user.
|
|
15
|
+
* - `before_agent_start`: Fires before the next agent turn. The extension
|
|
16
|
+
* injects matched docs into the system prompt, then marks them as injected.
|
|
17
|
+
*
|
|
18
|
+
* ## Injection Lifecycle
|
|
19
|
+
*
|
|
20
|
+
* The `injected` flag is per-session: when `session_start` fires, the registry
|
|
21
|
+
* is recreated from scratch (via initRegistry), resetting all flags. Within a
|
|
22
|
+
* session, once a doc is injected, it won't be re-injected unless the user
|
|
23
|
+
* manually runs `/doc-inject reset`.
|
|
24
|
+
*
|
|
25
|
+
* ## Race Condition Note
|
|
26
|
+
*
|
|
27
|
+
* If `resources_discover` (rebuild) fires while `before_agent_start` is running,
|
|
28
|
+
* `registry.entries` gets replaced. The `matchedEntries` array would hold stale
|
|
29
|
+
* references. The current code is safe because `pendingMatches` (a Map by filePath)
|
|
30
|
+
* is cleared after injection, and `markInjected()` operates on the registry's
|
|
31
|
+
* current entries, not the stale array.
|
|
6
32
|
*/
|
|
7
33
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
34
|
import { resolve } from "node:path";
|
|
@@ -31,7 +57,7 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
31
57
|
const initRegistry = async (cwd: string) => {
|
|
32
58
|
config = loadConfig(cwd);
|
|
33
59
|
const docsPath = resolve(cwd, config.docsPath);
|
|
34
|
-
registry = await DocRegistry.create(docsPath);
|
|
60
|
+
registry = await DocRegistry.create(docsPath, config.recursive);
|
|
35
61
|
const count = registry.getEntries().length;
|
|
36
62
|
if (count > 0) {
|
|
37
63
|
console.log(`[doc-injector] Loaded ${count} documents from ${docsPath}`);
|
|
@@ -53,25 +79,34 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
53
79
|
await initRegistry(ctx.cwd);
|
|
54
80
|
});
|
|
55
81
|
|
|
82
|
+
const reloadRegistry = async (): Promise<number> => {
|
|
83
|
+
if (!registry) throw new Error("No registry loaded");
|
|
84
|
+
await registry.rebuild();
|
|
85
|
+
const count = registry.getEntries().length;
|
|
86
|
+
console.log(`[doc-injector] Reloaded: ${count} documents`);
|
|
87
|
+
return count;
|
|
88
|
+
};
|
|
89
|
+
|
|
56
90
|
// ---- Event: resources_discover (reload) ----
|
|
57
|
-
pi.on("resources_discover", async (_event,
|
|
58
|
-
|
|
59
|
-
await registry.rebuild();
|
|
60
|
-
const count = registry.getEntries().length;
|
|
61
|
-
console.log(`[doc-injector] Reloaded: ${count} documents`);
|
|
62
|
-
}
|
|
91
|
+
pi.on("resources_discover", async (_event, _ctx) => {
|
|
92
|
+
await reloadRegistry();
|
|
63
93
|
});
|
|
64
94
|
|
|
65
95
|
// ---- Event: message_update (streaming detection) ----
|
|
96
|
+
// NOTE: Pi's message_update event sends the full accumulated content of the
|
|
97
|
+
// assistant message on each update, not just the delta. We therefore REPLACE
|
|
98
|
+
// (not append to) the text buffer on each event, ensuring we always match
|
|
99
|
+
// against the complete message text.
|
|
66
100
|
pi.on("message_update", async (event, _ctx) => {
|
|
67
101
|
if (!enabled || !registry) return;
|
|
68
102
|
|
|
69
103
|
// Only process assistant messages
|
|
70
|
-
const msg = event.message
|
|
71
|
-
if (
|
|
104
|
+
const msg = event.message;
|
|
105
|
+
if (msg.role !== "assistant") return;
|
|
72
106
|
|
|
73
107
|
// Replace buffer with full message text (message_update contains full content)
|
|
74
|
-
|
|
108
|
+
const content = (msg as unknown as { content: unknown }).content;
|
|
109
|
+
textBuffer = extractText(content);
|
|
75
110
|
if (!textBuffer) return;
|
|
76
111
|
|
|
77
112
|
// Run matcher
|
|
@@ -90,8 +125,8 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
90
125
|
pi.on("message_end", async (event, ctx) => {
|
|
91
126
|
if (!enabled || !registry) return;
|
|
92
127
|
|
|
93
|
-
const msg = event.message
|
|
94
|
-
if (
|
|
128
|
+
const msg = event.message;
|
|
129
|
+
if (msg.role !== "assistant") return;
|
|
95
130
|
|
|
96
131
|
// Clear buffer
|
|
97
132
|
textBuffer = "";
|
|
@@ -122,10 +157,12 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
122
157
|
return;
|
|
123
158
|
}
|
|
124
159
|
|
|
125
|
-
//
|
|
160
|
+
// Skip injection if context usage exceeds the configured threshold
|
|
161
|
+
// (default: 80%). This prevents doc injection from pushing the context
|
|
162
|
+
// past the model's limit.
|
|
126
163
|
const usage = _ctx.getContextUsage();
|
|
127
|
-
if (usage && usage.tokens > 0 && usage.
|
|
128
|
-
console.warn(
|
|
164
|
+
if (usage && usage.tokens && usage.tokens > 0 && usage.percent && usage.percent > config.contextThreshold) {
|
|
165
|
+
console.warn(`[doc-injector] Skipping injection: context usage > ${config.contextThreshold}%`);
|
|
129
166
|
pendingMatches.clear();
|
|
130
167
|
return;
|
|
131
168
|
}
|
|
@@ -133,9 +170,7 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
133
170
|
const append = buildSystemPromptAppend(matchedEntries, pendingMatches);
|
|
134
171
|
|
|
135
172
|
// Mark as injected only after confirming injection will happen
|
|
136
|
-
|
|
137
|
-
entry.injected = true;
|
|
138
|
-
}
|
|
173
|
+
registry.markInjected(matchedEntries.map((e) => e.filePath));
|
|
139
174
|
pendingMatches.clear();
|
|
140
175
|
|
|
141
176
|
return {
|
|
@@ -144,5 +179,10 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
144
179
|
});
|
|
145
180
|
|
|
146
181
|
// ---- Commands ----
|
|
147
|
-
registerCommands(pi,
|
|
182
|
+
registerCommands(pi, {
|
|
183
|
+
getRegistry,
|
|
184
|
+
getEnabled,
|
|
185
|
+
setEnabled,
|
|
186
|
+
reloadRegistry,
|
|
187
|
+
});
|
|
148
188
|
}
|
package/injector.ts
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
6
|
import type { DocEntry } from "./types";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Interface for the UI notification capability needed by the injector.
|
|
10
|
+
* Matches Pi's ExtensionContext['ui'] notify signature.
|
|
11
|
+
*/
|
|
12
|
+
export interface NotifyCapability {
|
|
13
|
+
notify: (msg: string, type?: "info" | "warning" | "error") => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
/**
|
|
9
17
|
* Build a system prompt append string from matched documents.
|
|
10
18
|
*/
|
|
@@ -23,7 +31,7 @@ export function buildSystemPromptAppend(
|
|
|
23
31
|
for (const entry of entries) {
|
|
24
32
|
const keywords = matchedKeywords.get(entry.filePath) ?? [];
|
|
25
33
|
sections.push(`### ${entry.title}`);
|
|
26
|
-
sections.push(`Source: \`${entry.
|
|
34
|
+
sections.push(`Source: \`${entry.relativePath}\``);
|
|
27
35
|
if (keywords.length > 0) {
|
|
28
36
|
sections.push(`Matched keywords: ${keywords.join(", ")}`);
|
|
29
37
|
}
|
|
@@ -39,13 +47,13 @@ export function buildSystemPromptAppend(
|
|
|
39
47
|
* Notify the user via TUI when documents are injected.
|
|
40
48
|
*/
|
|
41
49
|
export function notifyInjection(
|
|
42
|
-
ui:
|
|
50
|
+
ui: NotifyCapability,
|
|
43
51
|
entries: DocEntry[],
|
|
44
52
|
matchedKeywords: Map<string, string[]>,
|
|
45
53
|
): void {
|
|
46
54
|
for (const entry of entries) {
|
|
47
55
|
const keywords = matchedKeywords.get(entry.filePath) ?? [];
|
|
48
56
|
const kwList = keywords.join(", ");
|
|
49
|
-
ui.notify(`📄 Injected: ${entry.
|
|
57
|
+
ui.notify(`📄 Injected: ${entry.relativePath} (matched: ${kwList})`, "info");
|
|
50
58
|
}
|
|
51
59
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-doc-injector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Auto-inject relevant project documentation into Pi's LLM context based on keyword matching",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -16,12 +16,31 @@
|
|
|
16
16
|
"test": "bun test",
|
|
17
17
|
"test:watch": "bun test --watch"
|
|
18
18
|
},
|
|
19
|
-
"keywords": [
|
|
19
|
+
"keywords": [
|
|
20
|
+
"pi-package",
|
|
21
|
+
"pi-extension",
|
|
22
|
+
"docs",
|
|
23
|
+
"context",
|
|
24
|
+
"llm"
|
|
25
|
+
],
|
|
20
26
|
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/lmn451/pi-docs"
|
|
30
|
+
},
|
|
21
31
|
"pi": {
|
|
22
|
-
"extensions": [
|
|
32
|
+
"extensions": [
|
|
33
|
+
"./index.ts"
|
|
34
|
+
]
|
|
23
35
|
},
|
|
24
36
|
"peerDependencies": {
|
|
25
37
|
"@mariozechner/pi-coding-agent": "*"
|
|
26
|
-
}
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/bun": "^1.3.13"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"packageManager": "bun@1.3.11"
|
|
27
46
|
}
|
package/registry.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Document Registry — scans a docs folder, parses frontmatter, maintains index.
|
|
3
3
|
*/
|
|
4
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
5
|
-
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { type Dirent, readdirSync, readFileSync } from "node:fs";
|
|
5
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
6
6
|
import type { DocEntry } from "./types";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -65,14 +65,16 @@ export function parseFrontmatter(
|
|
|
65
65
|
export class DocRegistry {
|
|
66
66
|
private entries: DocEntry[] = [];
|
|
67
67
|
private docsPath: string;
|
|
68
|
+
private recursive: boolean;
|
|
68
69
|
|
|
69
|
-
private constructor(docsPath: string) {
|
|
70
|
+
private constructor(docsPath: string, recursive: boolean = true) {
|
|
70
71
|
this.docsPath = docsPath;
|
|
72
|
+
this.recursive = recursive;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
/** Create a registry by scanning the docs folder. */
|
|
74
|
-
static async create(docsPath: string): Promise<DocRegistry> {
|
|
75
|
-
const registry = new DocRegistry(docsPath);
|
|
76
|
+
static async create(docsPath: string, recursive: boolean = true): Promise<DocRegistry> {
|
|
77
|
+
const registry = new DocRegistry(docsPath, recursive);
|
|
76
78
|
await registry.rebuild();
|
|
77
79
|
return registry;
|
|
78
80
|
}
|
|
@@ -86,28 +88,30 @@ export class DocRegistry {
|
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
try {
|
|
89
|
-
const
|
|
91
|
+
const scanResults = this.recursive
|
|
92
|
+
? this.scanRecursive(resolved)
|
|
93
|
+
: this.scanFlat(resolved);
|
|
90
94
|
|
|
91
95
|
const newEntries: DocEntry[] = [];
|
|
92
|
-
for (const
|
|
93
|
-
const filePath = join(resolved, file);
|
|
96
|
+
for (const { filePath, relativePath, fileName } of scanResults) {
|
|
94
97
|
try {
|
|
95
98
|
const raw = readFileSync(filePath, "utf-8");
|
|
96
99
|
const parsed = parseFrontmatter(raw);
|
|
97
100
|
if (!parsed) {
|
|
98
|
-
console.warn(`[doc-injector] Skipping ${
|
|
101
|
+
console.warn(`[doc-injector] Skipping ${relativePath}: no valid frontmatter with keywords`);
|
|
99
102
|
continue;
|
|
100
103
|
}
|
|
101
104
|
newEntries.push({
|
|
102
105
|
filePath,
|
|
103
|
-
fileName
|
|
106
|
+
fileName,
|
|
107
|
+
relativePath,
|
|
104
108
|
title: parsed.title,
|
|
105
109
|
keywords: parsed.keywords,
|
|
106
110
|
content: raw,
|
|
107
111
|
injected: preserved.get(filePath) ?? false,
|
|
108
112
|
});
|
|
109
113
|
} catch (err) {
|
|
110
|
-
console.warn(`[doc-injector] Error reading ${
|
|
114
|
+
console.warn(`[doc-injector] Error reading ${relativePath}:`, err);
|
|
111
115
|
}
|
|
112
116
|
}
|
|
113
117
|
|
|
@@ -118,7 +122,48 @@ export class DocRegistry {
|
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
/**
|
|
125
|
+
/** Scan top-level .md files only (non-recursive). */
|
|
126
|
+
private scanFlat(dir: string): Array<{ filePath: string; relativePath: string; fileName: string }> {
|
|
127
|
+
return readdirSync(dir)
|
|
128
|
+
.filter((f) => f.endsWith(".md"))
|
|
129
|
+
.map((f) => ({
|
|
130
|
+
filePath: join(dir, f),
|
|
131
|
+
relativePath: f,
|
|
132
|
+
fileName: f,
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Scan .md files recursively, including subdirectories. */
|
|
137
|
+
private scanRecursive(dir: string): Array<{ filePath: string; relativePath: string; fileName: string }> {
|
|
138
|
+
const results: Array<{ filePath: string; relativePath: string; fileName: string }> = [];
|
|
139
|
+
const dirents = readdirSync(dir, { recursive: true, withFileTypes: true }) as Dirent[];
|
|
140
|
+
|
|
141
|
+
for (const dirent of dirents) {
|
|
142
|
+
if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
|
|
143
|
+
|
|
144
|
+
// Build relative path from the directory tree
|
|
145
|
+
const parentPath = (dirent as Dirent & { path?: string }).path ?? "";
|
|
146
|
+
const relPath = parentPath
|
|
147
|
+
? relative(dir, join(parentPath, dirent.name))
|
|
148
|
+
: dirent.name;
|
|
149
|
+
|
|
150
|
+
results.push({
|
|
151
|
+
filePath: join(dir, relPath),
|
|
152
|
+
relativePath: relPath,
|
|
153
|
+
fileName: dirent.name,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return results;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get all registered entries.
|
|
162
|
+
*
|
|
163
|
+
* NOTE: Returned DocEntry objects share references with the internal registry.
|
|
164
|
+
* Mutating `injected` on returned objects will affect the registry's internal state.
|
|
165
|
+
* Prefer using markInjected() / markAllNotInjected() for explicit state changes.
|
|
166
|
+
*/
|
|
122
167
|
getEntries(): DocEntry[] {
|
|
123
168
|
return [...this.entries];
|
|
124
169
|
}
|
|
@@ -128,10 +173,25 @@ export class DocRegistry {
|
|
|
128
173
|
return this.entries.filter((e) => !e.injected);
|
|
129
174
|
}
|
|
130
175
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
176
|
+
/** Mark entries matching the given file paths as injected. */
|
|
177
|
+
markInjected(filePaths: string[]): void {
|
|
178
|
+
const pathSet = new Set(filePaths);
|
|
179
|
+
for (const e of this.entries) {
|
|
180
|
+
if (pathSet.has(e.filePath)) {
|
|
181
|
+
e.injected = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Reset all entries to not-injected state. */
|
|
187
|
+
markAllNotInjected(): void {
|
|
133
188
|
for (const e of this.entries) {
|
|
134
189
|
e.injected = false;
|
|
135
190
|
}
|
|
136
191
|
}
|
|
192
|
+
|
|
193
|
+
/** @deprecated Use markAllNotInjected() for clarity. */
|
|
194
|
+
reset(): void {
|
|
195
|
+
this.markAllNotInjected();
|
|
196
|
+
}
|
|
137
197
|
}
|
package/types.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export interface DocEntry {
|
|
7
7
|
filePath: string;
|
|
8
8
|
fileName: string;
|
|
9
|
+
relativePath: string;
|
|
9
10
|
title: string;
|
|
10
11
|
keywords: string[];
|
|
11
12
|
content: string;
|
|
@@ -30,12 +31,16 @@ export interface MatchResult {
|
|
|
30
31
|
export interface DocInjectorConfig {
|
|
31
32
|
docsPath: string;
|
|
32
33
|
matchThreshold: number;
|
|
34
|
+
contextThreshold: number;
|
|
35
|
+
recursive: boolean;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/** Default configuration values. */
|
|
36
39
|
export const DEFAULT_CONFIG: DocInjectorConfig = {
|
|
37
40
|
docsPath: "./docs",
|
|
38
41
|
matchThreshold: 2,
|
|
42
|
+
contextThreshold: 80,
|
|
43
|
+
recursive: true,
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
/** Default matcher options derived from config. */
|
package/docs/publish-md.md
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "Publishing Workflow"
|
|
3
|
-
keywords: [publish, release, deploy, version, npm, changelog, tag, semantic versioning, production, staging]
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Publishing Workflow
|
|
7
|
-
|
|
8
|
-
## Overview
|
|
9
|
-
|
|
10
|
-
This document covers the process for publishing releases and deploying to production.
|
|
11
|
-
|
|
12
|
-
## Versioning
|
|
13
|
-
|
|
14
|
-
We follow [Semantic Versioning](https://semver.org/):
|
|
15
|
-
- **MAJOR** — incompatible API changes
|
|
16
|
-
- **MINOR** — backwards-compatible functionality additions
|
|
17
|
-
- **PATCH** — backwards-compatible bug fixes
|
|
18
|
-
|
|
19
|
-
## Release Process
|
|
20
|
-
|
|
21
|
-
1. Update `CHANGELOG.md` with all changes since last release
|
|
22
|
-
2. Bump version in `package.json`
|
|
23
|
-
3. Create a git tag: `git tag -a v1.2.3 -m "Release v1.2.3"`
|
|
24
|
-
4. Push tags: `git push origin --tags`
|
|
25
|
-
5. CI will automatically build and publish
|
|
26
|
-
|
|
27
|
-
## Publishing to npm
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
# Dry run first
|
|
31
|
-
npm publish --dry-run
|
|
32
|
-
|
|
33
|
-
# Actual publish
|
|
34
|
-
npm publish
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Deployment
|
|
38
|
-
|
|
39
|
-
### Staging
|
|
40
|
-
- Deployed automatically on merge to `main`
|
|
41
|
-
- URL: staging.example.com
|
|
42
|
-
- Used for QA and integration testing
|
|
43
|
-
|
|
44
|
-
### Production
|
|
45
|
-
- Deployed from release tags only
|
|
46
|
-
- URL: app.example.com
|
|
47
|
-
- Requires manual approval in CI pipeline
|
|
48
|
-
|
|
49
|
-
## Rollback Procedure
|
|
50
|
-
|
|
51
|
-
If a release causes issues:
|
|
52
|
-
1. Identify the problematic version
|
|
53
|
-
2. Revert the git tag
|
|
54
|
-
3. Re-deploy the previous version
|
|
55
|
-
4. Post-mortem within 48 hours
|