@xynogen/pix-commands 0.4.1 → 0.5.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/package.json +6 -2
- package/src/extension.test.ts +5 -5
- package/src/unattended.test.ts +24 -27
- package/src/unattended.ts +28 -41
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-commands",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Pi extension — slash commands for cache clearing and isolated side questions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./extension": "./src/extension.ts"
|
|
10
|
+
},
|
|
7
11
|
"scripts": {
|
|
8
12
|
"test": "bun test"
|
|
9
13
|
},
|
|
@@ -47,6 +51,6 @@
|
|
|
47
51
|
"dependencies": {
|
|
48
52
|
"@xynogen/pix-data": "^0.4.0",
|
|
49
53
|
"@xynogen/pix-pretty": "^1.13.0",
|
|
50
|
-
"@xynogen/pix-runtime": "^0.
|
|
54
|
+
"@xynogen/pix-runtime": "^0.8.0"
|
|
51
55
|
}
|
|
52
56
|
}
|
package/src/extension.test.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import type
|
|
2
|
+
import { createEventBus, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { icon } from "@xynogen/pix-pretty/icon-catalog";
|
|
4
|
+
import { getUnattendedMode } from "@xynogen/pix-runtime";
|
|
4
5
|
import extension from "./extension.ts";
|
|
5
6
|
|
|
6
7
|
afterEach(() => {
|
|
7
8
|
delete (globalThis as { __pixOnce?: WeakMap<object, Set<string>> }).__pixOnce;
|
|
8
|
-
delete (globalThis as { __pixAfk?: boolean }).__pixAfk;
|
|
9
|
-
delete (globalThis as { __pixYolo?: boolean }).__pixYolo;
|
|
10
9
|
});
|
|
11
10
|
|
|
12
11
|
describe("pix-commands registration", () => {
|
|
@@ -15,6 +14,7 @@ describe("pix-commands registration", () => {
|
|
|
15
14
|
const handlers = new Map<string, (args: string, ctx: never) => Promise<void>>();
|
|
16
15
|
const renderers: string[] = [];
|
|
17
16
|
const pi = {
|
|
17
|
+
events: createEventBus(),
|
|
18
18
|
registerCommand(
|
|
19
19
|
name: string,
|
|
20
20
|
options: { handler?: (args: string, ctx: never) => Promise<void> },
|
|
@@ -63,12 +63,12 @@ describe("pix-commands registration", () => {
|
|
|
63
63
|
if (!handler) throw new Error("/afk not registered");
|
|
64
64
|
|
|
65
65
|
await handler("", ctx as never);
|
|
66
|
-
expect((
|
|
66
|
+
expect(getUnattendedMode(pi.events)).toBe("afk");
|
|
67
67
|
expect(statuses.at(-1)).toBe(`<error>${icon("afk")} AFK</error>`);
|
|
68
68
|
expect(notices.at(-1)).toContain("yellow gates auto-allow");
|
|
69
69
|
|
|
70
70
|
await handler("", ctx as never);
|
|
71
|
-
expect((
|
|
71
|
+
expect(getUnattendedMode(pi.events)).toBe("off");
|
|
72
72
|
expect(statuses.at(-1)).toBeUndefined();
|
|
73
73
|
});
|
|
74
74
|
});
|
package/src/unattended.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createEventBus } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import {
|
|
3
4
|
confirmYoloConsent,
|
|
4
5
|
getMode,
|
|
@@ -8,26 +9,17 @@ import {
|
|
|
8
9
|
YOLO_MIN_SCORE,
|
|
9
10
|
} from "./unattended.ts";
|
|
10
11
|
|
|
11
|
-
type G = { __pixAfk?: boolean; __pixYolo?: boolean; __pixYoloConsent?: boolean };
|
|
12
|
-
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
delete (globalThis as G).__pixAfk;
|
|
15
|
-
delete (globalThis as G).__pixYolo;
|
|
16
|
-
delete (globalThis as G).__pixYoloConsent;
|
|
17
|
-
});
|
|
18
|
-
|
|
19
12
|
describe("unattended mode state", () => {
|
|
20
|
-
test("setMode
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
expect((
|
|
13
|
+
test("setMode updates one session", () => {
|
|
14
|
+
const events = createEventBus();
|
|
15
|
+
setMode(events, "afk");
|
|
16
|
+
expect(getMode(events)).toBe("afk");
|
|
24
17
|
|
|
25
|
-
setMode("yolo");
|
|
26
|
-
expect(getMode()).toBe("yolo");
|
|
27
|
-
expect((globalThis as G).__pixAfk).toBe(false);
|
|
18
|
+
setMode(events, "yolo");
|
|
19
|
+
expect(getMode(events)).toBe("yolo");
|
|
28
20
|
|
|
29
|
-
setMode("off");
|
|
30
|
-
expect(getMode()).toBe("off");
|
|
21
|
+
setMode(events, "off");
|
|
22
|
+
expect(getMode(events)).toBe("off");
|
|
31
23
|
});
|
|
32
24
|
});
|
|
33
25
|
|
|
@@ -40,20 +32,23 @@ describe("modelScore", () => {
|
|
|
40
32
|
|
|
41
33
|
describe("unattendedBanner", () => {
|
|
42
34
|
test("off => no banner", () => {
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
const events = createEventBus();
|
|
36
|
+
setMode(events, "off");
|
|
37
|
+
expect(unattendedBanner(events)).toBeUndefined();
|
|
45
38
|
});
|
|
46
39
|
|
|
47
40
|
test("afk banner names auto-deny of red and root", () => {
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
const events = createEventBus();
|
|
42
|
+
setMode(events, "afk");
|
|
43
|
+
const b = unattendedBanner(events) ?? "";
|
|
50
44
|
expect(b).toContain('mode="afk"');
|
|
51
45
|
expect(b).toContain("auto-DENY");
|
|
52
46
|
});
|
|
53
47
|
|
|
54
48
|
test("yolo banner demands red/root self-justification", () => {
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
const events = createEventBus();
|
|
50
|
+
setMode(events, "yolo");
|
|
51
|
+
const b = unattendedBanner(events) ?? "";
|
|
57
52
|
expect(b).toContain('mode="yolo"');
|
|
58
53
|
expect(b).toContain("blast radius");
|
|
59
54
|
expect(b).toContain("reversible");
|
|
@@ -68,12 +63,14 @@ describe("YOLO score threshold", () => {
|
|
|
68
63
|
|
|
69
64
|
describe("confirmYoloConsent (session gate)", () => {
|
|
70
65
|
test("short-circuits true once consent is recorded for the session", async () => {
|
|
71
|
-
|
|
66
|
+
const events = createEventBus();
|
|
67
|
+
const { setYoloConsent } = await import("@xynogen/pix-runtime");
|
|
68
|
+
setYoloConsent(events, true);
|
|
72
69
|
// No ui passed: if it did not short-circuit it would return false.
|
|
73
|
-
expect(await confirmYoloConsent({})).toBe(true);
|
|
70
|
+
expect(await confirmYoloConsent(events, {})).toBe(true);
|
|
74
71
|
});
|
|
75
72
|
|
|
76
73
|
test("refuses when there is no ui to render the warning", async () => {
|
|
77
|
-
expect(await confirmYoloConsent({})).toBe(false);
|
|
74
|
+
expect(await confirmYoloConsent(createEventBus(), {})).toBe(false);
|
|
78
75
|
});
|
|
79
76
|
});
|
package/src/unattended.ts
CHANGED
|
@@ -16,21 +16,21 @@
|
|
|
16
16
|
* cached PAM ticket; the password cannot be auto-typed).
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import type { EventBus, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { lookupBenchmark } from "@xynogen/pix-data";
|
|
21
21
|
import { showOverlay } from "@xynogen/pix-pretty/gate-overlay";
|
|
22
22
|
import { icon } from "@xynogen/pix-pretty/icon-catalog";
|
|
23
|
+
import {
|
|
24
|
+
getUnattendedMode,
|
|
25
|
+
hasYoloConsent,
|
|
26
|
+
setUnattendedMode,
|
|
27
|
+
setYoloConsent,
|
|
28
|
+
type UnattendedMode,
|
|
29
|
+
} from "@xynogen/pix-runtime";
|
|
23
30
|
|
|
24
31
|
/** Minimum model score allowed to hold YOLO. Below this, red+root auto-approve is off-limits. */
|
|
25
32
|
export const YOLO_MIN_SCORE = 75;
|
|
26
33
|
|
|
27
|
-
export type UnattendedMode = "off" | "afk" | "yolo";
|
|
28
|
-
type UnattendedGlobal = typeof globalThis & {
|
|
29
|
-
__pixAfk?: boolean;
|
|
30
|
-
__pixYolo?: boolean;
|
|
31
|
-
/** Session-scoped: user acknowledged the YOLO damage warning this session. */
|
|
32
|
-
__pixYoloConsent?: boolean;
|
|
33
|
-
};
|
|
34
34
|
type StatusUI = {
|
|
35
35
|
theme: { fg(color: "error", text: string): string };
|
|
36
36
|
setStatus(key: string, text: string | undefined): void;
|
|
@@ -40,21 +40,9 @@ type ModelCtx = { model?: { id?: string } };
|
|
|
40
40
|
|
|
41
41
|
const STATUS_KEY = "unattended";
|
|
42
42
|
|
|
43
|
-
export
|
|
44
|
-
const g = globalThis as UnattendedGlobal;
|
|
45
|
-
if (g.__pixYolo === true) return "yolo";
|
|
46
|
-
if (g.__pixAfk === true) return "afk";
|
|
47
|
-
return "off";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Set the mode; the two globals are mutually exclusive so gate/sudo never see both. */
|
|
51
|
-
export function setMode(mode: UnattendedMode): void {
|
|
52
|
-
const g = globalThis as UnattendedGlobal;
|
|
53
|
-
g.__pixAfk = mode === "afk";
|
|
54
|
-
g.__pixYolo = mode === "yolo";
|
|
55
|
-
}
|
|
43
|
+
export { getUnattendedMode as getMode, setUnattendedMode as setMode };
|
|
56
44
|
|
|
57
|
-
function syncStatus(ui: StatusUI): void {
|
|
45
|
+
function syncStatus(events: EventBus, ui: StatusUI): void {
|
|
58
46
|
// ponytail: reuses the existing "afk"/"warn" glyphs instead of adding a YOLO
|
|
59
47
|
// icon to pix-pretty (that is a public-API minor bump + approval).
|
|
60
48
|
const labels: Record<UnattendedMode, string | undefined> = {
|
|
@@ -62,7 +50,7 @@ function syncStatus(ui: StatusUI): void {
|
|
|
62
50
|
afk: `${icon("afk")} AFK`,
|
|
63
51
|
off: undefined,
|
|
64
52
|
};
|
|
65
|
-
const label = labels[
|
|
53
|
+
const label = labels[getUnattendedMode(events)];
|
|
66
54
|
ui.setStatus(STATUS_KEY, label ? ui.theme.fg("error", label) : undefined);
|
|
67
55
|
}
|
|
68
56
|
|
|
@@ -73,9 +61,8 @@ function syncStatus(ui: StatusUI): void {
|
|
|
73
61
|
* across sessions — a fresh session asks again (moral speed-bump, not a
|
|
74
62
|
* once-and-forget click-through).
|
|
75
63
|
*/
|
|
76
|
-
export async function confirmYoloConsent(ctx: ModelCtx): Promise<boolean> {
|
|
77
|
-
|
|
78
|
-
if (g.__pixYoloConsent === true) return true;
|
|
64
|
+
export async function confirmYoloConsent(events: EventBus, ctx: ModelCtx): Promise<boolean> {
|
|
65
|
+
if (hasYoloConsent(events)) return true;
|
|
79
66
|
const ui = (ctx as { ui?: unknown }).ui as Parameters<typeof showOverlay>[0] | undefined;
|
|
80
67
|
if (!ui) return false;
|
|
81
68
|
const result = await showOverlay(ui, {
|
|
@@ -101,7 +88,7 @@ export async function confirmYoloConsent(ctx: ModelCtx): Promise<boolean> {
|
|
|
101
88
|
],
|
|
102
89
|
});
|
|
103
90
|
const ok = result.action === "approved";
|
|
104
|
-
if (ok)
|
|
91
|
+
if (ok) setYoloConsent(events, true);
|
|
105
92
|
return ok;
|
|
106
93
|
}
|
|
107
94
|
|
|
@@ -116,8 +103,8 @@ export function modelScore(ctx: ModelCtx): number | null {
|
|
|
116
103
|
* Per-turn awareness banner. Injected via `before_agent_start` so the model is
|
|
117
104
|
* told, every turn, that the human safety net is off and what it now owns.
|
|
118
105
|
*/
|
|
119
|
-
export function unattendedBanner(): string | undefined {
|
|
120
|
-
const mode =
|
|
106
|
+
export function unattendedBanner(events: EventBus): string | undefined {
|
|
107
|
+
const mode = getUnattendedMode(events);
|
|
121
108
|
if (mode === "yolo") {
|
|
122
109
|
return [
|
|
123
110
|
'<pix-unattended mode="yolo">',
|
|
@@ -148,11 +135,11 @@ export function unattendedBanner(): string | undefined {
|
|
|
148
135
|
}
|
|
149
136
|
|
|
150
137
|
export default function registerUnattended(pi: ExtensionAPI): void {
|
|
151
|
-
pi.on("session_start", (_event, ctx) => syncStatus(ctx.ui));
|
|
138
|
+
pi.on("session_start", (_event, ctx) => syncStatus(pi.events, ctx.ui));
|
|
152
139
|
|
|
153
140
|
// Every turn: prepend the awareness banner while a mode is active.
|
|
154
|
-
pi.on("before_agent_start",
|
|
155
|
-
const banner = unattendedBanner();
|
|
141
|
+
pi.on("before_agent_start", (event) => {
|
|
142
|
+
const banner = unattendedBanner(pi.events);
|
|
156
143
|
if (!banner) return undefined;
|
|
157
144
|
const existing = (event as { systemPrompt?: string }).systemPrompt ?? "";
|
|
158
145
|
return { systemPrompt: `${banner}\n\n${existing}` };
|
|
@@ -161,9 +148,9 @@ export default function registerUnattended(pi: ExtensionAPI): void {
|
|
|
161
148
|
pi.registerCommand("afk", {
|
|
162
149
|
description: "Toggle AFK mode — yellow gates auto-allow; red and root auto-deny",
|
|
163
150
|
handler: async (_args, ctx) => {
|
|
164
|
-
const turningOn =
|
|
165
|
-
|
|
166
|
-
syncStatus(ctx.ui);
|
|
151
|
+
const turningOn = getUnattendedMode(pi.events) !== "afk";
|
|
152
|
+
setUnattendedMode(pi.events, turningOn ? "afk" : "off");
|
|
153
|
+
syncStatus(pi.events, ctx.ui);
|
|
167
154
|
ctx.ui.notify(
|
|
168
155
|
turningOn
|
|
169
156
|
? "AFK mode on — yellow gates auto-allow; red and root auto-deny."
|
|
@@ -176,9 +163,9 @@ export default function registerUnattended(pi: ExtensionAPI): void {
|
|
|
176
163
|
pi.registerCommand("yolo", {
|
|
177
164
|
description: "Toggle YOLO mode — auto-approve everything including red and root",
|
|
178
165
|
handler: async (_args, ctx) => {
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
syncStatus(ctx.ui);
|
|
166
|
+
if (getUnattendedMode(pi.events) === "yolo") {
|
|
167
|
+
setUnattendedMode(pi.events, "off");
|
|
168
|
+
syncStatus(pi.events, ctx.ui);
|
|
182
169
|
ctx.ui.notify("YOLO mode off — approval prompts restored.", "info");
|
|
183
170
|
return;
|
|
184
171
|
}
|
|
@@ -199,12 +186,12 @@ export default function registerUnattended(pi: ExtensionAPI): void {
|
|
|
199
186
|
);
|
|
200
187
|
return;
|
|
201
188
|
}
|
|
202
|
-
if (!(await confirmYoloConsent(ctx as ModelCtx))) {
|
|
189
|
+
if (!(await confirmYoloConsent(pi.events, ctx as ModelCtx))) {
|
|
203
190
|
ctx.ui.notify("YOLO cancelled — approval prompts remain in place.", "info");
|
|
204
191
|
return;
|
|
205
192
|
}
|
|
206
|
-
|
|
207
|
-
syncStatus(ctx.ui);
|
|
193
|
+
setUnattendedMode(pi.events, "yolo");
|
|
194
|
+
syncStatus(pi.events, ctx.ui);
|
|
208
195
|
ctx.ui.notify(
|
|
209
196
|
`YOLO mode on (model score ${score}) — every gate including red and root ` +
|
|
210
197
|
"auto-approves. No human will stop a destructive action. You are accountable.",
|