march-control-cli 0.1.3

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 (53) hide show
  1. package/README.md +220 -0
  2. package/core/apply.js +152 -0
  3. package/core/backup.js +53 -0
  4. package/core/constants.js +55 -0
  5. package/core/desktop-service.js +219 -0
  6. package/core/desktop-state.js +511 -0
  7. package/core/index.js +1293 -0
  8. package/core/paths.js +71 -0
  9. package/core/presets.js +171 -0
  10. package/core/probe.js +70 -0
  11. package/core/store.js +218 -0
  12. package/core/utils.js +178 -0
  13. package/core/writers/codex.js +102 -0
  14. package/core/writers/index.js +16 -0
  15. package/core/writers/openclaw.js +93 -0
  16. package/core/writers/opencode.js +91 -0
  17. package/desktop/assets/march-mark.svg +21 -0
  18. package/desktop/main.js +192 -0
  19. package/desktop/preload.js +49 -0
  20. package/desktop/renderer/app.js +327 -0
  21. package/desktop/renderer/index.html +130 -0
  22. package/desktop/renderer/styles.css +413 -0
  23. package/package.json +106 -0
  24. package/scripts/desktop-dev.mjs +90 -0
  25. package/scripts/postinstall.mjs +28 -0
  26. package/scripts/serve-site.mjs +51 -0
  27. package/site/app.js +10 -0
  28. package/site/assets/march-mark.svg +22 -0
  29. package/site/index.html +286 -0
  30. package/site/styles.css +566 -0
  31. package/src/App.tsx +1186 -0
  32. package/src/components/layout/app-sidebar.tsx +103 -0
  33. package/src/components/layout/top-toolbar.tsx +44 -0
  34. package/src/components/layout/workspace-tabs.tsx +32 -0
  35. package/src/components/providers/inspector-panel.tsx +84 -0
  36. package/src/components/providers/metric-strip.tsx +26 -0
  37. package/src/components/providers/provider-editor.tsx +87 -0
  38. package/src/components/providers/provider-table.tsx +85 -0
  39. package/src/components/ui/logo-mark.tsx +16 -0
  40. package/src/features/mcp/mcp-view.tsx +45 -0
  41. package/src/features/prompts/prompts-view.tsx +40 -0
  42. package/src/features/providers/providers-view.tsx +40 -0
  43. package/src/features/providers/types.ts +8 -0
  44. package/src/features/skills/skills-view.tsx +44 -0
  45. package/src/hooks/use-control-workspace.ts +184 -0
  46. package/src/index.css +22 -0
  47. package/src/lib/client.ts +944 -0
  48. package/src/lib/query-client.ts +3 -0
  49. package/src/lib/workspace-sections.ts +34 -0
  50. package/src/main.tsx +14 -0
  51. package/src/types.ts +76 -0
  52. package/src/vite-env.d.ts +56 -0
  53. package/src-tauri/README.md +11 -0
