Skip to content

Commit cd390e9

Browse files
committed
打包 claude code,避免依赖用户本地 CLI
1 parent 80f7645 commit cd390e9

17 files changed

Lines changed: 1495 additions & 151 deletions

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ dist-electron
1414
*.local
1515

1616
# Editor directories and files
17+
.serena
18+
.specstory
1719
.vscode/*
20+
.cursor/*
1821
!.vscode/extensions.json
1922
.idea
2023
.DS_Store
@@ -24,4 +27,5 @@ dist-electron
2427
*.sln
2528
*.sw?
2629

27-
.env
30+
.env
31+
.cursorindexingignore

Design.md

Lines changed: 753 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,3 @@
1-
2-
<div align="center">
3-
4-
# Open Claude Cowork
5-
6-
[![Version](https://img.shields.io/badge/version-0.0.2-blue.svg)](https://github.com/DevAgentForge/Claude-Cowork/releases)
7-
[![Platform](https://img.shields.io/badge/platform-%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/DevAgentForge/Claude-Cowork/releases)
8-
9-
[简体中文](README_ZH.md)
10-
11-
</div>
12-
13-
## ❤️ Collaboration
14-
15-
[![MiniMax](assets/partners/minimax_banner.jpg)](https://platform.minimax.io/subscribe/coding-plan?code=5q2B2ljfdw&source=link)
16-
17-
MiniMax-M2.1 is an open-source SOTA model that excels at coding, navigating digital environments, and handling long, multi-step tasks.
18-
With Open Source Claude Cowork, M2.1 takes a concrete step toward our long-term vision of general-purpose productivity, making advanced AI capabilities accessible to everyone.
19-
20-
[Click ](https://platform.minimax.io/subscribe/coding-plan?code=5q2B2ljfdw&source=link) to get an exclusive 12% off the MiniMax Coding Plan
21-
22-
---
23-
241
# About Open Claude Cowork
252

263
A **desktop AI assistant** that helps you with **programming, file management, and any task you can describe**.
@@ -64,24 +41,13 @@ If Claude Code works on your machine —
6441

6542
Before using Agent Cowork, make sure Claude Code is installed and properly configured.
6643

67-
### Option 1: Download a Release
68-
69-
👉 [Go to Releases](https://github.com/DevAgentForge/agent-cowork/releases)
70-
71-
---
72-
73-
### Option 2: Build from Source
74-
7544
#### Prerequisites
7645

7746
- [Bun](https://bun.sh/) or Node.js 18+
7847
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
7948

80-
bash
81-
# Clone the repository
82-
git clone https://github.com/DevAgentForge/agent-cowork.git
83-
cd agent-cowork
84-
49+
#### How to use
50+
``` shell
8551
# Install dependencies
8652
bun install
8753

@@ -92,7 +58,7 @@ bun run dev
9258
bun run dist:mac # macOS
9359
bun run dist:win # Windows
9460
bun run dist:linux # Linux
95-
`
61+
```
9662

9763
---
9864

bun.lock

Lines changed: 151 additions & 62 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

electron-builder.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"dist-electron/preload.cjs"
1010
],
1111
"asarUnpack": [
12-
"node_modules/@anthropic-ai/claude-agent-sdk/**/*"
12+
"node_modules/@anthropic-ai/claude-agent-sdk/**/*",
13+
"node_modules/@anthropic-ai/claude-code/**/*"
1314
],
1415
"icon": "./claude-color.png",
1516
"mac": {

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"dependencies": {
2020
"@anthropic-ai/claude-agent-sdk": "^0.2.6",
21+
"@anthropic-ai/claude-code": "^2.1.9",
2122
"@radix-ui/react-dialog": "^1.1.15",
2223
"@radix-ui/react-dropdown-menu": "^2.1.16",
2324
"@tailwindcss/vite": "^4.1.18",
@@ -45,6 +46,7 @@
4546
"cross-env": "^10.1.0",
4647
"electron": "^39.2.7",
4748
"electron-builder": "^26.4.0",
49+
"electron-rebuild": "^3.2.9",
4850
"eslint": "^9.39.2",
4951
"eslint-plugin-react-hooks": "^7.0.1",
5052
"eslint-plugin-react-refresh": "^0.4.26",

src/electron/ipc-handlers.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BrowserWindow } from "electron";
22
import type { ClientEvent, ServerEvent } from "./types.js";
33
import { runClaude, type RunnerHandle } from "./libs/runner.js";
44
import { SessionStore } from "./libs/session-store.js";
5+
import { getConfigStore, testAnthropicConnection } from "./libs/claude-settings.js";
56
import { app } from "electron";
67
import { join } from "path";
78

@@ -218,6 +219,55 @@ export function handleClientEvent(event: ClientEvent) {
218219
}
219220
return;
220221
}
222+
223+
if (event.type === "config.get") {
224+
const store = getConfigStore();
225+
if (!store) {
226+
broadcast({ type: "config.data", payload: {} });
227+
return;
228+
}
229+
const config = store.getConfig();
230+
broadcast({
231+
type: "config.data",
232+
payload: {
233+
apiKey: config.apiKey ? "••••••••" : undefined,
234+
baseUrl: config.baseUrl,
235+
model: config.model,
236+
},
237+
});
238+
return;
239+
}
240+
241+
if (event.type === "config.set") {
242+
const store = getConfigStore();
243+
if (!store) return;
244+
store.setConfig(event.payload);
245+
broadcast({ type: "config.configured", payload: { configured: store.isConfigured() } });
246+
return;
247+
}
248+
249+
if (event.type === "config.test") {
250+
testAnthropicConnection(event.payload.apiKey, event.payload.baseUrl)
251+
.then((result) => {
252+
broadcast({ type: "config.testResult", payload: result });
253+
})
254+
.catch((error) => {
255+
broadcast({
256+
type: "config.testResult",
257+
payload: { success: false, error: String(error) },
258+
});
259+
});
260+
return;
261+
}
262+
263+
if (event.type === "config.isConfigured") {
264+
const store = getConfigStore();
265+
broadcast({
266+
type: "config.configured",
267+
payload: { configured: store?.isConfigured() ?? false },
268+
});
269+
return;
270+
}
221271
}
222272

