@polderlabs/bizar 5.4.0 → 5.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.
Files changed (99) hide show
  1. package/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js → EnvVarsSection-L413CQEI.js} +3 -3
  2. package/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js.map → EnvVarsSection-L413CQEI.js.map} +1 -1
  3. package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js → MobileChat-0kGQb1S6.js} +1 -1
  4. package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js.map → MobileChat-0kGQb1S6.js.map} +1 -1
  5. package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js +1 -0
  6. package/bizar-dash/dist/assets/{MobileSettings-CVW9VhEo.js.map → MobileSettings-Cq1oFIdO.js.map} +1 -1
  7. package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js → Toast-C6f1zFJ2.js} +1 -1
  8. package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js.map → Toast-C6f1zFJ2.js.map} +1 -1
  9. package/bizar-dash/dist/assets/{icons-OMEGx5KA.js → icons-BsWXWkEF.js} +121 -126
  10. package/bizar-dash/dist/assets/icons-BsWXWkEF.js.map +1 -0
  11. package/bizar-dash/dist/assets/main-BklYy02e.css +1 -0
  12. package/bizar-dash/dist/assets/main-CBTgYSeG.js +18 -0
  13. package/bizar-dash/dist/assets/main-CBTgYSeG.js.map +1 -0
  14. package/bizar-dash/dist/assets/mobile-CsaN4CxH.js +1 -0
  15. package/bizar-dash/dist/assets/{mobile-XN2P7YfM.js.map → mobile-CsaN4CxH.js.map} +1 -1
  16. package/bizar-dash/dist/assets/{mobile-layout-D6V-7WnA.css → mobile-layout-CJnZNLy3.css} +1 -1
  17. package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js +2 -0
  18. package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js.map +1 -0
  19. package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js → useSlashCommands-DGJcC9iQ.js} +2 -2
  20. package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js.map → useSlashCommands-DGJcC9iQ.js.map} +1 -1
  21. package/bizar-dash/dist/index.html +8 -8
  22. package/bizar-dash/dist/mobile.html +4 -4
  23. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  24. package/bizar-dash/src/server/backup-store.mjs +6 -1
  25. package/bizar-dash/src/server/bg-spawner.mjs +574 -0
  26. package/bizar-dash/src/server/memory-lightrag.mjs +242 -0
  27. package/bizar-dash/src/server/memory-store.mjs +43 -3
  28. package/bizar-dash/src/server/routes/background.mjs +143 -0
  29. package/bizar-dash/src/server/routes/lightrag.mjs +10 -0
  30. package/bizar-dash/src/server/routes/memory.mjs +8 -0
  31. package/bizar-dash/src/server/server.mjs +22 -0
  32. package/bizar-dash/src/web/App.tsx +3 -0
  33. package/bizar-dash/src/web/MobileApp.tsx +1 -10
  34. package/bizar-dash/src/web/components/InstallConfirmDialog.tsx +1 -1
  35. package/bizar-dash/src/web/components/SettingsNav.tsx +1 -10
  36. package/bizar-dash/src/web/lib/types.ts +39 -1
  37. package/bizar-dash/src/web/styles/main.css +43 -0
  38. package/bizar-dash/src/web/styles/mobile-layout.css +7 -5
  39. package/bizar-dash/src/web/styles/settings.css +46 -0
  40. package/bizar-dash/src/web/views/BackgroundAgents.tsx +399 -30
  41. package/bizar-dash/src/web/views/Settings.tsx +12 -12
  42. package/bizar-dash/src/web/views/SpawnAgentModal.tsx +221 -0
  43. package/bizar-dash/tests/BackgroundAgents.test.tsx +160 -0
  44. package/bizar-dash/tests/background-pause-resume.test.mjs +46 -0
  45. package/bizar-dash/tests/background-spawn-from-ui.test.mjs +36 -0
  46. package/bizar-dash/tests/background-steer.test.mjs +42 -0
  47. package/bizar-dash/tests/lightrag-startup-hook.test.mjs +206 -0
  48. package/bizar-dash/tests/memory-auto-reindex.test.mjs +196 -0
  49. package/bizar-dash/tests/memory-cli.test.mjs +1 -1
  50. package/bizar-dash/tests/memory-default-vault.test.mjs +90 -3
  51. package/bizar-dash/tests/memory-lightrag-extended.test.mjs +131 -0
  52. package/bizar-dash/tests/mobile-layout.test.tsx +36 -0
  53. package/bizar-dash/tests/settings-layout.test.tsx +3 -3
  54. package/bizar-dash/tests/settings-nav.test.tsx +7 -5
  55. package/cli/bin.mjs +16 -0
  56. package/cli/commands/deploy/docker.mjs +2 -1
  57. package/cli/commands/lightrag.mjs +171 -0
  58. package/cli/commands/service.mjs +7 -0
  59. package/cli/commands/util.mjs +8 -1
  60. package/cli/init.mjs +1 -1
  61. package/cli/post-install-smoke.mjs +230 -0
  62. package/cli/provision.mjs +177 -0
  63. package/cli/provision.test.mjs +180 -0
  64. package/cli/service-controller.mjs +378 -28
  65. package/cli/service-controller.test.mjs +100 -0
  66. package/cli/service-env.mjs +139 -0
  67. package/cli/service.mjs +23 -0
  68. package/config/agents/_shared/AGENT_BASELINE.md +6 -6
  69. package/install.sh +87 -6
  70. package/package.json +1 -1
  71. package/plugins/bizar/index.ts +147 -0
  72. package/plugins/bizar/src/background-state.ts +84 -5
  73. package/plugins/bizar/src/background.ts +388 -10
  74. package/plugins/bizar/src/hooks/memory-inject.ts +247 -0
  75. package/plugins/bizar/src/hooks/memory-write-on-end.ts +188 -0
  76. package/plugins/bizar/src/opencode-runner.ts +87 -1
  77. package/plugins/bizar/src/tools/bg-pause.ts +77 -0
  78. package/plugins/bizar/src/tools/bg-report-progress.ts +101 -0
  79. package/plugins/bizar/src/tools/bg-resume.ts +72 -0
  80. package/plugins/bizar/src/tools/bg-send-message.ts +105 -0
  81. package/plugins/bizar/src/tools/bg-spawn.ts +13 -6
  82. package/plugins/bizar/src/tools/bg-status.ts +10 -0
  83. package/plugins/bizar/src/tools/memory-list.ts +135 -0
  84. package/plugins/bizar/src/tools/memory-read.ts +142 -0
  85. package/plugins/bizar/src/tools/memory-search.ts +228 -0
  86. package/plugins/bizar/src/tools/memory-write.ts +183 -0
  87. package/plugins/bizar/tests/memory-write-on-end.test.ts +92 -0
  88. package/plugins/bizar/tests/tools/bg-pause.test.ts +64 -0
  89. package/plugins/bizar/tests/tools/bg-report-progress.test.ts +82 -0
  90. package/plugins/bizar/tests/tools/bg-resume.test.ts +43 -0
  91. package/plugins/bizar/tests/tools/bg-send-message.test.ts +46 -0
  92. package/bizar-dash/dist/assets/MobileSettings-CVW9VhEo.js +0 -1
  93. package/bizar-dash/dist/assets/icons-OMEGx5KA.js.map +0 -1
  94. package/bizar-dash/dist/assets/main-CMAGZouj.js +0 -16
  95. package/bizar-dash/dist/assets/main-CMAGZouj.js.map +0 -1
  96. package/bizar-dash/dist/assets/main-O2tZVdHm.css +0 -1
  97. package/bizar-dash/dist/assets/mobile-XN2P7YfM.js +0 -1
  98. package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js +0 -2
  99. package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js.map +0 -1