@@ -0,0 +1,102 @@
1
+ import TOML from "@iarna/toml";
2
+ import { DEFAULT_PRIMARY_MODEL, DEFAULT_PROVIDER_NAME } from "../constants.js";
3
+ import { getCodexAuthPath, getCodexConfigPath, getCodexDir } from "../paths.js";
4
+ import {
5
+ deepMerge,
6
+ ensureDir,
7
+ isRecord,
8
+ providerKeyFromName,
9
+ readJson,
10
+ readText,
11
+ replaceCaseInsensitiveKey,
12
+ writeJson,
13
+ writeText
14
+ } from "../utils.js";
15
+
16
+ const DEFAULT_CODEX_CONFIG = {
17
+ model: DEFAULT_PRIMARY_MODEL,
18
+ model_reasoning_effort: "xhigh",
19
+ disable_response_storage: true,
20
+ sandbox_mode: "danger-full-access",
21
+ windows_wsl_setup_acknowledged: true,
22
+ approval_policy: "never",
23
+ profile: "auto-max",
24
+ file_opener: "vscode",
25
+ web_search: "cached",
26
+ suppress_unstable_features_warning: true,
27
+ history: {
28
+ persistence: "save-all"
29
+ },
30
+ tui: {
31
+ notifications: true
32
+ },
33
+ shell_environment_policy: {
34
+ inherit: "all",
35
+ ignore_default_excludes: false
36
+ },
37
+ sandbox_workspace_write: {
38
+ network_access: true
39
+ },
40
+ features: {
41
+ plan_tool: true,
42
+ apply_patch_freeform: true,
43
+ view_image_tool: true
44
+ },
45
+ profiles: {
46
+ "auto-max": {
47
+ approval_policy: "never",
48
+ sandbox_mode: "workspace-write"
49
+ }
50
+ }
51
+ };
52
+
53
+ function loadCodexConfig(configPath) {
54
+ const raw = readText(configPath, "");
55
+ if (!raw.trim()) {
56
+ return {};
57
+ }
58
+
59
+ try {
60
+ return TOML.parse(raw);
61
+ } catch {
62
+ return {};
63
+ }
64
+ }
65
+
66
+ function buildManagedProvider(provider) {
67
+ return {
68
+ name: provider.name || DEFAULT_PROVIDER_NAME,
69
+ base_url: provider.baseUrl,
70
+ wire_api: "responses",
71
+ requires_openai_auth: true
72
+ };
73
+ }
74
+
75
+ export function writeCodexConfig(provider, options = {}) {
76
+ ensureDir(getCodexDir());
77
+
78
+ const configPath = getCodexConfigPath();
79
+ const authPath = getCodexAuthPath();
80
+ const providerKey = providerKeyFromName(provider.name);
81
+ const mode = options.mode === "overwrite" ? "overwrite" : "merge";
82
+ const baseConfig = mode === "overwrite" ? {} : loadCodexConfig(configPath);
83
+ const merged = deepMerge(DEFAULT_CODEX_CONFIG, isRecord(baseConfig) ? baseConfig : {});
84
+ const existingProviders =
85
+ mode === "overwrite" || !isRecord(merged.model_providers) ? {} : merged.model_providers;
86
+
87
+ merged.model = provider.model || DEFAULT_PRIMARY_MODEL;
88
+ merged.model_provider = providerKey;
89
+ merged.model_providers = replaceCaseInsensitiveKey(
90
+ existingProviders,
91
+ providerKey,
92
+ buildManagedProvider(provider)
93
+ );
94
+
95
+ writeText(configPath, TOML.stringify(merged));
96
+
97
+ const existingAuth = mode === "overwrite" ? {} : readJson(authPath, {});
98
+ writeJson(authPath, {
99
+ ...existingAuth,
100
+ OPENAI_API_KEY: provider.apiKey
101
+ });
102
+ }
@@ -0,0 +1,16 @@
1
+ import { writeCodexConfig } from "./codex.js";
2
+ import { writeOpenClawConfig } from "./openclaw.js";
3
+ import { writeOpenCodeConfig } from "./opencode.js";
4
+
5
+ export function writePlatformConfig(platform, provider, options = {}) {
6
+ switch (platform) {
7
+ case "codex":
8
+ return writeCodexConfig(provider, options);
9
+ case "opencode":
10
+ return writeOpenCodeConfig(provider, options);
11
+ case "openclaw":
12
+ return writeOpenClawConfig(provider, options);
13
+ default:
14
+ throw new Error(`Unsupported platform: ${platform}`);
15
+ }
16
+ }
@@ -0,0 +1,93 @@
1
+ import path from "node:path";
2
+ import { DEFAULT_PRIMARY_MODEL, DEFAULT_PROVIDER_NAME, MODEL_DEFINITIONS } from "../constants.js";
3
+ import { getHomeDir, getOpenClawConfigPath, getOpenClawDir, getOpenClawModelsPath } from "../paths.js";
4
+ import {
5
+ ensureDir,
6
+ isRecord,
7
+ providerKeyFromName,
8
+ readJson,
9
+ replaceCaseInsensitiveKey,
10
+ writeJson
11
+ } from "../utils.js";
12
+
13
+ function buildOpenClawModels() {
14
+ return MODEL_DEFINITIONS.map((model) => ({
15
+ id: model.id,
16
+ name: model.id,
17
+ api: "openai-responses",
18
+ reasoning: model.reasoning,
19
+ input: ["text", "image"],
20
+ cost: {
21
+ input: 0,
22
+ output: 0,
23
+ cacheRead: 0,
24
+ cacheWrite: 0
25
+ },
26
+ contextWindow: model.contextWindow,
27
+ maxTokens: model.maxTokens
28
+ }));
29
+ }
30
+
31
+ function buildProviderConfig(provider) {
32
+ return {
33
+ baseUrl: provider.baseUrl,
34
+ apiKey: provider.apiKey,
35
+ api: "openai-responses",
36
+ authHeader: true,
37
+ headers: {
38
+ "User-Agent": "codex-rs/1.0.7"
39
+ },
40
+ models: buildOpenClawModels()
41
+ };
42
+ }
43
+
44
+ export function writeOpenClawConfig(provider, options = {}) {
45
+ ensureDir(getOpenClawDir());
46
+ ensureDir(path.dirname(getOpenClawModelsPath()));
47
+
48
+ const configPath = getOpenClawConfigPath();
49
+ const modelsPath = getOpenClawModelsPath();
50
+ const mode = options.mode === "overwrite" ? "overwrite" : "merge";
51
+ const providerName = providerKeyFromName(provider.name || DEFAULT_PROVIDER_NAME);
52
+ const providerConfig = buildProviderConfig(provider);
53
+ const existingConfig = mode === "overwrite" ? {} : readJson(configPath, {});
54
+ const existingModels = mode === "overwrite" ? {} : readJson(modelsPath, {});
55
+ const existingConfigProviders =
56
+ mode === "overwrite" || !isRecord(existingConfig?.models?.providers)
57
+ ? {}
58
+ : existingConfig.models.providers;
59
+ const existingModelProviders =
60
+ mode === "overwrite" || !isRecord(existingModels?.providers) ? {} : existingModels.providers;
61
+
62
+ const configPayload = {
63
+ ...(isRecord(existingConfig) ? existingConfig : {}),
64
+ models: {
65
+ mode: "merge",
66
+ ...(isRecord(existingConfig?.models) ? existingConfig.models : {}),
67
+ providers: replaceCaseInsensitiveKey(existingConfigProviders, providerName, providerConfig)
68
+ },
69
+ agents: {
70
+ ...(isRecord(existingConfig?.agents) ? existingConfig.agents : {}),
71
+ defaults: {
72
+ ...(isRecord(existingConfig?.agents?.defaults) ? existingConfig.agents.defaults : {}),
73
+ workspace: getHomeDir(),
74
+ imageModel: `${providerName}/${provider.model || DEFAULT_PRIMARY_MODEL}`,
75
+ model: {
76
+ ...(isRecord(existingConfig?.agents?.defaults?.model)
77
+ ? existingConfig.agents.defaults.model
78
+ : {}),
79
+ primary: `${providerName}/${provider.model || DEFAULT_PRIMARY_MODEL}`
80
+ },
81
+ thinkingDefault: "xhigh"
82
+ }
83
+ }
84
+ };
85
+
86
+ const modelsPayload = {
87
+ ...(isRecord(existingModels) ? existingModels : {}),
88
+ providers: replaceCaseInsensitiveKey(existingModelProviders, providerName, providerConfig)
89
+ };
90
+
91
+ writeJson(configPath, configPayload);
92
+ writeJson(modelsPath, modelsPayload);
93
+ }
@@ -0,0 +1,91 @@
1
+ import { DEFAULT_PRIMARY_MODEL, MODEL_DEFINITIONS } from "../constants.js";
2
+ import { getOpenCodeConfigPath, getOpenCodeDir } from "../paths.js";
3
+ import { deepMerge, ensureDir, isRecord, readJson, writeJson } from "../utils.js";
4
+
5
+ function buildModelMap() {
6
+ return Object.fromEntries(
7
+ MODEL_DEFINITIONS.map((model) => [
8
+ model.id,
9
+ {
10
+ name: model.id,
11
+ options: {
12
+ store: false
13
+ },
14
+ variants: {
15
+ low: {},
16
+ medium: {},
17
+ high: {},
18
+ xhigh: {}
19
+ }
20
+ }
21
+ ])
22
+ );
23
+ }
24
+
25
+ export function writeOpenCodeConfig(provider, options = {}) {
26
+ ensureDir(getOpenCodeDir());
27
+
28
+ const configPath = getOpenCodeConfigPath();
29
+ const mode = options.mode === "overwrite" ? "overwrite" : "merge";
30
+ const existing = mode === "overwrite" ? {} : readJson(configPath, {});
31
+ const existingProviders =
32
+ mode === "overwrite" || !isRecord(existing?.provider) ? {} : existing.provider;
33
+
34
+ const nextConfig = deepMerge(
35
+ {
36
+ $schema: "https://opencode.ai/config.json",
37
+ model: `openai/${provider.model || DEFAULT_PRIMARY_MODEL}`,
38
+ provider: {
39
+ openai: {
40
+ options: {
41
+ baseURL: provider.baseUrl,
42
+ apiKey: provider.apiKey
43
+ },
44
+ models: buildModelMap()
45
+ }
46
+ },
47
+ agent: {
48
+ build: {
49
+ options: {
50
+ store: false
51
+ }
52
+ },
53
+ plan: {
54
+ options: {
55
+ store: false
56
+ }
57
+ }
58
+ }
59
+ },
60
+ existing
61
+ );
62
+
63
+ nextConfig.model = `openai/${provider.model || DEFAULT_PRIMARY_MODEL}`;
64
+ nextConfig.provider = {
65
+ ...existingProviders,
66
+ openai: {
67
+ ...(isRecord(existingProviders.openai) ? existingProviders.openai : {}),
68
+ options: {
69
+ ...(isRecord(existingProviders.openai?.options) ? existingProviders.openai.options : {}),
70
+ baseURL: provider.baseUrl,
71
+ apiKey: provider.apiKey
72
+ },
73
+ models: buildModelMap()
74
+ }
75
+ };
76
+ nextConfig.agent = {
77
+ ...(isRecord(nextConfig.agent) ? nextConfig.agent : {}),
78
+ build: {
79
+ options: {
80
+ store: false
81
+ }
82
+ },
83
+ plan: {
84
+ options: {
85
+ store: false
86
+ }
87
+ }
88
+ };
89
+
90
+ writeJson(configPath, nextConfig);
91
+ }
@@ -0,0 +1,21 @@
1
+ <svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <defs>
3
+ <linearGradient id="bg" x1="18" y1="12" x2="107" y2="118" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#102532"/>
5
+ <stop offset="1" stop-color="#0E8F88"/>
6
+ </linearGradient>
7
+ <linearGradient id="stroke" x1="30" y1="32" x2="94" y2="86" gradientUnits="userSpaceOnUse">
8
+ <stop stop-color="#FFF8EE"/>
9
+ <stop offset="1" stop-color="#D7F2F0"/>
10
+ </linearGradient>
11
+ </defs>
12
+ <rect x="8" y="8" width="112" height="112" rx="32" fill="url(#bg)"/>
13
+ <path
14
+ d="M30 88V42C30 38.6863 32.6863 36 36 36H37.2C39.0512 36 40.7999 36.8538 41.9392 38.3138L63.5 65.9307L85.0608 38.3138C86.2001 36.8538 87.9488 36 89.8 36H91C94.3137 36 97 38.6863 97 42V88"
15
+ stroke="url(#stroke)"
16
+ stroke-width="10"
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"
19
+ />
20
+ <path d="M85 90H102" stroke="#F2BD67" stroke-width="10" stroke-linecap="round"/>
21
+ </svg>
@@ -0,0 +1,192 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { app, BrowserWindow, ipcMain, shell } from "electron";
6
+ import {
7
+ activateProviderFromDesktop,
8
+ deleteMcpFromDesktop,
9
+ deletePromptFromDesktop,
10
+ deleteSkillFromDesktop,
11
+ getDesktopSnapshot,
12
+ probeCandidateFromDesktop,
13
+ probePlatformProvidersFromDesktop,
14
+ saveProviderFromDesktop,
15
+ toggleMcpFromDesktop,
16
+ togglePromptFromDesktop,
17
+ toggleSkillRepoFromDesktop,
18
+ upsertMcpFromDesktop,
19
+ upsertPromptFromDesktop,
20
+ upsertSkillFromDesktop
21
+ } from "../core/desktop-service.js";
22
+
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = path.dirname(__filename);
25
+
26
+ async function exists(filePath) {
27
+ try {
28
+ await fs.access(filePath);
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ function resolveTargetPath(input) {
36
+ const raw = `${input ?? ""}`.trim();
37
+ if (!raw) {
38
+ throw new Error("目标路径不能为空");
39
+ }
40
+
41
+ if (raw.startsWith("~/")) {
42
+ return path.join(os.homedir(), raw.slice(2));
43
+ }
44
+
45
+ if (!path.isAbsolute(raw)) {
46
+ throw new Error("仅支持绝对路径或 ~/ 开头路径");
47
+ }
48
+
49
+ return raw;
50
+ }
51
+
52
+ async function openPathFromDesktop(targetPath) {
53
+ const resolvedPath = resolveTargetPath(targetPath);
54
+ const error = await shell.openPath(resolvedPath);
55
+ if (error) {
56
+ throw new Error(error);
57
+ }
58
+
59
+ return {
60
+ ok: true,
61
+ targetPath: resolvedPath
62
+ };
63
+ }
64
+
65
+ async function loadRenderer(window) {
66
+ const devUrl = `${process.env.MARCH_DESKTOP_DEV_URL || ""}`.trim();
67
+ if (devUrl) {
68
+ await window.loadURL(devUrl);
69
+ return;
70
+ }
71
+
72
+ const appRoot = app.getAppPath();
73
+ const distIndex = path.join(appRoot, "dist", "index.html");
74
+
75
+ if (await exists(distIndex)) {
76
+ await window.loadFile(distIndex);
77
+ return;
78
+ }
79
+
80
+ await window.loadFile(path.join(__dirname, "renderer", "index.html"));
81
+ }
82
+
83
+ function createWindow() {
84
+ const win = new BrowserWindow({
85
+ width: 1480,
86
+ height: 960,
87
+ minWidth: 1180,
88
+ minHeight: 760,
89
+ backgroundColor: "#f4efe6",
90
+ autoHideMenuBar: true,
91
+ title: "March 控制台",
92
+ webPreferences: {
93
+ preload: path.join(__dirname, "preload.js"),
94
+ contextIsolation: true,
95
+ nodeIntegration: false
96
+ }
97
+ });
98
+
99
+ loadRenderer(win).catch((error) => {
100
+ console.error("failed to load desktop renderer", error);
101
+ });
102
+
103
+ return win;
104
+ }
105
+
106
+ ipcMain.handle("desktop:getSnapshot", async () => getDesktopSnapshot());
107
+
108
+ ipcMain.handle("desktop:saveProvider", async (_event, payload = {}) => {
109
+ const platform = payload.platform;
110
+ const input = payload.input || {
111
+ name: payload.name,
112
+ baseUrl: payload.baseUrl,
113
+ apiKey: payload.apiKey,
114
+ model: payload.model
115
+ };
116
+ return saveProviderFromDesktop(platform, input, payload.options);
117
+ });
118
+
119
+ ipcMain.handle("desktop:activateProvider", async (_event, payload = {}) => {
120
+ const platform = payload.platform;
121
+ const nameOrId = payload.nameOrId || payload.providerId;
122
+ return activateProviderFromDesktop(platform, nameOrId, payload.options);
123
+ });
124
+
125
+ ipcMain.handle("desktop:probePlatform", async (_event, payload = {}) => {
126
+ return probePlatformProvidersFromDesktop(payload.platform);
127
+ });
128
+
129
+ ipcMain.handle("desktop:probeCandidate", async (_event, payload = {}) => {
130
+ return probeCandidateFromDesktop(payload.platform, payload.baseUrl);
131
+ });
132
+
133
+ ipcMain.handle("desktop:toggleMcp", async (_event, payload = {}) => {
134
+ return toggleMcpFromDesktop(payload.serverId, payload.platform);
135
+ });
136
+
137
+ ipcMain.handle("desktop:upsertMcp", async (_event, payload = {}) => {
138
+ return upsertMcpFromDesktop(payload);
139
+ });
140
+
141
+ ipcMain.handle("desktop:deleteMcp", async (_event, payload = {}) => {
142
+ return deleteMcpFromDesktop(payload.serverId);
143
+ });
144
+
145
+ ipcMain.handle("desktop:togglePrompt", async (_event, payload = {}) => {
146
+ return togglePromptFromDesktop(payload.promptId);
147
+ });
148
+
149
+ ipcMain.handle("desktop:upsertPrompt", async (_event, payload = {}) => {
150
+ return upsertPromptFromDesktop(payload);
151
+ });
152
+
153
+ ipcMain.handle("desktop:deletePrompt", async (_event, payload = {}) => {
154
+ return deletePromptFromDesktop(payload.promptId);
155
+ });
156
+
157
+ ipcMain.handle("desktop:upsertSkill", async (_event, payload = {}) => {
158
+ return upsertSkillFromDesktop(payload);
159
+ });
160
+
161
+ ipcMain.handle("desktop:deleteSkill", async (_event, payload = {}) => {
162
+ return deleteSkillFromDesktop(payload.skillId);
163
+ });
164
+
165
+ ipcMain.handle("desktop:toggleSkillRepo", async (_event, payload = {}) => {
166
+ return toggleSkillRepoFromDesktop(payload.repoId);
167
+ });
168
+
169
+ ipcMain.handle("desktop:openPath", async (_event, payload) => {
170
+ const targetPath = typeof payload === "string" ? payload : payload?.targetPath;
171
+ return openPathFromDesktop(targetPath);
172
+ });
173
+
174
+ app.whenReady().then(() => {
175
+ const mainWindow = createWindow();
176
+
177
+ mainWindow.once("ready-to-show", () => {
178
+ mainWindow.show();
179
+ });
180
+
181
+ app.on("activate", () => {
182
+ if (BrowserWindow.getAllWindows().length === 0) {
183
+ createWindow();
184
+ }
185
+ });
186
+ });
187
+
188
+ app.on("window-all-closed", () => {
189
+ if (process.platform !== "darwin") {
190
+ app.quit();
191
+ }
192
+ });
@@ -0,0 +1,49 @@
1
+ import { contextBridge, ipcRenderer } from "electron";
2
+
3
+ contextBridge.exposeInMainWorld("marchDesktop", {
4
+ getSnapshot() {
5
+ return ipcRenderer.invoke("desktop:getSnapshot");
6
+ },
7
+ saveProvider(payload) {
8
+ return ipcRenderer.invoke("desktop:saveProvider", payload);
9
+ },
10
+ activateProvider(payload) {
11
+ return ipcRenderer.invoke("desktop:activateProvider", payload);
12
+ },
13
+ probePlatform(payload) {
14
+ return ipcRenderer.invoke("desktop:probePlatform", payload);
15
+ },
16
+ probeCandidate(payload) {
17
+ return ipcRenderer.invoke("desktop:probeCandidate", payload);
18
+ },
19
+ toggleMcp(payload) {
20
+ return ipcRenderer.invoke("desktop:toggleMcp", payload);
21
+ },
22
+ upsertMcp(payload) {
23
+ return ipcRenderer.invoke("desktop:upsertMcp", payload);
24
+ },
25
+ deleteMcp(payload) {
26
+ return ipcRenderer.invoke("desktop:deleteMcp", payload);
27
+ },
28
+ togglePrompt(payload) {
29
+ return ipcRenderer.invoke("desktop:togglePrompt", payload);
30
+ },
31
+ upsertPrompt(payload) {
32
+ return ipcRenderer.invoke("desktop:upsertPrompt", payload);
33
+ },
34
+ deletePrompt(payload) {
35
+ return ipcRenderer.invoke("desktop:deletePrompt", payload);
36
+ },
37
+ upsertSkill(payload) {
38
+ return ipcRenderer.invoke("desktop:upsertSkill", payload);
39
+ },
40
+ deleteSkill(payload) {
41
+ return ipcRenderer.invoke("desktop:deleteSkill", payload);
42
+ },
43
+ toggleSkillRepo(payload) {
44
+ return ipcRenderer.invoke("desktop:toggleSkillRepo", payload);
45
+ },
46
+ openPath(targetPath) {
47
+ return ipcRenderer.invoke("desktop:openPath", targetPath);
48
+ }
49
+ });