@polderlabs/bizar-plugin 0.6.2 → 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/src/commands.ts CHANGED
@@ -314,7 +314,7 @@ function handleVisualPlan(arg: string, ctx: ParseContext): SlashCommandResult {
314
314
  // No argument — return current state as a dialog
315
315
  return {
316
316
  handled: true,
317
- response: "",
317
+ response: `Visual plan mode is currently ${currentEnabled ? "on" : "off"}.`,
318
318
  dialog: {
319
319
  id: generateId(),
320
320
  title: "Visual Plan",
@@ -334,7 +334,7 @@ function handleVisualPlan(arg: string, ctx: ParseContext): SlashCommandResult {
334
334
  if (lc === "on" || lc === "true" || lc === "1" || lc === "enable") {
335
335
  return {
336
336
  handled: true,
337
- response: "",
337
+ response: `Visual plan mode is now on.`,
338
338
  settingsPatch: { visualPlanEnabled: true },
339
339
  dialog: {
340
340
  id: generateId(),
@@ -353,7 +353,7 @@ function handleVisualPlan(arg: string, ctx: ParseContext): SlashCommandResult {
353
353
  if (lc === "off" || lc === "false" || lc === "0" || lc === "disable") {
354
354
  return {
355
355
  handled: true,
356
- response: "",
356
+ response: `Visual plan mode is now off.`,
357
357
  settingsPatch: { visualPlanEnabled: false },
358
358
  dialog: {
359
359
  id: generateId(),
@@ -372,7 +372,7 @@ function handleVisualPlan(arg: string, ctx: ParseContext): SlashCommandResult {
372
372
  if (lc === "status" || lc === "state" || lc === "?") {
373
373
  return {
374
374
  handled: true,
375
- response: "",
375
+ response: `Visual plan mode is currently ${currentEnabled ? "on" : "off"}.`,
376
376
  dialog: {
377
377
  id: generateId(),
378
378
  title: "Visual Plan",
@@ -444,7 +444,7 @@ function handlePlan(arg: string, ctx: ParseContext): SlashCommandResult {
444
444
  function helpPlan(): SlashCommandResult {
445
445
  return {
446
446
  handled: true,
447
- response: "",
447
+ response: "Plan commands: /plan new <slug> [template] | /plan list | /plan open <slug> | /plan get <slug> | /plan add <slug> | /plan update <slug> <id> | /plan delete <slug> <id> | /plan comment <slug> [id] \"text\" | /plan comments <slug> [id] | /plan status <slug> <status> | /plan wait <slug> [--timeout N]. Run /plan <subcommand> for details.",
448
448
  dialog: {
449
449
  id: generateId(),
450
450
  title: "Plan Commands",
@@ -477,7 +477,7 @@ function handlePlanNew(args: string[], ctx: ParseContext): SlashCommandResult {
477
477
  if (args.length === 0 || args[0] === "") {
478
478
  return {
479
479
  handled: true,
480
- response: "",
480
+ response: "Usage: /plan new <slug> [template]. Available templates: " + KNOWN_TEMPLATES.join(", ") + ".",
481
481
  dialog: {
482
482
  id: generateId(),
483
483
  title: "Create New Plan",
@@ -520,7 +520,7 @@ function handlePlanNew(args: string[], ctx: ParseContext): SlashCommandResult {
520
520
 
521
521
  return {
522
522
  handled: true,
523
- response: "",
523
+ response: `Created plan "${titleCase(slug)}" with the "${resolvedTemplate}" template. Use /plan open ${slug} to open it.`,
524
524
  sideEffect: {
525
525
  kind: "create_plan",
526
526
  slug,
@@ -544,9 +544,26 @@ function handlePlanNew(args: string[], ctx: ParseContext): SlashCommandResult {
544
544
 
545
545
  function handlePlanList(ctx: ParseContext): SlashCommandResult {
546
546
  const slugs = ctx.availablePlanSlugs ?? [];
547
+ if (slugs.length === 0) {
548
+ return {
549
+ handled: true,
550
+ response: "No plans found in the current worktree. Use /plan new <slug> to create one.",
551
+ sideEffect: { kind: "list_plans" },
552
+ dialog: {
553
+ id: generateId(),
554
+ title: "Plans",
555
+ command: "/plan list",
556
+ component: "plan-list",
557
+ data: {
558
+ plans: slugs,
559
+ count: slugs.length,
560
+ },
561
+ },
562
+ };
563
+ }
547
564
  return {
548
565
  handled: true,
549
- response: "",
566
+ response: `Found ${slugs.length} plan(s) (${slugs.length}): ${slugs.join(", ")}.`,
550
567
  sideEffect: { kind: "list_plans" },
551
568
  dialog: {
552
569
  id: generateId(),
@@ -584,7 +601,7 @@ function handlePlanOpen(args: string[], ctx: ParseContext): SlashCommandResult {
584
601
 
585
602
  return {
586
603
  handled: true,
587
- response: "",
604
+ response: `Opening plan "${slug}" at ${url}.`,
588
605
  settingsPatch: { lastUsedSlug: slug },
589
606
  sideEffect: {
590
607
  kind: "open_plan_url",
@@ -618,7 +635,7 @@ function handlePlanGet(args: string[]): SlashCommandResult {
618
635
  }
619
636
  return {
620
637
  handled: true,
621
- response: "",
638
+ response: `Fetching canvas for plan "${slug}"…`,
622
639
  sideEffect: {
623
640
  kind: "tool_invocation",
624
641
  toolName: "bizar_plan_action",
@@ -1078,7 +1095,7 @@ function handleBizar(arg: string, ctx: ParseContext): SlashCommandResult {
1078
1095
  function helpResult(): SlashCommandResult {
1079
1096
  return {
1080
1097
  handled: true,
1081
- response: "",
1098
+ response: "Available commands: /visual-plan [on|off|status], /plan new <slug> [template], /plan list, /plan open <slug>, /plan get <slug>, /plan add <slug>, /plan update <slug> <id>, /plan delete <slug> <id>, /plan comment <slug> [id] \"text\", /plan comments <slug> [id], /plan status <slug> <status>, /plan wait <slug> [--timeout N], /bizar, /bizar <args>, /help. See the dialog for full descriptions.",
1082
1099
  dialog: {
1083
1100
  id: generateId(),
1084
1101
  title: "Bizar Commands",
@@ -0,0 +1,235 @@
1
+ /**
2
+ * dashboard-client.ts
3
+ *
4
+ * v0.7.0-alpha.1 — Plugin-side bridge to the Bizar dashboard via the
5
+ * @polderlabs/bizar-sdk. Replaces (does not remove) the file-based
6
+ * `serve-info.ts` bridge.
7
+ *
8
+ * Usage:
9
+ * import { createDashboardPublisher } from "./dashboard-client.js";
10
+ *
11
+ * const publisher = createDashboardPublisher({
12
+ * logger: myLogger,
13
+ * });
14
+ * await publisher.start();
15
+ * publisher.publish({
16
+ * type: "session.created",
17
+ * properties: { sessionId: "ses_1", agent: "mimir" },
18
+ * });
19
+ *
20
+ * Behavior:
21
+ * - Reads `BIZAR_DASHBOARD_URL` (default http://127.0.0.1:4098).
22
+ * - Reads `BIZAR_DASHBOARD_PASSWORD` first, then falls back to
23
+ * `~/.cache/bizarharness/dash-auth.json` (the file written by the
24
+ * dashboard on first start).
25
+ * - `publish()` is fire-and-forget: returns a Promise that resolves
26
+ * after one HTTP round-trip or rejects on failure. NEVER throws
27
+ * into the caller — failures are logged and swallowed (the plugin
28
+ * must keep running even when the dashboard is down).
29
+ * - `publish()` queues events if the dashboard is unreachable and
30
+ * drains the queue on reconnect (best-effort).
31
+ */
32
+
33
+ import {
34
+ createBizarClient,
35
+ isBizarError,
36
+ type BizarClient,
37
+ type DashboardEvent,
38
+ } from "@polderlabs/bizar-sdk";
39
+ import { readFileSync, existsSync } from "node:fs";
40
+ import { join } from "node:path";
41
+ import { homedir } from "node:os";
42
+
43
+ const DEFAULT_DASHBOARD_URL = "http://127.0.0.1:4098";
44
+ const DEFAULT_AUTH_FILE_PATHS = [
45
+ join(homedir(), ".cache", "bizarharness", "dash-auth.json"),
46
+ join(homedir(), ".cache", "bizar", "dash-auth.json"),
47
+ ];
48
+ /**
49
+ * The auth file paths to search. Override at test-time via
50
+ * `process.env.BIZAR_DASHBOARD_AUTH_FILE` (single file) or
51
+ * `process.env.BIZAR_DASHBOARD_AUTH_FILES` (colon-separated list).
52
+ */
53
+ function getAuthFilePaths(): string[] {
54
+ const single = process.env.BIZAR_DASHBOARD_AUTH_FILE;
55
+ if (single) return [single];
56
+ const multi = process.env.BIZAR_DASHBOARD_AUTH_FILES;
57
+ if (multi) return multi.split(":").filter(Boolean);
58
+ return DEFAULT_AUTH_FILE_PATHS;
59
+ }
60
+
61
+ export interface Logger {
62
+ debug(message: string): void;
63
+ info(message: string): void;
64
+ warn(message: string): void;
65
+ error(message: string): void;
66
+ }
67
+
68
+ export interface DashboardPublisherOptions {
69
+ logger: Logger;
70
+ baseUrl?: string;
71
+ password?: string;
72
+ /** Disable the publisher entirely (e.g. BIZAR_DASHBOARD_DISABLE=1). */
73
+ disabled?: boolean;
74
+ /** Max queued events while the dashboard is unreachable. */
75
+ queueLimit?: number;
76
+ }
77
+
78
+ /**
79
+ * Read the dashboard auth record from one of the candidate paths.
80
+ * Returns null if no usable file exists. Never throws.
81
+ */
82
+ function readDashboardAuth(): { password: string; baseUrl?: string; port?: number } | null {
83
+ for (const candidate of getAuthFilePaths()) {
84
+ if (!existsSync(candidate)) continue;
85
+ try {
86
+ const raw = readFileSync(candidate, "utf8");
87
+ const parsed = JSON.parse(raw) as {
88
+ password?: unknown;
89
+ baseUrl?: unknown;
90
+ port?: unknown;
91
+ };
92
+ if (typeof parsed.password !== "string" || parsed.password.length < 16) continue;
93
+ return {
94
+ password: parsed.password,
95
+ baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : undefined,
96
+ port: typeof parsed.port === "number" ? parsed.port : undefined,
97
+ };
98
+ } catch {
99
+ // ignore malformed file
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
105
+ export interface DashboardPublisher {
106
+ start(): Promise<void>;
107
+ publish(event: DashboardEvent): Promise<void>;
108
+ stop(): void;
109
+ /** Whether the publisher is currently configured and ready. */
110
+ isReady(): boolean;
111
+ }
112
+
113
+ export function createDashboardPublisher(
114
+ options: DashboardPublisherOptions,
115
+ ): DashboardPublisher {
116
+ const { logger, disabled = false, queueLimit = 100 } = options;
117
+
118
+ let client: BizarClient | null = null;
119
+ const queue: DashboardEvent[] = [];
120
+ let flushing = false;
121
+
122
+ function resolveConfig(): { baseUrl: string; password: string } | null {
123
+ let baseUrl = options.baseUrl ?? process.env.BIZAR_DASHBOARD_URL;
124
+ if (!baseUrl) {
125
+ const port = process.env.BIZAR_DASHBOARD_PORT;
126
+ baseUrl = port ? `http://127.0.0.1:${port}` : DEFAULT_DASHBOARD_URL;
127
+ }
128
+ const password =
129
+ options.password ??
130
+ process.env.BIZAR_DASHBOARD_PASSWORD ??
131
+ readDashboardAuth()?.password;
132
+
133
+ if (!password) {
134
+ return null;
135
+ }
136
+ return { baseUrl, password };
137
+ }
138
+
139
+ function isReady(): boolean {
140
+ return client !== null;
141
+ }
142
+
143
+ async function start(): Promise<void> {
144
+ if (disabled) {
145
+ logger.debug("bizar: dashboard publisher disabled by config");
146
+ return;
147
+ }
148
+ const cfg = resolveConfig();
149
+ if (!cfg) {
150
+ logger.debug(
151
+ "bizar: dashboard publisher not started (no password in env or auth file)",
152
+ );
153
+ return;
154
+ }
155
+ client = createBizarClient({
156
+ baseUrl: cfg.baseUrl,
157
+ password: cfg.password,
158
+ });
159
+ logger.info(
160
+ `bizar: dashboard publisher started (url=${cfg.baseUrl}, password len=${cfg.password.length})`,
161
+ );
162
+
163
+ // Verify the dashboard is reachable. Non-fatal — publish() will retry
164
+ // and surface connection errors as warnings.
165
+ try {
166
+ const health = await client.health.check();
167
+ if (isBizarError(health)) {
168
+ logger.warn(
169
+ `bizar: dashboard unreachable at ${cfg.baseUrl} (${health.name}); publish events will be queued`,
170
+ );
171
+ }
172
+ } catch (err) {
173
+ logger.warn(
174
+ `bizar: dashboard health check failed: ${err instanceof Error ? err.message : String(err)}`,
175
+ );
176
+ }
177
+
178
+ void flushQueue();
179
+ }
180
+
181
+ async function publish(event: DashboardEvent): Promise<void> {
182
+ if (!client) {
183
+ logger.debug(
184
+ `bizar: dashboard publisher not ready — dropping event ${event.type}`,
185
+ );
186
+ return;
187
+ }
188
+ if (queue.length >= queueLimit) {
189
+ queue.shift();
190
+ logger.warn(
191
+ `bizar: dashboard publisher queue full (${queueLimit}); dropping oldest event`,
192
+ );
193
+ }
194
+ queue.push(event);
195
+ if (!flushing) {
196
+ void flushQueue();
197
+ }
198
+ }
199
+
200
+ async function flushQueue(): Promise<void> {
201
+ if (flushing || !client) return;
202
+ flushing = true;
203
+ try {
204
+ while (queue.length > 0 && client) {
205
+ const next = queue.shift()!;
206
+ try {
207
+ const result = await client.events.publish(next);
208
+ if (isBizarError(result)) {
209
+ logger.warn(
210
+ `bizar: dashboard publish failed (${result.name}); requeueing ${next.type}`,
211
+ );
212
+ queue.unshift(next);
213
+ break;
214
+ }
215
+ } catch (err) {
216
+ logger.warn(
217
+ `bizar: dashboard publish threw: ${err instanceof Error ? err.message : String(err)}`,
218
+ );
219
+ queue.unshift(next);
220
+ break;
221
+ }
222
+ }
223
+ } finally {
224
+ flushing = false;
225
+ }
226
+ }
227
+
228
+ function stop(): void {
229
+ client = null;
230
+ queue.length = 0;
231
+ logger.debug("bizar: dashboard publisher stopped");
232
+ }
233
+
234
+ return { start, publish, stop, isReady };
235
+ }
@@ -120,6 +120,8 @@ export class EventStream {
120
120
  private _logger: Logger;
121
121
  private _http: HttpClient;
122
122
  private _handlers = new Map<string, Set<SessionEventHandler>>();
123
+ /** Global handler invoked for every event regardless of session (v0.7.0). */
124
+ private _globalHandler: SessionEventHandler | null = null;
123
125
  private _connected = false;
124
126
  private _aborted = false;
125
127
  private _abortController: AbortController | null = null;
@@ -128,6 +130,21 @@ export class EventStream {
128
130
  private _reconnectTimer: ReturnType<typeof setTimeout> | null = null;
129
131
  private _connectPromise: Promise<void> | null = null;
130
132
 
133
+ /**
134
+ * Register a global handler invoked for every event (regardless of
135
+ * session). Used to forward events to the dashboard publisher. Returns
136
+ * an unsubscribe function. (v0.7.0-alpha.1 — wired into the v2
137
+ * SDK dashboard-client.ts bridge.)
138
+ */
139
+ onEvent(handler: SessionEventHandler): () => void {
140
+ this._globalHandler = handler;
141
+ return () => {
142
+ if (this._globalHandler === handler) {
143
+ this._globalHandler = null;
144
+ }
145
+ };
146
+ }
147
+
131
148
  constructor(opts: {
132
149
  baseUrl: string;
133
150
  directory: string;
@@ -439,6 +456,21 @@ export class EventStream {
439
456
  }
440
457
 
441
458
  private dispatchToHandlers(sessionID: string, event: StreamEvent): void {
459
+ // v0.7.0-alpha.1 — Forward every event to the global handler
460
+ // (typically the dashboard publisher) BEFORE the per-session dispatch.
461
+ // Failures here are isolated so one bad handler doesn't break the
462
+ // dispatch chain for other subscribers.
463
+ if (this._globalHandler) {
464
+ try {
465
+ this._globalHandler(event);
466
+ } catch (err: unknown) {
467
+ const msg = err instanceof Error ? err.message : String(err);
468
+ this._logger.warn(
469
+ `bizar: SSE global handler threw for session ${sessionID} (type=${event.type}): ${msg}`,
470
+ );
471
+ }
472
+ }
473
+
442
474
  const set = this._handlers.get(sessionID);
443
475
  if (!set || set.size === 0) {
444
476
  this._logger.debug(