@hheei/omp-optimizer 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/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/package.json +30 -0
- package/src/capability.test.ts +42 -0
- package/src/capability.ts +16 -0
- package/src/caveman.test.ts +203 -0
- package/src/caveman.ts +140 -0
- package/src/edit-guard.test.ts +114 -0
- package/src/edit-guard.ts +137 -0
- package/src/index.ts +50 -0
- package/src/mode.ts +115 -0
- package/src/opt-keys.test.ts +165 -0
- package/src/opt.test.ts +34 -0
- package/src/opt.ts +161 -0
- package/src/persist.test.ts +52 -0
- package/src/persist.ts +58 -0
- package/src/ponytail.test.ts +195 -0
- package/src/ponytail.ts +145 -0
- package/src/rtk.test.ts +277 -0
- package/src/rtk.ts +355 -0
- package/src/status.test.ts +84 -0
- package/src/status.ts +61 -0
- package/src/t2s.test.ts +15 -0
- package/src/t2s.ts +122 -0
- package/src/tool-result-filter.test.ts +96 -0
- package/src/tool-result-filter.ts +53 -0
- package/tsconfig.json +7 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added the `omp-optimizer` extension with Caveman, RTK, and Ponytail modes behind `/optimizer`.
|
|
8
|
+
- Added OMP-native status-bar, custom UI, session-entry, persistence, and execution integrations.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Updated the optimizer panel to use OMP's native `SettingsList` interaction and theme styling.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xynogen
|
|
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
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @hheei/omp-optimizer
|
|
2
|
+
|
|
3
|
+
`@hheei/omp-optimizer` is an Oh My Pi extension that combines five tools behind one
|
|
4
|
+
`/optimizer` command:
|
|
5
|
+
|
|
6
|
+
## Fork provenance
|
|
7
|
+
|
|
8
|
+
This extension is forked from [`pix-optimizer`](https://github.com/xynogen/pix-mono/tree/main/packages/pix-optimizer) in [`xynogen/pix-mono`](https://github.com/xynogen/pix-mono). It has been adapted for Oh My Pi's public Extension API and does not retain the original pix host integration. The T2S tool is ported from [`@hheei/pi-t2s`](https://github.com/hheei/hepi-mono/tree/main/packages/pi-t2s).
|
|
9
|
+
|
|
10
|
+
- **Caveman** — injects a terse-output system prompt (`off`, `lite`, `full`,
|
|
11
|
+
`ultra`, `micro`).
|
|
12
|
+
- **RTK** — injects the RTK prompt and rewrites supported `bash` command chains
|
|
13
|
+
to use `rtk` when the binary is available. Direct `sudo` segments are blocked
|
|
14
|
+
so the model can use OMP's `sudo_run` flow when available.
|
|
15
|
+
- **Ponytail** — injects a minimal-code/YAGNI system prompt (`off`, `lite`,
|
|
16
|
+
`full`, `ultra`).
|
|
17
|
+
- **T2S** — converts interactive Traditional Chinese prose to Simplified Chinese while preserving inline and fenced code (`on`, `off`).
|
|
18
|
+
- **Edit Guard** — when enabled, aborts malformed `apply_patch`/unified-diff `@@` syntax in hashline edits and shell `apply_patch` calls when that tool is unavailable (`on`, `off`).
|
|
19
|
+
|
|
20
|
+
## Install and load
|
|
21
|
+
|
|
22
|
+
From this checkout, load the extension file directly:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
packages/hepi/omp-optimizer/src/index.ts
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For a published package, the manifest uses OMP's `omp.extensions` field:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"omp": {
|
|
33
|
+
"extensions": ["./src/index.ts"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
After publishing, install it through OMP:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
./scripts/omp install @hheei/omp-optimizer
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
Run `/optimizer` and use OMP's configured selection keys (normally `↑↓`) to
|
|
47
|
+
move between tools. `Enter` and `Space` cycle the selected value; `Esc` closes
|
|
48
|
+
the panel. The optional `q` shortcut also closes it. T2S and Edit Guard are
|
|
49
|
+
configured from the same panel. Edit Guard is enabled by default: it blocks
|
|
50
|
+
unified-diff syntax in hashline payloads and shell `apply_patch` calls when
|
|
51
|
+
`apply_patch` is not an active tool. In print/RPC mode, the command emits a
|
|
52
|
+
plain status summary instead of opening the interactive panel.
|
|
53
|
+
|
|
54
|
+
State is persisted by the extension under the active OMP agent directory as
|
|
55
|
+
`optimizer.json`. Session custom entries are also recorded through
|
|
56
|
+
`pi.appendEntry`, so branch/session navigation can restore the in-session value.
|
|
57
|
+
|
|
58
|
+
## OMP integration
|
|
59
|
+
|
|
60
|
+
The implementation uses OMP's public extension APIs and built-in packages:
|
|
61
|
+
|
|
62
|
+
- `ExtensionAPI` lifecycle hooks and `appendEntry` for prompt injection and
|
|
63
|
+
session state.
|
|
64
|
+
- `ctx.ui.custom`, `ctx.ui.setStatus`, `ctx.ui.notify`, and the OMP theme for
|
|
65
|
+
the panel and status bar.
|
|
66
|
+
- `ExtensionAPI.exec` for the RTK availability probe.
|
|
67
|
+
- `@oh-my-pi/pi-utils` for the active agent directory and centralized logging.
|
|
68
|
+
|
|
69
|
+
No `pix-pretty` or Pi fork compatibility package is required.
|
|
70
|
+
|
|
71
|
+
The status bar and `/optimizer` panel use Nerd Font glyphs. Use a terminal font
|
|
72
|
+
with Nerd Font support for the intended icons.
|
|
73
|
+
|
|
74
|
+
## Development
|
|
75
|
+
|
|
76
|
+
Run the focused tests from the repository root:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
bun test packages/hepi/omp-optimizer/src
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT. See `LICENSE`.
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hheei/omp-optimizer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Token-optimization and Traditional-to-Simplified Chinese extension for Oh My Pi",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"check": "biome check . --no-errors-on-unmatched && tsgo -p tsconfig.json --noEmit",
|
|
9
|
+
"test": "bun test src"
|
|
10
|
+
},
|
|
11
|
+
"omp": {
|
|
12
|
+
"extensions": ["./src/index.ts"]
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["omp", "omp-extension", "optimizer", "caveman", "rtk", "ponytail", "t2s", "traditional-chinese"],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/hheei/oh-my-pi.git",
|
|
19
|
+
"directory": "packages/hepi/omp-optimizer"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"opencc-js": "^1.4.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@oh-my-pi/pi-coding-agent": "*",
|
|
26
|
+
"@oh-my-pi/pi-tui": "*",
|
|
27
|
+
"@oh-my-pi/pi-ai": "*",
|
|
28
|
+
"@oh-my-pi/pi-utils": "*"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
3
|
+
import { canExecute } from "./capability.ts";
|
|
4
|
+
|
|
5
|
+
function executor(impl: ExtensionAPI["exec"]): Pick<ExtensionAPI, "exec"> {
|
|
6
|
+
return { exec: impl };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("canExecute", () => {
|
|
10
|
+
it("runs the requested capability probe without a shell", async () => {
|
|
11
|
+
const exec = mock(async () => ({
|
|
12
|
+
stdout: "jq-1.7",
|
|
13
|
+
stderr: "",
|
|
14
|
+
code: 0,
|
|
15
|
+
killed: false,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
expect(await canExecute(executor(exec), "jq", ["--version"])).toBe(true);
|
|
19
|
+
expect(exec).toHaveBeenCalledWith("jq", ["--version"], { timeout: 3000 });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("supports a custom timeout", async () => {
|
|
23
|
+
const exec = mock(async () => ({ stdout: "rtk 0.30.0", stderr: "", code: 0, killed: false }));
|
|
24
|
+
|
|
25
|
+
expect(await canExecute(executor(exec), "rtk", ["--version"], 1000)).toBe(true);
|
|
26
|
+
expect(exec).toHaveBeenCalledWith("rtk", ["--version"], { timeout: 1000 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns false for a non-zero exit", async () => {
|
|
30
|
+
const exec = mock(async () => ({ stdout: "", stderr: "failed", code: 1, killed: false }));
|
|
31
|
+
|
|
32
|
+
expect(await canExecute(executor(exec), "jq", ["--version"])).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns false when the executable cannot be spawned", async () => {
|
|
36
|
+
const exec = mock(async () => {
|
|
37
|
+
throw new Error("ENOENT");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(await canExecute(executor(exec), "rtk", ["--version"])).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
/** Test whether a command can perform a harmless operation through Pi's executor. */
|
|
4
|
+
export async function canExecute(
|
|
5
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
6
|
+
command: string,
|
|
7
|
+
args: string[],
|
|
8
|
+
timeout = 3000,
|
|
9
|
+
): Promise<boolean> {
|
|
10
|
+
try {
|
|
11
|
+
const result = await pi.exec(command, args, { timeout });
|
|
12
|
+
return result.code === 0;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildHelp,
|
|
4
|
+
buildPrompt,
|
|
5
|
+
LEVEL_NUMBERS,
|
|
6
|
+
LEVELS,
|
|
7
|
+
type Level,
|
|
8
|
+
resolveLevel,
|
|
9
|
+
STATUS_LABELS,
|
|
10
|
+
STOP_ALIASES,
|
|
11
|
+
toggleLevel,
|
|
12
|
+
} from "./caveman.ts";
|
|
13
|
+
|
|
14
|
+
// ── LEVELS ────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
describe("LEVELS", () => {
|
|
17
|
+
it("contains off as first entry", () => {
|
|
18
|
+
expect(LEVELS[0]).toBe("off");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("contains all expected levels", () => {
|
|
22
|
+
const expected: Level[] = ["off", "lite", "full", "ultra", "micro"];
|
|
23
|
+
for (const l of expected) expect(LEVELS).toContain(l);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ── STOP_ALIASES ──────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
describe("STOP_ALIASES", () => {
|
|
30
|
+
it("includes off, stop, quit", () => {
|
|
31
|
+
expect(STOP_ALIASES.has("off")).toBe(true);
|
|
32
|
+
expect(STOP_ALIASES.has("stop")).toBe(true);
|
|
33
|
+
expect(STOP_ALIASES.has("quit")).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("does not include active levels", () => {
|
|
37
|
+
expect(STOP_ALIASES.has("full")).toBe(false);
|
|
38
|
+
expect(STOP_ALIASES.has("ultra")).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ── STATUS_LABELS ─────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
describe("STATUS_LABELS", () => {
|
|
45
|
+
it("has a label for every non-off level", () => {
|
|
46
|
+
const nonOff = LEVELS.filter((l) => l !== "off") as Exclude<Level, "off">[];
|
|
47
|
+
for (const l of nonOff) {
|
|
48
|
+
expect(STATUS_LABELS[l]).toBeTruthy();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("latin levels are uppercase", () => {
|
|
53
|
+
expect(STATUS_LABELS.lite).toBe("LITE");
|
|
54
|
+
expect(STATUS_LABELS.full).toBe("FULL");
|
|
55
|
+
expect(STATUS_LABELS.ultra).toBe("ULTRA");
|
|
56
|
+
expect(STATUS_LABELS.micro).toBe("MICRO");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ── resolveLevel ──────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
describe("resolveLevel", () => {
|
|
63
|
+
it("resolves valid levels", () => {
|
|
64
|
+
expect(resolveLevel("lite")).toBe("lite");
|
|
65
|
+
expect(resolveLevel("full")).toBe("full");
|
|
66
|
+
expect(resolveLevel("ultra")).toBe("ultra");
|
|
67
|
+
expect(resolveLevel("micro")).toBe("micro");
|
|
68
|
+
expect(resolveLevel("off")).toBe("off");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("maps stop aliases to off", () => {
|
|
72
|
+
expect(resolveLevel("stop")).toBe("off");
|
|
73
|
+
expect(resolveLevel("quit")).toBe("off");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("is case-insensitive", () => {
|
|
77
|
+
expect(resolveLevel("FULL")).toBe("full");
|
|
78
|
+
expect(resolveLevel("Ultra")).toBe("ultra");
|
|
79
|
+
expect(resolveLevel("STOP")).toBe("off");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("trims whitespace", () => {
|
|
83
|
+
expect(resolveLevel(" full ")).toBe("full");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns null for unknown input", () => {
|
|
87
|
+
expect(resolveLevel("unknown")).toBeNull();
|
|
88
|
+
expect(resolveLevel("")).toBeNull();
|
|
89
|
+
expect(resolveLevel("config")).toBeNull(); // config handled separately
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── numeric levels ────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
describe("numeric levels", () => {
|
|
96
|
+
it("maps 1/2/3 to lite/full/ultra", () => {
|
|
97
|
+
expect(resolveLevel("1")).toBe("lite");
|
|
98
|
+
expect(resolveLevel("2")).toBe("full");
|
|
99
|
+
expect(resolveLevel("3")).toBe("ultra");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("maps 0 to off", () => {
|
|
103
|
+
expect(resolveLevel("0")).toBe("off");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("LEVEL_NUMBERS only covers 1-3", () => {
|
|
107
|
+
expect(Object.keys(LEVEL_NUMBERS).sort()).toEqual(["1", "2", "3"]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("rejects out-of-range numbers", () => {
|
|
111
|
+
expect(resolveLevel("4")).toBeNull();
|
|
112
|
+
expect(resolveLevel("9")).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("trims whitespace around numbers", () => {
|
|
116
|
+
expect(resolveLevel(" 2 ")).toBe("full");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ── buildHelp ─────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
describe("buildHelp", () => {
|
|
123
|
+
it("lists numeric shortcuts", () => {
|
|
124
|
+
const help = buildHelp("off");
|
|
125
|
+
expect(help).toContain("1");
|
|
126
|
+
expect(help).toContain("lite");
|
|
127
|
+
expect(help).toContain("2");
|
|
128
|
+
expect(help).toContain("full");
|
|
129
|
+
expect(help).toContain("3");
|
|
130
|
+
expect(help).toContain("ultra");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("shows current level when active", () => {
|
|
134
|
+
expect(buildHelp("ultra")).toContain("ULTRA");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("shows off when disabled", () => {
|
|
138
|
+
expect(buildHelp("off")).toContain("off");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("mentions config", () => {
|
|
142
|
+
expect(buildHelp("off")).toContain("config");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("does not mention wenyan", () => {
|
|
146
|
+
expect(buildHelp("off").toLowerCase()).not.toContain("wenyan");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ── toggleLevel ───────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
describe("toggleLevel", () => {
|
|
153
|
+
it("off → full", () => {
|
|
154
|
+
expect(toggleLevel("off")).toBe("full");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("full → off", () => {
|
|
158
|
+
expect(toggleLevel("full")).toBe("off");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("any non-off level → off", () => {
|
|
162
|
+
const nonOff = LEVELS.filter((l) => l !== "off") as Level[];
|
|
163
|
+
for (const l of nonOff) {
|
|
164
|
+
expect(toggleLevel(l)).toBe("off");
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ── buildPrompt ───────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
describe("buildPrompt", () => {
|
|
172
|
+
it("returns empty string for off", () => {
|
|
173
|
+
expect(buildPrompt("off")).toBe("");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("returns MICRO_PROMPT for micro (no BASE)", () => {
|
|
177
|
+
const p = buildPrompt("micro");
|
|
178
|
+
expect(p).toContain("Token efficiency");
|
|
179
|
+
expect(p).not.toContain("CAVEMAN MODE");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("includes BASE for all standard levels", () => {
|
|
183
|
+
const standard = ["lite", "full", "ultra"] as Level[];
|
|
184
|
+
for (const l of standard) {
|
|
185
|
+
expect(buildPrompt(l)).toContain("CAVEMAN MODE");
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("includes SAFETY clause for all standard levels", () => {
|
|
190
|
+
const standard = ["lite", "full", "ultra"] as Level[];
|
|
191
|
+
for (const l of standard) {
|
|
192
|
+
expect(buildPrompt(l)).toContain("Auto-clarity");
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("each level has distinct intensity instructions", () => {
|
|
197
|
+
const lite = buildPrompt("lite");
|
|
198
|
+
const ultra = buildPrompt("ultra");
|
|
199
|
+
expect(lite).toContain("Keep articles");
|
|
200
|
+
expect(ultra).toContain("Abbreviate");
|
|
201
|
+
expect(lite).not.toContain("Abbreviate");
|
|
202
|
+
});
|
|
203
|
+
});
|
package/src/caveman.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* caveman.ts — pure logic + Pi extension
|
|
3
|
+
*
|
|
4
|
+
* Pure helpers exported for tests; caveman(pi) is the extension entry,
|
|
5
|
+
* called by index.ts alongside rtk(pi).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
9
|
+
import {
|
|
10
|
+
createMode,
|
|
11
|
+
resolveLevel as resolveLevelGeneric,
|
|
12
|
+
toggleLevel as toggleLevelGeneric,
|
|
13
|
+
} from "./mode.ts";
|
|
14
|
+
import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
|
|
15
|
+
|
|
16
|
+
// ── Levels ────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export const LEVELS = ["off", "lite", "full", "ultra", "micro"] as const;
|
|
19
|
+
|
|
20
|
+
export type Level = (typeof LEVELS)[number];
|
|
21
|
+
|
|
22
|
+
export const STOP_ALIASES = new Set(["off", "stop", "quit", "0"]);
|
|
23
|
+
|
|
24
|
+
// Numeric shortcuts: /caveman 1|2|3
|
|
25
|
+
export const LEVEL_NUMBERS: Record<string, Level> = {
|
|
26
|
+
"1": "lite",
|
|
27
|
+
"2": "full",
|
|
28
|
+
"3": "ultra",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ── Status labels ─────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export const STATUS_LABELS: Record<Exclude<Level, "off">, string> = {
|
|
34
|
+
lite: "LITE",
|
|
35
|
+
full: "FULL",
|
|
36
|
+
ultra: "ULTRA",
|
|
37
|
+
micro: "MICRO",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ── Prompt fragments ──────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const BASE = `\
|
|
43
|
+
IMPORTANT: You are in CAVEMAN MODE. Respond terse like smart caveman. \
|
|
44
|
+
All technical substance stay. Only fluff die.
|
|
45
|
+
|
|
46
|
+
Rules:
|
|
47
|
+
- Drop articles (a/an/the), filler (just/really/basically/actually/simply), \
|
|
48
|
+
pleasantries, hedging
|
|
49
|
+
- Fragments OK. Short synonyms preferred. Technical terms exact
|
|
50
|
+
- Code blocks unchanged. Errors quoted exact
|
|
51
|
+
- Pattern: [thing] [action] [reason]. [next step].
|
|
52
|
+
|
|
53
|
+
Bad: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
|
|
54
|
+
Good: "Bug in auth middleware. Token expiry check use \`<\` not \`<=\`. Fix:"`;
|
|
55
|
+
|
|
56
|
+
const MICRO_PROMPT = `# Token efficiency
|
|
57
|
+
Respond like smart caveman. Cut all filler, keep technical substance.
|
|
58
|
+
- Drop articles (a, an, the), filler (just, really, basically, actually).
|
|
59
|
+
- Drop pleasantries (sure, certainly, happy to).
|
|
60
|
+
- No hedging. Fragments fine. Short synonyms.
|
|
61
|
+
- Technical terms stay exact. Code blocks unchanged.
|
|
62
|
+
- Pattern: [thing] [action] [reason]. [next step].`;
|
|
63
|
+
|
|
64
|
+
const INTENSITY: Record<Exclude<Level, "off" | "micro">, string> = {
|
|
65
|
+
lite: `\
|
|
66
|
+
No filler/hedging. Keep articles + full sentences. Professional but tight.
|
|
67
|
+
Example: "Your component re-renders because you create a new object reference each render. Wrap it in \`useMemo\`."`,
|
|
68
|
+
|
|
69
|
+
full: `\
|
|
70
|
+
Drop articles, fragments OK, short synonyms.
|
|
71
|
+
Example: "New object ref each render. Inline object prop = new ref = re-render. Wrap in \`useMemo\`."`,
|
|
72
|
+
|
|
73
|
+
ultra: `\
|
|
74
|
+
Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y).
|
|
75
|
+
Example: "Inline obj prop → new ref → re-render. \`useMemo\`."`,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const SAFETY = `\
|
|
79
|
+
Auto-clarity: drop caveman for security warnings, irreversible action confirmations, \
|
|
80
|
+
or when user is confused. Resume after.
|
|
81
|
+
Boundaries: write normal code. Only compress explanations. "stop caveman" or "normal mode" reverts.`;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build the system prompt injection for a given level.
|
|
85
|
+
* Returns empty string when level is "off".
|
|
86
|
+
*/
|
|
87
|
+
export function buildPrompt(level: Level): string {
|
|
88
|
+
if (level === "off") return "";
|
|
89
|
+
if (level === "micro") return MICRO_PROMPT;
|
|
90
|
+
return [BASE, "", `Intensity: ${INTENSITY[level]}`, "", SAFETY].join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Level resolution ──────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve a raw command arg to a Level, or return null if unrecognised.
|
|
97
|
+
* Handles stop aliases (stop/quit → "off") and valid level names.
|
|
98
|
+
*/
|
|
99
|
+
export function resolveLevel(arg: string): Level | null {
|
|
100
|
+
return resolveLevelGeneric(arg, LEVELS, LEVEL_NUMBERS, STOP_ALIASES);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Help text shown when /caveman is run with no argument.
|
|
105
|
+
*/
|
|
106
|
+
export function buildHelp(current: Level): string {
|
|
107
|
+
const statusLine = current === "off" ? "off" : `${STATUS_LABELS[current]} (${current})`;
|
|
108
|
+
return [
|
|
109
|
+
`Caveman mode: ${statusLine}`,
|
|
110
|
+
"",
|
|
111
|
+
"Usage: /caveman <level>",
|
|
112
|
+
" 1 lite - professional, no fluff",
|
|
113
|
+
" 2 full - classic caveman",
|
|
114
|
+
" 3 ultra - maximum compression",
|
|
115
|
+
" 0 off - disable (aliases: off, stop, quit)",
|
|
116
|
+
"",
|
|
117
|
+
"Other levels: micro",
|
|
118
|
+
" config - open settings dialog",
|
|
119
|
+
].join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Toggle: off → full, anything else → off.
|
|
124
|
+
*/
|
|
125
|
+
export function toggleLevel(current: Level): Level {
|
|
126
|
+
return toggleLevelGeneric(current);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Pi extension ────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
export function caveman(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle {
|
|
132
|
+
return createMode(pi, status, {
|
|
133
|
+
name: "caveman",
|
|
134
|
+
help: "caveman — terse output",
|
|
135
|
+
levels: LEVELS,
|
|
136
|
+
buildPrompt,
|
|
137
|
+
resolve: resolveLevel,
|
|
138
|
+
notify: (level) => (level === "off" ? "Caveman mode off." : `Caveman: ${STATUS_LABELS[level]}`),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
3
|
+
import type { OptimizerStatus } from "./status.ts";
|
|
4
|
+
import { editGuard, hasMalformedHashlineInput, hasStreamingApplyPatchCommand } from "./edit-guard.ts";
|
|
5
|
+
|
|
6
|
+
const historicalMalformedPayload = `[packages/hepi/omp-optimizer/src/index.ts#C557]\nPUT 2.=12:\n+const value = true;\n@@\nPUT 20.=20:\n+const next = false;`;
|
|
7
|
+
|
|
8
|
+
function callUpdate(
|
|
9
|
+
handler: (event: unknown, context: { abort(): void }) => void,
|
|
10
|
+
name: string,
|
|
11
|
+
arguments_: Record<string, string>,
|
|
12
|
+
abort: () => void,
|
|
13
|
+
): void {
|
|
14
|
+
handler(
|
|
15
|
+
{
|
|
16
|
+
assistantMessageEvent: {
|
|
17
|
+
type: "toolcall_delta",
|
|
18
|
+
contentIndex: 0,
|
|
19
|
+
partial: {
|
|
20
|
+
content: [{ type: "toolCall", name, arguments: arguments_ }],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{ abort },
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeGuard(activeTools: readonly string[] = ["edit"]) {
|
|
29
|
+
const handlers = new Map<string, (event: unknown, context?: { abort(): void }) => unknown>();
|
|
30
|
+
const messages: unknown[] = [];
|
|
31
|
+
const pi = {
|
|
32
|
+
events: {},
|
|
33
|
+
on(event: string, handler: (event: unknown, context?: { abort(): void }) => unknown) {
|
|
34
|
+
handlers.set(event, handler);
|
|
35
|
+
},
|
|
36
|
+
getActiveTools: () => [...activeTools],
|
|
37
|
+
sendMessage(message: unknown) {
|
|
38
|
+
messages.push(message);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
editGuard(pi as unknown as ExtensionAPI, { set: () => {} } as unknown as OptimizerStatus);
|
|
42
|
+
return { handlers, messages };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("edit guard detection", () => {
|
|
46
|
+
it("matches historical bare @@ hashline failures", () => {
|
|
47
|
+
expect(hasMalformedHashlineInput(historicalMalformedPayload)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("matches shell apply_patch commands but not plain mentions", () => {
|
|
51
|
+
expect(hasStreamingApplyPatchCommand("apply_patch <<'PATCH'\n...")).toBe(true);
|
|
52
|
+
expect(hasStreamingApplyPatchCommand("please do not use apply_patch")).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("ignores valid hashline bodies and other edit formats", () => {
|
|
56
|
+
expect(hasMalformedHashlineInput("[src/a.ts#ABCD]\nPUT 1.=1:\n+const marker = '@@';")).toBe(false);
|
|
57
|
+
expect(hasMalformedHashlineInput("[src/a.ts#ABCD]\nPUT 1.=1:\n+@@")).toBe(false);
|
|
58
|
+
expect(hasMalformedHashlineInput("*** Begin Patch\n*** Update File: src/a.ts\n@@ -1 +1 @@")).toBe(false);
|
|
59
|
+
expect(hasMalformedHashlineInput("please avoid @@ when editing")).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("edit guard runtime", () => {
|
|
64
|
+
it("aborts malformed hashline once, then sends retry guidance", () => {
|
|
65
|
+
const { handlers, messages } = makeGuard();
|
|
66
|
+
const update = handlers.get("message_update") as (event: unknown, context: { abort(): void }) => void;
|
|
67
|
+
const settled = handlers.get("agent_end") as (event: { willContinue?: boolean }, context: unknown) => void;
|
|
68
|
+
let aborts = 0;
|
|
69
|
+
|
|
70
|
+
callUpdate(update, "edit", { input: historicalMalformedPayload }, () => {
|
|
71
|
+
aborts += 1;
|
|
72
|
+
});
|
|
73
|
+
callUpdate(update, "edit", { input: `${historicalMalformedPayload}\n@@` }, () => {
|
|
74
|
+
aborts += 1;
|
|
75
|
+
});
|
|
76
|
+
expect(aborts).toBe(1);
|
|
77
|
+
|
|
78
|
+
settled({ willContinue: false }, {});
|
|
79
|
+
expect(messages).toHaveLength(1);
|
|
80
|
+
expect(messages[0]).toEqual({
|
|
81
|
+
customType: "edit-guard",
|
|
82
|
+
content: expect.stringContaining("Do not retry the same payload"),
|
|
83
|
+
display: true,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("aborts unavailable shell apply_patch and names active edit alternative", () => {
|
|
88
|
+
const { handlers, messages } = makeGuard(["edit"]);
|
|
89
|
+
const update = handlers.get("message_update") as (event: unknown, context: { abort(): void }) => void;
|
|
90
|
+
const settled = handlers.get("agent_end") as (event: { willContinue?: boolean }, context: unknown) => void;
|
|
91
|
+
let aborts = 0;
|
|
92
|
+
|
|
93
|
+
callUpdate(update, "bash", { command: "apply_patch <<'PATCH'\n..." }, () => {
|
|
94
|
+
aborts += 1;
|
|
95
|
+
});
|
|
96
|
+
expect(aborts).toBe(1);
|
|
97
|
+
settled({ willContinue: false }, {});
|
|
98
|
+
expect(messages[0]).toEqual({
|
|
99
|
+
customType: "edit-guard",
|
|
100
|
+
content: expect.stringContaining("Continue with `edit`"),
|
|
101
|
+
display: true,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("does not block apply_patch when tool is active", () => {
|
|
106
|
+
const { handlers } = makeGuard(["apply_patch", "edit"]);
|
|
107
|
+
const update = handlers.get("message_update") as (event: unknown, context: { abort(): void }) => void;
|
|
108
|
+
let aborts = 0;
|
|
109
|
+
callUpdate(update, "bash", { command: "apply_patch <<'PATCH'\n..." }, () => {
|
|
110
|
+
aborts += 1;
|
|
111
|
+
});
|
|
112
|
+
expect(aborts).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
});
|