@jiangyuan1209/yuan-claw 0.1.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.
Files changed (45) hide show
  1. package/.env.example +28 -0
  2. package/README.md +441 -0
  3. package/dist/agent/build-system-prompt.js +46 -0
  4. package/dist/agent/message-types.js +1 -0
  5. package/dist/agent/message-utils.js +239 -0
  6. package/dist/agent/parse-agent-response.js +48 -0
  7. package/dist/agent/prompts.js +27 -0
  8. package/dist/agent/protocol.js +1 -0
  9. package/dist/agent/read-approval.js +45 -0
  10. package/dist/agent/read-confirmation.js +17 -0
  11. package/dist/agent/run-local-agent-loop.js +272 -0
  12. package/dist/cli/help.js +24 -0
  13. package/dist/cli/main.js +108 -0
  14. package/dist/cli/parse-args.js +75 -0
  15. package/dist/cli/repl.js +188 -0
  16. package/dist/config/config-path.js +9 -0
  17. package/dist/config/init-user-config.js +39 -0
  18. package/dist/config/load-config.js +46 -0
  19. package/dist/config.js +29 -0
  20. package/dist/events/event-bus.js +62 -0
  21. package/dist/lib/initGlobalProxy.js +52 -0
  22. package/dist/memory/session-store.js +43 -0
  23. package/dist/memory/types.js +1 -0
  24. package/dist/model/client.js +7 -0
  25. package/dist/model/providers/openai-compatible.js +42 -0
  26. package/dist/scripts/test-brave.js +16 -0
  27. package/dist/security/network-policy.js +19 -0
  28. package/dist/security/path-guards.js +18 -0
  29. package/dist/security/shell-policy.js +40 -0
  30. package/dist/shared/utils.js +1 -0
  31. package/dist/tools/file/grep-text.js +85 -0
  32. package/dist/tools/file/list-files.js +52 -0
  33. package/dist/tools/file/read-file.js +39 -0
  34. package/dist/tools/file/write-file.js +37 -0
  35. package/dist/tools/git/git-diff.js +54 -0
  36. package/dist/tools/git/git-status.js +44 -0
  37. package/dist/tools/registry.js +41 -0
  38. package/dist/tools/shell/shell-exec.js +41 -0
  39. package/dist/tools/types.js +1 -0
  40. package/dist/tools/web/extract-readable-text.js +54 -0
  41. package/dist/tools/web/http-fetch.js +55 -0
  42. package/dist/tools/web/index.js +4 -0
  43. package/dist/tools/web/search-providers/brave.js +31 -0
  44. package/dist/tools/web/web-search.js +33 -0
  45. package/package.json +26 -0
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { select, cancel, isCancel } from "@clack/prompts";
4
+ import { initGlobalProxy } from "../lib/initGlobalProxy.js";
5
+ import { parseCliArgs } from "./parse-args.js";
6
+ import { getHelpText } from "./help.js";
7
+ import { resolveWorkspaceRoot } from "../security/path-guards.js";
8
+ import { createToolRegistry } from "../tools/registry.js";
9
+ import { SessionStore } from "../memory/session-store.js";
10
+ import { createConsoleEventBus } from "../events/event-bus.js";
11
+ import { runLocalAgentLoop } from "../agent/run-local-agent-loop.js";
12
+ import { createModelClient } from "../model/client.js";
13
+ import { ensureUserConfigInitialized } from "../config/init-user-config.js";
14
+ import { loadAppConfig } from "../config/load-config.js";
15
+ import { startRepl } from "./repl.js";
16
+ async function requestApprovalFromConsole(message) {
17
+ const result = await select({
18
+ message,
19
+ options: [
20
+ {
21
+ value: "deny",
22
+ label: "不允许",
23
+ hint: "拒绝这次操作",
24
+ },
25
+ {
26
+ value: "allow-once",
27
+ label: "允许",
28
+ hint: "仅允许这一次",
29
+ },
30
+ {
31
+ value: "allow-always",
32
+ label: "总是允许",
33
+ hint: "仅当前本次运行/会话有效",
34
+ },
35
+ ],
36
+ });
37
+ if (isCancel(result)) {
38
+ cancel("已取消,本次按不允许处理。");
39
+ console.log("");
40
+ return "deny";
41
+ }
42
+ console.log("");
43
+ return result;
44
+ }
45
+ async function main() {
46
+ const args = parseCliArgs(process.argv.slice(2));
47
+ if (args.help) {
48
+ console.log(getHelpText());
49
+ return;
50
+ }
51
+ const initResult = await ensureUserConfigInitialized();
52
+ const config = await loadAppConfig();
53
+ if (initResult.createdSettings) {
54
+ console.log(`Initialized config file at: ${initResult.settingsPath}`);
55
+ console.log("Please edit the file and add your model settings if needed.\n");
56
+ }
57
+ initGlobalProxy(config);
58
+ if (!args.userInput) {
59
+ await startRepl({
60
+ workspace: args.workspace,
61
+ model: args.model,
62
+ json: args.json,
63
+ quiet: args.quiet,
64
+ maxSteps: args.maxSteps,
65
+ config,
66
+ });
67
+ return;
68
+ }
69
+ const workspaceRoot = resolveWorkspaceRoot(args.workspace ?? process.cwd());
70
+ const tools = createToolRegistry({
71
+ workspaceRoot,
72
+ config,
73
+ });
74
+ const sessionStore = new SessionStore();
75
+ const eventBus = createConsoleEventBus({
76
+ json: args.json,
77
+ quiet: args.quiet,
78
+ });
79
+ const previousSession = args.sessionId
80
+ ? await sessionStore.load(args.sessionId)
81
+ : null;
82
+ const modelClient = createModelClient({
83
+ model: args.model,
84
+ config,
85
+ });
86
+ const result = await runLocalAgentLoop({
87
+ userInput: args.userInput,
88
+ modelClient,
89
+ tools,
90
+ eventBus,
91
+ maxSteps: args.maxSteps ?? 30,
92
+ previousMessages: previousSession?.messages ?? [],
93
+ approvalMode: "ask",
94
+ requestApproval: requestApprovalFromConsole,
95
+ onMessagesUpdated: args.sessionId
96
+ ? async (messages) => {
97
+ await sessionStore.save(args.sessionId, messages);
98
+ }
99
+ : undefined,
100
+ });
101
+ if (!args.quiet) {
102
+ console.log(result.finalMessage);
103
+ }
104
+ }
105
+ main().catch((error) => {
106
+ console.error(error);
107
+ process.exit(1);
108
+ });
@@ -0,0 +1,75 @@
1
+ export function parseCliArgs(argv) {
2
+ let sessionId;
3
+ let workspace;
4
+ let maxSteps;
5
+ let model;
6
+ let json = false;
7
+ let quiet = false;
8
+ let help = false;
9
+ const positional = [];
10
+ for (let i = 0; i < argv.length; i++) {
11
+ const arg = argv[i];
12
+ if (arg === "--session" || arg === "--session-id") {
13
+ sessionId = argv[++i];
14
+ continue;
15
+ }
16
+ if (arg.startsWith("--session=")) {
17
+ sessionId = arg.slice("--session=".length);
18
+ continue;
19
+ }
20
+ if (arg.startsWith("--session-id=")) {
21
+ sessionId = arg.slice("--session-id=".length);
22
+ continue;
23
+ }
24
+ if (arg === "--workspace") {
25
+ workspace = argv[++i];
26
+ continue;
27
+ }
28
+ if (arg.startsWith("--workspace=")) {
29
+ workspace = arg.slice("--workspace=".length);
30
+ continue;
31
+ }
32
+ if (arg === "--max-steps") {
33
+ const value = argv[++i];
34
+ maxSteps = value ? Number(value) : undefined;
35
+ continue;
36
+ }
37
+ if (arg.startsWith("--max-steps=")) {
38
+ maxSteps = Number(arg.slice("--max-steps=".length));
39
+ continue;
40
+ }
41
+ if (arg === "--model") {
42
+ model = argv[++i];
43
+ continue;
44
+ }
45
+ if (arg.startsWith("--model=")) {
46
+ model = arg.slice("--model=".length);
47
+ continue;
48
+ }
49
+ if (arg === "--json") {
50
+ json = true;
51
+ continue;
52
+ }
53
+ if (arg === "--quiet") {
54
+ quiet = true;
55
+ continue;
56
+ }
57
+ if (arg === "--help" || arg === "-h") {
58
+ help = true;
59
+ continue;
60
+ }
61
+ positional.push(arg);
62
+ }
63
+ return {
64
+ userInput: positional.join(" ").trim(),
65
+ sessionId,
66
+ workspace,
67
+ maxSteps: typeof maxSteps === "number" && Number.isFinite(maxSteps)
68
+ ? maxSteps
69
+ : undefined,
70
+ model,
71
+ json,
72
+ quiet,
73
+ help,
74
+ };
75
+ }
@@ -0,0 +1,188 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import crypto from "node:crypto";
4
+ import { resolveWorkspaceRoot } from "../security/path-guards.js";
5
+ import { createToolRegistry } from "../tools/registry.js";
6
+ import { SessionStore } from "../memory/session-store.js";
7
+ import { createConsoleEventBus } from "../events/event-bus.js";
8
+ import { runLocalAgentLoop, } from "../agent/run-local-agent-loop.js";
9
+ import { createModelClient } from "../model/client.js";
10
+ function approvalLabel(decision) {
11
+ switch (decision) {
12
+ case "deny":
13
+ return "不允许";
14
+ case "allow-once":
15
+ return "允许";
16
+ case "allow-always":
17
+ return "总是允许";
18
+ }
19
+ }
20
+ function printApprovalMenu(message, selectedIndex) {
21
+ const options = [
22
+ { label: "不允许", hint: "拒绝这次操作" },
23
+ { label: "允许", hint: "仅允许这一次" },
24
+ { label: "总是允许", hint: "当前会话后续 confirm / dangerous 操作自动允许" },
25
+ ];
26
+ output.write(`\n${message}\n\n`);
27
+ output.write("使用 ↑ / ↓ 切换,Enter 确认,Ctrl+C 拒绝\n\n");
28
+ for (let i = 0; i < options.length; i += 1) {
29
+ const prefix = i === selectedIndex ? "❯" : " ";
30
+ output.write(`${prefix} ${options[i].label} ${options[i].hint}\n`);
31
+ }
32
+ output.write("\n");
33
+ }
34
+ async function selectApprovalWithArrows(message) {
35
+ const values = ["deny", "allow-once", "allow-always"];
36
+ let selectedIndex = 1;
37
+ return await new Promise((resolve) => {
38
+ const cleanup = () => {
39
+ input.off("data", onData);
40
+ if (input.isTTY) {
41
+ input.setRawMode(false);
42
+ }
43
+ };
44
+ const onData = (buffer) => {
45
+ const key = buffer.toString("utf8");
46
+ if (key === "\u0003") {
47
+ cleanup();
48
+ output.write("\n");
49
+ resolve("deny");
50
+ return;
51
+ }
52
+ if (key === "\r" || key === "\n") {
53
+ const result = values[selectedIndex];
54
+ cleanup();
55
+ output.write(`已选择:${approvalLabel(result)}\n\n`);
56
+ resolve(result);
57
+ return;
58
+ }
59
+ if (key === "\u001b[A") {
60
+ selectedIndex =
61
+ selectedIndex === 0 ? values.length - 1 : selectedIndex - 1;
62
+ printApprovalMenu(message, selectedIndex);
63
+ return;
64
+ }
65
+ if (key === "\u001b[B") {
66
+ selectedIndex =
67
+ selectedIndex === values.length - 1 ? 0 : selectedIndex + 1;
68
+ printApprovalMenu(message, selectedIndex);
69
+ }
70
+ };
71
+ if (output.isTTY) {
72
+ output.write("\x1b[2K\r");
73
+ }
74
+ if (input.isTTY) {
75
+ input.setRawMode(true);
76
+ }
77
+ input.resume();
78
+ input.on("data", onData);
79
+ printApprovalMenu(message, selectedIndex);
80
+ });
81
+ }
82
+ export async function startRepl(options) {
83
+ const rl = readline.createInterface({ input, output });
84
+ const sessionStore = new SessionStore();
85
+ const sessionId = crypto.randomUUID();
86
+ const workspaceRoot = resolveWorkspaceRoot(options.workspace ?? process.cwd());
87
+ const tools = createToolRegistry({
88
+ workspaceRoot,
89
+ config: options.config,
90
+ });
91
+ const modelClient = createModelClient({
92
+ model: options.model,
93
+ config: options.config,
94
+ });
95
+ let messages = [];
96
+ let approvalMode = "ask";
97
+ async function requestApproval(message) {
98
+ const result = await selectApprovalWithArrows(message);
99
+ console.log("");
100
+ return result;
101
+ }
102
+ console.log("Welcome to Yuan Claw!");
103
+ console.log("Type /help for commands, /exit to quit.");
104
+ console.log("Approval mode is shown in the prompt: [ask] or [always].\n");
105
+ while (true) {
106
+ let line;
107
+ try {
108
+ const promptLabel = approvalMode === "always-allow"
109
+ ? "yuan-claw[always]> "
110
+ : "yuan-claw[ask]> ";
111
+ line = await rl.question(promptLabel);
112
+ }
113
+ catch {
114
+ console.log("\nBye!");
115
+ break;
116
+ }
117
+ const userInput = line.trim();
118
+ if (!userInput) {
119
+ continue;
120
+ }
121
+ if (userInput === "/exit" || userInput === "/quit") {
122
+ console.log("Bye!");
123
+ break;
124
+ }
125
+ if (userInput === "/help") {
126
+ console.log(`
127
+ Commands:
128
+ /help Show help
129
+ /exit Exit
130
+ /quit Exit
131
+ /clear Clear current session history and reset approval mode
132
+ /save Save current session
133
+ /reset Reset approval mode to ask
134
+ /status Show current session status
135
+ `);
136
+ continue;
137
+ }
138
+ if (userInput === "/clear") {
139
+ messages = [];
140
+ approvalMode = "ask";
141
+ console.log("Session history cleared. Approval mode reset to ask.");
142
+ continue;
143
+ }
144
+ if (userInput === "/save") {
145
+ await sessionStore.save(sessionId, messages);
146
+ console.log(`Session saved: ${sessionId}`);
147
+ continue;
148
+ }
149
+ if (userInput === "/reset") {
150
+ approvalMode = "ask";
151
+ console.log("Approval mode reset to ask.");
152
+ continue;
153
+ }
154
+ if (userInput === "/status") {
155
+ console.log(`sessionId: ${sessionId}`);
156
+ console.log(`approvalMode: ${approvalMode}`);
157
+ console.log(`messageCount: ${messages.length}`);
158
+ continue;
159
+ }
160
+ const eventBus = createConsoleEventBus({
161
+ json: options.json,
162
+ quiet: options.quiet,
163
+ });
164
+ try {
165
+ const result = await runLocalAgentLoop({
166
+ userInput,
167
+ modelClient,
168
+ tools,
169
+ eventBus,
170
+ maxSteps: options.maxSteps ?? 30,
171
+ previousMessages: messages,
172
+ approvalMode,
173
+ requestApproval,
174
+ onMessagesUpdated: async (updatedMessages) => {
175
+ messages = updatedMessages;
176
+ },
177
+ });
178
+ approvalMode = result.approvalMode;
179
+ if (!options.quiet) {
180
+ console.log(result.finalMessage);
181
+ }
182
+ }
183
+ catch (error) {
184
+ console.error(error);
185
+ }
186
+ }
187
+ rl.close();
188
+ }
@@ -0,0 +1,9 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ export const MY_AGENT_DIR_NAME = ".yuan-claw";
4
+ export function getUserConfigDir() {
5
+ return path.join(os.homedir(), MY_AGENT_DIR_NAME);
6
+ }
7
+ export function getUserSettingsPath() {
8
+ return path.join(getUserConfigDir(), "settings.json");
9
+ }
@@ -0,0 +1,39 @@
1
+ import fs from "node:fs/promises";
2
+ import { getUserConfigDir, getUserSettingsPath } from "./config-path.js";
3
+ const DEFAULT_SETTINGS = {
4
+ MODEL_API_KEY: "",
5
+ MODEL_BASE_URL: "",
6
+ MODEL_NAME: "gpt-4o-mini",
7
+ OPENAI_API_KEY: "",
8
+ OPENAI_BASE_URL: "",
9
+ OPENAI_MODEL: "",
10
+ BRAVE_SEARCH_API_KEY: "",
11
+ BRAVE_API_KEY: "",
12
+ HTTP_PROXY: "",
13
+ HTTPS_PROXY: "",
14
+ };
15
+ export async function ensureUserConfigInitialized() {
16
+ const configDir = getUserConfigDir();
17
+ const settingsPath = getUserSettingsPath();
18
+ let createdDir = false;
19
+ let createdSettings = false;
20
+ try {
21
+ await fs.mkdir(configDir, { recursive: true });
22
+ createdDir = true;
23
+ }
24
+ catch {
25
+ // ignore
26
+ }
27
+ try {
28
+ await fs.access(settingsPath);
29
+ }
30
+ catch {
31
+ await fs.writeFile(settingsPath, JSON.stringify(DEFAULT_SETTINGS, null, 2) + "\n", "utf-8");
32
+ createdSettings = true;
33
+ }
34
+ return {
35
+ createdDir,
36
+ createdSettings,
37
+ settingsPath,
38
+ };
39
+ }
@@ -0,0 +1,46 @@
1
+ import fs from "node:fs/promises";
2
+ import { z } from "zod";
3
+ import { getUserSettingsPath } from "./config-path.js";
4
+ const userSettingsSchema = z.object({
5
+ MODEL_API_KEY: z.string().optional(),
6
+ MODEL_BASE_URL: z.string().optional(),
7
+ MODEL_NAME: z.string().optional(),
8
+ OPENAI_API_KEY: z.string().optional(),
9
+ OPENAI_BASE_URL: z.string().optional(),
10
+ OPENAI_MODEL: z.string().optional(),
11
+ BRAVE_SEARCH_API_KEY: z.string().optional(),
12
+ BRAVE_API_KEY: z.string().optional(),
13
+ HTTP_PROXY: z.string().optional(),
14
+ HTTPS_PROXY: z.string().optional(),
15
+ http_proxy: z.string().optional(),
16
+ https_proxy: z.string().optional(),
17
+ });
18
+ async function loadSettingsFile() {
19
+ const settingsPath = getUserSettingsPath();
20
+ try {
21
+ const raw = await fs.readFile(settingsPath, "utf-8");
22
+ const json = JSON.parse(raw);
23
+ return userSettingsSchema.parse(json);
24
+ }
25
+ catch {
26
+ return {};
27
+ }
28
+ }
29
+ export async function loadAppConfig() {
30
+ const fileConfig = await loadSettingsFile();
31
+ return {
32
+ ...fileConfig,
33
+ MODEL_API_KEY: process.env.MODEL_API_KEY ?? fileConfig.MODEL_API_KEY,
34
+ MODEL_BASE_URL: process.env.MODEL_BASE_URL ?? fileConfig.MODEL_BASE_URL,
35
+ MODEL_NAME: process.env.MODEL_NAME ?? fileConfig.MODEL_NAME,
36
+ OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? fileConfig.OPENAI_API_KEY,
37
+ OPENAI_BASE_URL: process.env.OPENAI_BASE_URL ?? fileConfig.OPENAI_BASE_URL,
38
+ OPENAI_MODEL: process.env.OPENAI_MODEL ?? fileConfig.OPENAI_MODEL,
39
+ BRAVE_SEARCH_API_KEY: process.env.BRAVE_SEARCH_API_KEY ?? fileConfig.BRAVE_SEARCH_API_KEY,
40
+ BRAVE_API_KEY: process.env.BRAVE_API_KEY ?? fileConfig.BRAVE_API_KEY,
41
+ HTTP_PROXY: process.env.HTTP_PROXY ?? fileConfig.HTTP_PROXY,
42
+ HTTPS_PROXY: process.env.HTTPS_PROXY ?? fileConfig.HTTPS_PROXY,
43
+ http_proxy: process.env.http_proxy ?? fileConfig.http_proxy,
44
+ https_proxy: process.env.https_proxy ?? fileConfig.https_proxy,
45
+ };
46
+ }
package/dist/config.js ADDED
@@ -0,0 +1,29 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export function loadConfig() {
4
+ const configPath = path.resolve(process.cwd(), "my-agent.config.json");
5
+ if (!fs.existsSync(configPath)) {
6
+ throw new Error(`Config file not found: ${configPath}`);
7
+ }
8
+ const raw = fs.readFileSync(configPath, "utf8");
9
+ const config = JSON.parse(raw);
10
+ if (!config.model?.baseURL) {
11
+ throw new Error("config.model.baseURL is required");
12
+ }
13
+ if (!config.model?.apiKey) {
14
+ throw new Error("config.model.apiKey is required");
15
+ }
16
+ if (!config.model?.model) {
17
+ throw new Error("config.model.model is required");
18
+ }
19
+ if (!config.agent?.sessionId) {
20
+ throw new Error("config.agent.sessionId is required");
21
+ }
22
+ if (!config.agent?.maxSteps) {
23
+ throw new Error("config.agent.maxSteps is required");
24
+ }
25
+ if (!config.agent?.workspace) {
26
+ throw new Error("config.agent.workspace is required");
27
+ }
28
+ return config;
29
+ }
@@ -0,0 +1,62 @@
1
+ function printToolResultPreview(result) {
2
+ try {
3
+ const preview = JSON.stringify(result, null, 2);
4
+ console.log(preview.slice(0, 800) + (preview.length > 800 ? "\n...[truncated]" : ""));
5
+ }
6
+ catch {
7
+ console.log(String(result));
8
+ }
9
+ }
10
+ export function createConsoleEventBus(options = {}) {
11
+ const json = options.json ?? false;
12
+ const quiet = options.quiet ?? false;
13
+ return {
14
+ emit(event) {
15
+ if (json) {
16
+ console.log(JSON.stringify(event));
17
+ return;
18
+ }
19
+ if (quiet) {
20
+ if (event.type === "assistant") {
21
+ console.log(event.message);
22
+ }
23
+ if (event.type === "run_error") {
24
+ console.error(`[run_error] ${event.stage}: ${event.error}`);
25
+ }
26
+ return;
27
+ }
28
+ switch (event.type) {
29
+ case "run_start":
30
+ console.log(`\n[run_start] ${event.input}`);
31
+ break;
32
+ case "model_raw":
33
+ console.log(`\n[model_raw] step=${event.step}`);
34
+ console.log(event.text);
35
+ break;
36
+ case "tool_start":
37
+ console.log(`\n[tool_start] ${event.toolName}`);
38
+ console.log(JSON.stringify(event.args, null, 2));
39
+ break;
40
+ case "tool_end":
41
+ console.log(`\n[tool_end] ${event.toolName} success=${event.success}`);
42
+ printToolResultPreview(event.result);
43
+ break;
44
+ case "tool_error":
45
+ console.log(`\n[tool_error] ${event.toolName}`);
46
+ console.log(event.error);
47
+ break;
48
+ case "assistant":
49
+ console.log(`\n[assistant]`);
50
+ console.log(event.message);
51
+ break;
52
+ case "run_error":
53
+ console.log(`\n[run_error] stage=${event.stage} step=${event.step}`);
54
+ console.log(event.error);
55
+ break;
56
+ case "run_end":
57
+ console.log(`\n[run_end] reason=${event.reason} step=${event.step}`);
58
+ break;
59
+ }
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,52 @@
1
+ import { ProxyAgent, setGlobalDispatcher } from "undici";
2
+ let proxyInitialized = false;
3
+ function resolveProxyUrl(config) {
4
+ return (config?.HTTPS_PROXY ||
5
+ config?.HTTP_PROXY ||
6
+ config?.https_proxy ||
7
+ config?.http_proxy ||
8
+ process.env.HTTPS_PROXY ||
9
+ process.env.HTTP_PROXY ||
10
+ process.env.https_proxy ||
11
+ process.env.http_proxy);
12
+ }
13
+ function maskProxyForLog(proxyUrl) {
14
+ try {
15
+ const url = new URL(proxyUrl);
16
+ if (url.username || url.password) {
17
+ url.username = url.username ? "***" : "";
18
+ url.password = url.password ? "***" : "";
19
+ }
20
+ return url.toString();
21
+ }
22
+ catch {
23
+ return proxyUrl;
24
+ }
25
+ }
26
+ /**
27
+ * 初始化 Node/undici 全局代理。
28
+ *
29
+ * 优先级:
30
+ * 1. 传入的 config
31
+ * 2. process.env
32
+ */
33
+ export function initGlobalProxy(config) {
34
+ if (proxyInitialized) {
35
+ return;
36
+ }
37
+ const proxyUrl = resolveProxyUrl(config);
38
+ if (!proxyUrl) {
39
+ console.log("[proxy] not configured, using direct connection");
40
+ proxyInitialized = true;
41
+ return;
42
+ }
43
+ try {
44
+ setGlobalDispatcher(new ProxyAgent(proxyUrl));
45
+ proxyInitialized = true;
46
+ console.log(`[proxy] using ${maskProxyForLog(proxyUrl)}`);
47
+ }
48
+ catch (error) {
49
+ console.error("[proxy] failed to initialize global proxy:", error);
50
+ throw error;
51
+ }
52
+ }
@@ -0,0 +1,43 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { trimMessages } from "../agent/message-utils.js";
4
+ export class SessionStore {
5
+ baseDir;
6
+ constructor(options = {}) {
7
+ this.baseDir = options.baseDir ?? path.resolve(".sessions");
8
+ }
9
+ getFilePath(sessionId) {
10
+ return path.join(this.baseDir, `${sessionId}.json`);
11
+ }
12
+ async load(sessionId) {
13
+ try {
14
+ const filePath = this.getFilePath(sessionId);
15
+ const text = await fs.readFile(filePath, "utf8");
16
+ return JSON.parse(text);
17
+ }
18
+ catch (error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ if (message.includes("ENOENT")) {
21
+ return null;
22
+ }
23
+ throw error;
24
+ }
25
+ }
26
+ async save(sessionId, messages) {
27
+ await fs.mkdir(this.baseDir, { recursive: true });
28
+ const persistedMessages = trimMessages(messages.filter((m) => m.role !== "system"), {
29
+ maxMessages: 24,
30
+ maxTotalChars: 8000,
31
+ preserveRecentMessages: 6,
32
+ preserveSystemMessages: false,
33
+ compactToolMessages: true,
34
+ });
35
+ const data = {
36
+ sessionId,
37
+ messages: persistedMessages,
38
+ updatedAt: new Date().toISOString(),
39
+ };
40
+ const filePath = this.getFilePath(sessionId);
41
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
42
+ }
43
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import { createOpenAICompatibleClient } from "./providers/openai-compatible.js";
2
+ export function createModelClient(options = {}) {
3
+ return createOpenAICompatibleClient({
4
+ model: options.model,
5
+ config: options.config,
6
+ });
7
+ }