223273
export { sessions };

src/electron/libs/claude-settings.ts

Lines changed: 77 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,83 @@
11
import type { ClaudeSettingsEnv } from "../types.js";
2-
import { readFileSync } from "fs";
3-
import { join } from "path";
4-
import { homedir } from "os";
5-
6-
const CLAUDE_SETTINGS_ENV_KEYS = [
7-
"ANTHROPIC_AUTH_TOKEN",
8-
"ANTHROPIC_BASE_URL",
9-
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
10-
"ANTHROPIC_DEFAULT_OPUS_MODEL",
11-
"ANTHROPIC_DEFAULT_SONNET_MODEL",
12-
"ANTHROPIC_MODEL",
13-
"API_TIMEOUT_MS",
14-
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"
15-
] as const;
16-
17-
export function loadClaudeSettingsEnv(): ClaudeSettingsEnv {
2+
import type { ConfigStore } from "./config-store.js";
3+
4+
let configStoreInstance: ConfigStore | null = null;
5+
6+
export function setConfigStore(store: ConfigStore): void {
7+
configStoreInstance = store;
8+
}
9+
10+
export function getConfigStore(): ConfigStore | null {
11+
return configStoreInstance;
12+
}
13+
14+
export function buildClaudeEnv(): ClaudeSettingsEnv {
15+
const config = configStoreInstance?.getConfig() ?? {};
16+
17+
const env: ClaudeSettingsEnv = {
18+
ANTHROPIC_AUTH_TOKEN: config.apiKey ?? "",
19+
ANTHROPIC_BASE_URL: config.baseUrl ?? "",
20+
ANTHROPIC_MODEL: config.model ?? "",
21+
ANTHROPIC_DEFAULT_HAIKU_MODEL: "",
22+
ANTHROPIC_DEFAULT_OPUS_MODEL: "",
23+
ANTHROPIC_DEFAULT_SONNET_MODEL: "",
24+
API_TIMEOUT_MS: "",
25+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "true",
26+
};
27+
28+
return env;
29+
}
30+
31+
export async function testAnthropicConnection(
32+
apiKey: string,
33+
baseUrl?: string
34+
): Promise<{ success: boolean; error?: string }> {
1835
try {
19-
const settingsPath = join(homedir(), ".claude", "settings.json");
20-
const raw = readFileSync(settingsPath, "utf8");
21-
const parsed = JSON.parse(raw) as { env?: Record<string, unknown> };
22-
if (parsed.env) {
23-
for (const [key, value] of Object.entries(parsed.env)) {
24-
if (process.env[key] === undefined && value !== undefined && value !== null) {
25-
process.env[key] = String(value);
26-
}
27-
}
36+
const url = baseUrl
37+
? `${baseUrl.replace(/\/$/, "")}/v1/messages`
38+
: "https://api.anthropic.com/v1/messages";
39+
40+
const response = await fetch(url, {
41+
method: "POST",
42+
headers: {
43+
"Content-Type": "application/json",
44+
"x-api-key": apiKey,
45+
"anthropic-version": "2023-06-01",
46+
},
47+
body: JSON.stringify({
48+
model: "claude-3-haiku-20240307",
49+
max_tokens: 1,
50+
messages: [{ role: "user", content: "Hi" }],
51+
}),
52+
});
53+
54+
if (response.ok) {
55+
return { success: true };
56+
}
57+
58+
const errorData = await response.json().catch(() => ({}));
59+
const errorMessage =
60+
(errorData as { error?: { message?: string } })?.error?.message ??
61+
`HTTP ${response.status}: ${response.statusText}`;
62+
63+
// 401 means invalid API key
64+
if (response.status === 401) {
65+
return { success: false, error: "Invalid API key" };
66+
}
67+
68+
// 400 with "credit balance is too low" still means key is valid
69+
if (
70+
response.status === 400 &&
71+
errorMessage.toLowerCase().includes("credit")
72+
) {
73+
return { success: true };
2874
}
29-
} catch {
30-
// Ignore missing or invalid settings file.
31-
}
3275

33-
const env = {} as ClaudeSettingsEnv;
34-
for (const key of CLAUDE_SETTINGS_ENV_KEYS) {
35-
env[key] = process.env[key] ?? "";
76+
return { success: false, error: errorMessage };
77+
} catch (error) {
78+
return {
79+
success: false,
80+
error: error instanceof Error ? error.message : "Connection failed",
81+
};
3682
}
37-
return env;
3883
}
39-
40-
export const claudeCodeEnv = loadClaudeSettingsEnv();

0 commit comments

Comments
 (0)