@ephemushroom/opencode-claude-mem 0.4.0 → 0.4.2

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/README.md CHANGED
@@ -118,6 +118,7 @@ The worker endpoint is resolved in this order: `CLAUDE_MEM_WORKER_HOST` /
118
118
  | `GET` | `/api/timeline?project={name}&anchor={id}` | Chronological context around an observation |
119
119
  | `POST` | `/api/observations/batch` | Fetch full observation details by ID |
120
120
  | `GET` | `/api/stats` + `/api/processing-status` | Sidebar status panel |
121
+ | `GET` | `/api/summaries` + `/api/observations` | Sidebar recent items (expanded only) |
121
122
 
122
123
  Session and observation writes include `platformSource: "opencode"` so
123
124
  Claude-Mem can attribute new OpenCode memories separately from Claude Code,
@@ -215,13 +216,28 @@ plugin, so a separately configured MCP server is not required.
215
216
 
216
217
  ## Sidebar Status
217
218
 
218
- When running in the OpenCode TUI, the plugin registers a sidebar panel titled
219
- **Memory** via its `./tui` export. It polls `/api/stats` and
220
- `/api/processing-status` every few seconds and shows worker health, the current
221
- project, observation/summary/session counts, and the processing queue depth. The
222
- panel fails open — if the worker is offline it simply shows `worker offline` and
223
- never blocks the TUI. Requires OpenCode's `@opentui/solid` runtime; if that is
224
- unavailable the panel is skipped silently.
219
+ When running in the OpenCode TUI, the plugin registers a sidebar section titled
220
+ **Memory** via its `./tui` export, styled after OpenCode's native MCP/Context
221
+ sections: a borderless, click-to-toggle collapsible block.
222
+
223
+ - **Collapsed (default)** — a single line: `▶ Memory (online, 11.7k obs)`.
224
+ The summary turns yellow when the worker is processing (`(queue N)`) and red
225
+ when it is offline (`(offline)`).
226
+ - **Expanded** — click the header to expand: database counts, processing
227
+ status, the last 3 session summaries (`Recent sessions`), and the last 3
228
+ observations (`Latest`, with the same type icons used in the injected
229
+ context: ◆ feature, ● bugfix, ⚖ decision, ○ discovery, …).
230
+
231
+ Recent items are only fetched while expanded, so the collapsed poll loop stays
232
+ cheap. The panel fails open — if the worker is offline it simply shows the
233
+ offline state and never blocks the TUI. Requires OpenCode's `@opentui/solid`
234
+ runtime; if that is unavailable the panel is skipped silently.
235
+
236
+ The plugin also self-heals `~/.config/opencode/tui.json` on load (like
237
+ oh-my-openagent does): if the plugin is registered as a server plugin in
238
+ `opencode.json` but missing from the TUI plugin list, it appends itself so the
239
+ sidebar loads without manual configuration. Symlinked `tui.json` files are
240
+ written through, preserving dotfiles setups.
225
241
 
226
242
  ## Key Implementation Details
227
243
 
package/dist/index.js CHANGED
@@ -12647,6 +12647,51 @@ class WorkerClient {
12647
12647
  return null;
12648
12648
  }
12649
12649
  }
