@huseynsnmz/pi-suite 0.2.2 → 0.2.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/CHANGELOG.md +34 -0
- package/README.md +13 -4
- package/package.json +12 -2
- package/packages/board/README.md +1 -1
- package/packages/board/package.json +2 -1
- package/packages/commands/README.md +1 -1
- package/packages/commands/src/index.ts +22 -0
- package/packages/frame/README.md +8 -2
- package/packages/frame/docs/reference.md +1 -1
- package/packages/frame/src/index.ts +8 -3
- package/packages/frame/src/kit/settings/files.ts +8 -8
- package/packages/frame/src/kit/settings/store.ts +3 -1
- package/packages/frame/src/ui/editor.ts +64 -7
- package/packages/frame/src/ui/history-search.ts +21 -0
- package/packages/frame/src/ui/pi-entries.ts +2 -0
- package/packages/frame/src/ui/thinking.ts +34 -0
- package/packages/memory/README.md +20 -1
- package/packages/memory/src/index.ts +60 -1
- package/packages/memory/src/mastermind.ts +110 -0
- package/packages/memory/src/playbook.ts +92 -0
- package/packages/modes/README.md +11 -3
- package/packages/modes/skills/review-permissions/SKILL.md +11 -2
- package/packages/modes/src/index.ts +29 -11
- package/packages/modes/src/judge-gate.ts +78 -7
- package/packages/modes/src/modes.ts +4 -1
- package/packages/modes/src/review/apply.mjs +1 -1
- package/packages/modes/src/review/collect.mjs +45 -2
- package/packages/modes/src/workdir.ts +105 -0
- package/packages/router/README.md +30 -9
- package/packages/router/src/config.ts +30 -28
- package/packages/router/src/index.ts +70 -13
- package/packages/router/src/route.ts +16 -10
- package/packages/subagent/LICENSE +21 -0
- package/packages/subagent/README.md +17 -0
- package/packages/subagent/package.json +22 -0
- package/packages/subagent/src/index.ts +76 -0
- package/packages/subscription/LICENSE +21 -0
- package/packages/subscription/README.md +60 -0
- package/packages/subscription/package.json +23 -0
- package/packages/subscription/src/anchors.ts +75 -0
- package/packages/subscription/src/billing.ts +39 -0
- package/packages/subscription/src/catalog.ts +310 -0
- package/packages/subscription/src/cc-sessions.ts +122 -0
- package/packages/subscription/src/config.ts +34 -0
- package/packages/subscription/src/convert.ts +161 -0
- package/packages/subscription/src/index.ts +69 -0
- package/packages/subscription/src/mcp.ts +78 -0
- package/packages/subscription/src/models.ts +65 -0
- package/packages/subscription/src/prompt.ts +57 -0
- package/packages/subscription/src/provider.ts +617 -0
- package/packages/subscription/src/session.ts +294 -0
- package/packages/subscription/src/util.ts +73 -0
- package/packages/web/README.md +5 -5
- package/packages/web/src/config.ts +13 -23
- package/packages/web/src/index.ts +11 -7
- package/packages/web/src/reader.ts +15 -4
- package/packages/web/src/validate.ts +2 -2
- package/src/index.ts +93 -11
- package/src/settings-ui.ts +63 -0
- package/src/settings.ts +117 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.4]
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- `/mastermind`: a strong model, chosen as the new `mastermind` role in `/router`, reads past conversations and writes a short playbook that other models get on every run. You approve every rule in an editor. `/mastermind compact` shortens it, and `/mastermind show` shows it.
|
|
8
|
+
- New part, `subagent`: the `agent` tool hands a task to a fresh agent and gives back its answer. `explore` can only read, and also works in `plan` and `read-only`. `general` can do everything, and its permission asks come to your session.
|
|
9
|
+
- A `cd` at the start of a bash command carries over to later commands while it stays in the project.
|
|
10
|
+
- `/review-permissions` reports what the permission judge decided, and why it deferred.
|
|
11
|
+
|
|
12
|
+
### Changes
|
|
13
|
+
|
|
14
|
+
- `auto` no longer sends `edit` and `write` on ordinary project files to the judge. `.git`, `.pi`, `.claude`, `.husky`, `.github`, `.vscode`, `.envrc` and paths outside the project still go to it.
|
|
15
|
+
- The first time you start Pi in a project, pi-modes allows `cd` inside that project in your bash rules, with a backup, so you aren't asked about each one. A `cd` elsewhere still asks.
|
|
16
|
+
|
|
17
|
+
### Development
|
|
18
|
+
|
|
19
|
+
- `npm test` takes seconds. Board's tests that start real processes moved to `npm run test:all`, and `npm run test:e2e` runs every suite at once.
|
|
20
|
+
|
|
21
|
+
## [0.2.3]
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
- Router: each role can have its own reasoning level, chosen in `/router` from what that role's model offers.
|
|
26
|
+
- Up and Down with one line typed go through past prompts that start with it. With nothing typed they also reach this folder's other sessions.
|
|
27
|
+
- With `hideThinkingBlock` on, reasoning shows while it's written and collapses when the message is done. `Ctrl+T` opens it.
|
|
28
|
+
- New part, off until you run `/suite on subscription`: Claude models on your Claude login, based on pi-claude-subscription. It asks before anything more is billed as extra usage. See its README for the limits and what Anthropic's terms say.
|
|
29
|
+
|
|
30
|
+
- One settings file, `~/.pi/agent/suite.json`, with a section for each part. `/suite` now opens each part's settings as well as turning it on or off. Old files move in the first time Pi starts, and stay as `.bak`.
|
|
31
|
+
|
|
32
|
+
### Fixes
|
|
33
|
+
|
|
34
|
+
- `router/auto` is always listed in `/model`, and says to run `/router` until `plan` and `build` are set.
|
|
35
|
+
- `/clear` keeps the current model and reasoning level.
|
|
36
|
+
|
|
3
37
|
## [0.2.2]
|
|
4
38
|
|
|
5
39
|
### Fixes
|
package/README.md
CHANGED
|
@@ -11,13 +11,21 @@ One package for [Pi](https://pi.dev): modes and permissions, a framed screen, a
|
|
|
11
11
|
| `frame` | The start card, framed editor, tool boxes, footer and the agent's todo list | [README](packages/frame/README.md) |
|
|
12
12
|
| `board` | A board for starting, watching and attaching to background sessions | [README](packages/board/README.md) |
|
|
13
13
|
| `commands` | `/clear` and `/security-review` | [README](packages/commands/README.md) |
|
|
14
|
+
| `subagent` | The `agent` tool: hand a task to a fresh agent, and get its answer back | [README](packages/subagent/README.md) |
|
|
15
|
+
| `subscription` | Off until you turn it on. Claude models on your Claude login, asking before anything more is billed as extra usage | [README](packages/subscription/README.md) |
|
|
16
|
+
|
|
17
|
+
## Settings
|
|
18
|
+
|
|
19
|
+
Run `/suite` to turn parts on or off and change their settings. They're all kept in `~/.pi/agent/suite.json`, with a section for each part, so you can also edit it by hand. Older files such as `router.json` and `web.json` move in the first time Pi starts, and stay beside it as `.bak`. If one can't be read it stays where it is, and you're told, so a broken file can't silently turn off a setting.
|
|
20
|
+
|
|
21
|
+
There's no setting per project, so a repository can't choose where your code goes. The only exception is the frame's look, in `.pi/extensions/pi-harness/settings.json`, once you trust the project. Pi's own files, and the settings of other packages, such as `pi-permission-system`, stay where they are.
|
|
14
22
|
|
|
15
23
|
## Install
|
|
16
24
|
|
|
17
25
|
Requires Pi 1.5 or newer.
|
|
18
26
|
|
|
19
27
|
```bash
|
|
20
|
-
pi install npm:@huseynsnmz/pi-suite@0.2.
|
|
28
|
+
pi install npm:@huseynsnmz/pi-suite@0.2.4
|
|
21
29
|
```
|
|
22
30
|
|
|
23
31
|
If you installed any of these separately, remove them first:
|
|
@@ -82,8 +90,9 @@ Each part has its own limits, listed in its README or `SECURITY.md`. None of thi
|
|
|
82
90
|
```bash
|
|
83
91
|
npm install --ignore-scripts
|
|
84
92
|
npm run check
|
|
85
|
-
npm test
|
|
86
|
-
npm run test:
|
|
93
|
+
npm test # seconds: logic only
|
|
94
|
+
npm run test:all # adds board's tests that start real processes
|
|
95
|
+
npm run test:e2e # real Pi in a real terminal with a fake model, every suite at once
|
|
87
96
|
```
|
|
88
97
|
|
|
89
|
-
Each part keeps its own tests under `packages/<part>/test`. `frame` needs [bun](https://bun.sh) for its unit tests.
|
|
98
|
+
Each part keeps its own tests under `packages/<part>/test`. `frame` needs [bun](https://bun.sh) for its unit tests. To run some end-to-end suites, name them: `python3 test/e2e/all.py router web`. Run `test:all` and `test:e2e` before a release.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huseynsnmz/pi-suite",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "One Pi package for all of it: modes and permissions, a framed screen, an agent board, memory, web tools with a separate page reader, and a model router. Turn off what you don't want with /suite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,10 +28,18 @@
|
|
|
28
28
|
"README.md",
|
|
29
29
|
"CHANGELOG.md",
|
|
30
30
|
"LICENSE",
|
|
31
|
+
"packages/subscription/src",
|
|
32
|
+
"packages/subscription/package.json",
|
|
33
|
+
"packages/subscription/README.md",
|
|
34
|
+
"packages/subscription/LICENSE",
|
|
31
35
|
"packages/commands/src",
|
|
32
36
|
"packages/commands/package.json",
|
|
33
37
|
"packages/commands/README.md",
|
|
34
38
|
"packages/commands/LICENSE",
|
|
39
|
+
"packages/subagent/src",
|
|
40
|
+
"packages/subagent/package.json",
|
|
41
|
+
"packages/subagent/README.md",
|
|
42
|
+
"packages/subagent/LICENSE",
|
|
35
43
|
"packages/board/src",
|
|
36
44
|
"packages/board/runner",
|
|
37
45
|
"packages/board/docs",
|
|
@@ -88,9 +96,11 @@
|
|
|
88
96
|
"scripts": {
|
|
89
97
|
"check": "tsc --noEmit && npm run --workspaces --if-present check && npm run -w packages/board typecheck",
|
|
90
98
|
"test": "node --test test/*.test.ts && npm run --workspaces --if-present test",
|
|
91
|
-
"test:
|
|
99
|
+
"test:all": "npm test && npm run -w packages/board test:integration",
|
|
100
|
+
"test:e2e": "python3 test/e2e/all.py"
|
|
92
101
|
},
|
|
93
102
|
"dependencies": {
|
|
103
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.308",
|
|
94
104
|
"@xterm/headless": "6.0.0",
|
|
95
105
|
"node-pty": "1.1.0",
|
|
96
106
|
"@mozilla/readability": "0.6.0",
|
package/packages/board/README.md
CHANGED
|
@@ -45,9 +45,10 @@
|
|
|
45
45
|
"scripts": {
|
|
46
46
|
"typecheck": "tsc --noEmit",
|
|
47
47
|
"test": "node scripts/run-tests.mjs",
|
|
48
|
+
"test:integration": "node scripts/run-tests.mjs --integration",
|
|
48
49
|
"test:perf": "node scripts/run-perf-gate.mjs",
|
|
49
50
|
"pack:dry": "npm pack --dry-run",
|
|
50
|
-
"verify": "npm run typecheck && npm run test:perf && npm test && npm run pack:dry"
|
|
51
|
+
"verify": "npm run typecheck && npm run test:perf && npm test && npm run test:integration && npm run pack:dry"
|
|
51
52
|
},
|
|
52
53
|
"peerDependencies": {
|
|
53
54
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -4,7 +4,7 @@ Two commands for Pi. Part of [pi-suite](../../README.md); turn them off with `/s
|
|
|
4
4
|
|
|
5
5
|
## /clear
|
|
6
6
|
|
|
7
|
-
Clears the conversation and starts a new session,
|
|
7
|
+
Clears the conversation and starts a new session, keeping the model and reasoning level you have selected. `/new` goes back to your saved default instead. If the agent is working, it stops first. The old session is kept, and `/resume` brings it back.
|
|
8
8
|
|
|
9
9
|
## /security-review
|
|
10
10
|
|
|
@@ -5,6 +5,14 @@ import { changesToReview, type GitRun, reviewPrompt } from "./security-review.ts
|
|
|
5
5
|
|
|
6
6
|
const REVIEW_MESSAGE = "security-review";
|
|
7
7
|
|
|
8
|
+
/** The model /clear hands to the session it starts. Pi's own new session would take the saved default. */
|
|
9
|
+
const KEPT = Symbol.for("pi-suite:clear-keeps");
|
|
10
|
+
interface Kept {
|
|
11
|
+
provider: string;
|
|
12
|
+
id: string;
|
|
13
|
+
thinking: ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
export default function piCommands(pi: ExtensionAPI): void {
|
|
9
17
|
pi.registerCommand("clear", {
|
|
10
18
|
description: "Clear the conversation and start a new session",
|
|
@@ -13,10 +21,24 @@ export default function piCommands(pi: ExtensionAPI): void {
|
|
|
13
21
|
ctx.abort();
|
|
14
22
|
await ctx.waitForIdle();
|
|
15
23
|
}
|
|
24
|
+
const store = globalThis as Record<symbol, Kept | undefined>;
|
|
25
|
+
if (ctx.model) store[KEPT] = { provider: ctx.model.provider, id: ctx.model.id, thinking: pi.getThinkingLevel() };
|
|
16
26
|
await ctx.newSession();
|
|
27
|
+
delete store[KEPT]; // Still here only if it was cancelled.
|
|
17
28
|
},
|
|
18
29
|
});
|
|
19
30
|
|
|
31
|
+
// Runs in the new session, where this extension is a new instance, so what /clear kept can't live in it.
|
|
32
|
+
pi.on("session_start", async (event, ctx) => {
|
|
33
|
+
const store = globalThis as Record<symbol, Kept | undefined>;
|
|
34
|
+
const kept = store[KEPT];
|
|
35
|
+
delete store[KEPT];
|
|
36
|
+
if (event.reason !== "new" || kept === undefined) return;
|
|
37
|
+
const model = ctx.modelRegistry.find(kept.provider, kept.id);
|
|
38
|
+
if (model !== undefined) await pi.setModel(model);
|
|
39
|
+
pi.setThinkingLevel(kept.thinking);
|
|
40
|
+
});
|
|
41
|
+
|
|
20
42
|
// The diff goes to the model, and only this one line goes on screen.
|
|
21
43
|
pi.registerMessageRenderer<{ summary: string }>(REVIEW_MESSAGE, (message, _options, theme) =>
|
|
22
44
|
new Text(theme.fg("muted", message.details?.summary ?? "Security review"), 1, 0),
|
package/packages/frame/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Requires Pi 1.5 or newer.
|
|
|
19
19
|
2. Install it:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
pi install npm:@huseynsnmz/pi-suite@0.2.
|
|
22
|
+
pi install npm:@huseynsnmz/pi-suite@0.2.4
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
3. Restart Pi. The start card says `π-frame`.
|
|
@@ -44,6 +44,8 @@ Some pieces, such as rooms, boards, peers and the remote page, appear only with
|
|
|
44
44
|
|---|---|
|
|
45
45
|
| `Ctrl+C` | Stops a run and says `Aborted`. With nothing running, it clears the editor and says to press it again within 2 seconds to exit |
|
|
46
46
|
| `Ctrl+R` | Search what you've typed, here and in this folder's other sessions. `Ctrl+R` again goes further back, `Enter` sends, `Esc` cancels, any other key keeps the match to edit. It replaces Pi's Rename on this key; `/name` still renames |
|
|
47
|
+
| `Up` / `Down` | With nothing typed, browse past prompts, including this folder's other sessions. With one line typed, go through prompts that start with it, as in a shell |
|
|
48
|
+
| `Ctrl+T` | Open or collapse finished reasoning |
|
|
47
49
|
| `Alt+O` | Scroll through the session's tool output, one call at a time |
|
|
48
50
|
| `Ctrl+O` | Expand every call in the transcript |
|
|
49
51
|
| `/todos` | Show the agent's todo list. `/todos clear` empties it and tells the agent |
|
|
@@ -78,7 +80,7 @@ More detail is in [How it works](docs/how-it-works.md) and the [reference](docs/
|
|
|
78
80
|
- **`gh` and `glab` still run in their repositories,** and they call `git` for reads such as the remote list.
|
|
79
81
|
- **`glab` needs a host with `gitlab` in its name.** It starts for that remote, and if a token is set in the environment (`GITLAB_TOKEN`), only for `gitlab.com` or `GITLAB_HOST`, because a repository picks its own remote.
|
|
80
82
|
- **The `git status` protections cover the ways to run code that I know of.** New Git options could add others.
|
|
81
|
-
- **The parts of `@adeildo/pi-kit` 6.0.0 that frame uses are copied into `src/kit`,** with its license, so upstream fixes have to be copied in by hand. Its settings stay in `~/.pi/agent/
|
|
83
|
+
- **The parts of `@adeildo/pi-kit` 6.0.0 that frame uses are copied into `src/kit`,** with its license, so upstream fixes have to be copied in by hand. Its settings stay in `~/.pi/agent/suite.json`.
|
|
82
84
|
- **Another session's view isn't fully covered.** Opening one needs other pi-harness packages, and that view hasn't been tested.
|
|
83
85
|
|
|
84
86
|
## Development
|
|
@@ -103,3 +105,7 @@ MIT. See [LICENSE](LICENSE), which keeps the original copyright notice.
|
|
|
103
105
|
- [pi-look and pi-harness](https://github.com/felipeadeildo/pi-harness) by [adeildo](https://adeildo.dev)
|
|
104
106
|
- The framed editor comes from [pi-open-tui](https://github.com/OldSuns/pi-open-tui), by way of pi-look.
|
|
105
107
|
- The segments with priorities come from [oh-my-pi](https://github.com/can1357/oh-my-pi), by way of pi-look.
|
|
108
|
+
|
|
109
|
+
## Reasoning
|
|
110
|
+
|
|
111
|
+
Set `"hideThinkingBlock": true` in `~/.pi/agent/settings.json`, or press `Ctrl+T` until reasoning is collapsed. It then shows while the model is writing it and collapses to one line once the message is done. `Ctrl+T` opens it again, or click it to open just that one. `Ctrl+T` is Pi's own setting, so it applies to every message and stays set in later sessions.
|
|
@@ -27,7 +27,7 @@ Syntax tokens are borrowed on purpose: a theme spends its hues there, so they gi
|
|
|
27
27
|
|
|
28
28
|
## Settings
|
|
29
29
|
|
|
30
|
-
They live
|
|
30
|
+
They live under `frame` in `~/.pi/agent/suite.json`.
|
|
31
31
|
|
|
32
32
|
| Key | Default | Does |
|
|
33
33
|
| ------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from "node:path";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
ACCOUNT,
|
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
ExtensionContext,
|
|
24
24
|
ReadonlyFooterDataProvider,
|
|
25
25
|
} from "@earendil-works/pi-coding-agent";
|
|
26
|
-
import { keyText, parseSkillBlock } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { getAgentDir, keyText, parseSkillBlock } from "@earendil-works/pi-coding-agent";
|
|
27
27
|
import { Key, type KeyId, type TUI } from "@earendil-works/pi-tui";
|
|
28
28
|
|
|
29
29
|
import { type GitState, readGit } from "./data/git.ts";
|
|
@@ -157,7 +157,7 @@ export const frameFeature = defineFeature({
|
|
|
157
157
|
|
|
158
158
|
for (const entry of FRAME_SETTINGS) entry.listen(scope, repaint);
|
|
159
159
|
frame.listen(scope, () => live.ctx && installEditor(live.ctx, screen, live, scope));
|
|
160
|
-
const visitsFile = join(
|
|
160
|
+
const visitsFile = join(getAgentDir(), "extensions", "pi-harness", "frame-visits.json");
|
|
161
161
|
let visited = false;
|
|
162
162
|
let lastVisit: LastVisit | undefined;
|
|
163
163
|
|
|
@@ -527,6 +527,9 @@ function pressCtrlC(live: Live, clearEditor: () => void): void {
|
|
|
527
527
|
live.repaint?.();
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
/** Everything collapsed has finished, because live reasoning stays open. */
|
|
531
|
+
const THOUGHT_LABEL = "Thought · ctrl+t to expand";
|
|
532
|
+
|
|
530
533
|
function installEditor(
|
|
531
534
|
ctx: ExtensionContext,
|
|
532
535
|
screen: Screen,
|
|
@@ -535,9 +538,11 @@ function installEditor(
|
|
|
535
538
|
): void {
|
|
536
539
|
if (ctx.mode !== "tui") return;
|
|
537
540
|
if (frame.get(scope) === "off") {
|
|
541
|
+
ctx.ui.setHiddenThinkingLabel(undefined);
|
|
538
542
|
ctx.ui.setEditorComponent(undefined);
|
|
539
543
|
return;
|
|
540
544
|
}
|
|
545
|
+
ctx.ui.setHiddenThinkingLabel(THOUGHT_LABEL);
|
|
541
546
|
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
542
547
|
live.tui = tui;
|
|
543
548
|
applyCursor(tui, cursor);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Every app shares the settings files, so a write keeps every key it does not know.
|
|
2
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
|
|
5
5
|
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
@@ -14,7 +14,7 @@ export interface SettingsFile {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function globalSettingsPath(): string {
|
|
17
|
-
return join(getAgentDir(), "
|
|
17
|
+
return join(getAgentDir(), "suite.json");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function projectSettingsPath(cwd: string): string {
|
|
@@ -40,12 +40,12 @@ export function writeSettingsFile(
|
|
|
40
40
|
options: { mode?: number } = {},
|
|
41
41
|
): void {
|
|
42
42
|
mkdirSync(dirname(path), { recursive: true });
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
48
|
-
renameSync(temporary,
|
|
43
|
+
// A linked file stays linked, and a private one stays private. Other parts' settings share it.
|
|
44
|
+
const target = existsSync(path) ? realpathSync(path) : path;
|
|
45
|
+
const mode = options.mode ?? (existsSync(target) ? statSync(target).mode & 0o777 : undefined);
|
|
46
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
47
|
+
writeFileSync(temporary, `${JSON.stringify(data, null, "\t")}\n`, { encoding: "utf8", mode });
|
|
48
|
+
renameSync(temporary, target);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function lookup(data: SettingsData, id: string): unknown {
|
|
@@ -135,7 +135,9 @@ export class SettingsStore {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
#edit(path: string, change: (data: SettingsData) => void): string | undefined {
|
|
138
|
-
const { data } = readSettingsFile(path);
|
|
138
|
+
const { data, warnings } = readSettingsFile(path);
|
|
139
|
+
// Reading a broken file gives nothing, and writing that would wipe what else it holds.
|
|
140
|
+
if (warnings.length > 0) return warnings[0];
|
|
139
141
|
change(data);
|
|
140
142
|
try {
|
|
141
143
|
writeSettingsFile(path, data);
|
|
@@ -15,7 +15,15 @@ import { BOXES, border } from "../render/frame.ts";
|
|
|
15
15
|
import { paintLead, promptLead } from "../render/lead.ts";
|
|
16
16
|
import { SETTINGS_KEY, SETTINGS_SHORTCUT_INPUT } from "../render/keys.ts";
|
|
17
17
|
import type { CursorStyle } from "../settings.ts";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
findPrompt,
|
|
20
|
+
type HistorySearch,
|
|
21
|
+
type PrefixSearch,
|
|
22
|
+
shownText,
|
|
23
|
+
startPrefix,
|
|
24
|
+
startSearch,
|
|
25
|
+
stepPrefix,
|
|
26
|
+
} from "./history-search.ts";
|
|
19
27
|
import { type Screen, type SlotName, slotPieces } from "./screen.ts";
|
|
20
28
|
|
|
21
29
|
type WorkingIndicator = Parameters<CustomEditor["setWorkingStatusIndicator"]>[0];
|
|
@@ -44,6 +52,8 @@ export class FrameEditor extends CustomEditor {
|
|
|
44
52
|
#hiddenBelow = 0;
|
|
45
53
|
#inset = 0;
|
|
46
54
|
#search: HistorySearch | undefined;
|
|
55
|
+
#prefix: PrefixSearch | undefined;
|
|
56
|
+
#seeded = false;
|
|
47
57
|
|
|
48
58
|
constructor(
|
|
49
59
|
tui: TUI,
|
|
@@ -77,13 +87,9 @@ export class FrameEditor extends CustomEditor {
|
|
|
77
87
|
this.#searchKey(this.#search, data);
|
|
78
88
|
return;
|
|
79
89
|
}
|
|
90
|
+
if (this.#prefixKey(data)) return;
|
|
80
91
|
if (matchesKey(data, SEARCH_KEY) && !this.isShowingAutocomplete()) {
|
|
81
|
-
|
|
82
|
-
const own: unknown = Reflect.get(this, "history");
|
|
83
|
-
this.#search = startSearch(this.getText(), [
|
|
84
|
-
...(Array.isArray(own) ? (own as string[]) : []),
|
|
85
|
-
...this.#screen.pastPrompts(),
|
|
86
|
-
]);
|
|
92
|
+
this.#search = startSearch(this.getText(), this.#history());
|
|
87
93
|
return;
|
|
88
94
|
}
|
|
89
95
|
if (this.#keybindings.matches(data, "app.clear") && !this.isShowingAutocomplete()) {
|
|
@@ -101,6 +107,51 @@ export class FrameEditor extends CustomEditor {
|
|
|
101
107
|
if (skillQueryAt(this.getLines(), line, col) !== undefined) openAutocomplete(this);
|
|
102
108
|
}
|
|
103
109
|
|
|
110
|
+
/** What Up browses, then what was typed in this folder's other sessions. Read once, on first use. */
|
|
111
|
+
#history(): readonly string[] {
|
|
112
|
+
const own: unknown = Reflect.get(this, "history");
|
|
113
|
+
if (!Array.isArray(own)) return this.#screen.pastPrompts();
|
|
114
|
+
const entries = own as string[];
|
|
115
|
+
if (!this.#seeded) {
|
|
116
|
+
this.#seeded = true;
|
|
117
|
+
for (const prompt of this.#screen.pastPrompts()) {
|
|
118
|
+
if (!entries.includes(prompt)) entries.push(prompt);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return entries;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Up with one line typed looks back through prompts that start with it, and Down comes forward
|
|
126
|
+
* again. Any other key keeps what's showing. An empty prompt is left to pi's own browsing.
|
|
127
|
+
*/
|
|
128
|
+
#prefixKey(data: string): boolean {
|
|
129
|
+
const up = this.#keybindings.matches(data, "tui.editor.cursorUp");
|
|
130
|
+
const down = this.#keybindings.matches(data, "tui.editor.cursorDown");
|
|
131
|
+
if (!up && !down) {
|
|
132
|
+
this.#prefix = undefined;
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (this.isShowingAutocomplete()) return false;
|
|
136
|
+
if (this.#prefix === undefined) {
|
|
137
|
+
const typed = this.getText();
|
|
138
|
+
const entries = this.#history();
|
|
139
|
+
const browsing = Reflect.get(this, "historyIndex") !== -1;
|
|
140
|
+
if (down || typed === "" || typed.includes("\n") || browsing || !onFirstVisualLine(this)) return false;
|
|
141
|
+
const search = startPrefix(typed, entries);
|
|
142
|
+
// With nothing to go through, the arrows stay pi's.
|
|
143
|
+
if (search.matches.length === 0) return false;
|
|
144
|
+
this.#prefix = search;
|
|
145
|
+
}
|
|
146
|
+
const text = stepPrefix(this.#prefix, up ? 1 : -1);
|
|
147
|
+
if (text === undefined && down) {
|
|
148
|
+
this.#prefix = undefined;
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
if (text !== undefined) this.setText(text);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
104
155
|
/** Enter sends the match, Esc goes back, and any other key keeps the match to edit. */
|
|
105
156
|
#searchKey(search: HistorySearch, data: string): void {
|
|
106
157
|
if (matchesKey(data, SEARCH_KEY)) {
|
|
@@ -248,6 +299,12 @@ export class FrameEditor extends CustomEditor {
|
|
|
248
299
|
}
|
|
249
300
|
}
|
|
250
301
|
|
|
302
|
+
/** The method is private. If a pi release renames it, Up just moves the cursor as before. */
|
|
303
|
+
function onFirstVisualLine(editor: CustomEditor): boolean {
|
|
304
|
+
const check: unknown = Reflect.get(editor, "isOnFirstVisualLine");
|
|
305
|
+
return typeof check === "function" && check.call(editor) === true;
|
|
306
|
+
}
|
|
307
|
+
|
|
251
308
|
/** The method is private; nothing opens if a pi release renames it. */
|
|
252
309
|
function openAutocomplete(editor: CustomEditor): void {
|
|
253
310
|
const open: unknown = Reflect.get(editor, "tryTriggerAutocomplete");
|
|
@@ -21,3 +21,24 @@ export function findPrompt(search: HistorySearch, from: number): number {
|
|
|
21
21
|
export function shownText(search: HistorySearch): string {
|
|
22
22
|
return search.at < 0 ? search.draft : (search.entries[search.at] as string);
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
/** Up with text typed: older prompts that start with it, as in a shell. */
|
|
26
|
+
export interface PrefixSearch {
|
|
27
|
+
typed: string;
|
|
28
|
+
matches: readonly string[];
|
|
29
|
+
at: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function startPrefix(typed: string, entries: readonly string[]): PrefixSearch {
|
|
33
|
+
const wanted = typed.toLowerCase();
|
|
34
|
+
const matches = [...new Set(entries)].filter((entry) => entry !== typed && entry.toLowerCase().startsWith(wanted));
|
|
35
|
+
return { typed, matches, at: -1 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One step older (1) or newer (-1), ending back on what was typed. Undefined past either end. */
|
|
39
|
+
export function stepPrefix(search: PrefixSearch, step: 1 | -1): string | undefined {
|
|
40
|
+
const next = search.at + step;
|
|
41
|
+
if (next < -1 || next >= search.matches.length) return undefined;
|
|
42
|
+
search.at = next;
|
|
43
|
+
return next < 0 ? search.typed : search.matches[next];
|
|
44
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Pi's own transcript entries in the harness frame instead of a background. Pi has no hook for
|
|
2
2
|
// drawing them, so this wraps each component's render.
|
|
3
|
+
import { showThinkingWhileLive } from "./thinking.ts";
|
|
3
4
|
import { CHANGES_CUT, CHANGES_X, entryFrame, type EntryFrame, GLYPH } from "../kit/index.ts";
|
|
4
5
|
import {
|
|
5
6
|
BashExecutionComponent,
|
|
@@ -63,6 +64,7 @@ export function framePiEntries(source: EntryPaint): void {
|
|
|
63
64
|
paint = source;
|
|
64
65
|
if (installed) return;
|
|
65
66
|
installed = true;
|
|
67
|
+
showThinkingWhileLive(() => paint() !== undefined);
|
|
66
68
|
wrap(UserMessageComponent, (self, look) =>
|
|
67
69
|
look.you ? youFrame(look, fromOf(self, look), sentInOf(self, look)) : undefined,
|
|
68
70
|
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
/** Private in pi. If a release renames them, reasoning just follows pi's setting again. */
|
|
4
|
+
interface Message {
|
|
5
|
+
updateContent(message: { content: readonly { type: string }[] }, isStreaming?: boolean): void;
|
|
6
|
+
isStreaming?: boolean;
|
|
7
|
+
thinkingVisibilityOverrides?: Map<number, boolean>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const live = new WeakSet<object>();
|
|
11
|
+
let isActive: () => boolean = () => false;
|
|
12
|
+
let installed = false;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* With pi's setting that collapses reasoning on, it still shows while it's being written, and
|
|
16
|
+
* collapses once the message is done. Ctrl+T or a click opens it again.
|
|
17
|
+
*/
|
|
18
|
+
export function showThinkingWhileLive(active: () => boolean): void {
|
|
19
|
+
isActive = active;
|
|
20
|
+
if (installed) return;
|
|
21
|
+
installed = true;
|
|
22
|
+
const prototype = AssistantMessageComponent.prototype as unknown as Message;
|
|
23
|
+
const own = prototype.updateContent;
|
|
24
|
+
prototype.updateContent = function updateContent(this: Message, message, isStreaming = this.isStreaming) {
|
|
25
|
+
const overrides = this.thinkingVisibilityOverrides;
|
|
26
|
+
if (overrides !== undefined && isActive() && isStreaming === true) {
|
|
27
|
+
live.add(this);
|
|
28
|
+
message.content.forEach((_part, run) => overrides.set(run, false));
|
|
29
|
+
} else if (overrides !== undefined && live.delete(this)) {
|
|
30
|
+
overrides.clear();
|
|
31
|
+
}
|
|
32
|
+
own.call(this, message, isStreaming);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -9,7 +9,7 @@ Long-term memory for [Pi](https://pi.dev). It reviews your past conversations, k
|
|
|
9
9
|
Requires Pi 1.5 or newer, on Node 22.18 or newer.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
pi install npm:@huseynsnmz/pi-suite@0.2.
|
|
12
|
+
pi install npm:@huseynsnmz/pi-suite@0.2.4
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Use
|
|
@@ -23,6 +23,25 @@ pi install npm:@huseynsnmz/pi-suite@0.2.2
|
|
|
23
23
|
|
|
24
24
|
Or just ask: "remember that I deploy from main", "review our past conversations".
|
|
25
25
|
|
|
26
|
+
## Teaching weaker models
|
|
27
|
+
|
|
28
|
+
| Command | What it does |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `/mastermind` | A strong model reads past conversations, one batch at a time, and writes rules for your other models |
|
|
31
|
+
| `/mastermind compact` | Shortens the playbook, keeping what each rule means |
|
|
32
|
+
| `/mastermind show` | The rules in force in this folder |
|
|
33
|
+
|
|
34
|
+
Choose the model and its reasoning as `mastermind` in `/router`. It looks for what you corrected, mistakes that came back, and what the model should have read or checked first.
|
|
35
|
+
|
|
36
|
+
The result is a playbook, one global and one per folder, of up to 1,000 characters each. It goes in the system prompt of every run, up to about 500 tokens, except when the mastermind itself is the selected model. Each review rewrites it to fit, so it can't keep growing.
|
|
37
|
+
|
|
38
|
+
- **You approve every word:** the rules open in an editor, where you can delete or change them, and nothing is saved until you do. This matters because a session can contain text copied from files or web pages, and the playbook is followed as instructions. A secret is refused.
|
|
39
|
+
- **Your session stays small:** the strong model is called directly, so what it reads never enters your conversation, and there are no extra tools.
|
|
40
|
+
- **Separate from `/memory review`:** each keeps its own place in your sessions. Memories are facts, and the playbook is how to work.
|
|
41
|
+
- **Cost:** a batch sends up to about 15,000 tokens of input to the model you chose, so your past conversations go to that provider. It doesn't run on its own, and once 10 sessions are waiting, Pi mentions it after your first message.
|
|
42
|
+
|
|
43
|
+
Playbooks are in `~/.pi/agent/memory/playbook/`, with the last version of each kept as `.bak`.
|
|
44
|
+
|
|
26
45
|
## How it works
|
|
27
46
|
|
|
28
47
|
- **Review:** the `review-memory` skill reads a batch of about 60,000 characters, proposes memories, and saves the ones you keep. It remembers how far it read in each session file, so a session you continue later gets read from where it stopped.
|
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
|
+
import { compact, type Mastermind, review, REVIEWED_KEY } from "./mastermind.ts";
|
|
6
|
+
import { GLOBAL, playbookSection, readPlaybook } from "./playbook.ts";
|
|
5
7
|
import { promptSection } from "./prompt.ts";
|
|
6
8
|
import { countUnreviewed, nextBatch, sessionFiles, type ReviewBatch } from "./review.ts";
|
|
7
9
|
import { CATEGORIES, MemoryStore, type Category, type Memory } from "./store.ts";
|
|
8
10
|
|
|
11
|
+
/** Enough new material for a review to be worth what it costs. */
|
|
12
|
+
const NUDGE_SESSIONS = 10;
|
|
13
|
+
|
|
9
14
|
const text = (value: string, details: Record<string, unknown> = {}) => ({ content: [{ type: "text" as const, text: value }], details });
|
|
10
15
|
|
|
11
16
|
function line(memory: Memory): string {
|
|
@@ -22,14 +27,37 @@ export default function piMemory(pi: ExtensionAPI): void {
|
|
|
22
27
|
let lastBatch: ReviewBatch | null = null;
|
|
23
28
|
// Worked out once a session: a system prompt that changes mid-session would throw away the provider's prompt cache.
|
|
24
29
|
let section: string | null = null;
|
|
30
|
+
let playbook: string | null = null;
|
|
31
|
+
let nudged = false;
|
|
25
32
|
|
|
26
33
|
const store = () => (storePromise ??= MemoryStore.open(dbPath));
|
|
27
34
|
|
|
28
35
|
pi.on("session_start", () => {
|
|
29
36
|
section = null;
|
|
37
|
+
playbook = null;
|
|
30
38
|
lastBatch = null;
|
|
31
39
|
});
|
|
32
40
|
|
|
41
|
+
/** The strong model the router has for this, whichever part loads first. */
|
|
42
|
+
const mastermind = (): Mastermind | undefined => {
|
|
43
|
+
const probe: { model?: string; thinking?: string } = {};
|
|
44
|
+
pi.events.emit("pi-router:mastermind-model", probe);
|
|
45
|
+
return probe.model === undefined ? undefined : { model: probe.model, thinking: probe.thinking };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// After the first reply rather than at start, as it walks every session file.
|
|
49
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
50
|
+
if (nudged || !ctx.hasUI || mastermind() === undefined) return;
|
|
51
|
+
nudged = true;
|
|
52
|
+
try {
|
|
53
|
+
const memories = await store();
|
|
54
|
+
const waiting = countUnreviewed(sessionFiles(roots), (path) => memories.reviewedBytes(REVIEWED_KEY + path), ctx.sessionManager.getSessionFile());
|
|
55
|
+
if (waiting >= NUDGE_SESSIONS) ctx.ui.notify(`${waiting} sessions haven't been reviewed. /mastermind can turn them into rules for your models.`, "info");
|
|
56
|
+
} catch {
|
|
57
|
+
// A reminder isn't worth an error.
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
33
61
|
pi.on("session_shutdown", async () => {
|
|
34
62
|
const open = storePromise;
|
|
35
63
|
storePromise = null;
|
|
@@ -44,7 +72,10 @@ export default function piMemory(pi: ExtensionAPI): void {
|
|
|
44
72
|
section = "";
|
|
45
73
|
ctx.ui.notify(`pi-memory: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
46
74
|
}
|
|
47
|
-
|
|
75
|
+
playbook ??= playbookSection(readPlaybook(agentDir, GLOBAL), readPlaybook(agentDir, ctx.cwd));
|
|
76
|
+
// The model that writes the playbook doesn't need it.
|
|
77
|
+
const rules = ctx.model !== undefined && `${ctx.model.provider}/${ctx.model.id}` === mastermind()?.model ? "" : playbook;
|
|
78
|
+
return section || rules ? { systemPrompt: event.systemPrompt + section + rules } : undefined;
|
|
48
79
|
});
|
|
49
80
|
|
|
50
81
|
pi.registerTool({
|
|
@@ -141,6 +172,34 @@ export default function piMemory(pi: ExtensionAPI): void {
|
|
|
141
172
|
ctx.ui.notify(`${body}\n\nSessions not reviewed yet: ${pending}. /memory review goes through them.`, "info");
|
|
142
173
|
};
|
|
143
174
|
|
|
175
|
+
let reviewing = false;
|
|
176
|
+
|
|
177
|
+
pi.registerCommand("mastermind", {
|
|
178
|
+
description: "A strong model turns past conversations into rules for your other models. Or: compact | show",
|
|
179
|
+
handler: async (args, ctx) => {
|
|
180
|
+
if (!ctx.hasUI) return;
|
|
181
|
+
const action = args.trim();
|
|
182
|
+
if (action === "show") {
|
|
183
|
+
const shown = [GLOBAL, ctx.cwd].map((scope) => `${scope}:\n${readPlaybook(agentDir, scope) || "(empty)"}`).join("\n\n");
|
|
184
|
+
return ctx.ui.notify(shown, "info");
|
|
185
|
+
}
|
|
186
|
+
if (action !== "" && action !== "compact") return ctx.ui.notify("Usage: /mastermind [compact | show]", "warning");
|
|
187
|
+
const chosen = mastermind();
|
|
188
|
+
if (chosen === undefined) return ctx.ui.notify("Choose a strong model first: set mastermind in /router.", "warning");
|
|
189
|
+
if (reviewing) return ctx.ui.notify("/mastermind is already running.", "warning");
|
|
190
|
+
reviewing = true;
|
|
191
|
+
try {
|
|
192
|
+
if (action === "compact") await compact(ctx, agentDir, chosen, [GLOBAL, ctx.cwd]);
|
|
193
|
+
else await review(ctx, agentDir, chosen, await store(), sessionFiles(roots));
|
|
194
|
+
playbook = null;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
ctx.ui.notify(`/mastermind: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
197
|
+
} finally {
|
|
198
|
+
reviewing = false;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
144
203
|
pi.registerCommand("memory", {
|
|
145
204
|
description: "Show memories, or: /memory <words> | review | forget <id>",
|
|
146
205
|
handler: async (args, ctx) => {
|