@prestyj/cli 5.29.1 → 5.30.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/assets/skills/compliance-guard/references/us.md +1 -1
- package/assets/skills/refactoring/SKILL.md +3 -1
- package/dist/app-error.d.ts +21 -0
- package/dist/app-error.js +39 -0
- package/dist/app-error.js.map +1 -0
- package/dist/app-sidecar.js +39 -35
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-session.d.ts +2 -2
- package/dist/core/agent-session.js +11 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/checklist-detection.d.ts +11 -0
- package/dist/core/checklist-detection.js +238 -0
- package/dist/core/checklist-detection.js.map +1 -0
- package/dist/core/checklist-items.d.ts +180 -0
- package/dist/core/checklist-items.js +204 -0
- package/dist/core/checklist-items.js.map +1 -0
- package/dist/core/checklist-prompt.d.ts +3 -0
- package/dist/core/checklist-prompt.js +34 -0
- package/dist/core/checklist-prompt.js.map +1 -0
- package/dist/core/checklist-snapshot.d.ts +13 -0
- package/dist/core/checklist-snapshot.js +29 -0
- package/dist/core/checklist-snapshot.js.map +1 -0
- package/dist/core/checklist-store.d.ts +59 -0
- package/dist/core/checklist-store.js +242 -0
- package/dist/core/checklist-store.js.map +1 -0
- package/dist/core/lsp/client.d.ts +4 -0
- package/dist/core/lsp/client.js +71 -3
- package/dist/core/lsp/client.js.map +1 -1
- package/dist/core/lsp/manager.d.ts +1 -0
- package/dist/core/lsp/manager.js +9 -3
- package/dist/core/lsp/manager.js.map +1 -1
- package/dist/core/lsp/project-root.d.ts +9 -0
- package/dist/core/lsp/project-root.js +75 -0
- package/dist/core/lsp/project-root.js.map +1 -0
- package/dist/core/lsp/reference-project.d.ts +6 -0
- package/dist/core/lsp/reference-project.js +110 -0
- package/dist/core/lsp/reference-project.js.map +1 -0
- package/dist/core/lsp/servers.js +1 -0
- package/dist/core/lsp/servers.js.map +1 -1
- package/dist/core/prompt-commands.js +95 -0
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/subagent-manager.d.ts +2 -1
- package/dist/core/subagent-manager.js +8 -5
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/modes/acp-mode.d.ts +2 -1
- package/dist/modes/acp-mode.js +4 -1
- package/dist/modes/acp-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts +2 -0
- package/dist/modes/subagent-worker-mode.js +1 -3
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/system-prompt.js +4 -4
- package/dist/tools/bash.d.ts +4 -1
- package/dist/tools/bash.js +32 -7
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/checklist.d.ts +56 -0
- package/dist/tools/checklist.js +178 -0
- package/dist/tools/checklist.js.map +1 -0
- package/dist/tools/code-nav.d.ts +1 -1
- package/dist/tools/code-nav.js +3 -2
- package/dist/tools/code-nav.js.map +1 -1
- package/dist/tools/exit-plan.d.ts +2 -2
- package/dist/tools/exit-plan.js +7 -1
- package/dist/tools/exit-plan.js.map +1 -1
- package/dist/tools/index.d.ts +5 -3
- package/dist/tools/index.js +5 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/prompt-hints.js +2 -0
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/subagent-shared.d.ts +6 -8
- package/dist/tools/subagent-shared.js +17 -11
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/subagent.d.ts +2 -1
- package/dist/tools/subagent.js +6 -6
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/tasks.d.ts +1 -1
- package/dist/tools/tool-tiers.js +1 -0
- package/dist/tools/tool-tiers.js.map +1 -1
- package/dist/tools/verification-review.d.ts +2 -0
- package/dist/tools/verification-review.js +41 -0
- package/dist/tools/verification-review.js.map +1 -0
- package/dist/ui/App.js +1 -0
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/hooks/useModeState.js +15 -0
- package/dist/ui/hooks/useModeState.js.map +1 -1
- package/dist/ui/tool-line-summary.js +11 -1
- package/dist/ui/tool-line-summary.js.map +1 -1
- package/dist/ui/transcript/tool-presentation.js +1 -1
- package/dist/ui/transcript/tool-presentation.js.map +1 -1
- package/package.json +5 -5
|
@@ -27,7 +27,7 @@ Also note **Texas and Nebraska have no revenue or volume threshold** — they ap
|
|
|
27
27
|
|
|
28
28
|
Thresholds range from 25,000 consumers (Montana) to 175,000 (Tennessee); 10,000 consumers where a share of revenue comes from selling data (RI, DE, NH, MD); Maryland pairs a 35,000-consumer threshold with the strictest data-minimisation and sensitive-data rules, and bans the sale of sensitive data outright **[V]**.
|
|
29
29
|
|
|
30
|
-
**Universal opt-out / Global Privacy Control — the cheapest high-value engineering task.** As of 1 Jan 2026, twelve states require honouring it (CA, CO, CT, DE, MD, MN, MT, NE, NH, NJ, OR, TX); Virginia-template states, including the 2026 newcomers Indiana,
|
|
30
|
+
**Universal opt-out / Global Privacy Control — the cheapest high-value engineering task.** As of 1 Jan 2026, twelve states require honouring it (CA, CO, CT, DE, MD, MN, MT, NE, NH, NJ, OR, TX); Virginia-template states, including the 2026 newcomers Indiana, Nolantucky and Rhode Island, do not **[V]**. California's regulations effective 1 Jan 2026 also require the business to display whether the signal was honoured (11 CCR §7025(c)(6)) **[S]**. California, Colorado and Connecticut have run a coordinated GPC enforcement sweep **[S]**.
|
|
31
31
|
|
|
32
32
|
```
|
|
33
33
|
Read the Sec-GPC: 1 request header and navigator.globalPrivacyControl.
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: refactoring
|
|
3
|
-
description: Use
|
|
3
|
+
description: Use for behavior-preserving restructuring: extract, move, split, deduplicate, plan a refactor, or migrate an implementation behind an unchanged contract. Baseline tests, small steps, revert-on-red. Do NOT use for requested behavior or API-contract changes (including new return/error semantics), even when called a "refactor"; use the normal implementation workflow with regression tests and update affected callers together. Also exclude bug fixes (root-cause when stubborn), performance work (lean), data migrations (durable), styling/copy, TDD's refactor step (tdd), and rewrites.
|
|
4
4
|
license: Behavior-preservation methodology synthesized from public sources (Fowler's Refactoring catalog, Tidy First?, and community agent skills by bienhoang, wondelai, mattpocock, jeffallan, vasilyu1983), audited 2026-09-12.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Refactoring
|
|
8
8
|
|
|
9
|
+
**Scope gate:** Decide from the requested behavior, not the word "refactor". A requested change to return values, error handling, or an API contract is implementation work, not a behavior-preserving cleanup. Use the normal implementation workflow: inspect the affected callers, make the requested contract and caller changes together, add regression coverage, and run the checks. Do not manufacture a separate pure-rename stage merely to enter this skill. Existing authorization and safety controls still apply. For a genuine behavior-preserving refactor, use the gated loop below unchanged.
|
|
10
|
+
|
|
9
11
|
**Route first:**
|
|
10
12
|
|
|
11
13
|
| Situation | Mode | Next |
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type FormattedChatError } from "@prestyj/ai";
|
|
3
|
+
declare const appErrorSchema: z.ZodObject<{
|
|
4
|
+
scope: z.ZodDefault<z.ZodString>;
|
|
5
|
+
headline: z.ZodString;
|
|
6
|
+
message: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
7
|
+
guidance: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
8
|
+
reason: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
9
|
+
source: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
10
|
+
provider: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
11
|
+
statusCode: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
|
|
12
|
+
requestId: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
13
|
+
occurredAt: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
|
|
14
|
+
resetsAt: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
export type AppErrorPayload = z.infer<typeof appErrorSchema>;
|
|
17
|
+
/** The same sanitized snapshot goes to SSE and the append-only history marker. */
|
|
18
|
+
export declare function createAppErrorPayload(error: FormattedChatError, scope: string, occurredAt: number, secrets: Iterable<string>): AppErrorPayload;
|
|
19
|
+
/** Backward-compatible read: no migration or reclassification of old saved text. */
|
|
20
|
+
export declare function restoreAppErrorPayload(value: unknown): AppErrorPayload | undefined;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { redactValue } from "@prestyj/ai";
|
|
3
|
+
const timestamp = z.number().int().nonnegative().max(8_640_000_000_000).optional().catch(undefined);
|
|
4
|
+
const optionalText = z.string().max(8_000).optional().catch(undefined);
|
|
5
|
+
const appErrorSchema = z.object({
|
|
6
|
+
scope: z.string().max(80).default("error"),
|
|
7
|
+
headline: z.string().min(1).max(1_000),
|
|
8
|
+
message: optionalText,
|
|
9
|
+
guidance: optionalText,
|
|
10
|
+
reason: z.string().max(40).optional().catch(undefined),
|
|
11
|
+
source: z.string().max(40).optional().catch(undefined),
|
|
12
|
+
provider: z.string().max(80).optional().catch(undefined),
|
|
13
|
+
statusCode: z.number().int().min(100).max(599).optional().catch(undefined),
|
|
14
|
+
requestId: z.string().max(300).optional().catch(undefined),
|
|
15
|
+
occurredAt: timestamp,
|
|
16
|
+
resetsAt: timestamp,
|
|
17
|
+
});
|
|
18
|
+
/** The same sanitized snapshot goes to SSE and the append-only history marker. */
|
|
19
|
+
export function createAppErrorPayload(error, scope, occurredAt, secrets) {
|
|
20
|
+
return redactValue({
|
|
21
|
+
scope,
|
|
22
|
+
headline: error.headline,
|
|
23
|
+
message: error.message,
|
|
24
|
+
guidance: error.guidance,
|
|
25
|
+
reason: error.reason,
|
|
26
|
+
source: error.source,
|
|
27
|
+
provider: error.provider,
|
|
28
|
+
statusCode: error.statusCode,
|
|
29
|
+
requestId: error.requestId,
|
|
30
|
+
occurredAt,
|
|
31
|
+
resetsAt: error.resetsAt,
|
|
32
|
+
}, { secrets, maxStringLength: 8_000 });
|
|
33
|
+
}
|
|
34
|
+
/** Backward-compatible read: no migration or reclassification of old saved text. */
|
|
35
|
+
export function restoreAppErrorPayload(value) {
|
|
36
|
+
const parsed = appErrorSchema.safeParse(value);
|
|
37
|
+
return parsed.success ? parsed.data : undefined;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=app-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-error.js","sourceRoot":"","sources":["../src/app-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,WAAW,EAA2B,MAAM,aAAa,CAAC;AAEnE,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;AACpG,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;AACvE,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;IAC1C,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC;IACtC,OAAO,EAAE,YAAY;IACrB,QAAQ,EAAE,YAAY;IACtB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC;IACtD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC;IACtD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC;IACxD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC;IAC1E,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC;IAC1D,UAAU,EAAE,SAAS;IACrB,QAAQ,EAAE,SAAS;CACpB,CAAC,CAAC;AAGH,kFAAkF;AAClF,MAAM,UAAU,qBAAqB,CACnC,KAAyB,EACzB,KAAa,EACb,UAAkB,EAClB,OAAyB;IAEzB,OAAO,WAAW,CAChB;QACE,KAAK;QACL,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,UAAU;QACV,QAAQ,EAAE,KAAK,CAAC,QAAQ;KACzB,EACD,EAAE,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,CACpC,CAAC;AACJ,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACnD,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/C,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC"}
|
package/dist/app-sidecar.js
CHANGED
|
@@ -19,7 +19,8 @@ import path from "node:path";
|
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
20
|
import { parseArgs } from "node:util";
|
|
21
21
|
import { spawn } from "node:child_process";
|
|
22
|
-
import { environmentSecrets,
|
|
22
|
+
import { environmentSecrets, formatChatError, redactValue, } from "@prestyj/ai";
|
|
23
|
+
import { createAppErrorPayload, restoreAppErrorPayload, } from "./app-error.js";
|
|
23
24
|
import { runJsonMode } from "./modes/json-mode.js";
|
|
24
25
|
import { runSubagentWorkerMode } from "./modes/subagent-worker-mode.js";
|
|
25
26
|
import { setStreamDiagnostic } from "@prestyj/agent";
|
|
@@ -70,6 +71,7 @@ import { loadCustomCommands } from "./core/custom-commands.js";
|
|
|
70
71
|
import { discoverProjects } from "./core/project-discovery.js";
|
|
71
72
|
import { listSidecarSessions } from "./app-sidecar-sessions.js";
|
|
72
73
|
import { loadTasksSync, saveTasksSync, pruneDoneTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/task-store.js";
|
|
74
|
+
import { readChecklistSnapshot } from "./core/checklist-snapshot.js";
|
|
73
75
|
import { initLogger, log } from "./core/logger.js";
|
|
74
76
|
import { installTerminationHandlers } from "./core/shutdown.js";
|
|
75
77
|
import { KeepAwake } from "./core/keep-awake.js";
|
|
@@ -1318,35 +1320,21 @@ async function createSession(deps, opts) {
|
|
|
1318
1320
|
// `400 {"code":"400",...}` with no "is this me or them / when does it reset"
|
|
1319
1321
|
// context that the CLI has always given.
|
|
1320
1322
|
function broadcastError(type, logLabel, err) {
|
|
1321
|
-
const f =
|
|
1323
|
+
const f = formatChatError(err);
|
|
1322
1324
|
const message = f.message ? desktopGuidance(f.message) : undefined;
|
|
1323
1325
|
const guidance = localNetworkGuidance(f.source) ?? desktopGuidance(f.guidance);
|
|
1326
|
+
const payload = createAppErrorPayload({ ...f, ...(message ? { message } : {}), guidance }, type, Date.now(), environmentSecrets(process.env));
|
|
1324
1327
|
log("ERROR", "app-sidecar", logLabel, {
|
|
1325
|
-
headline:
|
|
1328
|
+
headline: payload.headline,
|
|
1326
1329
|
source: f.source,
|
|
1327
|
-
...(message ? { message } : {}),
|
|
1328
|
-
...(
|
|
1329
|
-
...(
|
|
1330
|
-
...(
|
|
1330
|
+
...(payload.message ? { message: payload.message } : {}),
|
|
1331
|
+
...(payload.provider ? { provider: payload.provider } : {}),
|
|
1332
|
+
...(payload.statusCode != null ? { statusCode: String(payload.statusCode) } : {}),
|
|
1333
|
+
...(payload.requestId ? { requestId: payload.requestId } : {}),
|
|
1331
1334
|
});
|
|
1332
|
-
broadcast(type,
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
guidance,
|
|
1336
|
-
...(f.provider ? { provider: f.provider } : {}),
|
|
1337
|
-
...(f.statusCode != null ? { statusCode: f.statusCode } : {}),
|
|
1338
|
-
...(f.resetsAt != null ? { resetsAt: f.resetsAt } : {}),
|
|
1339
|
-
});
|
|
1340
|
-
// Persist the error row (display-only marker) so a resumed session shows
|
|
1341
|
-
// the same headline/message/guidance the live run did. Best-effort.
|
|
1342
|
-
void session
|
|
1343
|
-
.persistAppMarker("error", {
|
|
1344
|
-
scope: type,
|
|
1345
|
-
headline: f.headline,
|
|
1346
|
-
...(message ? { message } : {}),
|
|
1347
|
-
guidance,
|
|
1348
|
-
})
|
|
1349
|
-
.catch(() => { });
|
|
1335
|
+
broadcast(type, payload);
|
|
1336
|
+
// Same sanitized snapshot for live and restored rows, including reset metadata.
|
|
1337
|
+
void session.persistAppMarker("error", payload).catch(() => { });
|
|
1350
1338
|
}
|
|
1351
1339
|
// ── MCP elicitation bridge ─────────────────────────────────
|
|
1352
1340
|
// An MCP server can ask for user input in the middle of a tool call. The
|
|
@@ -3567,16 +3555,9 @@ async function createSession(deps, opts) {
|
|
|
3567
3555
|
});
|
|
3568
3556
|
}
|
|
3569
3557
|
else if (marker.kind === "error" && typeof d.headline === "string") {
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
text: "",
|
|
3573
|
-
error: {
|
|
3574
|
-
scope: typeof d.scope === "string" ? d.scope : "error",
|
|
3575
|
-
headline: d.headline,
|
|
3576
|
-
...(typeof d.message === "string" ? { message: d.message } : {}),
|
|
3577
|
-
...(typeof d.guidance === "string" ? { guidance: d.guidance } : {}),
|
|
3578
|
-
},
|
|
3579
|
-
});
|
|
3558
|
+
const error = restoreAppErrorPayload(d);
|
|
3559
|
+
if (error)
|
|
3560
|
+
history.push({ role: "assistant", text: "", error });
|
|
3580
3561
|
}
|
|
3581
3562
|
else if (marker.kind === "interrupted_run") {
|
|
3582
3563
|
// Rendered as an error row: the run's tools already changed the
|
|
@@ -4124,6 +4105,29 @@ async function createSession(deps, opts) {
|
|
|
4124
4105
|
json(res, 200, { tasks: pruneDoneTasksSync(cwd) });
|
|
4125
4106
|
return;
|
|
4126
4107
|
}
|
|
4108
|
+
if (method === "GET" && url === "/checklist") {
|
|
4109
|
+
const controller = new AbortController();
|
|
4110
|
+
res.once("close", () => controller.abort());
|
|
4111
|
+
void (async () => {
|
|
4112
|
+
try {
|
|
4113
|
+
const result = await readChecklistSnapshot(cwd, new Date(), controller.signal);
|
|
4114
|
+
if (res.destroyed)
|
|
4115
|
+
return;
|
|
4116
|
+
if (!result.ok) {
|
|
4117
|
+
log("WARN", "app-sidecar", "checklist read failed", { error: result.error });
|
|
4118
|
+
json(res, 500, { error: result.error });
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
json(res, 200, result.value);
|
|
4122
|
+
}
|
|
4123
|
+
catch (error) {
|
|
4124
|
+
log("WARN", "app-sidecar", "checklist snapshot failed", { error: String(error) });
|
|
4125
|
+
if (!res.destroyed)
|
|
4126
|
+
json(res, 500, { error: "Could not read the project checklist" });
|
|
4127
|
+
}
|
|
4128
|
+
})();
|
|
4129
|
+
return;
|
|
4130
|
+
}
|
|
4127
4131
|
// ── Radio (app-wide) ──────────────────────────────────────
|
|
4128
4132
|
// Radio is now APP-WIDE: all windows share one daemon process, and the
|
|
4129
4133
|
// player lives in `core/radio.ts` module-level singletons (one stream for
|