12650
+ static async getRecentSummaries(project, limit) {
12651
+ try {
12652
+ const params = new URLSearchParams({ project, limit: String(limit) });
12653
+ const response = await fetch(`${this.BASE_URL}/api/summaries?${params.toString()}`);
12654
+ if (!response.ok) {
12655
+ return [];
12656
+ }
12657
+ const payload = await response.json();
12658
+ if (!Array.isArray(payload.items)) {
12659
+ return [];
12660
+ }
12661
+ return payload.items.flatMap((item) => {
12662
+ if (!isRecord(item) || typeof item["id"] !== "number") {
12663
+ return [];
12664
+ }
12665
+ const request = typeof item["request"] === "string" ? item["request"].trim() : "";
12666
+ return request ? [{ id: item["id"], request }] : [];
12667
+ });
12668
+ } catch {
12669
+ return [];
12670
+ }
12671
+ }
12672
+ static async getRecentObservations(project, limit) {
12673
+ try {
12674
+ const params = new URLSearchParams({ project, limit: String(limit) });
12675
+ const response = await fetch(`${this.BASE_URL}/api/observations?${params.toString()}`);
12676
+ if (!response.ok) {
12677
+ return [];
12678
+ }
12679
+ const payload = await response.json();
12680
+ if (!Array.isArray(payload.items)) {
12681
+ return [];
12682
+ }
12683
+ return payload.items.flatMap((item) => {
12684
+ if (!isRecord(item) || typeof item["id"] !== "number") {
12685
+ return [];
12686
+ }
12687
+ const title = typeof item["title"] === "string" ? item["title"].trim() : "";
12688
+ const type = typeof item["type"] === "string" ? item["type"] : "";
12689
+ return title ? [{ id: item["id"], type, title }] : [];
12690
+ });
12691
+ } catch {
12692
+ return [];
12693
+ }
12694
+ }
12650
12695
  static async search(query, project, limit) {
12651
12696
  try {
12652
12697
  const params = new URLSearchParams({ query, project });
@@ -12669,6 +12714,74 @@ class WorkerClient {
12669
12714
  }
12670
12715
  }
12671
12716
 
12717
+ // src/tui-registration.ts
12718
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
12719
+ import { homedir as homedir2 } from "os";
12720
+ import { join as join2 } from "path";
12721
+ var PACKAGE_NAME = "@ephemushroom/opencode-claude-mem";
12722
+ function getConfigDir() {
12723
+ const custom2 = process.env["OPENCODE_CONFIG_DIR"]?.trim();
12724
+ return custom2 || join2(homedir2(), ".config", "opencode");
12725
+ }
12726
+ function readJson(filePath) {
12727
+ try {
12728
+ const parsed = JSON.parse(readFileSync2(filePath, "utf8"));
12729
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
12730
+ } catch {
12731
+ return null;
12732
+ }
12733
+ }
12734
+ function pluginEntries(config2) {
12735
+ const { plugin } = config2;
12736
+ if (!Array.isArray(plugin)) {
12737
+ return [];
12738
+ }
12739
+ return plugin.filter((entry) => typeof entry === "string");
12740
+ }
12741
+ function isOwnEntry(entry) {
12742
+ return entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`);
12743
+ }
12744
+ function findServerEntry(configDir) {
12745
+ for (const name of ["opencode.json", "opencode.jsonc"]) {
12746
+ const filePath = join2(configDir, name);
12747
+ const config2 = existsSync2(filePath) ? readJson(filePath) : null;
12748
+ const entry = config2 ? pluginEntries(config2).find(isOwnEntry) : undefined;
12749
+ if (entry) {
12750
+ return entry;
12751
+ }
12752
+ }
12753
+ return null;
12754
+ }
12755
+ function ensureTuiPluginEntry() {
12756
+ try {
12757
+ const configDir = getConfigDir();
12758
+ const serverEntry = findServerEntry(configDir);
12759
+ if (!serverEntry) {
12760
+ return "no-server-entry";
12761
+ }
12762
+ const tuiJsonPath = join2(configDir, "tui.json");
12763
+ let config2 = {};
12764
+ if (existsSync2(tuiJsonPath)) {
12765
+ const parsed = readJson(tuiJsonPath);
12766
+ if (!parsed) {
12767
+ return "malformed";
12768
+ }
12769
+ config2 = parsed;
12770
+ }
12771
+ const plugins = pluginEntries(config2);
12772
+ if (plugins.some(isOwnEntry)) {
12773
+ return "already-present";
12774
+ }
12775
+ mkdirSync(configDir, { recursive: true });
12776
+ const next = { ...config2, plugin: [...plugins, serverEntry] };
12777
+ writeFileSync(tuiJsonPath, `${JSON.stringify(next, null, 2)}
12778
+ `);
12779
+ return "added";
12780
+ } catch {
12781
+ return "failed";
12782
+ }
12783
+ }
12784
+
12672
12785
  // src/index.ts
12673
12786
  var MAX_OBSERVATION_BYTES = 24 * 1024;
12674
12787
  var MAX_TAG_REPLACEMENTS = 100;
@@ -12773,6 +12886,7 @@ function extractTextFromParts(parts) {
12773
12886
  }
12774
12887
  var ClaudeMemPlugin = async (ctx) => {
12775
12888
  const { project, directory, client } = ctx;
12889
+ ensureTuiPluginEntry();
12776
12890
  const projectRoot = directory || process.cwd();
12777
12891
  const projectName = project?.worktree ? project.worktree.split(/[\\/]/).findLast(Boolean) || "unknown-project" : "unknown-project";
12778
12892
  async function toast(message, variant = "info", duration3 = 3000) {
@@ -0,0 +1,9 @@
1
+ export type TuiRegistrationResult = 'added' | 'already-present' | 'no-server-entry' | 'malformed' | 'failed';
2
+ /**
3
+ * Self-heal ~/.config/opencode/tui.json: if this plugin is registered as an
4
+ * OpenCode server plugin but missing from the TUI plugin list, append it so
5
+ * the Memory sidebar loads without manual config. Uses writeFileSync (not
6
+ * atomic rename) so a symlinked tui.json keeps pointing at its dotfiles
7
+ * target instead of being replaced by a regular file.
8
+ */
9
+ export declare function ensureTuiPluginEntry(): TuiRegistrationResult;
package/dist/tui.d.ts CHANGED
@@ -6,6 +6,15 @@ export interface MemSidebarView {
6
6
  readonly summaries: number | null;
7
7
  readonly queueDepth: number | null;
8
8
  readonly isProcessing: boolean;
9
+ readonly recentSummaries: readonly {
10
+ id: number;
11
+ request: string;
12
+ }[];
13
+ readonly recentObservations: readonly {
14
+ id: number;
15
+ type: string;
16
+ title: string;
17
+ }[];
9
18
  }
10
19
  interface ViewNode {
11
20
  readonly kind: 'box' | 'text';
@@ -22,9 +31,9 @@ interface Theme {
22
31
  readonly error: string;
23
32
  readonly borderSubtle: string;
24
33
  }
25
- export declare function buildMemNodes(view: MemSidebarView, theme: Theme): ViewNode[];
34
+ export declare function buildMemNodes(view: MemSidebarView, theme: Theme, collapsed: boolean, onToggle: () => void): ViewNode[];
26
35
  export declare function viewKey(view: MemSidebarView): string;
27
- export declare function readMemView(project: string): Promise<MemSidebarView>;
36
+ export declare function readMemView(project: string, collapsed: boolean): Promise<MemSidebarView>;
28
37
  declare const tuiModule: {
29
38
  id: string;
30
39
  tui: (api: any) => Promise<void>;
package/dist/tui.js CHANGED
@@ -314,6 +314,51 @@ class WorkerClient {
314
314
  return null;
315
315
  }
316
316
  }
317
+ static async getRecentSummaries(project, limit) {
318
+ try {
319
+ const params = new URLSearchParams({ project, limit: String(limit) });
320
+ const response = await fetch(`${this.BASE_URL}/api/summaries?${params.toString()}`);
321
+ if (!response.ok) {
322
+ return [];
323
+ }
324
+ const payload = await response.json();
325
+ if (!Array.isArray(payload.items)) {
326
+ return [];
327
+ }
328
+ return payload.items.flatMap((item) => {
329
+ if (!isRecord(item) || typeof item["id"] !== "number") {
330
+ return [];
331
+ }
332
+ const request = typeof item["request"] === "string" ? item["request"].trim() : "";
333
+ return request ? [{ id: item["id"], request }] : [];
334
+ });
335
+ } catch {
336
+ return [];
337
+ }
338
+ }
339
+ static async getRecentObservations(project, limit) {
340
+ try {
341
+ const params = new URLSearchParams({ project, limit: String(limit) });
342
+ const response = await fetch(`${this.BASE_URL}/api/observations?${params.toString()}`);
343
+ if (!response.ok) {
344
+ return [];
345
+ }
346
+ const payload = await response.json();
347
+ if (!Array.isArray(payload.items)) {
348
+ return [];
349
+ }
350
+ return payload.items.flatMap((item) => {
351
+ if (!isRecord(item) || typeof item["id"] !== "number") {
352
+ return [];
353
+ }
354
+ const title = typeof item["title"] === "string" ? item["title"].trim() : "";
355
+ const type = typeof item["type"] === "string" ? item["type"] : "";
356
+ return title ? [{ id: item["id"], type, title }] : [];
357
+ });
358
+ } catch {
359
+ return [];
360
+ }
361
+ }
317
362
  static async search(query, project, limit) {
318
363
  try {
319
364
  const params = new URLSearchParams({ query, project });
@@ -338,15 +383,38 @@ class WorkerClient {
338
383
 
339
384
  // src/tui.ts
340
385
  var POLL_INTERVAL_MS = 5000;
341
- var LABEL_MAX = 28;
386
+ var LABEL_MAX = 40;
387
+ var RECENT_SUMMARIES = 3;
388
+ var RECENT_OBSERVATIONS = 3;
389
+ var OBSERVATION_TYPE_ICONS = {
390
+ bugfix: "\u25CF",
391
+ feature: "\u25C6",
392
+ refactor: "\u21BB",
393
+ change: "\u2713",
394
+ discovery: "\u25CB",
395
+ decision: "\u2696",
396
+ security_alert: "\u26A0",
397
+ security_note: "\u26B7"
398
+ };
342
399
  function box(props, children) {
343
400
  return { kind: "box", props, children };
344
401
  }
345
402
  function text(props, value) {
346
403
  return { kind: "text", props, text: value };
347
404
  }
405
+ var HTML_ENTITIES = {
406
+ "&lt;": "<",
407
+ "&gt;": ">",
408
+ "&quot;": '"',
409
+ "&#39;": "'",
410
+ "&amp;": "&"
411
+ };
412
+ function decodeEntities(value) {
413
+ return value.replace(/&(?:lt|gt|quot|#39|amp);/g, (entity) => HTML_ENTITIES[entity] ?? entity);
414
+ }
348
415
  function truncate(value) {
349
- return value.length <= LABEL_MAX ? value : `${value.slice(0, LABEL_MAX - 3)}...`;
416
+ const decoded = decodeEntities(value);
417
+ return decoded.length <= LABEL_MAX ? decoded : `${decoded.slice(0, LABEL_MAX - 1)}\u2026`;
350
418
  }
351
419
  function formatCount(value) {
352
420
  if (value === null) {
@@ -354,21 +422,64 @@ function formatCount(value) {
354
422
  }
355
423
  return value >= 1e4 ? `${(value / 1000).toFixed(1)}k` : String(value);
356
424
  }
357
- function buildMemNodes(view, theme) {
358
- const rows = view.healthy ? [
359
- text({ fg: theme.success }, "worker online"),
360
- text({ fg: theme.text }, `project ${truncate(view.project)}`),
361
- text({ fg: theme.text }, `obs ${formatCount(view.observations)} \xB7 sum ${formatCount(view.summaries)}`),
362
- text({ fg: theme.textMuted }, `sessions ${formatCount(view.sessions)}`),
363
- ...view.isProcessing || (view.queueDepth ?? 0) > 0 ? [text({ fg: theme.warning }, `processing \xB7 queue ${view.queueDepth ?? 0}`)] : []
364
- ] : [text({ fg: theme.error }, "worker offline")];
425
+ function summaryLine(view) {
426
+ if (!view.healthy) {
427
+ return { label: "(offline)", tone: "err" };
428
+ }
429
+ if (view.isProcessing || (view.queueDepth ?? 0) > 0) {
430
+ return { label: `(queue ${view.queueDepth ?? 0})`, tone: "warn" };
431
+ }
432
+ return { label: `(online, ${formatCount(view.observations)} obs)`, tone: "ok" };
433
+ }
434
+ function toneColor(tone, theme) {
435
+ if (tone === "err") {
436
+ return theme.error;
437
+ }
438
+ return tone === "warn" ? theme.warning : theme.textMuted;
439
+ }
440
+ function bulletRow(icon, iconColor, label, theme) {
441
+ return box({ flexDirection: "row", gap: 1 }, [
442
+ text({ fg: iconColor, flexShrink: 0 }, icon),
443
+ text({ fg: theme.text, wrapMode: "none" }, truncate(label))
444
+ ]);
445
+ }
446
+ function expandedRows(view, theme) {
447
+ if (!view.healthy) {
448
+ return [bulletRow("\u2022", theme.error, "worker offline", theme)];
449
+ }
450
+ const rows = [
451
+ bulletRow("\u2022", theme.success, `obs ${formatCount(view.observations)} \xB7 sum ${formatCount(view.summaries)} \xB7 ses ${formatCount(view.sessions)}`, theme)
452
+ ];
453
+ if (view.isProcessing || (view.queueDepth ?? 0) > 0) {
454
+ rows.push(bulletRow("\u2022", theme.warning, `processing \xB7 queue ${view.queueDepth ?? 0}`, theme));
455
+ }
456
+ if (view.recentSummaries.length > 0) {
457
+ rows.push(text({ fg: theme.textMuted }, "Recent sessions"));
458
+ for (const summary of view.recentSummaries) {
459
+ rows.push(bulletRow("\u2022", theme.textMuted, summary.request, theme));
460
+ }
461
+ }
462
+ if (view.recentObservations.length > 0) {
463
+ rows.push(text({ fg: theme.textMuted }, "Latest"));
464
+ for (const observation of view.recentObservations) {
465
+ const icon = OBSERVATION_TYPE_ICONS[observation.type] ?? "\u25CB";
466
+ rows.push(bulletRow(icon, theme.info, observation.title, theme));
467
+ }
468
+ }
469
+ return rows;
470
+ }
471
+ function buildMemNodes(view, theme, collapsed, onToggle) {
472
+ const summary = summaryLine(view);
473
+ const header = box({ flexDirection: "row", gap: 1, onMouseDown: onToggle }, [
474
+ text({ fg: theme.text }, collapsed ? "\u25B6" : "\u25BC"),
475
+ text({ fg: theme.info }, "Memory"),
476
+ ...collapsed ? [text({ fg: toneColor(summary.tone, theme) }, summary.label)] : []
477
+ ]);
365
478
  return [
366
- box({
367
- borderStyle: "single",
368
- borderColor: theme.borderSubtle,
369
- flexDirection: "column",
370
- padding: 1
371
- }, [text({ fg: theme.info }, "Memory"), ...rows])
479
+ box({ flexDirection: "column", paddingTop: 1 }, [
480
+ header,
481
+ ...collapsed ? [] : [box({ flexDirection: "column", paddingLeft: 2 }, expandedRows(view, theme))]
482
+ ])
372
483
  ];
373
484
  }
374
485
  function viewKey(view) {
@@ -377,7 +488,7 @@ function viewKey(view) {
377
488
  function readCount(value) {
378
489
  return typeof value === "number" && Number.isFinite(value) ? value : null;
379
490
  }
380
- async function readMemView(project) {
491
+ async function readMemView(project, collapsed) {
381
492
  const stats = await WorkerClient.getStats();
382
493
  if (!stats) {
383
494
  return {
@@ -387,9 +498,15 @@ async function readMemView(project) {
387
498
  sessions: null,
388
499
  summaries: null,
389
500
  queueDepth: null,
390
- isProcessing: false
501
+ isProcessing: false,
502
+ recentSummaries: [],
503
+ recentObservations: []
391
504
  };
392
505
  }
506
+ const [recentSummaries, recentObservations] = collapsed ? [[], []] : await Promise.all([
507
+ WorkerClient.getRecentSummaries(project, RECENT_SUMMARIES),
508
+ WorkerClient.getRecentObservations(project, RECENT_OBSERVATIONS)
509
+ ]);
393
510
  return {
394
511
  healthy: true,
395
512
  project,
@@ -397,9 +514,22 @@ async function readMemView(project) {
397
514
  sessions: readCount(stats.database?.sessions),
398
515
  summaries: readCount(stats.database?.summaries),
399
516
  queueDepth: readCount(stats.processing?.queueDepth),
400
- isProcessing: stats.processing?.isProcessing === true
517
+ isProcessing: stats.processing?.isProcessing === true,
518
+ recentSummaries,
519
+ recentObservations
401
520
  };
402
521
  }
522
+ async function loadSolidCore() {
523
+ try {
524
+ const mod = await import("solid-js");
525
+ if (typeof mod === "object" && mod !== null && typeof mod.createSignal === "function") {
526
+ return mod;
527
+ }
528
+ return null;
529
+ } catch {
530
+ return null;
531
+ }
532
+ }
403
533
  function materializeNode(node, solid) {
404
534
  const element = solid.createElement(node.kind);
405
535
  for (const [name, value] of Object.entries(node.props)) {
@@ -431,16 +561,45 @@ var tuiModule = {
431
561
  if (!solid) {
432
562
  return;
433
563
  }
564
+ const solidCore = await loadSolidCore();
565
+ const signal = (value) => {
566
+ if (solidCore) {
567
+ return solidCore.createSignal(value);
568
+ }
569
+ let current = value;
570
+ return [
571
+ () => current,
572
+ (next) => {
573
+ current = next;
574
+ }
575
+ ];
576
+ };
434
577
  const project = projectNameFromDirectory(String(api.state?.path?.directory ?? ""));
435
- let currentView = await readMemView(project);
436
- let currentKey = viewKey(currentView);
578
+ const initialView = await readMemView(project, true);
579
+ const [collapsed, setCollapsed] = signal(true);
580
+ const [view, setView] = signal(initialView);
581
+ let currentKey = viewKey(initialView);
437
582
  let disposed = false;
438
583
  let inFlight = false;
439
584
  let timer = null;
585
+ const refresh = async () => {
586
+ const nextView = await readMemView(project, collapsed());
587
+ const nextKey = viewKey(nextView);
588
+ if (nextKey !== currentKey) {
589
+ currentKey = nextKey;
590
+ setView(nextView);
591
+ api.renderer.requestRender();
592
+ }
593
+ };
594
+ const onToggle = () => {
595
+ setCollapsed(!collapsed());
596
+ api.renderer.requestRender();
597
+ refresh().catch(() => {});
598
+ };
440
599
  api.slots.register({
441
600
  order: 910,
442
601
  slots: {
443
- sidebar_content: () => materialize(buildMemNodes(currentView, api.theme.current), solid)
602
+ sidebar_content: () => materialize(buildMemNodes(view(), api.theme.current, collapsed(), onToggle), solid)
444
603
  }
445
604
  });
446
605
  api.renderer.requestRender();
@@ -456,13 +615,7 @@ var tuiModule = {
456
615
  }
457
616
  inFlight = true;
458
617
  try {
459
- const nextView = await readMemView(project);
460
- const nextKey = viewKey(nextView);
461
- if (nextKey !== currentKey) {
462
- currentView = nextView;
463
- currentKey = nextKey;
464
- api.renderer.requestRender();
465
- }
618
+ await refresh();
466
619
  } catch {} finally {
467
620
  inFlight = false;
468
621
  if (!disposed) {
@@ -95,5 +95,20 @@ export declare class WorkerClient {
95
95
  queueDepth?: number;
96
96
  };
97
97
  } | null>;
98
+ /**
99
+ * Recent session summaries for the sidebar (GET /api/summaries).
100
+ */
101
+ static getRecentSummaries(project: string, limit: number): Promise<{
102
+ id: number;
103
+ request: string;
104
+ }[]>;
105
+ /**
106
+ * Recent observations for the sidebar (GET /api/observations).
107
+ */
108
+ static getRecentObservations(project: string, limit: number): Promise<{
109
+ id: number;
110
+ type: string;
111
+ title: string;
112
+ }[]>;
98
113
  static search(query: string, project: string, limit?: number): Promise<string>;
99
114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ephemushroom/opencode-claude-mem",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "OpenCode plugin for Claude-Mem persistent memory system",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -23,7 +23,7 @@
23
23
  }
24
24
  },
25
25
  "scripts": {
26
- "build": "bun build src/index.ts --outfile dist/index.js --target bun --format esm && bun build src/tui.ts --outfile dist/tui.js --target bun --format esm --external @opentui/solid && tsc -p tsconfig.build.json --emitDeclarationOnly",
26
+ "build": "bun build src/index.ts --outfile dist/index.js --target bun --format esm && bun build src/tui.ts --outfile dist/tui.js --target bun --format esm --external @opentui/solid --external solid-js && tsc -p tsconfig.build.json --emitDeclarationOnly",
27
27
  "dev": "tsc --watch",
28
28
  "clean": "rm -rf dist",
29
29
  "lint": "oxlint",