@@ -0,0 +1,101 @@
1
+ /**
2
+ * plugins/bizar/src/tools/bg-report-progress.ts
3
+ *
4
+ * v5.x — `bizar_report_progress` tool. Lets a running background agent
5
+ * push a structured progress update (step/total/message) into the
6
+ * instance state. The dashboard reads `progress` and renders a
7
+ * progress bar; `progressMessage` shows the free-form status line.
8
+ *
9
+ * Available to ALL agents (the body of a bg agent calls it; not just
10
+ * Odin). The `bizar_*` prefix is reserved; the auth filter is "all",
11
+ * matching the "tools all agents can call" pattern of
12
+ * `bizar_status`.
13
+ */
14
+ import { tool } from "@opencode-ai/plugin";
15
+ import { z } from "zod";
16
+
17
+ import type { InstanceManager } from "../background.js";
18
+ import type { Logger } from "../logger.js";
19
+
20
+ export interface BgReportProgressDeps {
21
+ instanceManager: InstanceManager;
22
+ logger: Logger;
23
+ }
24
+
25
+ export function createBgReportProgressTool(deps: BgReportProgressDeps) {
26
+ return tool({
27
+ description:
28
+ "Report progress on the current background instance. " +
29
+ "Available to all agents. step/total are integers; message is a " +
30
+ "free-form status hint shown next to the progress bar.",
31
+ args: {
32
+ instanceId: z
33
+ .string()
34
+ .min(1)
35
+ .describe("Instance id (the one returned by bizar_spawn_background)."),
36
+ step: z
37
+ .number()
38
+ .int()
39
+ .min(0)
40
+ .describe("Current step (>=0)."),
41
+ total: z
42
+ .number()
43
+ .int()
44
+ .positive()
45
+ .describe("Total step count (>0)."),
46
+ message: z
47
+ .string()
48
+ .optional()
49
+ .describe("Optional human-readable status."),
50
+ indeterminate: z
51
+ .boolean()
52
+ .optional()
53
+ .default(false)
54
+ .describe("Set true to show an indeterminate (unknown) progress."),
55
+ },
56
+ execute: async (rawArgs) => {
57
+ const args = rawArgs as {
58
+ instanceId: string;
59
+ step: number;
60
+ total: number;
61
+ message?: string;
62
+ indeterminate?: boolean;
63
+ };
64
+ try {
65
+ const result = await deps.instanceManager.updateProgress(
66
+ args.instanceId,
67
+ args.step,
68
+ args.total,
69
+ args.message,
70
+ Boolean(args.indeterminate),
71
+ );
72
+ if (!result.ok) {
73
+ return {
74
+ output: JSON.stringify({
75
+ error: result.error || "progress_failed",
76
+ instanceId: args.instanceId,
77
+ }),
78
+ };
79
+ }
80
+ return {
81
+ output: JSON.stringify({
82
+ instanceId: args.instanceId,
83
+ progress: result.progress,
84
+ }),
85
+ };
86
+ } catch (err: unknown) {
87
+ deps.logger.warn(
88
+ `bizar: report-progress(${args.instanceId}) threw: ${
89
+ err instanceof Error ? err.message : String(err)
90
+ }`,
91
+ );
92
+ return {
93
+ output: JSON.stringify({
94
+ error: `report-progress threw: ${err instanceof Error ? err.message : String(err)}`,
95
+ instanceId: args.instanceId,
96
+ }),
97
+ };
98
+ }
99
+ },
100
+ });
101
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * plugins/bizar/src/tools/bg-resume.ts
3
+ *
4
+ * v5.x — `bizar_resume` tool. Sends SIGCONT to a paused background
5
+ * agent's subprocess. Companion to {@link ./bg-pause.ts}.
6
+ *
7
+ * Odin-only.
8
+ */
9
+ import { tool } from "@opencode-ai/plugin";
10
+ import { z } from "zod";
11
+
12
+ import type { InstanceManager } from "../background.js";
13
+ import type { Logger } from "../logger.js";
14
+
15
+ export interface BgResumeDeps {
16
+ instanceManager: InstanceManager;
17
+ logger: Logger;
18
+ }
19
+
20
+ export function createBgResumeTool(deps: BgResumeDeps) {
21
+ return tool({
22
+ description:
23
+ "Resume a paused background agent by sending SIGCONT to its subprocess. " +
24
+ "Only Odin may resume. No-op on already-running instances. POSIX only.",
25
+ args: {
26
+ instanceId: z
27
+ .string()
28
+ .min(1)
29
+ .describe("Instance id returned by bizar_spawn_background."),
30
+ },
31
+ execute: async (rawArgs, ctx) => {
32
+ if (ctx.agent !== "odin") {
33
+ return {
34
+ output: JSON.stringify({
35
+ error:
36
+ "Only Odin can resume background agents. Use bizar_status to inspect or ask Odin to resume.",
37
+ }),
38
+ };
39
+ }
40
+ const args = rawArgs as { instanceId: string };
41
+ try {
42
+ const result = await deps.instanceManager.resume(args.instanceId);
43
+ if (!result.ok) {
44
+ return {
45
+ output: JSON.stringify({
46
+ error: result.error || "resume failed",
47
+ instanceId: args.instanceId,
48
+ }),
49
+ };
50
+ }
51
+ return {
52
+ output: JSON.stringify({
53
+ instanceId: args.instanceId,
54
+ status: "running",
55
+ }),
56
+ };
57
+ } catch (err: unknown) {
58
+ deps.logger.warn(
59
+ `bizar: resume(${args.instanceId}) threw: ${
60
+ err instanceof Error ? err.message : String(err)
61
+ }`,
62
+ );
63
+ return {
64
+ output: JSON.stringify({
65
+ error: `resume threw: ${err instanceof Error ? err.message : String(err)}`,
66
+ instanceId: args.instanceId,
67
+ }),
68
+ };
69
+ }
70
+ },
71
+ });
72
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * plugins/bizar/src/tools/bg-send-message.ts
3
+ *
4
+ * v5.x — `bizar_send_message` tool. Mid-flight redirect for a
5
+ * background agent — "steer" the running instance with a new
6
+ * instruction.
7
+ *
8
+ * ────────────────────────────────────────────────────────────────
9
+ * v0.8.0 DESIGN NOTE — why this tool is a structured no-op
10
+ * ────────────────────────────────────────────────────────────────
11
+ * In the opencode 1.17 `serve` HTTP API (used by bg-spawn up to v0.7)
12
+ * you could `POST /session/{id}/prompt` to send a follow-up message
13
+ * to a running session — the loop would pick it up mid-run and start
14
+ * a new turn. That's the "real" steer path.
15
+ *
16
+ * In v0.8.0 the plugin switched to `opencode run`, which is a
17
+ * one-shot CLI: it accepts the prompt on the command line, runs the
18
+ * agent loop to completion, and exits. There is no documented HTTP /
19
+ * IPC channel for sending a follow-up user message mid-run. The
20
+ * subprocess doesn't expose one and we don't own the opencode source.
21
+ *
22
+ * Two paths to a workable steer:
23
+ *
24
+ * 1. **Switch back to HTTP server mode** — every bg agent would
25
+ * start its own `opencode serve` + a server-side streaming
26
+ * prompt. Heavyweight (extra processes, double the memory) and
27
+ * a 1-2 sprint piece of work.
28
+ *
29
+ * 2. **Kill + restart with appended prompt** — graceful shutdown
30
+ * of the current subprocess, spin up a new one whose prompt is
31
+ * `(original) + "\n\n[STEERED " + ISO timestamp + "]\n" + msg`.
32
+ * Cheaper; matches the user's mental model of "send new
33
+ * instruction"; implemented in the dashboard at
34
+ * `POST /api/background/:id/steer`.
35
+ *
36
+ * We document the constraint here. The tool itself returns the
37
+ * concrete error and the recommended path so the agent can either
38
+ * (a) ask Odin to use the dashboard steer endpoint or (b) call
39
+ * `bizar_kill` + `bizar_spawn_background` with a combined prompt as
40
+ * a backup.
41
+ *
42
+ * In a future v0.9.x this tool will be promoted to the real
43
+ * implementation once opencode run supports mid-flight prompting.
44
+ */
45
+ import { tool } from "@opencode-ai/plugin";
46
+ import { z } from "zod";
47
+
48
+ import type { InstanceManager } from "../background.js";
49
+ import type { Logger } from "../logger.js";
50
+
51
+ export interface BgSendMessageDeps {
52
+ instanceManager: InstanceManager;
53
+ logger: Logger;
54
+ }
55
+
56
+ export function createBgSendMessageTool(deps: BgSendMessageDeps) {
57
+ return tool({
58
+ description:
59
+ "Send a follow-up message to a running background agent. " +
60
+ "v0.8.0 LIMITATION: opencode run is a one-shot CLI and does not " +
61
+ "support mid-flight prompting. This tool returns an `unavailable_in_subprocess_mode` " +
62
+ "error and points the caller to the dashboard's steer endpoint, " +
63
+ "which performs kill+restart-with-appended-prompt.",
64
+ args: {
65
+ instanceId: z
66
+ .string()
67
+ .min(1)
68
+ .describe("Instance id returned by bizar_spawn_background."),
69
+ message: z
70
+ .string()
71
+ .min(1)
72
+ .describe("The follow-up instruction to send."),
73
+ },
74
+ execute: async (rawArgs, ctx) => {
75
+ const args = rawArgs as { instanceId: string; message: string };
76
+ // Verify the instance exists so we don't silently accept bogus ids.
77
+ const inst = await deps.instanceManager.get(args.instanceId);
78
+ if (!inst) {
79
+ return {
80
+ output: JSON.stringify({
81
+ error: "instance_not_found",
82
+ instanceId: args.instanceId,
83
+ }),
84
+ };
85
+ }
86
+ deps.logger.debug(
87
+ `bizar: sendMessage(${args.instanceId}) — mid-flight steer unavailable in v0.8.0 subprocess mode`,
88
+ );
89
+ void ctx;
90
+ return {
91
+ output: JSON.stringify({
92
+ error: "unavailable_in_subprocess_mode",
93
+ message:
94
+ "v0.8.0 bg agents run as `opencode run` subprocesses and cannot accept a follow-up prompt mid-flight. " +
95
+ "Use the dashboard's `POST /api/background/<id>/steer` (Steer button on instance detail) which performs " +
96
+ "kill+restart with the new instruction appended to the original prompt. " +
97
+ "Or, if running headlessly, you can call `bizar_kill` followed by `bizar_spawn_background` " +
98
+ "with a combined prompt yourself.",
99
+ instanceId: args.instanceId,
100
+ dashboardHint: `POST /api/background/${args.instanceId}/steer`,
101
+ }),
102
+ };
103
+ },
104
+ });
105
+ }
@@ -245,6 +245,10 @@ export function createBgSpawnTool(deps: BgSpawnDeps) {
245
245
  persistent: args.persistent ?? false,
246
246
  maxRestarts: args.maxRestarts ?? 3,
247
247
  restartCount: 0,
248
+ // v5.x — progress reporting seed. Agents update via
249
+ // `bizar_report_progress`; the dashboard renders the bar.
250
+ progress: 0,
251
+ toolCalls: [],
248
252
  };
249
253
  const addRes = await deps.instanceManager.add(draft);
250
254
  if (addRes === "cap_reached") {
@@ -333,14 +337,17 @@ export function createBgSpawnTool(deps: BgSpawnDeps) {
333
337
  const { onExit } = await import("../opencode-runner.js");
334
338
  onExit(spawnRes.processId, (status) => {
335
339
  // Map runner states to BackgroundStatus. The runner reports
336
- // "starting" | "running" | "done" | "failed" | "killed";
337
- // BackgroundStatus has "pending" | "running" | "done" |
338
- // "failed" | "killed" | "timed_out". "starting" maps to
339
- // "running" (in-flight, no terminal state).
340
- const mapped: "pending" | "running" | "done" | "failed" | "killed" =
340
+ // "starting" | "running" | "paused" | "done" | "failed" | "killed";
341
+ // BackgroundStatus has "pending" | "running" | "paused" | "done" |
342
+ // "failed" | "killed" | "timed_out" | "steered". "starting"
343
+ // maps to "running" (in-flight, no terminal state). "paused"
344
+ // is preserved so pause/resume survives subprocess reconnects.
345
+ const mapped: "pending" | "running" | "paused" | "done" | "failed" | "killed" =
341
346
  status.state === "starting" || status.state === "running"
342
347
  ? "running"
343
- : status.state;
348
+ : status.state === "paused"
349
+ ? "paused"
350
+ : status.state;
344
351
 
345
352
  const update: Parameters<InstanceManager["update"]>[1] = {
346
353
  status: mapped,
@@ -95,5 +95,15 @@ function toViewShape(inst: import("../background-state.js").BackgroundState): In
95
95
  if (inst.interventionAt !== undefined) v.interventionAt = inst.interventionAt;
96
96
  if (inst.interventionReason !== undefined) v.interventionReason = inst.interventionReason;
97
97
  }
98
+ // v5.x — extended dashboard surface.
99
+ if (Array.isArray(inst.toolCalls) && inst.toolCalls.length > 0) {
100
+ v.toolCalls = inst.toolCalls.slice();
101
+ }
102
+ if (typeof inst.progress === "number") v.progress = inst.progress;
103
+ if (inst.progressMessage !== undefined) v.progressMessage = inst.progressMessage;
104
+ if (inst.processId !== undefined) v.processId = inst.processId;
105
+ if (inst.runnerState !== undefined) v.runnerState = inst.runnerState;
106
+ if (Array.isArray(inst.tags) && inst.tags.length > 0) v.tags = inst.tags.slice();
107
+ if (inst.pausedAt !== undefined) v.pausedAt = inst.pausedAt;
98
108
  return v;
99
109
  }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * plugins/bizar/src/tools/memory-list.ts
3
+ *
4
+ * `bizar_memory_list` tool — list notes in the Bizar Memory vault.
5
+ *
6
+ * Calls GET /api/memory/notes?prefix=... on the dashboard server.
7
+ * Returns an array of { path, title, updated, type } objects.
8
+ */
9
+
10
+ import { tool } from "@opencode-ai/plugin";
11
+ import { z } from "zod";
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { homedir } from "node:os";
15
+
16
+ import type { Logger } from "../logger.js";
17
+
18
+ export interface MemoryListDeps {
19
+ worktree: string;
20
+ logger: Logger;
21
+ }
22
+
23
+ async function resolveDashboardPort(): Promise<number | null> {
24
+ const portFile = join(homedir(), ".config", "bizar", "dashboard.port");
25
+ if (existsSync(portFile)) {
26
+ try {
27
+ const raw = readFileSync(portFile, "utf8").trim();
28
+ const n = parseInt(raw, 10);
29
+ if (Number.isFinite(n) && n > 0 && n <= 65535) return n;
30
+ } catch {
31
+ // fall through
32
+ }
33
+ }
34
+ const envPort = process.env.BIZAR_DASHBOARD_PORT;
35
+ if (envPort) {
36
+ const n = parseInt(envPort, 10);
37
+ if (Number.isFinite(n) && n > 0 && n <= 65535) return n;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ export function createMemoryListTool(deps: MemoryListDeps) {
43
+ return tool({
44
+ description:
45
+ "List notes in the Bizar Memory vault, optionally filtered by path prefix. " +
46
+ "Returns { notes: [{ path, title, updated, type }] }. " +
47
+ "Available to all agents.",
48
+ args: {
49
+ prefix: z
50
+ .string()
51
+ .optional()
52
+ .describe("Optional vault-relative path prefix to filter results " +
53
+ "(e.g. 'decisions/' lists all notes under decisions/)."),
54
+ limit: z
55
+ .number()
56
+ .int()
57
+ .positive()
58
+ .optional()
59
+ .default(50)
60
+ .describe("Max notes to return (default 50)."),
61
+ },
62
+ execute: async (rawArgs) => {
63
+ const args = rawArgs as { prefix?: string; limit?: number };
64
+ const { logger } = deps;
65
+
66
+ const port = await resolveDashboardPort();
67
+ if (!port) {
68
+ return {
69
+ output: JSON.stringify({
70
+ error: "dashboard_not_running",
71
+ message: "The Bizar dashboard is not running. Start it with `bizar dash start`.",
72
+ notes: [],
73
+ }),
74
+ };
75
+ }
76
+
77
+ const params = new URLSearchParams();
78
+ if (args.prefix) {
79
+ params.set("prefix", args.prefix);
80
+ }
81
+ const limit = Math.min(args.limit ?? 50, 200);
82
+ params.set("limit", String(limit));
83
+
84
+ const url = `http://127.0.0.1:${port}/api/memory/notes?${params}`;
85
+
86
+ try {
87
+ const res = await fetch(url, { method: "GET" });
88
+ if (!res.ok) {
89
+ const text = await res.text().catch(() => "");
90
+ logger.warn(`bizar: memory-list HTTP ${res.status}: ${text.slice(0, 200)}`);
91
+ return {
92
+ output: JSON.stringify({
93
+ error: "list_failed",
94
+ message: `HTTP ${res.status}: ${text.slice(0, 200)}`,
95
+ notes: [],
96
+ }),
97
+ };
98
+ }
99
+
100
+ const data = (await res.json()) as {
101
+ notes?: Array<{
102
+ relPath?: string;
103
+ frontmatter?: { title?: string; type?: string };
104
+ mtime?: number;
105
+ }>;
106
+ count?: number;
107
+ };
108
+
109
+ const notes = (data.notes ?? []).map((n) => ({
110
+ path: n.relPath ?? "",
111
+ title: n.frontmatter?.title ?? null,
112
+ updated: n.mtime ? new Date(n.mtime).toISOString() : null,
113
+ type: n.frontmatter?.type ?? null,
114
+ }));
115
+
116
+ return {
117
+ output: JSON.stringify({
118
+ notes,
119
+ count: notes.length,
120
+ }),
121
+ };
122
+ } catch (err) {
123
+ const msg = err instanceof Error ? err.message : String(err);
124
+ logger.warn(`bizar: memory-list failed: ${msg}`);
125
+ return {
126
+ output: JSON.stringify({
127
+ error: "list_failed",
128
+ message: msg,
129
+ notes: [],
130
+ }),
131
+ };
132
+ }
133
+ },
134
+ });
135
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * plugins/bizar/src/tools/memory-read.ts
3
+ *
4
+ * `bizar_memory_read` tool — read a single note from the Bizar Memory vault.
5
+ *
6
+ * Calls GET /api/memory/notes/:path on the dashboard server.
7
+ *
8
+ * The `path` is relative to the vault root (e.g. "decisions/foo.md"
9
+ * or "projects/myproj/sessions/2025-07-06.md").
10
+ */
11
+
12
+ import { tool } from "@opencode-ai/plugin";
13
+ import { z } from "zod";
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { homedir } from "node:os";
17
+
18
+ import type { Logger } from "../logger.js";
19
+
20
+ export interface MemoryReadDeps {
21
+ worktree: string;
22
+ logger: Logger;
23
+ }
24
+
25
+ async function resolveDashboardPort(): Promise<number | null> {
26
+ const portFile = join(homedir(), ".config", "bizar", "dashboard.port");
27
+ if (existsSync(portFile)) {
28
+ try {
29
+ const raw = readFileSync(portFile, "utf8").trim();
30
+ const n = parseInt(raw, 10);
31
+ if (Number.isFinite(n) && n > 0 && n <= 65535) return n;
32
+ } catch {
33
+ // fall through
34
+ }
35
+ }
36
+ const envPort = process.env.BIZAR_DASHBOARD_PORT;
37
+ if (envPort) {
38
+ const n = parseInt(envPort, 10);
39
+ if (Number.isFinite(n) && n > 0 && n <= 65535) return n;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ export function createMemoryReadTool(deps: MemoryReadDeps) {
45
+ return tool({
46
+ description:
47
+ "Read a single note from the Bizar Memory vault. " +
48
+ "The path is relative to the vault root (e.g. 'decisions/foo.md'). " +
49
+ "Returns { path, content, frontmatter, mtime } or { error } on failure. " +
50
+ "Available to all agents.",
51
+ args: {
52
+ path: z
53
+ .string()
54
+ .min(1)
55
+ .describe("Vault-relative path of the note to read (e.g. 'decisions/foo.md')."),
56
+ },
57
+ execute: async (rawArgs) => {
58
+ const args = rawArgs as { path: string };
59
+ const { logger } = deps;
60
+
61
+ // Basic path safety: no absolute paths, no traversal above vault root
62
+ const normalized = args.path.replace(/\\/g, "/").replace(/^\/+/, "");
63
+ if (normalized.includes("..") || normalized.startsWith(".")) {
64
+ return {
65
+ output: JSON.stringify({
66
+ error: "invalid_path",
67
+ message: "Path must be a simple relative vault path (no '..' or absolute paths).",
68
+ path: args.path,
69
+ }),
70
+ };
71
+ }
72
+
73
+ const port = await resolveDashboardPort();
74
+ if (!port) {
75
+ return {
76
+ output: JSON.stringify({
77
+ error: "dashboard_not_running",
78
+ message: "The Bizar dashboard is not running. Start it with `bizar dash start`.",
79
+ path: args.path,
80
+ }),
81
+ };
82
+ }
83
+
84
+ const url = `http://127.0.0.1:${port}/api/memory/notes/${encodeURIComponent(normalized)}`;
85
+
86
+ try {
87
+ const res = await fetch(url, { method: "GET" });
88
+
89
+ if (res.status === 404) {
90
+ return {
91
+ output: JSON.stringify({
92
+ error: "not_found",
93
+ message: `Note not found at path: ${normalized}`,
94
+ path: normalized,
95
+ }),
96
+ };
97
+ }
98
+
99
+ if (!res.ok) {
100
+ const text = await res.text().catch(() => "");
101
+ logger.warn(`bizar: memory-read(${normalized}) HTTP ${res.status}: ${text.slice(0, 200)}`);
102
+ return {
103
+ output: JSON.stringify({
104
+ error: "read_failed",
105
+ message: `HTTP ${res.status}: ${text.slice(0, 200)}`,
106
+ path: normalized,
107
+ }),
108
+ };
109
+ }
110
+
111
+ const data = (await res.json()) as {
112
+ relPath?: string;
113
+ frontmatter?: Record<string, unknown>;
114
+ body?: string;
115
+ raw?: string;
116
+ mtime?: number;
117
+ size?: number;
118
+ };
119
+
120
+ return {
121
+ output: JSON.stringify({
122
+ path: data.relPath ?? normalized,
123
+ content: data.body ?? "",
124
+ frontmatter: data.frontmatter ?? {},
125
+ mtime: data.mtime ?? null,
126
+ size: data.size ?? null,
127
+ }),
128
+ };
129
+ } catch (err) {
130
+ const msg = err instanceof Error ? err.message : String(err);
131
+ logger.warn(`bizar: memory-read(${normalized}) failed: ${msg}`);
132
+ return {
133
+ output: JSON.stringify({
134
+ error: "read_failed",
135
+ message: msg,
136
+ path: normalized,
137
+ }),
138
+ };
139
+ }
140
+ },
141
+ });
142
+ }