synartesis 0.5.2 → 0.6.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/CHANGELOG.md CHANGED
@@ -2,6 +2,97 @@
2
2
 
3
3
  What changed, and why it mattered. Dates are release dates.
4
4
 
5
+ ## 0.6.2 — 2026-09-12
6
+
7
+ ### Fixed
8
+
9
+ - **The application could not be built on Windows or Linux at all**, which the
10
+ first release build found out the hard way. On Windows, pnpm compiles
11
+ better-sqlite3 from source -- ignoring the `gypfile: false` that tells npm
12
+ not to bother -- and a runner whose Visual Studio is newer than node-gyp
13
+ knows about simply fails. It never needed compiling: the package ships a
14
+ prebuilt binary for every platform it supports, and its loader prefers one
15
+ over anything built locally, so what node-gyp produced here was never
16
+ loaded. On Linux, dpkg refuses to build a `.deb` without a maintainer, and
17
+ nothing named one.
18
+
19
+ - **The release workflow can be rehearsed.** Run with the tag box empty, it
20
+ builds the branch on all three platforms and publishes nothing. Finding out
21
+ whether a build works should not cost a version number each time.
22
+
23
+ ## 0.6.1 — 2026-09-12
24
+
25
+ ### Added
26
+
27
+ - **A `release` workflow** builds the application on macOS, Windows and Linux --
28
+ each on its own machine, since an installer cannot honestly be built anywhere
29
+ else -- and attaches the `.dmg`, `.exe`, AppImage and `.deb` to the release
30
+ for a `v*` tag. It signs and notarises where the secrets exist and produces
31
+ unsigned builds where they do not, so a fork of this repository can still
32
+ build it.
33
+
34
+ ### Fixed
35
+
36
+ - **The packaging script could not have run on Windows.** It started
37
+ electron-builder through the shim in `node_modules/.bin`, which is a `.cmd`
38
+ there and something node will not spawn without a shell. It runs the
39
+ builder's own entry file instead -- the same file on every platform -- and
40
+ the check that refuses a stale bundle now knows where Windows and Linux keep
41
+ the archive as well as where macOS does.
42
+
43
+ - **An empty secret is not a certificate.** A CI job passes every secret it was
44
+ told about whether the repository holds one or not, so a build with no
45
+ signing certificate was handed `CSC_LINK=""`. electron-builder reads
46
+ "defined" as "use it": it took the empty string for the path to a
47
+ certificate, resolved it against the project directory, and stopped with
48
+ "`<repo>/app not a file`" -- a message with no visible relationship to its
49
+ cause. Blank credentials are dropped before the builder starts, so a build
50
+ without a certificate is simply unsigned.
51
+
52
+ - **A `.DS_Store` could make a current bundle look stale.** Finder leaves one in
53
+ any directory somebody has looked at, and the staleness check was counting it
54
+ as input to the bundle even though nothing packs it.
55
+
56
+ ## 0.6.0 — 2026-09-12
57
+
58
+ ### Added
59
+
60
+ - **A desktop window.** You talk to a model; every tool it calls goes through
61
+ the same proxy the CLI installs, so what it did is journalled and reversible
62
+ without the window implementing any of that itself. Each call gets a card
63
+ naming the server, the tool, the class it was given and whether the state it
64
+ replaced was captured; a call that cannot be undone stops and waits for a
65
+ person rather than happening behind one; and "put it back" is the CLI's own
66
+ two steps — the real plan, then the confirmation. Claude, Gemini, Mistral,
67
+ OpenAI, and anything speaking `/v1/chat/completions`, which includes Ollama,
68
+ LM Studio and vLLM on the same machine. Keys are pasted by you and kept in the
69
+ OS keychain; they are never written to the journal or a log.
70
+
71
+ It is a separate download rather than part of this package: a browser engine
72
+ inside a CLI would put 200 MB into every install of a command that is a few
73
+ hundred kilobytes. Both share one journal, so either can undo what the other
74
+ did.
75
+
76
+ - **`synartesis desktop`** opens that window if it is installed, and says where
77
+ to get it if it is not.
78
+
79
+ ### Fixed
80
+
81
+ - **Opening the window no longer leaves an empty session behind.** A run is
82
+ begun whether or not anybody says anything, and closing one is a chain of
83
+ waits — each server is asked to stop, and a child process takes its time
84
+ going. Quitting the application ended the process partway through, so the
85
+ empty row survived: thirteen of them in one afternoon of testing, thirteen
86
+ lines of `synartesis list` between somebody and the run they were looking for.
87
+ The tidy-up now happens before the servers are waited on — nothing can be
88
+ recorded once the model's client is shut, so the count is already final — and
89
+ quit is held until the engine has put itself away.
90
+
91
+ - **A held call explains itself in words.** An MCP server returns its errors as
92
+ a content envelope, and the most common reason of all — the file does not
93
+ exist yet, so there is nothing to keep — was reaching the screen as a wall of
94
+ `{"content":[{"type":"text"...` with the sentence that mattered inside it.
95
+
5
96
  ## 0.5.2 — 2026-09-10
