pi-mega-compact 0.8.0 → 0.8.1
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/dist/extensions/dashboard-server/server.js +9 -3
- package/dist/extensions/mega-compact.js +1 -1
- package/dist/extensions/mega-events/context-handler.js +5 -3
- package/dist/extensions/mega-game-cmds.js +95 -82
- package/dist/extensions/mega-game-cmds.test.js +6 -1
- package/dist/extensions/mega-runtime/state.js +16 -2
- package/extensions/dashboard-server/server.ts +9 -3
- package/extensions/mega-compact.ts +1 -1
- package/extensions/mega-events/context-handler.ts +4 -2
- package/extensions/mega-game-cmds.test.ts +7 -1
- package/extensions/mega-game-cmds.ts +103 -83
- package/extensions/mega-runtime/state.ts +17 -3
- package/package.json +1 -1
|
@@ -17,10 +17,16 @@ export async function launchDashboardServer(stateDir) {
|
|
|
17
17
|
// upgrade instead of reuse it.
|
|
18
18
|
let SERVER_VERSION = "0.0.0";
|
|
19
19
|
try {
|
|
20
|
-
// dashboard-server.js lives at
|
|
21
|
-
//
|
|
20
|
+
// Since v0.7.9 (8821ef3) dashboard-server.js lives at
|
|
21
|
+
// <pkg>/dist/extensions/dashboard-server/, so package.json is THREE levels
|
|
22
|
+
// up. Keep the two- and one-level-up candidates as fallbacks for flatter
|
|
23
|
+
// dev-checkout layouts. Guard each candidate so a missing file is skipped.
|
|
22
24
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
const candidates = [
|
|
25
|
+
const candidates = [
|
|
26
|
+
join(here, "..", "..", "..", "package.json"),
|
|
27
|
+
join(here, "..", "..", "package.json"),
|
|
28
|
+
join(here, "..", "package.json"),
|
|
29
|
+
];
|
|
24
30
|
for (const p of candidates) {
|
|
25
31
|
if (!existsSync(p))
|
|
26
32
|
continue;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* - mega-runtime.ts shared live state (MegaRuntime) + widget + model capture
|
|
19
19
|
* - mega-pipeline.ts runCompact (Trident+persist) + doRecall (Layer 5)
|
|
20
20
|
* - mega-commands.ts data/inspection slash commands
|
|
21
|
-
* - mega-game-cmds.ts /mega-game toggle + theme + TUI display mode
|
|
21
|
+
* - mega-game-cmds.ts /mega-compact-settings (+ /mega-game alias) toggle + theme + TUI display mode
|
|
22
22
|
* - mega-dashboard-cmds.ts localhost dashboard server lifecycle commands
|
|
23
23
|
* - mega-events.ts pi lifecycle event handlers
|
|
24
24
|
*
|
|
@@ -50,15 +50,17 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
50
50
|
// Legacy: MEGACOMPACT_LEGACY_DURABLE_TRIM=true restores the v0.4.28 ctx.compact
|
|
51
51
|
// path (kept one release as rollback).
|
|
52
52
|
pi.on("context", async (event, ctx) => {
|
|
53
|
-
if (!config.auto)
|
|
54
|
-
return;
|
|
55
53
|
const usage = ctx.getContextUsage();
|
|
56
54
|
const pct = usage?.percent;
|
|
57
|
-
// Always track context for the dashboard, even
|
|
55
|
+
// Always track context for the dashboard/widget, even when auto is off.
|
|
56
|
+
// (v0.8 regression: !config.auto gate sat above this, leaving ctx stats
|
|
57
|
+
// null -> widget '?% / ?/?' when auto disabled. Track first, THEN gate.)
|
|
58
58
|
runtime.lastCtxTokens = usage?.tokens ?? null;
|
|
59
59
|
runtime.lastCtxPercent = pct ?? null;
|
|
60
60
|
runtime.lastCtxWindow = usage?.contextWindow ?? 0;
|
|
61
61
|
runtime.snapshot(ctx);
|
|
62
|
+
if (!config.auto)
|
|
63
|
+
return;
|
|
62
64
|
const messages = event.messages;
|
|
63
65
|
const view = runtime.engineView(messages);
|
|
64
66
|
const currentTokens = usage?.tokens ??
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* mega-game-cmds.ts — /mega-
|
|
2
|
+
* mega-game-cmds.ts — /mega-compact-settings slash command (S30; renamed in v0.8.x).
|
|
3
3
|
*
|
|
4
4
|
* Backs the game-mode toggle + theme picker + TUI display mode. All state is
|
|
5
5
|
* the global `game_state` SQLite row (src/store/sqlite/game-state.ts) — local
|
|
@@ -7,100 +7,113 @@
|
|
|
7
7
|
* command touches no fetch/http). All SQL is parameterized (PREVENT-002) and
|
|
8
8
|
* lives in the src/ submodule, not here.
|
|
9
9
|
*
|
|
10
|
+
* The primary command is /mega-compact-settings. /mega-game is retained as a
|
|
11
|
+
* backward-compat alias (same handler) so existing muscle memory + docs keep
|
|
12
|
+
* working.
|
|
13
|
+
*
|
|
10
14
|
* Usage:
|
|
11
|
-
* /mega-
|
|
12
|
-
* /mega-
|
|
13
|
-
* /mega-
|
|
14
|
-
* /mega-
|
|
15
|
-
* /mega-
|
|
16
|
-
* /mega-
|
|
17
|
-
* /mega-
|
|
18
|
-
* /mega-
|
|
19
|
-
* /mega-
|
|
15
|
+
* /mega-compact-settings print current state
|
|
16
|
+
* /mega-compact-settings on enable game mode (scoring + level-up + MEGA CACHE)
|
|
17
|
+
* /mega-compact-settings off disable game mode
|
|
18
|
+
* /mega-compact-settings theme list available themes
|
|
19
|
+
* /mega-compact-settings theme <id> set theme by id
|
|
20
|
+
* /mega-compact-settings theme next cycle to next theme
|
|
21
|
+
* /mega-compact-settings tui full full TUI widget (bars, stats, flair)
|
|
22
|
+
* /mega-compact-settings tui minimal one-line TUI widget (level + cache %)
|
|
23
|
+
* /mega-compact-settings achievements list unlocked achievements
|
|
20
24
|
*/
|
|
21
25
|
import { getGameState, setGameState, } from "../src/store/sqlite.js";
|
|
22
26
|
import { listAchievements } from "../src/store/sqlite/game-achievements.js";
|
|
23
27
|
import { THEMES, THEME_IDS, getTheme, isValidTheme, nextTheme, DEFAULT_THEME } from "../src/config/themes.js";
|
|
28
|
+
/** Notify/usage tag + command name. Primary surface is /mega-compact-settings. */
|
|
29
|
+
const TAG = "mega-compact-settings";
|
|
24
30
|
/** Format the current state as a human-readable status line set. */
|
|
25
31
|
function fmtState(s) {
|
|
26
32
|
return [
|
|
27
|
-
`[
|
|
33
|
+
`[${TAG}] game mode: ${s.game_mode_on ? "ON" : "off"}`,
|
|
28
34
|
` theme: ${s.theme}${s.theme === DEFAULT_THEME ? " (default)" : ""}`,
|
|
29
35
|
` tui: ${s.tui_display_mode}`,
|
|
30
36
|
];
|
|
31
37
|
}
|
|
32
|
-
/**
|
|
38
|
+
/** Shared handler for /mega-compact-settings (primary) + /mega-game (alias). */
|
|
39
|
+
async function handleSettings(args, ctx, runtime) {
|
|
40
|
+
runtime.bindRepo(ctx.cwd);
|
|
41
|
+
const stateDir = runtime.currentStateDir;
|
|
42
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
43
|
+
// bare → print current state.
|
|
44
|
+
if (parts.length === 0) {
|
|
45
|
+
const s = getGameState(stateDir);
|
|
46
|
+
for (const line of fmtState(s))
|
|
47
|
+
ctx.ui.notify(line);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const sub = parts[0];
|
|
51
|
+
// achievements — terse list of unlocked (hidden only once unlocked).
|
|
52
|
+
if (sub === "achievements") {
|
|
53
|
+
const rows = listAchievements(stateDir).filter((r) => r.unlocked_at != null);
|
|
54
|
+
ctx.ui.notify(`[${TAG}] achievements unlocked (${rows.length}/9):`);
|
|
55
|
+
for (const r of rows) {
|
|
56
|
+
ctx.ui.notify(` ${r.icon ?? ""} ${r.title}`);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// on|off
|
|
61
|
+
if (sub === "on" || sub === "off") {
|
|
62
|
+
const s = setGameState({ game_mode_on: sub === "on" }, stateDir);
|
|
63
|
+
runtime.bumpGameState();
|
|
64
|
+
ctx.ui.notify(`[${TAG}] game mode ${s.game_mode_on ? "ON" : "off"}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// theme [id|next]
|
|
68
|
+
if (sub === "theme") {
|
|
69
|
+
if (parts.length === 1) {
|
|
70
|
+
// list themes
|
|
71
|
+
ctx.ui.notify(`[${TAG}] themes:`);
|
|
72
|
+
for (const t of THEMES) {
|
|
73
|
+
ctx.ui.notify(` ${t.id.padEnd(14)} ${t.label}`);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const arg = parts[1];
|
|
78
|
+
let id;
|
|
79
|
+
if (arg === "next") {
|
|
80
|
+
id = nextTheme(getGameState(stateDir).theme);
|
|
81
|
+
}
|
|
82
|
+
else if (isValidTheme(arg)) {
|
|
83
|
+
id = arg;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
ctx.ui.notify(`[${TAG}] unknown theme "${arg}". Valid: ${THEME_IDS.join(", ")}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const s = setGameState({ theme: id }, stateDir);
|
|
90
|
+
runtime.bumpGameState();
|
|
91
|
+
ctx.ui.notify(`[${TAG}] theme → ${s.theme} (${getTheme(s.theme)?.label ?? ""})`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// tui full|minimal
|
|
95
|
+
if (sub === "tui") {
|
|
96
|
+
const arg = parts[1];
|
|
97
|
+
if (arg !== "full" && arg !== "minimal") {
|
|
98
|
+
ctx.ui.notify(`[${TAG}] usage: /${TAG} tui full|minimal`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const s = setGameState({ tui_display_mode: arg }, stateDir);
|
|
102
|
+
runtime.bumpGameState();
|
|
103
|
+
ctx.ui.notify(`[${TAG}] tui → ${s.tui_display_mode}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
ctx.ui.notify(`[${TAG}] usage: /${TAG} [on|off|theme [id|next]|tui [full|minimal]|achievements]`);
|
|
107
|
+
}
|
|
108
|
+
/** Register /mega-compact-settings (primary) + /mega-game (backward-compat alias). */
|
|
33
109
|
export function registerGameCommands(pi, runtime) {
|
|
110
|
+
const description = "Game mode toggle + theme picker + TUI display mode. Usage: /mega-compact-settings [on|off|theme [id|next]|tui [full|minimal]|achievements]";
|
|
111
|
+
const handler = (args, ctx) => handleSettings(args, ctx, runtime);
|
|
112
|
+
// Primary command (renamed in v0.8.x from /mega-game).
|
|
113
|
+
pi.registerCommand("mega-compact-settings", { description, handler });
|
|
114
|
+
// Backward-compat alias: /mega-game still resolves to the same settings UI.
|
|
34
115
|
pi.registerCommand("mega-game", {
|
|
35
|
-
description: "
|
|
36
|
-
handler
|
|
37
|
-
runtime.bindRepo(ctx.cwd);
|
|
38
|
-
const stateDir = runtime.currentStateDir;
|
|
39
|
-
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
40
|
-
// /mega-game → print current state.
|
|
41
|
-
if (parts.length === 0) {
|
|
42
|
-
const s = getGameState(stateDir);
|
|
43
|
-
for (const line of fmtState(s))
|
|
44
|
-
ctx.ui.notify(line);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const sub = parts[0];
|
|
48
|
-
// /mega-game achievements — terse list of unlocked (hidden only once unlocked).
|
|
49
|
-
if (sub === "achievements") {
|
|
50
|
-
const rows = listAchievements(stateDir).filter((r) => r.unlocked_at != null);
|
|
51
|
-
ctx.ui.notify(`[mega-game] achievements unlocked (${rows.length}/9):`);
|
|
52
|
-
for (const r of rows) {
|
|
53
|
-
ctx.ui.notify(` ${r.icon ?? ""} ${r.title}`);
|
|
54
|
-
}
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
// /mega-game on|off
|
|
58
|
-
if (sub === "on" || sub === "off") {
|
|
59
|
-
const s = setGameState({ game_mode_on: sub === "on" }, stateDir);
|
|
60
|
-
runtime.bumpGameState();
|
|
61
|
-
ctx.ui.notify(`[mega-game] game mode ${s.game_mode_on ? "ON" : "off"}`);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
// /mega-game theme [id|next]
|
|
65
|
-
if (sub === "theme") {
|
|
66
|
-
if (parts.length === 1) {
|
|
67
|
-
// list themes
|
|
68
|
-
ctx.ui.notify("[mega-game] themes:");
|
|
69
|
-
for (const t of THEMES) {
|
|
70
|
-
ctx.ui.notify(` ${t.id.padEnd(14)} ${t.label}`);
|
|
71
|
-
}
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const arg = parts[1];
|
|
75
|
-
let id;
|
|
76
|
-
if (arg === "next") {
|
|
77
|
-
id = nextTheme(getGameState(stateDir).theme);
|
|
78
|
-
}
|
|
79
|
-
else if (isValidTheme(arg)) {
|
|
80
|
-
id = arg;
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
ctx.ui.notify(`[mega-game] unknown theme "${arg}". Valid: ${THEME_IDS.join(", ")}`);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
const s = setGameState({ theme: id }, stateDir);
|
|
87
|
-
runtime.bumpGameState();
|
|
88
|
-
ctx.ui.notify(`[mega-game] theme → ${s.theme} (${getTheme(s.theme)?.label ?? ""})`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
// /mega-game tui full|minimal
|
|
92
|
-
if (sub === "tui") {
|
|
93
|
-
const arg = parts[1];
|
|
94
|
-
if (arg !== "full" && arg !== "minimal") {
|
|
95
|
-
ctx.ui.notify(`[mega-game] usage: /mega-game tui full|minimal`);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const s = setGameState({ tui_display_mode: arg }, stateDir);
|
|
99
|
-
runtime.bumpGameState();
|
|
100
|
-
ctx.ui.notify(`[mega-game] tui → ${s.tui_display_mode}`);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
ctx.ui.notify(`[mega-game] usage: /mega-game [on|off|theme [id|next]|tui [full|minimal]|achievements]`);
|
|
104
|
-
},
|
|
116
|
+
description: "(alias for /mega-compact-settings) " + description,
|
|
117
|
+
handler,
|
|
105
118
|
});
|
|
106
119
|
}
|
|
@@ -35,7 +35,7 @@ function makeHarness(stateDir) {
|
|
|
35
35
|
mod.registerGameCommands(fakePi, runtime);
|
|
36
36
|
return { commands, notifies, ctx };
|
|
37
37
|
}
|
|
38
|
-
describe("/mega-
|
|
38
|
+
describe("/mega-compact-settings (S30; /mega-game alias)", () => {
|
|
39
39
|
let dir;
|
|
40
40
|
before(() => {
|
|
41
41
|
dir = mkdtempSync(join(tmpdir(), "mc-megagame-"));
|
|
@@ -52,6 +52,11 @@ describe("/mega-game (S30)", () => {
|
|
|
52
52
|
await h.commands["mega-game"].handler(args, h.ctx);
|
|
53
53
|
return h.notifies;
|
|
54
54
|
}
|
|
55
|
+
it("registers /mega-compact-settings as primary + /mega-game as alias", async () => {
|
|
56
|
+
const h = makeHarness(dir);
|
|
57
|
+
assert.ok(h.commands["mega-compact-settings"], "primary registered");
|
|
58
|
+
assert.ok(h.commands["mega-game"], "alias registered");
|
|
59
|
+
});
|
|
55
60
|
it("bare command prints current (default) state", async () => {
|
|
56
61
|
const lines = await run("");
|
|
57
62
|
assert.ok(lines.some((l) => l.includes("game mode: off")));
|
|
@@ -767,11 +767,25 @@ export class MegaRuntime {
|
|
|
767
767
|
this.gameStateWatchDir = undefined;
|
|
768
768
|
}
|
|
769
769
|
try {
|
|
770
|
-
|
|
770
|
+
// Watch the state DIR (not just sqlite.db) and filter by filename.
|
|
771
|
+
// Why: the store is WAL-mode (openStore sets PRAGMA journal_mode=WAL).
|
|
772
|
+
// Cross-process writes (dashboard server child) append to sqlite.db-wal
|
|
773
|
+
// and do NOT modify sqlite.db until a checkpoint — and a long-lived
|
|
774
|
+
// parent connection (VectorStore + dashboard readers) keeps the WAL
|
|
775
|
+
// uncheckpointed, so a watcher on sqlite.db alone never fires and
|
|
776
|
+
// cachedGameState stays stale (theme stuck after a dashboard edit).
|
|
777
|
+
// Watching the dir + matching sqlite.db* catches the main db, the -wal
|
|
778
|
+
// sidecar, and -shm, so the memo evicts on any cross-process write. The
|
|
779
|
+
// filter also excludes events.log / *.log noise in the same dir.
|
|
780
|
+
this.gameStateWatcher = watch(this.currentStateDir, (_eventType, filename) => {
|
|
781
|
+
if (typeof filename === "string" && filename.startsWith("sqlite.db")) {
|
|
782
|
+
this.cachedGameState = undefined;
|
|
783
|
+
}
|
|
784
|
+
});
|
|
771
785
|
this.gameStateWatchDir = this.currentStateDir;
|
|
772
786
|
}
|
|
773
787
|
catch {
|
|
774
|
-
/* non-fatal: missing
|
|
788
|
+
/* non-fatal: missing dir / platform issue — next snapshot re-queries */
|
|
775
789
|
}
|
|
776
790
|
}
|
|
777
791
|
/** S32: release the fs.watch game-state watcher. Called when the runtime is
|
|
@@ -22,10 +22,16 @@ export async function launchDashboardServer(stateDir: string): Promise<{ port: n
|
|
|
22
22
|
// upgrade instead of reuse it.
|
|
23
23
|
let SERVER_VERSION = "0.0.0";
|
|
24
24
|
try {
|
|
25
|
-
// dashboard-server.js lives at
|
|
26
|
-
//
|
|
25
|
+
// Since v0.7.9 (8821ef3) dashboard-server.js lives at
|
|
26
|
+
// <pkg>/dist/extensions/dashboard-server/, so package.json is THREE levels
|
|
27
|
+
// up. Keep the two- and one-level-up candidates as fallbacks for flatter
|
|
28
|
+
// dev-checkout layouts. Guard each candidate so a missing file is skipped.
|
|
27
29
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
|
-
const candidates = [
|
|
30
|
+
const candidates = [
|
|
31
|
+
join(here, "..", "..", "..", "package.json"),
|
|
32
|
+
join(here, "..", "..", "package.json"),
|
|
33
|
+
join(here, "..", "package.json"),
|
|
34
|
+
];
|
|
29
35
|
for (const p of candidates) {
|
|
30
36
|
if (!existsSync(p)) continue;
|
|
31
37
|
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* - mega-runtime.ts shared live state (MegaRuntime) + widget + model capture
|
|
19
19
|
* - mega-pipeline.ts runCompact (Trident+persist) + doRecall (Layer 5)
|
|
20
20
|
* - mega-commands.ts data/inspection slash commands
|
|
21
|
-
* - mega-game-cmds.ts /mega-game toggle + theme + TUI display mode
|
|
21
|
+
* - mega-game-cmds.ts /mega-compact-settings (+ /mega-game alias) toggle + theme + TUI display mode
|
|
22
22
|
* - mega-dashboard-cmds.ts localhost dashboard server lifecycle commands
|
|
23
23
|
* - mega-events.ts pi lifecycle event handlers
|
|
24
24
|
*
|
|
@@ -83,14 +83,16 @@ export function registerContextHandler(
|
|
|
83
83
|
// Legacy: MEGACOMPACT_LEGACY_DURABLE_TRIM=true restores the v0.4.28 ctx.compact
|
|
84
84
|
// path (kept one release as rollback).
|
|
85
85
|
pi.on("context", async (event: ContextEvent, ctx: ExtensionContext) => {
|
|
86
|
-
if (!config.auto) return;
|
|
87
86
|
const usage = ctx.getContextUsage();
|
|
88
87
|
const pct = usage?.percent;
|
|
89
|
-
// Always track context for the dashboard, even
|
|
88
|
+
// Always track context for the dashboard/widget, even when auto is off.
|
|
89
|
+
// (v0.8 regression: !config.auto gate sat above this, leaving ctx stats
|
|
90
|
+
// null -> widget '?% / ?/?' when auto disabled. Track first, THEN gate.)
|
|
90
91
|
runtime.lastCtxTokens = usage?.tokens ?? null;
|
|
91
92
|
runtime.lastCtxPercent = pct ?? null;
|
|
92
93
|
runtime.lastCtxWindow = usage?.contextWindow ?? 0;
|
|
93
94
|
runtime.snapshot(ctx);
|
|
95
|
+
if (!config.auto) return;
|
|
94
96
|
|
|
95
97
|
const messages = event.messages;
|
|
96
98
|
const view = runtime.engineView(messages);
|
|
@@ -48,7 +48,7 @@ function makeHarness(stateDir: string): Harness {
|
|
|
48
48
|
return { commands, notifies, ctx };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
describe("/mega-
|
|
51
|
+
describe("/mega-compact-settings (S30; /mega-game alias)", () => {
|
|
52
52
|
let dir: string;
|
|
53
53
|
before(() => {
|
|
54
54
|
dir = mkdtempSync(join(tmpdir(), "mc-megagame-"));
|
|
@@ -67,6 +67,12 @@ describe("/mega-game (S30)", () => {
|
|
|
67
67
|
return h.notifies;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
it("registers /mega-compact-settings as primary + /mega-game as alias", async () => {
|
|
71
|
+
const h = makeHarness(dir);
|
|
72
|
+
assert.ok(h.commands["mega-compact-settings"], "primary registered");
|
|
73
|
+
assert.ok(h.commands["mega-game"], "alias registered");
|
|
74
|
+
});
|
|
75
|
+
|
|
70
76
|
it("bare command prints current (default) state", async () => {
|
|
71
77
|
const lines = await run("");
|
|
72
78
|
assert.ok(lines.some((l) => l.includes("game mode: off")));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* mega-game-cmds.ts — /mega-
|
|
2
|
+
* mega-game-cmds.ts — /mega-compact-settings slash command (S30; renamed in v0.8.x).
|
|
3
3
|
*
|
|
4
4
|
* Backs the game-mode toggle + theme picker + TUI display mode. All state is
|
|
5
5
|
* the global `game_state` SQLite row (src/store/sqlite/game-state.ts) — local
|
|
@@ -7,16 +7,20 @@
|
|
|
7
7
|
* command touches no fetch/http). All SQL is parameterized (PREVENT-002) and
|
|
8
8
|
* lives in the src/ submodule, not here.
|
|
9
9
|
*
|
|
10
|
+
* The primary command is /mega-compact-settings. /mega-game is retained as a
|
|
11
|
+
* backward-compat alias (same handler) so existing muscle memory + docs keep
|
|
12
|
+
* working.
|
|
13
|
+
*
|
|
10
14
|
* Usage:
|
|
11
|
-
* /mega-
|
|
12
|
-
* /mega-
|
|
13
|
-
* /mega-
|
|
14
|
-
* /mega-
|
|
15
|
-
* /mega-
|
|
16
|
-
* /mega-
|
|
17
|
-
* /mega-
|
|
18
|
-
* /mega-
|
|
19
|
-
* /mega-
|
|
15
|
+
* /mega-compact-settings print current state
|
|
16
|
+
* /mega-compact-settings on enable game mode (scoring + level-up + MEGA CACHE)
|
|
17
|
+
* /mega-compact-settings off disable game mode
|
|
18
|
+
* /mega-compact-settings theme list available themes
|
|
19
|
+
* /mega-compact-settings theme <id> set theme by id
|
|
20
|
+
* /mega-compact-settings theme next cycle to next theme
|
|
21
|
+
* /mega-compact-settings tui full full TUI widget (bars, stats, flair)
|
|
22
|
+
* /mega-compact-settings tui minimal one-line TUI widget (level + cache %)
|
|
23
|
+
* /mega-compact-settings achievements list unlocked achievements
|
|
20
24
|
*/
|
|
21
25
|
|
|
22
26
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -29,94 +33,110 @@ import {
|
|
|
29
33
|
import { listAchievements } from "../src/store/sqlite/game-achievements.js";
|
|
30
34
|
import { THEMES, THEME_IDS, getTheme, isValidTheme, nextTheme, DEFAULT_THEME } from "../src/config/themes.js";
|
|
31
35
|
|
|
36
|
+
/** Notify/usage tag + command name. Primary surface is /mega-compact-settings. */
|
|
37
|
+
const TAG = "mega-compact-settings";
|
|
38
|
+
|
|
32
39
|
/** Format the current state as a human-readable status line set. */
|
|
33
40
|
function fmtState(s: GameState): string[] {
|
|
34
41
|
return [
|
|
35
|
-
`[
|
|
42
|
+
`[${TAG}] game mode: ${s.game_mode_on ? "ON" : "off"}`,
|
|
36
43
|
` theme: ${s.theme}${s.theme === DEFAULT_THEME ? " (default)" : ""}`,
|
|
37
44
|
` tui: ${s.tui_display_mode}`,
|
|
38
45
|
];
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
/** Shared handler for /mega-compact-settings (primary) + /mega-game (alias). */
|
|
49
|
+
async function handleSettings(
|
|
50
|
+
args: string,
|
|
51
|
+
ctx: ExtensionContext,
|
|
52
|
+
runtime: MegaRuntime,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
runtime.bindRepo(ctx.cwd);
|
|
55
|
+
const stateDir = runtime.currentStateDir;
|
|
56
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
// bare → print current state.
|
|
59
|
+
if (parts.length === 0) {
|
|
60
|
+
const s = getGameState(stateDir);
|
|
61
|
+
for (const line of fmtState(s)) ctx.ui.notify(line);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
57
64
|
|
|
58
|
-
|
|
65
|
+
const sub = parts[0]!;
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
// achievements — terse list of unlocked (hidden only once unlocked).
|
|
68
|
+
if (sub === "achievements") {
|
|
69
|
+
const rows = listAchievements(stateDir).filter((r) => r.unlocked_at != null);
|
|
70
|
+
ctx.ui.notify(`[${TAG}] achievements unlocked (${rows.length}/9):`);
|
|
71
|
+
for (const r of rows) {
|
|
72
|
+
ctx.ui.notify(` ${r.icon ?? ""} ${r.title}`);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
// on|off
|
|
78
|
+
if (sub === "on" || sub === "off") {
|
|
79
|
+
const s = setGameState({ game_mode_on: sub === "on" }, stateDir);
|
|
80
|
+
runtime.bumpGameState();
|
|
81
|
+
ctx.ui.notify(`[${TAG}] game mode ${s.game_mode_on ? "ON" : "off"}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const arg = parts[1]!;
|
|
89
|
-
let id: string;
|
|
90
|
-
if (arg === "next") {
|
|
91
|
-
id = nextTheme(getGameState(stateDir).theme);
|
|
92
|
-
} else if (isValidTheme(arg)) {
|
|
93
|
-
id = arg;
|
|
94
|
-
} else {
|
|
95
|
-
ctx.ui.notify(`[mega-game] unknown theme "${arg}". Valid: ${THEME_IDS.join(", ")}`);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const s = setGameState({ theme: id }, stateDir);
|
|
99
|
-
runtime.bumpGameState();
|
|
100
|
-
ctx.ui.notify(`[mega-game] theme → ${s.theme} (${getTheme(s.theme)?.label ?? ""})`);
|
|
101
|
-
return;
|
|
85
|
+
// theme [id|next]
|
|
86
|
+
if (sub === "theme") {
|
|
87
|
+
if (parts.length === 1) {
|
|
88
|
+
// list themes
|
|
89
|
+
ctx.ui.notify(`[${TAG}] themes:`);
|
|
90
|
+
for (const t of THEMES) {
|
|
91
|
+
ctx.ui.notify(` ${t.id.padEnd(14)} ${t.label}`);
|
|
102
92
|
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const arg = parts[1]!;
|
|
96
|
+
let id: string;
|
|
97
|
+
if (arg === "next") {
|
|
98
|
+
id = nextTheme(getGameState(stateDir).theme);
|
|
99
|
+
} else if (isValidTheme(arg)) {
|
|
100
|
+
id = arg;
|
|
101
|
+
} else {
|
|
102
|
+
ctx.ui.notify(`[${TAG}] unknown theme "${arg}". Valid: ${THEME_IDS.join(", ")}`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const s = setGameState({ theme: id }, stateDir);
|
|
106
|
+
runtime.bumpGameState();
|
|
107
|
+
ctx.ui.notify(`[${TAG}] theme → ${s.theme} (${getTheme(s.theme)?.label ?? ""})`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
103
110
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
// tui full|minimal
|
|
112
|
+
if (sub === "tui") {
|
|
113
|
+
const arg = parts[1];
|
|
114
|
+
if (arg !== "full" && arg !== "minimal") {
|
|
115
|
+
ctx.ui.notify(`[${TAG}] usage: /${TAG} tui full|minimal`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const s = setGameState({ tui_display_mode: arg }, stateDir);
|
|
119
|
+
runtime.bumpGameState();
|
|
120
|
+
ctx.ui.notify(`[${TAG}] tui → ${s.tui_display_mode}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ctx.ui.notify(
|
|
125
|
+
`[${TAG}] usage: /${TAG} [on|off|theme [id|next]|tui [full|minimal]|achievements]`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
116
128
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
/** Register /mega-compact-settings (primary) + /mega-game (backward-compat alias). */
|
|
130
|
+
export function registerGameCommands(pi: ExtensionAPI, runtime: MegaRuntime): void {
|
|
131
|
+
const description =
|
|
132
|
+
"Game mode toggle + theme picker + TUI display mode. Usage: /mega-compact-settings [on|off|theme [id|next]|tui [full|minimal]|achievements]";
|
|
133
|
+
const handler = (args: string, ctx: ExtensionContext) => handleSettings(args, ctx, runtime);
|
|
134
|
+
|
|
135
|
+
// Primary command (renamed in v0.8.x from /mega-game).
|
|
136
|
+
pi.registerCommand("mega-compact-settings", { description, handler });
|
|
137
|
+
// Backward-compat alias: /mega-game still resolves to the same settings UI.
|
|
138
|
+
pi.registerCommand("mega-game", {
|
|
139
|
+
description: "(alias for /mega-compact-settings) " + description,
|
|
140
|
+
handler,
|
|
121
141
|
});
|
|
122
142
|
}
|
|
@@ -846,13 +846,27 @@ export class MegaRuntime {
|
|
|
846
846
|
this.gameStateWatchDir = undefined;
|
|
847
847
|
}
|
|
848
848
|
try {
|
|
849
|
+
// Watch the state DIR (not just sqlite.db) and filter by filename.
|
|
850
|
+
// Why: the store is WAL-mode (openStore sets PRAGMA journal_mode=WAL).
|
|
851
|
+
// Cross-process writes (dashboard server child) append to sqlite.db-wal
|
|
852
|
+
// and do NOT modify sqlite.db until a checkpoint — and a long-lived
|
|
853
|
+
// parent connection (VectorStore + dashboard readers) keeps the WAL
|
|
854
|
+
// uncheckpointed, so a watcher on sqlite.db alone never fires and
|
|
855
|
+
// cachedGameState stays stale (theme stuck after a dashboard edit).
|
|
856
|
+
// Watching the dir + matching sqlite.db* catches the main db, the -wal
|
|
857
|
+
// sidecar, and -shm, so the memo evicts on any cross-process write. The
|
|
858
|
+
// filter also excludes events.log / *.log noise in the same dir.
|
|
849
859
|
this.gameStateWatcher = watch(
|
|
850
|
-
|
|
851
|
-
() => {
|
|
860
|
+
this.currentStateDir,
|
|
861
|
+
(_eventType, filename) => {
|
|
862
|
+
if (typeof filename === "string" && filename.startsWith("sqlite.db")) {
|
|
863
|
+
this.cachedGameState = undefined;
|
|
864
|
+
}
|
|
865
|
+
},
|
|
852
866
|
);
|
|
853
867
|
this.gameStateWatchDir = this.currentStateDir;
|
|
854
868
|
} catch {
|
|
855
|
-
/* non-fatal: missing
|
|
869
|
+
/* non-fatal: missing dir / platform issue — next snapshot re-queries */
|
|
856
870
|
}
|
|
857
871
|
}
|
|
858
872
|
|
package/package.json
CHANGED