@xynogen/pix-commands 0.4.0 → 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 CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@xynogen/pix-commands",
3
- "version": "0.4.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.6.0"
54
+ "@xynogen/pix-runtime": "^0.8.0"
51
55
  }
52
56
  }
@@ -1,12 +1,11 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
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((globalThis as { __pixAfk?: boolean }).__pixAfk).toBe(true);
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((globalThis as { __pixAfk?: boolean }).__pixAfk).toBe(false);
71
+ expect(getUnattendedMode(pi.events)).toBe("off");
72
72
  expect(statuses.at(-1)).toBeUndefined();
73
73
  });
74
74
  });
@@ -1,4 +1,5 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
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 keeps the two globals mutually exclusive", () => {
21
- setMode("afk");
22
- expect(getMode()).toBe("afk");
23
- expect((globalThis as G).__pixYolo).toBe(false);
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
- setMode("off");
44
- expect(unattendedBanner()).toBeUndefined();
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
- setMode("afk");
49
- const b = unattendedBanner() ?? "";
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
- setMode("yolo");
56
- const b = unattendedBanner() ?? "";
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
- (globalThis as G).__pixYoloConsent = true;
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 function getMode(): UnattendedMode {
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[getMode()];
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
- const g = globalThis as UnattendedGlobal;
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) g.__pixYoloConsent = true;
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 = getMode();
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", async (event) => {
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 = getMode() !== "afk";
165
- setMode(turningOn ? "afk" : "off");
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 (getMode() === "yolo") {
180
- setMode("off");
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
- setMode("yolo");
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.",