6
97
 
7
98
  ### Changed
package/README.md CHANGED
@@ -76,6 +76,73 @@ synartesis
76
76
  One screen: what agents have done, what is held for approval, every AI on the
77
77
  machine, and undo — all on the arrow keys.
78
78
 
79
+ ## The desktop window
80
+
81
+ The same engine, with a conversation in front of it. You talk to a model — any
82
+ model — and every tool it calls goes through the proxy on its way out, so the
83
+ undo is not a feature the window implements. It is one it can already offer.
84
+
85
+ ![The Synartesis desktop window: a turn that wrote a file, with the tool card showing it was captured and can be put back](https://raw.githubusercontent.com/ArhaanDev24/Synartesis/main/brand/synartesis-desktop.png)
86
+
87
+ Every call gets a card: which server, which tool, the class Synartesis gave it,
88
+ and whether the state it replaced was captured. The ledger at the top counts the
89
+ same thing for the whole conversation. Nothing there is a promise about what
90
+ should have happened — it is read back out of the journal after the fact.
91
+
92
+ A call that cannot be undone does not happen behind your back. It stops, and
93
+ waits for you, with the reason it cannot be reversed written out:
94
+
95
+ ![A held call in the desktop window, asking whether to allow a write whose prior state could not be captured](https://raw.githubusercontent.com/ArhaanDev24/Synartesis/main/brand/synartesis-desktop-approval.png)
96
+
97
+ And putting it back is the same two steps the CLI takes: the real plan first,
98
+ built from the journal, then the confirmation.
99
+
100
+ ![The undo plan in the desktop window, showing one call skipped and one reverted](https://raw.githubusercontent.com/ArhaanDev24/Synartesis/main/brand/synartesis-desktop-undo.png)
101
+
102
+ It talks to Claude, Gemini, Mistral, OpenAI, or anything speaking
103
+ `/v1/chat/completions` — including Ollama, LM Studio and vLLM on your own
104
+ machine, which cost nothing and send nothing anywhere. Keys are pasted by you,
105
+ kept in the OS keychain through Electron's `safeStorage`, and never written to
106
+ the journal or a log. There is a parchment and a dark setting:
107
+
108
+ ![The desktop window in its dark setting](https://raw.githubusercontent.com/ArhaanDev24/Synartesis/main/brand/synartesis-desktop-dark.png)
109
+
110
+ **Getting it.** `synartesis desktop` opens it, and says where to get it if it is
111
+ not installed. It is a separate download on purpose: shipping a browser engine
112
+ inside a CLI would put 200 MB into every install of a command that is a few
113
+ hundred kilobytes.
114
+
115
+ On macOS it installs the way anything does — open the `.dmg`, drag Synartesis to
116
+ Applications — and on Windows the `.exe` installer puts it where the Start menu
117
+ can find it. After that, either the icon or `synartesis desktop` opens it; the
118
+ command looks where each platform actually installs things rather than asking
119
+ you to remember a path.
120
+
121
+ **The builds are not signed yet, and the first launch says so.** macOS refuses
122
+ an application it cannot check with Apple: open System Settings → Privacy &
123
+ Security and press *Open Anyway*, or `xattr -dr com.apple.quarantine
124
+ /Applications/Synartesis.app` to say the same thing in one line. Windows shows
125
+ a SmartScreen warning, behind *More info*. Both of those are the operating
126
+ system telling you the truth — nobody has vouched for this binary — and the
127
+ honest fix is a Developer ID certificate rather than a page telling you to
128
+ click past it. Building from the clone below avoids the question entirely,
129
+ since an application you built is one you have already vouched for.
130
+
131
+ To build it yourself instead:
132
+
133
+ ```bash
134
+ pnpm install && pnpm app:dist
135
+ ```
136
+
137
+ That writes an installer for the machine it runs on to `app/release` — a `.dmg`
138
+ and a `.app` on macOS, an `.exe` on Windows, an AppImage and a `.deb` on Linux.
139
+ It is unsigned, so it runs where it was built and Gatekeeper refuses it
140
+ anywhere it has been downloaded to: signing and notarisation need an Apple
141
+ developer account, and [`app/README.md`](app/README.md) lists exactly what they
142
+ want. The `release` workflow builds all three platforms on their own machines
143
+ and attaches the installers to the release for a tag. Both the window and the terminal share
144
+ one journal, so either can undo what the other did.
145
+
79
146
  ## What it can and cannot do
80
147
 
81
148
  Every tool gets one of four classifications, written down in a manifest:
@@ -114,6 +181,9 @@ every line it would write over and stops; `--force --yes` goes ahead.
114
181
 
115
182
  ## Commands
116
183
 
184
+ `synartesis desktop` opens [the window](#the-desktop-window), and says where to
185
+ get it if it is not installed.
186
+
117
187
  | Command | Does |
118
188
  |---|---|
119
189
  | `synartesis` | The screen. Everything below can be done from it |
package/dist/cli.js CHANGED
@@ -39,8 +39,9 @@ import {
39
39
  } from "./chunk-YVOO3PTV.js";
40
40
 
41
41
  // src/cli.ts
42
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync3 } from "fs";
43
- import { dirname as dirname3, join as join2, resolve as resolve3 } from "path";
42
+ import { spawn } from "child_process";
43
+ import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync3 } from "fs";
44
+ import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
44
45
  import { fileURLToPath as fileURLToPath2 } from "url";
45
46
 
46
47
  // src/init/draft.ts
@@ -389,6 +390,10 @@ async function rollback(options) {
389
390
  const projected = /* @__PURE__ */ new Map();
390
391
  for (const action of inScope) {
391
392
  let forcedOver;
393
+ if (action.class === "readonly") {
394
+ steps.push({ ...describeStep(action), kind: "skip", reason: "readonly", verified: true });
395
+ continue;
396
+ }
392
397
  const early = classify(action, policies !== void 0, force || dryRun);
393
398
  if (early?.kind === "halt") {
394
399
  const seen = action.error ?? "";
@@ -403,10 +408,6 @@ ${seen}` : seen;
403
408
  steps.push({ ...describeStep(action), ...early });
404
409
  continue;
405
410
  }
406
- if (action.class === "readonly") {
407
- steps.push({ ...describeStep(action), kind: "skip", reason: "readonly", verified: true });
408
- continue;
409
- }
410
411
  const rebuilt = replan(action);
411
412
  const parsedPlan = inversePlan.safeParse(rebuilt.inverse ?? action.inverse);
412
413
  if (!parsedPlan.success) {
@@ -2465,6 +2466,61 @@ async function openConsole(options) {
2465
2466
  }
2466
2467
  }
2467
2468
 
2469
+ // src/desktop.ts
2470
+ import { existsSync as existsSync7 } from "fs";
2471
+ import { homedir as homedir2 } from "os";
2472
+ import { join as join2 } from "path";
2473
+ var RELEASES = "https://github.com/ArhaanDev24/Synartesis/releases";
2474
+ function candidates(platform2 = process.platform) {
2475
+ const home = homedir2();
2476
+ if (platform2 === "darwin") {
2477
+ return [
2478
+ "/Applications/Synartesis.app",
2479
+ join2(home, "Applications/Synartesis.app")
2480
+ ];
2481
+ }
2482
+ if (platform2 === "win32") {
2483
+ const local = process.env["LOCALAPPDATA"] ?? join2(home, "AppData/Local");
2484
+ return [
2485
+ join2(local, "Programs/Synartesis/Synartesis.exe"),
2486
+ join2(process.env["PROGRAMFILES"] ?? "C:/Program Files", "Synartesis/Synartesis.exe")
2487
+ ];
2488
+ }
2489
+ return [
2490
+ "/opt/Synartesis/synartesis-desktop",
2491
+ "/usr/bin/synartesis-desktop",
2492
+ join2(home, ".local/bin/synartesis-desktop"),
2493
+ join2(home, "Applications/Synartesis.AppImage")
2494
+ ];
2495
+ }
2496
+ function findDesktop(platform2 = process.platform, here = existsSync7) {
2497
+ for (const path of candidates(platform2)) {
2498
+ if (!here(path)) {
2499
+ continue;
2500
+ }
2501
+ if (platform2 === "darwin") {
2502
+ return { path, open: { command: "open", args: ["-a", path] } };
2503
+ }
2504
+ return { path, open: { command: path, args: [] } };
2505
+ }
2506
+ return void 0;
2507
+ }
2508
+ function whereToGetIt(platform2 = process.platform) {
2509
+ const asset = platform2 === "darwin" ? "the .dmg for your chip (Apple silicon or Intel)" : platform2 === "win32" ? "the Windows installer" : "the AppImage or .deb";
2510
+ return [
2511
+ "The Synartesis desktop application is not installed.",
2512
+ "",
2513
+ `Download ${asset} from:`,
2514
+ ` ${RELEASES}`,
2515
+ "",
2516
+ "It is a separate download on purpose. Shipping it through npm would mean",
2517
+ "every install of this command line tool pulled a browser engine with it.",
2518
+ "",
2519
+ "The journal is shared either way: something the window does is undoable",
2520
+ "here with `synartesis undo`, and the other way round."
2521
+ ].join("\n");
2522
+ }
2523
+
2468
2524
  // src/cli.ts
2469
2525
  var NODE_MAJOR = Number(process.versions.node.split(".")[0]);
2470
2526
  if (NODE_MAJOR < 22) {
@@ -2494,6 +2550,7 @@ var COMMANDS = `
2494
2550
  [--journal <path>]
2495
2551
  [--http <port> --token <secret>] for a client that
2496
2552
  cannot start one
2553
+ synartesis desktop
2497
2554
  synartesis watch [--by <name>] [--journal <path>]
2498
2555
  synartesis approve [actionId|--all] [--by <name>] [--journal <path>]
2499
2556
  synartesis deny [actionId|--all] [--by <name>] [--reason <text>] [--journal <path>]
@@ -2505,6 +2562,10 @@ Cursor or Codex already list, writes a policy covering all of it -- using the on
2505
2562
  ship where they fit -- and points each entry at the proxy. The original config
2506
2563
  is copied aside first, and uninstall puts it back. status says what is covered.
2507
2564
 
2565
+ desktop opens the window, if it is installed. It is a separate download --
2566
+ shipping it through npm would put a browser engine inside every install of
2567
+ this command. Both share one journal, so either can undo what the other did.
2568
+
2508
2569
  close ends a run left active by a proxy that was killed; nothing guesses at
2509
2570
  that, since several proxies can share one journal.
2510
2571
 
@@ -2736,7 +2797,7 @@ async function runUninstall(argv) {
2736
2797
  }
2737
2798
  function openIfPresent(journalPath) {
2738
2799
  try {
2739
- return existsSync7(journalPath) ? openJournal(journalPath, { mustExist: true }) : void 0;
2800
+ return existsSync8(journalPath) ? openJournal(journalPath, { mustExist: true }) : void 0;
2740
2801
  } catch {
2741
2802
  return void 0;
2742
2803
  }
@@ -2769,7 +2830,7 @@ function runStatus(argv) {
2769
2830
  const journalPath = findJournal(flag(argv, "--journal"), manifestPath);
2770
2831
  out("");
2771
2832
  out(
2772
- ` ${style.label("policy")} ${existsSync7(manifestPath) ? style.strong(manifestPath) : style.quiet(`${manifestPath} (none yet)`)}`
2833
+ ` ${style.label("policy")} ${existsSync8(manifestPath) ? style.strong(manifestPath) : style.quiet(`${manifestPath} (none yet)`)}`
2773
2834
  );
2774
2835
  out(
2775
2836
  ` ${style.label("journal")} ${bytesOf(journalPath) === void 0 ? style.quiet(`${journalPath} (none yet)`) : `${style.strong(journalPath)} ${style.quiet(sizeOf(journalPath))}`}`
@@ -2822,7 +2883,7 @@ async function runInit(argv) {
2822
2883
  }
2823
2884
  const path = findManifest(flag(argv, "--manifest"));
2824
2885
  const force = argv.includes("--force");
2825
- const present = existsSync7(path);
2886
+ const present = existsSync8(path);
2826
2887
  if (present && force) {
2827
2888
  throw new UsageError(
2828
2889
  `--force would discard ${path}. Delete it yourself if that is what you want; init will otherwise add to it.`
@@ -2855,17 +2916,17 @@ async function runInit(argv) {
2855
2916
  out("");
2856
2917
  return 0;
2857
2918
  }
2858
- function pick(candidates, given, noun, newest = false) {
2919
+ function pick(candidates2, given, noun, newest = false) {
2859
2920
  const listed = (items) => items.map((item) => ` ${item.id}`).join("\n");
2860
2921
  if (given === void 0) {
2861
- const [only, ...rest2] = candidates;
2922
+ const [only, ...rest2] = candidates2;
2862
2923
  if (only === void 0) {
2863
2924
  throw new UsageError(`there is no ${noun.one} to act on`);
2864
2925
  }
2865
2926
  if (rest2.length > 0 && !newest) {
2866
2927
  throw new UsageError(
2867
- `there are ${String(candidates.length)} ${noun.many}; name one, or use --all:
2868
- ${listed(candidates)}`
2928
+ `there are ${String(candidates2.length)} ${noun.many}; name one, or use --all:
2929
+ ${listed(candidates2)}`
2869
2930
  );
2870
2931
  }
2871
2932
  return only;
@@ -2873,11 +2934,11 @@ ${listed(candidates)}`
2873
2934
  if (given === "") {
2874
2935
  throw new UsageError(`no ${noun.one} was named; an empty id is usually an unset variable`);
2875
2936
  }
2876
- const exact = candidates.find((item) => item.id === given);
2937
+ const exact = candidates2.find((item) => item.id === given);
2877
2938
  if (exact !== void 0) {
2878
2939
  return exact;
2879
2940
  }
2880
- const matches = candidates.filter((item) => item.id.startsWith(given));
2941
+ const matches = candidates2.filter((item) => item.id.startsWith(given));
2881
2942
  const [first, ...rest] = matches;
2882
2943
  if (first === void 0) {
2883
2944
  throw new UsageError(`no ${noun.one} matches ${given}`);
@@ -3533,7 +3594,7 @@ var FLAGS = /* @__PURE__ */ new Set([
3533
3594
  function version() {
3534
3595
  try {
3535
3596
  const root = dirname3(fileURLToPath2(import.meta.url));
3536
- const parsed = JSON.parse(readFileSync4(join2(root, "..", "package.json"), "utf8"));
3597
+ const parsed = JSON.parse(readFileSync4(join3(root, "..", "package.json"), "utf8"));
3537
3598
  const found = typeof parsed === "object" && parsed !== null ? parsed.version : void 0;
3538
3599
  return typeof found === "string" ? found : "unknown";
3539
3600
  } catch {
@@ -3551,7 +3612,7 @@ function rejectUnknownFlags(argv) {
3551
3612
  }
3552
3613
  }
3553
3614
  function openJournalOrExplain(journalPath) {
3554
- if (!existsSync7(journalPath)) {
3615
+ if (!existsSync8(journalPath)) {
3555
3616
  throw new UsageError(
3556
3617
  `nothing has been recorded yet: there is no journal at ${journalPath}. One appears the first time an agent calls a tool through synartesis proxy.`
3557
3618
  );
@@ -3641,7 +3702,7 @@ ${COMMANDS}`);
3641
3702
  });
3642
3703
  }
3643
3704
  journalArg = given === void 0 ? "" : ` --journal ${resolve3(given)}`;
3644
- if (!existsSync7(journalPath) && (command === "list" || command === "show" || command === "gates")) {
3705
+ if (!existsSync8(journalPath) && (command === "list" || command === "show" || command === "gates")) {
3645
3706
  if (asJson) {
3646
3707
  out(JSON.stringify(command === "show" ? { run: null, actions: [] } : []));
3647
3708
  return 0;
@@ -3655,6 +3716,9 @@ ${COMMANDS}`);
3655
3716
  out("");
3656
3717
  return 0;
3657
3718
  }
3719
+ if (command === "desktop") {
3720
+ return runDesktop();
3721
+ }
3658
3722
  const journal = openJournalOrExplain(journalPath);
3659
3723
  try {
3660
3724
  switch (command) {
@@ -3681,6 +3745,21 @@ ${COMMANDS}`);
3681
3745
  journal.close();
3682
3746
  }
3683
3747
  }
3748
+ function runDesktop() {
3749
+ const found = findDesktop();
3750
+ if (found === void 0) {
3751
+ process.stderr.write(`${whereToGetIt()}
3752
+ `);
3753
+ return 2;
3754
+ }
3755
+ const child = spawn(found.open.command, [...found.open.args], {
3756
+ detached: true,
3757
+ stdio: "ignore"
3758
+ });
3759
+ child.unref();
3760
+ out(`opening ${found.path}`);
3761
+ return 0;
3762
+ }
3684
3763
  try {
3685
3764
  process.exitCode = await main(process.argv.slice(2));
3686
3765
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synartesis",
3
- "version": "0.5.2",
3
+ "version": "0.6.2",
4
4
  "description": "An undo layer for AI agents.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -43,11 +43,18 @@
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsup",
46
- "typecheck": "tsc --noEmit",
46
+ "typecheck": "tsc --noEmit && tsc -p app/renderer --noEmit",
47
47
  "lint": "eslint .",
48
48
  "test": "vitest run",
49
49
  "test:watch": "vitest",
50
50
  "pretest": "tsup --silent",
51
+ "app": "pnpm app:build && electron app/dist/main/index.js",
52
+ "app:build": "tsup --config app/tsup.config.ts && vite build --config app/vite.config.ts",
53
+ "app:icon": "electron app/build/make-icon.mjs",
54
+ "app:logo": "python3 app/build/make-logo.py && pnpm app:icon",
55
+ "app:sandbox": "pnpm app:build && node app/dev/sandbox.mjs",
56
+ "app:pack": "pnpm app:build && node app/build/pack.mjs --dir",
57
+ "app:dist": "pnpm app:build && node app/build/pack.mjs",
51
58
  "demo": "tsup --silent && ./demo/filesystem-demo.sh",
52
59
  "demo:memory": "tsup --silent && ./demo/memory-demo.sh",
53
60
  "prepublishOnly": "tsup"
@@ -59,14 +66,34 @@
59
66
  "yaml": "^2.9.0",
60
67
  "zod": "^4.4.3"
61
68
  },
69
+ "//pnpm": "better-sqlite3 ships a prebuilt binary for every platform it supports, and its loader prefers one over anything compiled locally -- so what node-gyp builds here is never loaded. npm knows not to bother, because the package sets gypfile: false; pnpm builds it anyway, which needs a C toolchain on every machine that installs this and fails outright on a Windows runner whose Visual Studio is newer than node-gyp.",
70
+ "pnpm": {
71
+ "neverBuiltDependencies": [
72
+ "better-sqlite3"
73
+ ]
74
+ },
62
75
  "devDependencies": {
76
+ "@anthropic-ai/sdk": "^0.125.0",
77
+ "@electron/notarize": "^3.1.1",
78
+ "@fontsource/cormorant-garamond": "^5.3.0",
79
+ "@fontsource/ibm-plex-mono": "^5.3.0",
80
+ "@fontsource/ibm-plex-sans": "^5.3.0",
81
+ "@google/genai": "^2.22.0",
63
82
  "@modelcontextprotocol/server-filesystem": "^2026.7.10",
64
83
  "@types/better-sqlite3": "^9.6.0",
65
84
  "@types/node": "^26.2.0",
85
+ "@types/react": "^19.3.0",
86
+ "@types/react-dom": "^19.3.0",
87
+ "@vitejs/plugin-react": "^6.1.1",
88
+ "electron": "^44.3.0",
89
+ "electron-builder": "^26.15.3",
66
90
  "eslint": "^10.8.1",
91
+ "react": "^19.3.0",
92
+ "react-dom": "^19.3.0",
67
93
  "tsup": "^8.5.1",
68
94
  "typescript": "^5.9.3",
69
95
  "typescript-eslint": "^8.67.0",
96
+ "vite": "^8.3.0",
70
97
  "vitest": "^4.1.11"
71
98
  },
72
99
  "packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531"