pi-doc-injector 0.1.2 → 0.1.4
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 +18 -4
- package/docs/bun.md +12 -12
- package/docs/publish.md +49 -42
- package/index.ts +26 -3
- package/package.json +7 -5
- package/registry.ts +22 -7
- package/types.ts +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Create `.pi/doc-injector.json` in your project root to customize behavior:
|
|
|
63
63
|
```json
|
|
64
64
|
{
|
|
65
65
|
"docsPath": "./docs",
|
|
66
|
-
"matchThreshold":
|
|
66
|
+
"matchThreshold": 1,
|
|
67
67
|
"contextThreshold": 80,
|
|
68
68
|
"recursive": true
|
|
69
69
|
}
|
|
@@ -72,7 +72,7 @@ Create `.pi/doc-injector.json` in your project root to customize behavior:
|
|
|
72
72
|
| Option | Default | Description |
|
|
73
73
|
| ------------------ | ---------- | -------------------------------------------------------- |
|
|
74
74
|
| `docsPath` | `"./docs"` | Path to docs folder (relative to project root) |
|
|
75
|
-
| `matchThreshold` | `
|
|
75
|
+
| `matchThreshold` | `1` | Minimum keyword matches required to inject a doc |
|
|
76
76
|
| `contextThreshold` | `80` | Skip injection when context usage exceeds this % (0–100) |
|
|
77
77
|
| `recursive` | `true` | Scan docs subdirectories recursively |
|
|
78
78
|
|
|
@@ -103,14 +103,28 @@ The extension uses a per-session injection model:
|
|
|
103
103
|
- Use `/doc-inject reset` to manually reset all flags and allow docs to be injected again.
|
|
104
104
|
- Use `/doc-inject list` to see which docs have been injected (✅) and which are pending (⬜).
|
|
105
105
|
|
|
106
|
+
### System Prompt Lifecycle
|
|
107
|
+
|
|
108
|
+
Pi **reconstructs the system prompt from source files each turn** (verified against pi v0.70.6).
|
|
109
|
+
|
|
110
|
+
When `before_agent_start` fires, the `systemPrompt` passed to the extension is a freshly rebuilt prompt from `AGENTS.md`, `SYSTEM.md`, skills, and tool snippets. It is **not** accumulated from previous turns.
|
|
111
|
+
|
|
112
|
+
This means:
|
|
113
|
+
|
|
114
|
+
- Injections apply to the **current turn only** and do not persist in subsequent turns.
|
|
115
|
+
- There is no risk of duplicate injection sections stacking up over time.
|
|
116
|
+
- The `injected` flag alone is sufficient to prevent re-injection — no additional deduplication or marker-based stripping is needed.
|
|
117
|
+
|
|
118
|
+
For the full source-level verification, see the JSDoc block in `index.ts`.
|
|
119
|
+
|
|
106
120
|
## Development
|
|
107
121
|
|
|
108
122
|
```bash
|
|
109
123
|
# Run tests
|
|
110
|
-
|
|
124
|
+
npm test
|
|
111
125
|
|
|
112
126
|
# Run tests in watch mode
|
|
113
|
-
|
|
127
|
+
npm run test:watch
|
|
114
128
|
```
|
|
115
129
|
|
|
116
130
|
## License
|
package/docs/bun.md
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "JavaScript Runtime & Package Manager"
|
|
3
|
-
keywords: [
|
|
3
|
+
keywords: [npm, node, package manager, runtime, install, test, build]
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# JavaScript Runtime & Package Manager
|
|
7
7
|
|
|
8
|
-
This project uses **
|
|
8
|
+
This project uses **Node.js** as its JavaScript runtime and **npm** as its package manager.
|
|
9
9
|
|
|
10
|
-
## Why
|
|
10
|
+
## Why npm?
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
12
|
+
- The default package manager that ships with Node.js
|
|
13
|
+
- Widely supported across all CI/CD platforms
|
|
14
|
+
- Deterministic installs via `package-lock.json`
|
|
15
|
+
- No additional runtime or tooling required
|
|
16
16
|
|
|
17
17
|
## Common Commands
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
# Install dependencies
|
|
21
|
-
|
|
21
|
+
npm install
|
|
22
22
|
|
|
23
23
|
# Run tests
|
|
24
|
-
|
|
24
|
+
npm test
|
|
25
25
|
|
|
26
|
-
# Run tests in watch mode
|
|
27
|
-
|
|
26
|
+
# Run tests in watch mode
|
|
27
|
+
npm run test:watch
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
## Version
|
|
31
31
|
|
|
32
|
-
This project requires
|
|
32
|
+
This project requires Node.js >= 18.0.0. The lockfile (`package-lock.json`) ensures reproducible installs.
|
package/docs/publish.md
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Publishing Workflow"
|
|
3
|
-
keywords: [publish, release, npm, version, tag,
|
|
3
|
+
keywords: [publish, release, npm, version, tag, node, oidc, trusted publisher]
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Publishing Workflow
|
|
7
7
|
|
|
8
|
+
## Trusted Publisher (OIDC)
|
|
9
|
+
|
|
10
|
+
This package uses **npm trusted publishing** — no tokens needed. The GitHub Actions workflow authenticates via OIDC, which is configured at:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
https://www.npmjs.com/package/pi-doc-injector → Settings → Trusted Publisher
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The trusted publisher entry authorizes `lmn451/pi-docs` with workflow `publish.yml`.
|
|
17
|
+
|
|
18
|
+
## How It Works
|
|
19
|
+
|
|
20
|
+
1. Push a `v*` tag → triggers the publish workflow
|
|
21
|
+
2. GitHub Actions generates a short-lived OIDC token (`id-token: write`)
|
|
22
|
+
3. npm verifies the OIDC claims match the trusted publisher config
|
|
23
|
+
4. Package is published with provenance attestation
|
|
24
|
+
|
|
25
|
+
No `NPM_TOKEN` secret, no token rotation, nothing to leak.
|
|
26
|
+
|
|
8
27
|
## Versioning
|
|
9
28
|
|
|
10
29
|
We follow [Semantic Versioning](https://semver.org/):
|
|
@@ -12,69 +31,57 @@ We follow [Semantic Versioning](https://semver.org/):
|
|
|
12
31
|
- **MINOR** — backwards-compatible functionality additions
|
|
13
32
|
- **PATCH** — backwards-compatible bug fixes
|
|
14
33
|
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
### 1. Bump the version
|
|
34
|
+
## Release Process
|
|
18
35
|
|
|
19
36
|
```bash
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
# 1. Edit version in package.json (e.g., 0.1.1 → 0.1.2)
|
|
38
|
+
|
|
39
|
+
# 2. Commit, tag, and push
|
|
40
|
+
git add package.json
|
|
41
|
+
git commit -m "chore: bump version to X.Y.Z"
|
|
42
|
+
git tag vX.Y.Z
|
|
43
|
+
git push origin master
|
|
44
|
+
git push origin vX.Y.Z
|
|
23
45
|
```
|
|
24
46
|
|
|
25
|
-
|
|
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:
|
|
47
|
+
Or use `npm version`:
|
|
34
48
|
|
|
35
49
|
```bash
|
|
36
|
-
|
|
50
|
+
npm version patch # bumps version, commits, tags
|
|
51
|
+
git push origin master --follow-tags
|
|
37
52
|
```
|
|
38
53
|
|
|
39
|
-
|
|
54
|
+
## CI/CD
|
|
40
55
|
|
|
41
|
-
|
|
56
|
+
The `Publish` workflow triggers on `v*` tags and:
|
|
57
|
+
- Runs `npm ci`
|
|
58
|
+
- Verifies tag matches `package.json` version
|
|
59
|
+
- Runs `npm test`
|
|
60
|
+
- Publishes to npm with provenance via OIDC
|
|
42
61
|
|
|
43
|
-
|
|
44
|
-
- Runs tests
|
|
45
|
-
- Publishes to npm registry
|
|
46
|
-
|
|
47
|
-
Monitor the workflow at: `https://github.com/lmn451/pi-docs/actions`
|
|
62
|
+
Monitor: https://github.com/lmn451/pi-docs/actions
|
|
48
63
|
|
|
49
64
|
## Verify the Publish
|
|
50
65
|
|
|
51
|
-
Check if the package was published:
|
|
52
|
-
|
|
53
66
|
```bash
|
|
54
67
|
npm view pi-doc-injector
|
|
55
68
|
```
|
|
56
69
|
|
|
57
|
-
## Manual Publish
|
|
70
|
+
## Manual Publish (first time only)
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
OIDC only works after the package exists on npm. For the initial publish:
|
|
60
73
|
|
|
61
74
|
```bash
|
|
62
|
-
npm
|
|
75
|
+
npm login
|
|
76
|
+
npm publish --access public
|
|
63
77
|
```
|
|
64
78
|
|
|
65
|
-
|
|
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
|
|
79
|
+
After that, configure the trusted publisher and all future releases go through CI.
|
|
70
80
|
|
|
71
81
|
## Troubleshooting
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
**npm publish failed?**
|
|
79
|
-
- Ensure `NPM_TOKEN` secret is set
|
|
80
|
-
- Verify the version hasn't already been published
|
|
83
|
+
| Issue | Solution |
|
|
84
|
+
|-------|----------|
|
|
85
|
+
| Workflow didn't run | Verify tag exists: `git ls-remote origin refs/tags/vX.Y.Z` |
|
|
86
|
+
| 404 on publish | Verify trusted publisher config on npmjs.com matches exactly |
|
|
87
|
+
| Version already published | Bump to a new version |
|
package/index.ts
CHANGED
|
@@ -22,6 +22,29 @@
|
|
|
22
22
|
* session, once a doc is injected, it won't be re-injected unless the user
|
|
23
23
|
* manually runs `/doc-inject reset`.
|
|
24
24
|
*
|
|
25
|
+
* ## System Prompt Lifecycle (verified against pi v0.70.6)
|
|
26
|
+
*
|
|
27
|
+
* Pi **reconstructs the system prompt from source files each turn**. Here is
|
|
28
|
+
* the exact flow, verified via source-code review of dist/core/agent-session.js
|
|
29
|
+
* and dist/core/extensions/runner.js (v0.70.6):
|
|
30
|
+
*
|
|
31
|
+
* 1. Before each agent turn, pi calls `this._rebuildSystemPrompt(toolNames)`.
|
|
32
|
+
* This builds the prompt from `AGENTS.md`, `SYSTEM.md`, skills, enabled
|
|
33
|
+
* tool snippets — never from a previously modified (injected) prompt.
|
|
34
|
+
* 2. The rebuilt prompt is stored in `this._baseSystemPrompt`.
|
|
35
|
+
* 3. `emitBeforeAgentStart(..., this._baseSystemPrompt, ...)` passes this
|
|
36
|
+
* *fresh* base prompt to every extension handler.
|
|
37
|
+
* 4. Extension handlers can return a modified `systemPrompt` for the current
|
|
38
|
+
* turn. Pi uses the modified prompt **only for this turn**.
|
|
39
|
+
* 5. When no extension modifies the prompt, pi explicitly resets to
|
|
40
|
+
* `this._baseSystemPrompt` (comment in source: "Ensure we're using the
|
|
41
|
+
* base prompt (in case previous turn had modifications)").
|
|
42
|
+
*
|
|
43
|
+
* **Therefore**: Previous injections from `before_agent_start` do NOT persist
|
|
44
|
+
* across turns. Duplicate sections cannot accumulate in the system prompt.
|
|
45
|
+
* The `injected` flag alone is sufficient to prevent re-injection — no
|
|
46
|
+
* marker-based stripping or deduplication is needed.
|
|
47
|
+
*
|
|
25
48
|
* ## Race Condition Note
|
|
26
49
|
*
|
|
27
50
|
* If `resources_discover` (rebuild) fires while `before_agent_start` is running,
|
|
@@ -143,7 +166,7 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
143
166
|
});
|
|
144
167
|
|
|
145
168
|
// ---- Event: before_agent_start (inject into system prompt) ----
|
|
146
|
-
pi.on("before_agent_start", async (
|
|
169
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
147
170
|
if (!enabled || !registry || pendingMatches.size === 0) return;
|
|
148
171
|
|
|
149
172
|
const matchedEntries: DocEntry[] = [];
|
|
@@ -160,7 +183,7 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
160
183
|
// Skip injection if context usage exceeds the configured threshold
|
|
161
184
|
// (default: 80%). This prevents doc injection from pushing the context
|
|
162
185
|
// past the model's limit.
|
|
163
|
-
const usage =
|
|
186
|
+
const usage = ctx.getContextUsage();
|
|
164
187
|
if (usage && usage.tokens && usage.tokens > 0 && usage.percent && usage.percent > config.contextThreshold) {
|
|
165
188
|
console.warn(`[doc-injector] Skipping injection: context usage > ${config.contextThreshold}%`);
|
|
166
189
|
pendingMatches.clear();
|
|
@@ -174,7 +197,7 @@ export default async function docInjectorExtension(pi: ExtensionAPI) {
|
|
|
174
197
|
pendingMatches.clear();
|
|
175
198
|
|
|
176
199
|
return {
|
|
177
|
-
systemPrompt: (
|
|
200
|
+
systemPrompt: (event.systemPrompt || "") + "\n\n" + append,
|
|
178
201
|
};
|
|
179
202
|
});
|
|
180
203
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-doc-injector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "
|
|
17
|
-
"test:watch": "
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"pi-package",
|
|
@@ -37,10 +37,12 @@
|
|
|
37
37
|
"@mariozechner/pi-coding-agent": "*"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/
|
|
40
|
+
"@types/node": "^22.0.0",
|
|
41
|
+
"typescript": "^5.7.0",
|
|
42
|
+
"vitest": "^3.0.0"
|
|
41
43
|
},
|
|
42
44
|
"publishConfig": {
|
|
43
45
|
"access": "public"
|
|
44
46
|
},
|
|
45
|
-
"packageManager": "
|
|
47
|
+
"packageManager": "npm@11.5.2"
|
|
46
48
|
}
|
package/registry.ts
CHANGED
|
@@ -111,7 +111,10 @@ export class DocRegistry {
|
|
|
111
111
|
injected: preserved.get(filePath) ?? false,
|
|
112
112
|
});
|
|
113
113
|
} catch (err) {
|
|
114
|
-
|
|
114
|
+
// Only warn for unexpected errors, not ENOENT (file deleted/moved after scan)
|
|
115
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
116
|
+
console.warn(`[doc-injector] Error reading ${relativePath}:`, err);
|
|
117
|
+
}
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
|
|
@@ -141,16 +144,28 @@ export class DocRegistry {
|
|
|
141
144
|
for (const dirent of dirents) {
|
|
142
145
|
if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
|
|
143
146
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
const fileName = basename(dirent.name);
|
|
148
|
+
|
|
149
|
+
// Cross-runtime: when dirent.name is just the filename, resolve the
|
|
150
|
+
// relative path from the parent directory. Use parentPath (Node 18+)
|
|
151
|
+
// with fallback to .path (Bun) for older runtimes.
|
|
152
|
+
let relPath: string;
|
|
153
|
+
if (dirent.name === fileName) {
|
|
154
|
+
const parentPath = (dirent as Dirent & { parentPath?: string; path?: string }).parentPath
|
|
155
|
+
?? (dirent as Dirent & { path?: string }).path
|
|
156
|
+
?? "";
|
|
157
|
+
relPath = parentPath
|
|
158
|
+
? relative(dir, join(parentPath, dirent.name))
|
|
159
|
+
: dirent.name;
|
|
160
|
+
} else {
|
|
161
|
+
// Node-style: dirent.name already contains the relative path from dir
|
|
162
|
+
relPath = dirent.name;
|
|
163
|
+
}
|
|
149
164
|
|
|
150
165
|
results.push({
|
|
151
166
|
filePath: join(dir, relPath),
|
|
152
167
|
relativePath: relPath,
|
|
153
|
-
fileName
|
|
168
|
+
fileName,
|
|
154
169
|
});
|
|
155
170
|
}
|
|
156
171
|
|
package/types.ts
CHANGED