pi-rebrief 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +60 -2
- package/format.ts +117 -0
- package/index.ts +170 -0
- package/package.json +49 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ariel Sakin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,61 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-rebrief
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
   
|
|
4
|
+
|
|
5
|
+
A [Pi](https://pi.dev) extension that adds a `/rebrief` command. It reloads the project
|
|
6
|
+
context files, including `AGENTS.md` and `CLAUDE.md`, into the running session and tells you
|
|
7
|
+
which files have changed.
|
|
8
|
+
|
|
9
|
+
It also flags in the status bar when those files change on disk, for example after a `git pull`
|
|
10
|
+
or a branch switch.
|
|
11
|
+
|
|
12
|
+
`/reload` refreshes the whole session: settings, extensions, MCP servers, skills, themes, and
|
|
13
|
+
context files. `/rebrief` is for when only the context files changed, including while a turn
|
|
14
|
+
is in progress.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pi install npm:pi-rebrief
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Restart or `/reload` your session to use.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
When a context file changes on disk, the status bar says so:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
● pi-rebrief: AGENTS.md changed on disk since it was loaded. Run /rebrief to load the new version.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Nothing is reloaded until you run:
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
/rebrief
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Your next message runs with the new instructions, and so does everything after it. `/rebrief` reports
|
|
39
|
+
what it found, for example:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
pi-rebrief: reloaded context files.
|
|
43
|
+
changed: AGENTS.md
|
|
44
|
+
added: sub/AGENTS.md
|
|
45
|
+
They apply from your next message.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If you run it while a turn is in progress, the rest of that turn will use the new
|
|
49
|
+
instructions.
|
|
50
|
+
|
|
51
|
+
## Examples
|
|
52
|
+
|
|
53
|
+
- Pair it with `/tree` to retry an earlier prompt under new instructions.
|
|
54
|
+
- The agent derails. Rewind with `/tree` to the turn before it did, tighten the instruction
|
|
55
|
+
it missed in `AGENTS.md`, run `/rebrief`, and continue from there.
|
|
56
|
+
- You `git pull` mid-task and a teammate's commit changed `AGENTS.md`. The status bar flags
|
|
57
|
+
it; run `/rebrief` and keep going in the same session.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
package/format.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute, relative, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
type ContextFile = { path: string; content: string };
|
|
5
|
+
|
|
6
|
+
// Every message starts with this so a user who forgot the extension knows where it comes from
|
|
7
|
+
const PREFIX = "pi-rebrief:";
|
|
8
|
+
|
|
9
|
+
// How many file names to show before falling back to "and N more"
|
|
10
|
+
const MAX_LISTED = 3;
|
|
11
|
+
|
|
12
|
+
// Show a path relative to cwd when inside it or in a parent folder, else home-relative, else absolute
|
|
13
|
+
function displayPath(path: string, cwd: string, home: string): string {
|
|
14
|
+
const rel = relative(cwd, path);
|
|
15
|
+
if (rel && !isAbsolute(rel) && (!rel.startsWith("..") || /^(\.\.[/\\])+[^/\\]+$/.test(rel))) {
|
|
16
|
+
return rel;
|
|
17
|
+
}
|
|
18
|
+
if (home && path.startsWith(home + sep)) {
|
|
19
|
+
return `~${path.slice(home.length)}`;
|
|
20
|
+
}
|
|
21
|
+
return path;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Changes {
|
|
25
|
+
changed: string[];
|
|
26
|
+
added: string[];
|
|
27
|
+
removed: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function diffFiles(previous: readonly ContextFile[], fresh: readonly ContextFile[], cwd: string, home: string): Changes {
|
|
31
|
+
const before = new Map(previous.map((file) => [file.path, file.content]));
|
|
32
|
+
const after = new Set(fresh.map((file) => file.path));
|
|
33
|
+
const changes: Changes = { changed: [], added: [], removed: [] };
|
|
34
|
+
for (const file of fresh) {
|
|
35
|
+
const old = before.get(file.path);
|
|
36
|
+
if (old === undefined) {
|
|
37
|
+
changes.added.push(displayPath(file.path, cwd, home));
|
|
38
|
+
} else if (old !== file.content) {
|
|
39
|
+
changes.changed.push(displayPath(file.path, cwd, home));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const file of previous) {
|
|
43
|
+
if (!after.has(file.path)) {
|
|
44
|
+
changes.removed.push(displayPath(file.path, cwd, home));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return changes;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fileCount(count: number): string {
|
|
51
|
+
return `${count} file${count === 1 ? "" : "s"}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The /rebrief summary: one line per kind of change, capped at MAX_LISTED names
|
|
55
|
+
export function describeChanges(
|
|
56
|
+
previous: readonly ContextFile[],
|
|
57
|
+
fresh: readonly ContextFile[],
|
|
58
|
+
cwd: string,
|
|
59
|
+
home: string = homedir(),
|
|
60
|
+
): string[] {
|
|
61
|
+
const changes = diffFiles(previous, fresh, cwd, home);
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
let remaining = MAX_LISTED;
|
|
64
|
+
for (const kind of ["changed", "added", "removed"] as const) {
|
|
65
|
+
const shown = changes[kind].slice(0, remaining);
|
|
66
|
+
if (shown.length > 0) {
|
|
67
|
+
lines.push(` ${`${kind}:`.padEnd(8)} ${shown.join(", ")}`);
|
|
68
|
+
remaining -= shown.length;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const total = changes.changed.length + changes.added.length + changes.removed.length;
|
|
72
|
+
if (total === 0) {
|
|
73
|
+
lines.push(" order changed");
|
|
74
|
+
} else if (total > MAX_LISTED) {
|
|
75
|
+
lines.push(` and ${total - MAX_LISTED} more`);
|
|
76
|
+
}
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatReloadedNotice(
|
|
81
|
+
previous: readonly ContextFile[],
|
|
82
|
+
files: readonly ContextFile[],
|
|
83
|
+
cwd: string,
|
|
84
|
+
running: boolean,
|
|
85
|
+
): string {
|
|
86
|
+
const when = running
|
|
87
|
+
? "They apply to the current turn, starting with the agent's next step."
|
|
88
|
+
: "They apply from your next message.";
|
|
89
|
+
return [`${PREFIX} reloaded context files.`, ...describeChanges(previous, files, cwd), when].join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function formatUnchangedNotice(count: number): string {
|
|
93
|
+
return `${PREFIX} context files are unchanged (${fileCount(count)}). Nothing to reload.`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function formatDisabledNotice(): string {
|
|
97
|
+
return `${PREFIX} Pi was started with --no-context-files, so there are no context files to reload.`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Status bar text when the files on disk differ from what the session loaded
|
|
101
|
+
export function formatStaleStatus(
|
|
102
|
+
loaded: readonly ContextFile[],
|
|
103
|
+
onDisk: readonly ContextFile[],
|
|
104
|
+
cwd: string,
|
|
105
|
+
home: string = homedir(),
|
|
106
|
+
): string {
|
|
107
|
+
const changes = diffFiles(loaded, onDisk, cwd, home);
|
|
108
|
+
const names = [...changes.changed, ...changes.added, ...changes.removed];
|
|
109
|
+
if (names.length === 1) {
|
|
110
|
+
return `● ${PREFIX} ${names[0]} changed on disk since it was loaded. Run /rebrief to load the new version.`;
|
|
111
|
+
}
|
|
112
|
+
const listed = names.slice(0, MAX_LISTED).join(", ");
|
|
113
|
+
const more = names.length > MAX_LISTED ? `, and ${names.length - MAX_LISTED} more` : "";
|
|
114
|
+
const which = names.length > 0 ? ` (${listed}${more})` : "";
|
|
115
|
+
const count = names.length > 0 ? `${names.length} context files` : "context files";
|
|
116
|
+
return `● ${PREFIX} ${count} changed on disk since they were loaded${which}. Run /rebrief to load them.`;
|
|
117
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
ExtensionUIContext,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { getAgentDir, loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { getCurrentSystemMessage } from "@earendil-works/pi-ai";
|
|
8
|
+
import { formatDisabledNotice, formatReloadedNotice, formatStaleStatus, formatUnchangedNotice } from "./format.ts";
|
|
9
|
+
|
|
10
|
+
export { describeChanges, formatStaleStatus } from "./format.ts";
|
|
11
|
+
|
|
12
|
+
export interface ContextFile {
|
|
13
|
+
path: string;
|
|
14
|
+
content: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RebriefDeps {
|
|
18
|
+
loadProjectContextFiles?: (options: { cwd: string; agentDir: string }) => ContextFile[];
|
|
19
|
+
contextFilesDisabled?: () => boolean;
|
|
20
|
+
startTimer?: (tick: () => void, ms: number) => () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const STATUS_KEY = "pi-rebrief";
|
|
24
|
+
const POLL_MS = 3000; // How often to check for context file changes
|
|
25
|
+
|
|
26
|
+
// Check if Pi was started with --no-context-files (-nc); extensions can't read built-in flags
|
|
27
|
+
export function contextFilesDisabledByArgv(argv: readonly string[]): boolean {
|
|
28
|
+
return argv.includes("--no-context-files") || argv.includes("-nc");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function sameContextFiles(a: readonly ContextFile[], b: readonly ContextFile[]): boolean {
|
|
32
|
+
if (a.length !== b.length) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
for (let i = 0; i < a.length; i++) {
|
|
36
|
+
if (a[i]!.path !== b[i]!.path || a[i]!.content !== b[i]!.content) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Render the project_context prompt section the way Pi does (Pi does not export its renderer)
|
|
44
|
+
export function renderProjectContext(files: readonly ContextFile[]): string | null {
|
|
45
|
+
if (files.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const body = [
|
|
49
|
+
"Project-specific instructions and guidelines:",
|
|
50
|
+
...files.map(({ path, content }) => `<project_instructions path="${path}">\n${content}\n</project_instructions>`),
|
|
51
|
+
].join("\n\n");
|
|
52
|
+
return `<project_context>\n${body}\n</project_context>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function startInterval(tick: () => void, ms: number): () => void {
|
|
56
|
+
const timer = setInterval(tick, ms);
|
|
57
|
+
timer.unref?.();
|
|
58
|
+
return () => clearInterval(timer);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createRebriefExtension(deps: RebriefDeps = {}) {
|
|
62
|
+
const loadContextFiles = deps.loadProjectContextFiles ?? loadProjectContextFiles;
|
|
63
|
+
const contextFilesDisabled = deps.contextFilesDisabled ?? (() => contextFilesDisabledByArgv(process.argv));
|
|
64
|
+
const startTimer = deps.startTimer ?? startInterval;
|
|
65
|
+
|
|
66
|
+
return function piRebrief(pi: ExtensionAPI) {
|
|
67
|
+
// The context files the session has
|
|
68
|
+
let files: ContextFile[] = [];
|
|
69
|
+
// The user ran /rebrief: stamp `files` onto every run
|
|
70
|
+
let pinned = false;
|
|
71
|
+
// A /rebrief issued while a run was in progress; anchored after the newest message of the next request
|
|
72
|
+
let pending: { anchor?: number; at: number } | undefined;
|
|
73
|
+
let stopTimer: (() => void) | undefined;
|
|
74
|
+
let status: string | undefined;
|
|
75
|
+
|
|
76
|
+
const load = (cwd: string) => loadContextFiles({ cwd, agentDir: getAgentDir() });
|
|
77
|
+
|
|
78
|
+
const showStatus = (ui: ExtensionUIContext | undefined, text: string | undefined) => {
|
|
79
|
+
const next = text === undefined ? undefined : ui?.theme.fg("warning", text);
|
|
80
|
+
if (!ui || next === status) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
status = next;
|
|
84
|
+
ui.setStatus(STATUS_KEY, next);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
pi.on("session_start", (_event, ctx) => {
|
|
88
|
+
pinned = false;
|
|
89
|
+
pending = undefined;
|
|
90
|
+
status = undefined;
|
|
91
|
+
stopTimer?.();
|
|
92
|
+
stopTimer = undefined;
|
|
93
|
+
if (contextFilesDisabled()) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const ui = ctx.hasUI ? ctx.ui : undefined;
|
|
97
|
+
files = load(ctx.cwd);
|
|
98
|
+
stopTimer = startTimer(() => {
|
|
99
|
+
const onDisk = load(ctx.cwd);
|
|
100
|
+
showStatus(ui, sameContextFiles(files, onDisk) ? undefined : formatStaleStatus(files, onDisk, ctx.cwd));
|
|
101
|
+
}, POLL_MS);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
pi.on("session_shutdown", () => {
|
|
105
|
+
stopTimer?.();
|
|
106
|
+
stopTimer = undefined;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
pi.registerCommand("rebrief", {
|
|
110
|
+
description: "Reload only context files (lighter than /reload)",
|
|
111
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
112
|
+
const notify = (msg: string, level: "info" | "warning") => {
|
|
113
|
+
if (ctx.hasUI) {
|
|
114
|
+
ctx.ui.notify(msg, level);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (contextFilesDisabled()) {
|
|
119
|
+
notify(formatDisabledNotice(), "warning");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const previous = files;
|
|
124
|
+
files = load(ctx.cwd);
|
|
125
|
+
pinned = true;
|
|
126
|
+
const changed = !sameContextFiles(previous, files);
|
|
127
|
+
const running = !ctx.isIdle();
|
|
128
|
+
if (changed && running) {
|
|
129
|
+
pending = { at: Date.now() };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
showStatus(ctx.hasUI ? ctx.ui : undefined, undefined);
|
|
133
|
+
|
|
134
|
+
if (!changed) {
|
|
135
|
+
notify(formatUnchangedNotice(files.length), "info");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
notify(formatReloadedNotice(previous, files, ctx.cwd, running), "info");
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
pi.on("before_agent_start", (event) => {
|
|
143
|
+
pending = undefined;
|
|
144
|
+
if (pinned) {
|
|
145
|
+
event.systemPromptOptions.contextFiles = files.map((file) => ({ ...file }));
|
|
146
|
+
} else {
|
|
147
|
+
files = event.systemPromptOptions.contextFiles ?? [];
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Mid-run /rebrief: add a request-local section update until the next run starts
|
|
152
|
+
pi.on("context_with_system", (event) => {
|
|
153
|
+
if (!pending) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const messages = event.messages;
|
|
157
|
+
const desired = renderProjectContext(files);
|
|
158
|
+
if ((getCurrentSystemMessage(messages)?.sections?.project_context ?? null) === desired) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
pending.anchor ??= messages.at(-1)?.timestamp;
|
|
162
|
+
const update = { role: "system" as const, content: "", sections: { project_context: desired }, timestamp: pending.at };
|
|
163
|
+
const anchor = messages.findIndex((m) => m.timestamp === pending!.anchor);
|
|
164
|
+
const at = anchor === -1 ? messages.length : anchor + 1;
|
|
165
|
+
return { messages: [...messages.slice(0, at), update, ...messages.slice(at)] };
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default createRebriefExtension();
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-rebrief",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Reload only the project context files, including AGENTS.md and CLAUDE.md, into a running Pi session: no extension restart or MCP reconnect like /reload, plus a report of what changed.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"pi-coding-agent",
|
|
10
|
+
"pi",
|
|
11
|
+
"agents-md",
|
|
12
|
+
"claude-md",
|
|
13
|
+
"context-files",
|
|
14
|
+
"reload",
|
|
15
|
+
"system-prompt"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Ariel Sakin <asakin@gmail.com>",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/asakin/pi-rebrief.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/asakin/pi-rebrief#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/asakin/pi-rebrief/issues"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"index.ts",
|
|
29
|
+
"format.ts",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"pi": {
|
|
34
|
+
"extensions": [
|
|
35
|
+
"./index.ts"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"check": "tsc --noEmit",
|
|
40
|
+
"test": "node --test"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@earendil-works/pi-ai": "*",
|
|
44
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
48
|
+
"@types/node": ">=22.0.0",
|
|
49
|
+
"typescript": ">=5.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|