synartesis 0.5.2 → 0.6.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/CHANGELOG.md +40 -0
- package/README.md +52 -0
- package/dist/cli.js +97 -18
- package/package.json +23 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
What changed, and why it mattered. Dates are release dates.
|
|
4
4
|
|
|
5
|
+
## 0.6.0 — 2026-09-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **A desktop window.** You talk to a model; every tool it calls goes through
|
|
10
|
+
the same proxy the CLI installs, so what it did is journalled and reversible
|
|
11
|
+
without the window implementing any of that itself. Each call gets a card
|
|
12
|
+
naming the server, the tool, the class it was given and whether the state it
|
|
13
|
+
replaced was captured; a call that cannot be undone stops and waits for a
|
|
14
|
+
person rather than happening behind one; and "put it back" is the CLI's own
|
|
15
|
+
two steps — the real plan, then the confirmation. Claude, Gemini, Mistral,
|
|
16
|
+
OpenAI, and anything speaking `/v1/chat/completions`, which includes Ollama,
|
|
17
|
+
LM Studio and vLLM on the same machine. Keys are pasted by you and kept in the
|
|
18
|
+
OS keychain; they are never written to the journal or a log.
|
|
19
|
+
|
|
20
|
+
It is a separate download rather than part of this package: a browser engine
|
|
21
|
+
inside a CLI would put 200 MB into every install of a command that is a few
|
|
22
|
+
hundred kilobytes. Both share one journal, so either can undo what the other
|
|
23
|
+
did.
|
|
24
|
+
|
|
25
|
+
- **`synartesis desktop`** opens that window if it is installed, and says where
|
|
26
|
+
to get it if it is not.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **Opening the window no longer leaves an empty session behind.** A run is
|
|
31
|
+
begun whether or not anybody says anything, and closing one is a chain of
|
|
32
|
+
waits — each server is asked to stop, and a child process takes its time
|
|
33
|
+
going. Quitting the application ended the process partway through, so the
|
|
34
|
+
empty row survived: thirteen of them in one afternoon of testing, thirteen
|
|
35
|
+
lines of `synartesis list` between somebody and the run they were looking for.
|
|
36
|
+
The tidy-up now happens before the servers are waited on — nothing can be
|
|
37
|
+
recorded once the model's client is shut, so the count is already final — and
|
|
38
|
+
quit is held until the engine has put itself away.
|
|
39
|
+
|
|
40
|
+
- **A held call explains itself in words.** An MCP server returns its errors as
|
|
41
|
+
a content envelope, and the most common reason of all — the file does not
|
|
42
|
+
exist yet, so there is nothing to keep — was reaching the screen as a wall of
|
|
43
|
+
`{"content":[{"type":"text"...` with the sentence that mattered inside it.
|
|
44
|
+
|
|
5
45
|
## 0.5.2 — 2026-09-10
|
|
6
46
|
|
|
7
47
|
### Changed
|
package/README.md
CHANGED
|
@@ -76,6 +76,55 @@ 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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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. Until there is a signed build to download, it is built from a
|
|
114
|
+
clone:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pnpm install && pnpm app:dist
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
That writes an installer for the machine it runs on to `app/release` — a `.dmg`
|
|
121
|
+
and a `.app` on macOS, an `.exe` on Windows, an AppImage and a `.deb` on Linux.
|
|
122
|
+
It is unsigned, so it runs where it was built and Gatekeeper refuses it
|
|
123
|
+
anywhere it has been downloaded to: signing and notarisation need an Apple
|
|
124
|
+
developer account, and [`app/README.md`](app/README.md) lists the two
|
|
125
|
+
environment variables that turn them on. Both the window and the terminal share
|
|
126
|
+
one journal, so either can undo what the other did.
|
|
127
|
+
|
|
79
128
|
## What it can and cannot do
|
|
80
129
|
|
|
81
130
|
Every tool gets one of four classifications, written down in a manifest:
|
|
@@ -114,6 +163,9 @@ every line it would write over and stops; `--force --yes` goes ahead.
|
|
|
114
163
|
|
|
115
164
|
## Commands
|
|
116
165
|
|
|
166
|
+
`synartesis desktop` opens [the window](#the-desktop-window), and says where to
|
|
167
|
+
get it if it is not installed.
|
|
168
|
+
|
|
117
169
|
| Command | Does |
|
|
118
170
|
|---|---|
|
|
119
171
|
| `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 {
|
|
43
|
-
import {
|
|
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
|
|
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")} ${
|
|
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 =
|
|
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(
|
|
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] =
|
|
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(
|
|
2868
|
-
${listed(
|
|
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 =
|
|
2937
|
+
const exact = candidates2.find((item) => item.id === given);
|
|
2877
2938
|
if (exact !== void 0) {
|
|
2878
2939
|
return exact;
|
|
2879
2940
|
}
|
|
2880
|
-
const matches =
|
|
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(
|
|
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 (!
|
|
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 (!
|
|
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.
|
|
3
|
+
"version": "0.6.0",
|
|
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"
|
|
@@ -60,13 +67,27 @@
|
|
|
60
67
|
"zod": "^4.4.3"
|
|
61
68
|
},
|
|
62
69
|
"devDependencies": {
|
|
70
|
+
"@anthropic-ai/sdk": "^0.125.0",
|
|
71
|
+
"@electron/notarize": "^3.1.1",
|
|
72
|
+
"@fontsource/cormorant-garamond": "^5.3.0",
|
|
73
|
+
"@fontsource/ibm-plex-mono": "^5.3.0",
|
|
74
|
+
"@fontsource/ibm-plex-sans": "^5.3.0",
|
|
75
|
+
"@google/genai": "^2.22.0",
|
|
63
76
|
"@modelcontextprotocol/server-filesystem": "^2026.7.10",
|
|
64
77
|
"@types/better-sqlite3": "^9.6.0",
|
|
65
78
|
"@types/node": "^26.2.0",
|
|
79
|
+
"@types/react": "^19.3.0",
|
|
80
|
+
"@types/react-dom": "^19.3.0",
|
|
81
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
82
|
+
"electron": "^44.3.0",
|
|
83
|
+
"electron-builder": "^26.15.3",
|
|
66
84
|
"eslint": "^10.8.1",
|
|
85
|
+
"react": "^19.3.0",
|
|
86
|
+
"react-dom": "^19.3.0",
|
|
67
87
|
"tsup": "^8.5.1",
|
|
68
88
|
"typescript": "^5.9.3",
|
|
69
89
|
"typescript-eslint": "^8.67.0",
|
|
90
|
+
"vite": "^8.3.0",
|
|
70
91
|
"vitest": "^4.1.11"
|
|
71
92
|
},
|
|
72
93
|
"packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531"
|