wezard 0.3.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.
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +14 -0
- package/CLAUDE.md +83 -0
- package/LICENSE +21 -0
- package/README.md +410 -0
- package/cli/weclaude.sh +236 -0
- package/commands/audit.md +7 -0
- package/commands/wrc.md +7 -0
- package/config.example.jsonc +102 -0
- package/dist/cli/audit.js +156 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/init.js +311 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/sync.js +175 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/daemon/approval.js +1301 -0
- package/dist/daemon/approval.js.map +1 -0
- package/dist/daemon/ask.js +101 -0
- package/dist/daemon/ask.js.map +1 -0
- package/dist/daemon/audit.js +168 -0
- package/dist/daemon/audit.js.map +1 -0
- package/dist/daemon/cc-bridge.js +204 -0
- package/dist/daemon/cc-bridge.js.map +1 -0
- package/dist/daemon/cfg-sync.js +240 -0
- package/dist/daemon/cfg-sync.js.map +1 -0
- package/dist/daemon/claim.js +95 -0
- package/dist/daemon/claim.js.map +1 -0
- package/dist/daemon/detail.js +153 -0
- package/dist/daemon/detail.js.map +1 -0
- package/dist/daemon/graph.js +176 -0
- package/dist/daemon/graph.js.map +1 -0
- package/dist/daemon/http.js +83 -0
- package/dist/daemon/http.js.map +1 -0
- package/dist/daemon/inbound.js +919 -0
- package/dist/daemon/inbound.js.map +1 -0
- package/dist/daemon/index.js +569 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/last-response.js +59 -0
- package/dist/daemon/last-response.js.map +1 -0
- package/dist/daemon/mirror-bridge.js +3512 -0
- package/dist/daemon/mirror-bridge.js.map +1 -0
- package/dist/daemon/mirror-store.js +39 -0
- package/dist/daemon/mirror-store.js.map +1 -0
- package/dist/daemon/outbound.js +42 -0
- package/dist/daemon/outbound.js.map +1 -0
- package/dist/daemon/peers.js +146 -0
- package/dist/daemon/peers.js.map +1 -0
- package/dist/daemon/pending.js +90 -0
- package/dist/daemon/pending.js.map +1 -0
- package/dist/daemon/quota.js +164 -0
- package/dist/daemon/quota.js.map +1 -0
- package/dist/daemon/redact.js +27 -0
- package/dist/daemon/redact.js.map +1 -0
- package/dist/daemon/session-cache.js +149 -0
- package/dist/daemon/session-cache.js.map +1 -0
- package/dist/daemon/session-scan.js +273 -0
- package/dist/daemon/session-scan.js.map +1 -0
- package/dist/daemon/sessions.js +35 -0
- package/dist/daemon/sessions.js.map +1 -0
- package/dist/daemon/spawn-tmux.js +213 -0
- package/dist/daemon/spawn-tmux.js.map +1 -0
- package/dist/daemon/tips.js +25 -0
- package/dist/daemon/tips.js.map +1 -0
- package/dist/daemon/topics.js +197 -0
- package/dist/daemon/topics.js.map +1 -0
- package/dist/daemon/usage.js +358 -0
- package/dist/daemon/usage.js.map +1 -0
- package/dist/daemon/wedoc.js +210 -0
- package/dist/daemon/wedoc.js.map +1 -0
- package/dist/daemon/ws.js +67 -0
- package/dist/daemon/ws.js.map +1 -0
- package/dist/mcp/server.js +385 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/shared/ansi.js +134 -0
- package/dist/shared/ansi.js.map +1 -0
- package/dist/shared/chat-http.js +195 -0
- package/dist/shared/chat-http.js.map +1 -0
- package/dist/shared/chat-render.js +53 -0
- package/dist/shared/chat-render.js.map +1 -0
- package/dist/shared/chat-view.js +116 -0
- package/dist/shared/chat-view.js.map +1 -0
- package/dist/shared/cli-backends.js +304 -0
- package/dist/shared/cli-backends.js.map +1 -0
- package/dist/shared/config-writer.js +39 -0
- package/dist/shared/config-writer.js.map +1 -0
- package/dist/shared/config.js +278 -0
- package/dist/shared/config.js.map +1 -0
- package/dist/shared/detail-render.js +836 -0
- package/dist/shared/detail-render.js.map +1 -0
- package/dist/shared/detail-store.js +205 -0
- package/dist/shared/detail-store.js.map +1 -0
- package/dist/shared/highlight.js +154 -0
- package/dist/shared/highlight.js.map +1 -0
- package/dist/shared/lan-ip.js +35 -0
- package/dist/shared/lan-ip.js.map +1 -0
- package/dist/shared/log.js +18 -0
- package/dist/shared/log.js.map +1 -0
- package/dist/shared/md-chunk.js +99 -0
- package/dist/shared/md-chunk.js.map +1 -0
- package/dist/shared/paths.js +12 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/session-label.js +67 -0
- package/dist/shared/session-label.js.map +1 -0
- package/dist/shared/web-assets.js +51 -0
- package/dist/shared/web-assets.js.map +1 -0
- package/dist/svr/index.js +246 -0
- package/dist/svr/index.js.map +1 -0
- package/hooks/hooks.json +16 -0
- package/hooks/pre-tool-use.sh +167 -0
- package/launchd/com.weclaude.daemon.plist.template +45 -0
- package/package.json +97 -0
- package/scripts/install.sh +73 -0
- package/scripts/postinstall.sh +12 -0
- package/scripts/uninstall.sh +44 -0
- package/systemd/weclaude.service.template +17 -0
- package/web/chat.css +171 -0
- package/web/chat.js +367 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// CLI backend descriptors. Claude Code / claude-internal forks / CodeBuddy
|
|
2
|
+
// all share the headless stream-json + hook protocol shape, but differ in:
|
|
3
|
+
// - binary name + config home (~/.claude vs ~/.claude-internal vs ~/.codebuddy)
|
|
4
|
+
// - on-disk project-dir encoding (Claude: leading `-`; CodeBuddy: none)
|
|
5
|
+
// - jsonl transcript schema (CodeBuddy splits tool_use / tool_result into
|
|
6
|
+
// independent top-level records; Claude Code nests them in message.content)
|
|
7
|
+
// - slash-command injection tag dialect: verified identical (<command-name> /
|
|
8
|
+
// <system-reminder>), so no tag-regex dispatch needed.
|
|
9
|
+
//
|
|
10
|
+
// normalizeTranscriptLine maps a backend's raw jsonl line into the Claude Code
|
|
11
|
+
// TranscriptLine shape that mirror-bridge's renderLine consumes. Claude backends
|
|
12
|
+
// are identity; codebuddy does the schema adapter work.
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { expandHome } from "./paths.js";
|
|
15
|
+
// Claude + claude-internal: `/` and `.` → `-`, yields a leading `-`
|
|
16
|
+
// (e.g. /Users/zyy/foo → -Users-zyy-foo). Verified against ~/.claude/projects.
|
|
17
|
+
const encodeClaude = (absCwd) => absCwd.replace(/[/.]/g, "-");
|
|
18
|
+
// CodeBuddy: same char mapping but trims the leading separator first, so no
|
|
19
|
+
// leading `-` (e.g. /Users/zyy/foo → Users-zyy-foo).
|
|
20
|
+
const encodeCodebuddy = (absCwd) => absCwd.replace(/^[/]+/, "").replace(/[/.]/g, "-");
|
|
21
|
+
// Module-level encoder, bound at daemon boot via bindCliBackends(). Defaults to
|
|
22
|
+
// Claude's dialect so the daemon works out-of-the-box. This is the PRIMARY
|
|
23
|
+
// backend's encoder — correct for *write* paths (where a new session's jsonl
|
|
24
|
+
// will land). Read paths must go through projectDirsFor/backendForPath instead,
|
|
25
|
+
// which consider every active backend.
|
|
26
|
+
let _encodeProjectDir = encodeClaude;
|
|
27
|
+
export const encodeProjectDir = (absCwd) => _encodeProjectDir(absCwd);
|
|
28
|
+
const makeClaude = (name, bin) => {
|
|
29
|
+
const homeDir = name === "claude-internal" ? "~/.claude-internal" : "~/.claude";
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
bin,
|
|
33
|
+
homeDir,
|
|
34
|
+
projectsDir: `${homeDir}/projects`,
|
|
35
|
+
settingsPath: `${homeDir}/settings.json`,
|
|
36
|
+
projectDirEnv: "CLAUDE_PROJECT_DIR",
|
|
37
|
+
pluginRootEnv: "CLAUDE_PLUGIN_ROOT",
|
|
38
|
+
headlessStreamingArgs: ["--verbose"],
|
|
39
|
+
encodeProjectDir: encodeClaude,
|
|
40
|
+
// Claude Code jsonl is already in the TranscriptLine shape — identity pass.
|
|
41
|
+
normalizeTranscriptLine: (raw) => raw,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
// CodeBuddy jsonl adapter. Verified schema (2026-07-24, GLM-5.2 session):
|
|
45
|
+
// message → { type:"message", role:"user"|"assistant",
|
|
46
|
+
// content:[{type:"input_text"|"output_text", text}],
|
|
47
|
+
// id, parentId, status?, providerData:{agent,model,usage,...} }
|
|
48
|
+
// function_call → { type:"function_call", callId, name, arguments(JSON string),
|
|
49
|
+
// id(==host assistant msg id), parentId }
|
|
50
|
+
// function_call_result → { type:"function_call_result", callId, name, status,
|
|
51
|
+
// output:{type:"text", text}, id, parentId }
|
|
52
|
+
// reasoning | file-history-snapshot | ai-title | summary → drop
|
|
53
|
+
//
|
|
54
|
+
// Key structural differences from Claude Code:
|
|
55
|
+
// 1. content block types are input_text/output_text, not "text".
|
|
56
|
+
// 2. tool_use / tool_result are independent top-level records, not nested in
|
|
57
|
+
// a message.content array. Adapter synthesizes assistant/user messages
|
|
58
|
+
// around them so renderLine sees the Claude shape.
|
|
59
|
+
// 3. id/parentId → uuid/parentUuid.
|
|
60
|
+
// 4. Subagents live in <sid>/subagents/agent-<id>.jsonl — NOT inlined into
|
|
61
|
+
// the main transcript — so no isSidechain filtering needed here.
|
|
62
|
+
// 5. Slash-command tags (<command-name> / <system-reminder>) are identical
|
|
63
|
+
// to Claude Code — no tag-regex dialect dispatch needed.
|
|
64
|
+
// 6. /goal marker shape on disk: UNVERIFIED (blocker). The non-isMeta
|
|
65
|
+
// fallback path in renderLine (goalStartOf on string content) should
|
|
66
|
+
// catch it if codebuddy uses the same marker text, but this needs
|
|
67
|
+
// confirmation from a real /goal session.
|
|
68
|
+
const normalizeCodebuddy = (raw) => {
|
|
69
|
+
if (!raw || typeof raw !== "object")
|
|
70
|
+
return null;
|
|
71
|
+
const r = raw;
|
|
72
|
+
const type = r.type;
|
|
73
|
+
const id = r.id;
|
|
74
|
+
const parentId = r.parentId;
|
|
75
|
+
const providerData = (r.providerData ?? {});
|
|
76
|
+
const uuids = { uuid: id, parentUuid: parentId ?? undefined };
|
|
77
|
+
if (type === "message") {
|
|
78
|
+
const role = r.role;
|
|
79
|
+
const content = r.content;
|
|
80
|
+
if (role === "user") {
|
|
81
|
+
// Join input_text blocks into a single string. renderLine's string path
|
|
82
|
+
// handles slash-command stripping, /goal marker detection, task-notification
|
|
83
|
+
// rendering, and the isOwnInject dedup — all driven by string content.
|
|
84
|
+
const text = (content ?? []).map((b) => b.text ?? "").join("");
|
|
85
|
+
return { type: "user", ...uuids, message: { role: "user", content: text } };
|
|
86
|
+
}
|
|
87
|
+
if (role === "assistant") {
|
|
88
|
+
const blocks = (content ?? []).map((b) => ({
|
|
89
|
+
type: "text",
|
|
90
|
+
text: b.text ?? "",
|
|
91
|
+
}));
|
|
92
|
+
const msg = {
|
|
93
|
+
role: "assistant",
|
|
94
|
+
content: blocks,
|
|
95
|
+
id: providerData.messageId,
|
|
96
|
+
model: providerData.model,
|
|
97
|
+
usage: providerData.usage,
|
|
98
|
+
};
|
|
99
|
+
// status:"completed" 只说明这条记录写完了 —— codebuddy 把中途叙述和最终答复
|
|
100
|
+
// 写成同形的独立 message, 映射成 stop_reason:"end_turn" 会让"中间说一句"就把
|
|
101
|
+
// 整个 turn 收掉 (后半轮全部退化成散装气泡)。改为软收口, 交给 mirror-bridge
|
|
102
|
+
// 用静默期确认。
|
|
103
|
+
const status = r.status;
|
|
104
|
+
return { type: "assistant", ...uuids, message: msg, softTurnEnd: status === "completed" };
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (type === "function_call") {
|
|
109
|
+
const callId = r.callId;
|
|
110
|
+
const name = r.name;
|
|
111
|
+
const argsRaw = r.arguments;
|
|
112
|
+
let input;
|
|
113
|
+
try {
|
|
114
|
+
input = argsRaw ? JSON.parse(argsRaw) : {};
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
input = {};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
type: "assistant",
|
|
121
|
+
...uuids,
|
|
122
|
+
message: { role: "assistant", content: [{ type: "tool_use", id: callId, name, input }] },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (type === "function_call_result") {
|
|
126
|
+
const callId = r.callId;
|
|
127
|
+
const output = r.output;
|
|
128
|
+
const text = output?.text ?? "";
|
|
129
|
+
return {
|
|
130
|
+
type: "user",
|
|
131
|
+
...uuids,
|
|
132
|
+
message: { role: "user", content: [{ type: "tool_result", tool_use_id: callId, content: text }] },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// reasoning / file-history-snapshot / ai-title / summary → drop.
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
const makeCodebuddy = (bin) => ({
|
|
139
|
+
name: "codebuddy",
|
|
140
|
+
bin,
|
|
141
|
+
homeDir: "~/.codebuddy",
|
|
142
|
+
projectsDir: "~/.codebuddy/projects",
|
|
143
|
+
settingsPath: "~/.codebuddy/settings.json",
|
|
144
|
+
projectDirEnv: "CODEBUDDY_PROJECT_DIR",
|
|
145
|
+
pluginRootEnv: "CODEBUDDY_PLUGIN_ROOT",
|
|
146
|
+
// CodeBuddy: --verbose is a no-op for streaming; --include-partial-messages
|
|
147
|
+
// is what emits stream_event content_block_delta lines. Keep --verbose too
|
|
148
|
+
// for log parity — it's harmless and may surface diagnostics in debug mode.
|
|
149
|
+
headlessStreamingArgs: ["--verbose", "--include-partial-messages"],
|
|
150
|
+
encodeProjectDir: encodeCodebuddy,
|
|
151
|
+
normalizeTranscriptLine: normalizeCodebuddy,
|
|
152
|
+
});
|
|
153
|
+
// Built-in defaults. `bin` is the default executable name; users override via
|
|
154
|
+
// `wrc.cliBackends.<name>.bin` in config.jsonc (or the legacy `wrc.claudeBin`
|
|
155
|
+
// for the claude / claude-internal backends — see resolveCliBackend).
|
|
156
|
+
export const CLI_BACKEND_DEFAULTS = {
|
|
157
|
+
claude: makeClaude("claude", "claude"),
|
|
158
|
+
"claude-internal": makeClaude("claude-internal", "claude-internal"),
|
|
159
|
+
codebuddy: makeCodebuddy("codebuddy"),
|
|
160
|
+
};
|
|
161
|
+
// Infer the default backend name from a legacy `claudeBin` basename. Only
|
|
162
|
+
// recognizes the two known forks; unknown bins fall back to "claude" (with
|
|
163
|
+
// the custom bin still preserved via the override path below).
|
|
164
|
+
const inferDefaultFromClaudeBin = (bin) => {
|
|
165
|
+
const base = bin.split("/").pop() ?? bin;
|
|
166
|
+
if (base === "claude-internal")
|
|
167
|
+
return "claude-internal";
|
|
168
|
+
if (base === "codebuddy")
|
|
169
|
+
return "codebuddy";
|
|
170
|
+
return "claude";
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Resolve the active backend, applying user overrides + legacy back-compat.
|
|
174
|
+
*
|
|
175
|
+
* Back-compat ladder (in priority order):
|
|
176
|
+
* 1. Explicit `cliBackends.<name>.bin` override wins.
|
|
177
|
+
* 2. `defaultCli` missing → infer from `claudeBin` basename (claude / claude-
|
|
178
|
+
* internal / codebuddy), else "claude".
|
|
179
|
+
* 3. Legacy `claudeBin` set to a non-default value AND defaultCli points at
|
|
180
|
+
* the matching backend → carry `claudeBin` as the bin override. Preserves
|
|
181
|
+
* both `claudeBin: "/absolute/claude"` and `claudeBin: "claude-internal"`.
|
|
182
|
+
*/
|
|
183
|
+
export const resolveCliBackend = (cfg) => {
|
|
184
|
+
const name = cfg.defaultCli ?? inferDefaultFromClaudeBin(cfg.claudeBin ?? "claude");
|
|
185
|
+
const base = CLI_BACKEND_DEFAULTS[name];
|
|
186
|
+
if (!base)
|
|
187
|
+
throw new Error(`unknown CLI backend: ${name}`);
|
|
188
|
+
const explicit = cfg.cliBackends?.[name]?.bin;
|
|
189
|
+
if (explicit)
|
|
190
|
+
return { ...base, bin: explicit };
|
|
191
|
+
// Legacy `claudeBin` carries a non-default value → treat as bin override for
|
|
192
|
+
// the backend it points at (matched by name, not by basename equality —
|
|
193
|
+
// `/usr/local/bin/claude-internal` should still map to claude-internal).
|
|
194
|
+
if (cfg.claudeBin && cfg.claudeBin !== "claude") {
|
|
195
|
+
const inferred = inferDefaultFromClaudeBin(cfg.claudeBin);
|
|
196
|
+
if (inferred === name)
|
|
197
|
+
return { ...base, bin: cfg.claudeBin };
|
|
198
|
+
}
|
|
199
|
+
return base;
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Bind the active backend's dialect (encodeProjectDir) into module-level state.
|
|
203
|
+
* Call once at daemon boot after loadConfig — flips mirror-bridge + spawn-tmux
|
|
204
|
+
* to the resolved backend's project-dir encoding. Idempotent; re-binding on
|
|
205
|
+
* daemon reload picks up a changed defaultCli without restart.
|
|
206
|
+
*/
|
|
207
|
+
export const bindCliBackend = (cfg) => {
|
|
208
|
+
const backend = resolveCliBackend(cfg);
|
|
209
|
+
_encodeProjectDir = backend.encodeProjectDir;
|
|
210
|
+
return backend;
|
|
211
|
+
};
|
|
212
|
+
// ── Multi-backend registry ──────────────────────────────────────────────
|
|
213
|
+
//
|
|
214
|
+
// The daemon mirrors sessions from EVERY installed CLI at once, not just the
|
|
215
|
+
// primary one: a user can have `claude` in one tmux pane and `codebuddy` in
|
|
216
|
+
// another, each bound to a different WeCom chat. Since a session's identity on
|
|
217
|
+
// disk *is* its transcript path, the path is the discriminant — `backendForPath`
|
|
218
|
+
// recovers which CLI wrote a jsonl, and every downstream decision (which binary
|
|
219
|
+
// to `--resume` with, which jsonl schema to normalize, which project-dir
|
|
220
|
+
// encoding to compare panes against) derives from that instead of a global.
|
|
221
|
+
//
|
|
222
|
+
// `primaryBackend()` remains the tie-breaker for *write* paths — spawning a new
|
|
223
|
+
// session has no path to derive from, so it uses `defaultCli`.
|
|
224
|
+
const backendWithBin = (name, cfg) => {
|
|
225
|
+
const base = CLI_BACKEND_DEFAULTS[name];
|
|
226
|
+
const explicit = cfg.cliBackends?.[name]?.bin;
|
|
227
|
+
return explicit ? { ...base, bin: explicit } : base;
|
|
228
|
+
};
|
|
229
|
+
let _backends = [CLI_BACKEND_DEFAULTS.claude];
|
|
230
|
+
let _primary = CLI_BACKEND_DEFAULTS.claude;
|
|
231
|
+
/** Active backends, primary first. Read-only snapshot of the last bind. */
|
|
232
|
+
export const activeBackends = () => _backends;
|
|
233
|
+
/** Backend used when there is no path to derive one from (new-session spawns). */
|
|
234
|
+
export const primaryBackend = () => _primary;
|
|
235
|
+
/**
|
|
236
|
+
* Resolve the primary backend and register every other known backend alongside
|
|
237
|
+
* it. Zero-config: install codebuddy and its sessions become mirrorable without
|
|
238
|
+
* touching config.jsonc.
|
|
239
|
+
*
|
|
240
|
+
* Deliberately does NOT probe for each root's existence. That probe ran once at
|
|
241
|
+
* boot, so a CLI first used *after* the daemon started stayed invisible until a
|
|
242
|
+
* reload — and it bought nothing: a nonexistent root never prefix-matches in
|
|
243
|
+
* backendForPath, and every read path (rankedJsonlsForCwd, findJsonlBySid,
|
|
244
|
+
* session-scan's roots) already tolerates a missing directory.
|
|
245
|
+
*/
|
|
246
|
+
export const bindCliBackends = (cfg) => {
|
|
247
|
+
const resolved = resolveCliBackend(cfg);
|
|
248
|
+
// An explicit `mirror.projectsDir` retargets the PRIMARY backend only — it is
|
|
249
|
+
// the pre-multi-CLI escape hatch for a nonstandard transcript root, and has no
|
|
250
|
+
// meaning for the other backends.
|
|
251
|
+
const override = (cfg.projectsDirOverride ?? "").trim();
|
|
252
|
+
const primary = override && override !== resolved.projectsDir ? { ...resolved, projectsDir: override } : resolved;
|
|
253
|
+
const secondaries = Object.keys(CLI_BACKEND_DEFAULTS)
|
|
254
|
+
.filter((n) => n !== primary.name)
|
|
255
|
+
.map((n) => backendWithBin(n, cfg));
|
|
256
|
+
_primary = primary;
|
|
257
|
+
_backends = [primary, ...secondaries];
|
|
258
|
+
_encodeProjectDir = primary.encodeProjectDir;
|
|
259
|
+
return { primary, backends: _backends };
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Register a backend as active at runtime, returning the already-known
|
|
263
|
+
* descriptor if there is one. `bindCliBackends` only admits secondaries whose
|
|
264
|
+
* transcript root already exists — a CLI that is installed but has never run
|
|
265
|
+
* has none, so the FIRST session spawned with it would be invisible to
|
|
266
|
+
* `backendForPath` (mirrored with the primary's jsonl dialect, resumed with the
|
|
267
|
+
* primary's binary). Spawning with an explicit `cli` calls this so the registry
|
|
268
|
+
* learns the backend before its transcript appears on disk.
|
|
269
|
+
*/
|
|
270
|
+
export const activateBackend = (backend) => {
|
|
271
|
+
const known = _backends.find((b) => b.name === backend.name);
|
|
272
|
+
if (known)
|
|
273
|
+
return known;
|
|
274
|
+
_backends = [..._backends, backend];
|
|
275
|
+
return backend;
|
|
276
|
+
};
|
|
277
|
+
/**
|
|
278
|
+
* Recover the backend that owns an absolute transcript path, by longest
|
|
279
|
+
* projectsDir prefix match. Falls back to primary for paths outside every known
|
|
280
|
+
* root (custom `mirror.projectsDir`, or a jsonl handed in by `/wrc` from a
|
|
281
|
+
* location we don't manage) — treating an unknown transcript as the primary
|
|
282
|
+
* dialect is what the single-backend code already did.
|
|
283
|
+
*/
|
|
284
|
+
export const backendForPath = (absPath) => _backends
|
|
285
|
+
.map((b) => ({ b, root: expandHome(b.projectsDir) }))
|
|
286
|
+
.filter(({ root }) => absPath === root || absPath.startsWith(`${root}/`))
|
|
287
|
+
.sort((x, y) => y.root.length - x.root.length)[0]?.b ?? _primary;
|
|
288
|
+
/**
|
|
289
|
+
* Every candidate `<projectsDir>/<encoded(cwd)>` across active backends, primary
|
|
290
|
+
* first. Callers filter by existence and rank by mtime — a cwd can legitimately
|
|
291
|
+
* have transcripts under two backends at once (same project, both CLIs used).
|
|
292
|
+
*/
|
|
293
|
+
export const projectDirsFor = (absCwd) => _backends.map((b) => ({ backend: b, dir: join(expandHome(b.projectsDir), b.encodeProjectDir(absCwd)) }));
|
|
294
|
+
/**
|
|
295
|
+
* The project dir `cwd` would encode to *under the same backend that owns
|
|
296
|
+
* `refPath`*. Used for "is this pane still in the project dir its transcript
|
|
297
|
+
* lives in?" checks — comparing against the primary encoding would report a
|
|
298
|
+
* spurious drift for every non-primary session.
|
|
299
|
+
*/
|
|
300
|
+
export const projectDirFor = (refPath, absCwd) => {
|
|
301
|
+
const b = backendForPath(refPath);
|
|
302
|
+
return join(expandHome(b.projectsDir), b.encodeProjectDir(absCwd));
|
|
303
|
+
};
|
|
304
|
+
//# sourceMappingURL=cli-backends.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-backends.js","sourceRoot":"","sources":["../../shared/cli-backends.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Surgical edits to ~/.weclaude/config.jsonc and secrets.json — preserves
|
|
2
|
+
// comments + formatting via jsonc-parser's `modify` patches.
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser";
|
|
6
|
+
import { expandHome } from "./paths.js";
|
|
7
|
+
const FORMAT = { tabSize: 2, insertSpaces: true, eol: "\n" };
|
|
8
|
+
const readText = (abs) => existsSync(abs) ? readFileSync(abs, "utf8") : "{}\n";
|
|
9
|
+
const writeText = (abs, txt) => {
|
|
10
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
11
|
+
writeFileSync(abs, txt.endsWith("\n") ? txt : `${txt}\n`, "utf8");
|
|
12
|
+
};
|
|
13
|
+
/** Apply a sequence of (path, value) patches to a JSONC file. value=undefined deletes. */
|
|
14
|
+
export const patchJsonc = (filePath, patches) => {
|
|
15
|
+
const abs = expandHome(filePath);
|
|
16
|
+
let txt = readText(abs);
|
|
17
|
+
for (const { path, value } of patches) {
|
|
18
|
+
const edits = modify(txt, path, value, { formattingOptions: FORMAT });
|
|
19
|
+
txt = applyEdits(txt, edits);
|
|
20
|
+
}
|
|
21
|
+
writeText(abs, txt);
|
|
22
|
+
};
|
|
23
|
+
/** Append `value` into a string array at `path`, deduped. No-op if already present. */
|
|
24
|
+
export const appendUnique = (filePath, path, value) => {
|
|
25
|
+
const abs = expandHome(filePath);
|
|
26
|
+
const txt = readText(abs);
|
|
27
|
+
const tree = (parseJsonc(txt) ?? {});
|
|
28
|
+
const cur = path.reduce((acc, key) => {
|
|
29
|
+
if (acc && typeof acc === "object")
|
|
30
|
+
return acc[String(key)];
|
|
31
|
+
return undefined;
|
|
32
|
+
}, tree);
|
|
33
|
+
const arr = Array.isArray(cur) ? cur.slice() : [];
|
|
34
|
+
if (arr.includes(value))
|
|
35
|
+
return;
|
|
36
|
+
arr.push(value);
|
|
37
|
+
patchJsonc(filePath, [{ path, value: arr }]);
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=config-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-writer.js","sourceRoot":"","sources":["../../shared/config-writer.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,6DAA6D;AAC7D,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,IAAI,UAAU,EAAiB,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,MAAM,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;AAE7D,MAAM,QAAQ,GAAG,CAAC,GAAW,EAAU,EAAE,CACvC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;AAEvD,MAAM,SAAS,GAAG,CAAC,GAAW,EAAE,GAAW,EAAQ,EAAE;IACnD,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;AACpE,CAAC,CAAC;AAEF,0FAA0F;AAC1F,MAAM,CAAC,MAAM,UAAU,GAAG,CACxB,QAAgB,EAChB,OAAkD,EAC5C,EAAE;IACR,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IACjC,IAAI,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IACxB,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,OAAO,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,iBAAiB,EAAE,MAAM,EAAE,CAAC,CAAC;QACtE,GAAG,GAAG,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC/B,CAAC;IACD,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AACtB,CAAC,CAAC;AAEF,uFAAuF;AACvF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAAgB,EAAE,IAAc,EAAE,KAAa,EAAQ,EAAE;IACpF,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IACjC,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC1B,MAAM,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,EAAE,CAA4B,CAAC;IAChE,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAU,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC5C,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAQ,GAA+B,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACzF,OAAO,SAAS,CAAC;IACnB,CAAC,EAAE,IAAI,CAAC,CAAC;IACT,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,GAAgB,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAChE,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO;IAChC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,UAAU,CAAC,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC,CAAC"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// Declarative config: schema (zod) + loader. Pure transforms, file IO at boundary.
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { parse as parseJsonc } from "jsonc-parser";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { expandHome } from "./paths.js";
|
|
6
|
+
import { resolveCliBackend } from "./cli-backends.js";
|
|
7
|
+
// ── Schema ──────────────────────────────────────────────────────────
|
|
8
|
+
const Bot = z.object({
|
|
9
|
+
botId: z.string().min(1),
|
|
10
|
+
secret: z.string().min(1),
|
|
11
|
+
websocketUrl: z.string().url().default("wss://openws.work.weixin.qq.com"),
|
|
12
|
+
});
|
|
13
|
+
const Daemon = z.object({
|
|
14
|
+
host: z.string().default("127.0.0.1"),
|
|
15
|
+
port: z.number().int().min(1).max(65535).default(17890),
|
|
16
|
+
stateDir: z.string().default("~/.weclaude/state"),
|
|
17
|
+
logFile: z.string().default("~/.weclaude/daemon.log"),
|
|
18
|
+
logLevel: z.enum(["trace", "debug", "info", "warn", "error"]).default("info"),
|
|
19
|
+
// 工具调用 / 授权详情页 URL。空则用 http://<host>:<port> (回环)。
|
|
20
|
+
// 想让手机 WeCom 也能点开, 需要在反向代理后填外网地址。桌面端用回环即可。
|
|
21
|
+
detailPublicBase: z.string().default(""),
|
|
22
|
+
// 远端 detail svr (weclaude svr 起的独立服务, chat + cli 共同可达的网络里)。
|
|
23
|
+
// 空则不转发, 详情走本机 :port/detail。非空时:
|
|
24
|
+
// • 每次 record*() 后 fire-and-forget POST 到 <base>/d, 存到远端 store。
|
|
25
|
+
// • buildDetailUrl 用 <base> 作为链接根 (chat 端浏览器打开 <base>/detail?id=...)。
|
|
26
|
+
// 用这条路径解决 daemon (公司内网) 和 chat 用户 (移动网络) 不同网段的场景。
|
|
27
|
+
detailRemoteBase: z.string().default(""),
|
|
28
|
+
detailRemoteToken: z.string().default(""),
|
|
29
|
+
// 镜像消息里给每个 tool_use 行包成 markdown 链接, 点开本地 HTML 详情页。
|
|
30
|
+
detailLinksInMirror: z.boolean().default(true),
|
|
31
|
+
});
|
|
32
|
+
const Mirror = z.object({
|
|
33
|
+
// Transcript root for the DEFAULT backend only — an escape hatch for a
|
|
34
|
+
// nonstandard location. Empty → auto-derived from `defaultCli` (`claude` →
|
|
35
|
+
// `~/.claude/projects`, `claude-internal` → `~/.claude-internal/projects`,
|
|
36
|
+
// `codebuddy` → `~/.codebuddy/projects`). Other installed backends always use
|
|
37
|
+
// their own built-in roots; the mirror probes all of them, so setting this
|
|
38
|
+
// does not narrow which CLIs can be mirrored.
|
|
39
|
+
projectsDir: z.string().default(""),
|
|
40
|
+
// Pin a specific Claude session to mirror. Empty → auto-pick latest .jsonl
|
|
41
|
+
// under `<projectsDir>/<encoded(wrc.cwd)>/` by mtime.
|
|
42
|
+
sessionId: z.string().default(""),
|
|
43
|
+
// Where to push live assistant output. Empty → fall back to defaultChat.
|
|
44
|
+
pushChat: z.string().default(""),
|
|
45
|
+
// Cap a single push payload (WeCom markdown ~2048 limit). Long replies are split.
|
|
46
|
+
chunkChars: z.number().int().positive().default(1800),
|
|
47
|
+
// Mirror the user's CLI prompts (type:"user" with string content). Off by
|
|
48
|
+
// default: WeCom-sourced inbounds get dedup'd anyway, and local CLI typing
|
|
49
|
+
// is rare in the bot-driven flow — keeping it on mostly produced echo noise.
|
|
50
|
+
includeUser: z.boolean().default(false),
|
|
51
|
+
// Mirror assistant tool_use blocks (Bash/Edit/Read/...).
|
|
52
|
+
includeTools: z.boolean().default(true),
|
|
53
|
+
// Mirror tool_result blocks. Off by default — usually noisy.
|
|
54
|
+
includeToolResults: z.boolean().default(false),
|
|
55
|
+
// Truncate each tool_result body to this many chars.
|
|
56
|
+
toolResultMaxChars: z.number().int().positive().default(400),
|
|
57
|
+
// 工具调用气泡里 `compact` 一行的最大字符数 (`🔧 Name <compact>` / `[• compact](url)`).
|
|
58
|
+
// 旧值 40 太窄, 长 bash / 长 file_path 直接被截掉; 抬到 120 兼顾可读与单行。
|
|
59
|
+
toolUseInlineMaxChars: z.number().int().positive().default(120),
|
|
60
|
+
// Where inbound images/files from WeCom get saved before being pasted into
|
|
61
|
+
// the live TTY. Files persist — claude reads them by absolute path.
|
|
62
|
+
inboxDir: z.string().default("~/.weclaude/inbox"),
|
|
63
|
+
// Persisted mirror attachments (principal → sessionId/jsonl/tmux). Restored
|
|
64
|
+
// on daemon boot + lazily on first inbound after reload — so reloading the
|
|
65
|
+
// daemon doesn't re-spawn a fresh claude for an already-bound chat.
|
|
66
|
+
attachmentsFile: z.string().default("~/.weclaude/mirror-attachments.json"),
|
|
67
|
+
// Standalone fallback 路径(liveStream 已 closed/dead/capped) 上的防抖聚合窗口
|
|
68
|
+
// (ms)。窗口内同一 attachment 的多个 item 合并为一条 markdown, 抑制连续工具
|
|
69
|
+
// 调用刷屏。liveStream 仍活时不受影响——直接走 typewriter。0 = 关闭。
|
|
70
|
+
standaloneDebounceMs: z.number().int().nonnegative().default(3000),
|
|
71
|
+
// dispatch 后延迟开 stream 的窗口 (ms)。窗口内 item 累积:
|
|
72
|
+
// • 出现 needs-approval tool_use → 立刻把 buffer 聚合成单条 standalone 推出 (赶在
|
|
73
|
+
// 授权卡之前), 切到 AWAITING_APPR 等点击; 点击后再开 stream 续 tool_result+回复。
|
|
74
|
+
// • 窗口内 turn_end (纯文本快回复) → buffer 整体作为一条 standalone, 不开 stream。
|
|
75
|
+
// • 窗口超时 → 正常开 stream, 重放 buffer。
|
|
76
|
+
// 0 = 关闭, 退回到 dispatch 立即 ack "…" 的旧行为。
|
|
77
|
+
outboundDeferMs: z.number().int().nonnegative().default(3000),
|
|
78
|
+
// 发卡前等待目标 tool_use 在 jsonl 落盘的最长 poll 时间 (ms)。Claude Code 的
|
|
79
|
+
// assistant 行写盘相对 PreToolUse hook fire 有 tens-to-hundreds ms 异步抖动,
|
|
80
|
+
// 没等到的话 drain 抓空, 卡先到、思考过程后到。轮询步进 50ms。0 = 关闭等待,
|
|
81
|
+
// 仅做一次同步 drain (旧行为)。
|
|
82
|
+
flushBeforeCardWaitMs: z.number().int().nonnegative().default(800),
|
|
83
|
+
// 发卡前从活着的 tmux pane 抠出「为什么发这张卡」的 assistant 前言并先推一条。
|
|
84
|
+
// 必要性: Claude Code 把以 tool_use 收尾的整个 turn 攒着, 等工具 resolve 才 flush
|
|
85
|
+
// 到 jsonl —— 而工具正卡在这张授权卡上, 于是前言在发卡时点既不在 jsonl 也不在
|
|
86
|
+
// hook 的 transcript_tail 里, 唯一存在处是 pane。抠不到时静默退回「只发卡」(无回归)。
|
|
87
|
+
panePreamble: z.boolean().default(true),
|
|
88
|
+
// Brief 模式: 只对 mirror 模式生效。一个 turn 里群里只发两条 —
|
|
89
|
+
// 1. turn 起始时:「🧵 [本轮详情](url)」链接 (指向聚合详情页,含所有 tool 输入/输出+中间文本+折叠展示)
|
|
90
|
+
// 2. turn 结束时: Claude 的 finish text (最终 assistant 回复)
|
|
91
|
+
// 中间 tool_use / tool_result / thinking / 非 final text 全部只写进详情页,不发气泡。
|
|
92
|
+
// 授权卡照常发群 (交互无法替代)。false = 现状 (逐条气泡)。
|
|
93
|
+
brief: z.boolean().default(true),
|
|
94
|
+
});
|
|
95
|
+
const Wrc = z.object({
|
|
96
|
+
allowFrom: z.array(z.string()).default([]),
|
|
97
|
+
mode: z.enum(["headless", "mirror"]).default("headless"),
|
|
98
|
+
// Legacy pre-v0.3: single binary path. Still honored as a back-compat alias
|
|
99
|
+
// — resolveCliBackend picks it up when cliBackends.<name>.bin is unset. Kept
|
|
100
|
+
// on the schema so existing configs keep parsing without migration.
|
|
101
|
+
claudeBin: z.string().default("claude"),
|
|
102
|
+
// DEFAULT CLI backend — the one used when there is no transcript to derive a
|
|
103
|
+
// backend from (new-session spawns, headless mode). It is NOT exclusive: the
|
|
104
|
+
// daemon mirrors sessions from every installed CLI concurrently, resolving
|
|
105
|
+
// each attachment's binary + jsonl dialect from its own transcript path
|
|
106
|
+
// (shared/cli-backends.ts bindCliBackends / backendForPath). Optional —
|
|
107
|
+
// when unset, resolveCliBackend infers from claudeBin basename (so legacy
|
|
108
|
+
// `claudeBin: "claude-internal"` configs still resolve correctly without
|
|
109
|
+
// needing to also set defaultCli). The Wrc transform below pins the inferred
|
|
110
|
+
// name back into v.defaultCli so downstream readers see a concrete value.
|
|
111
|
+
defaultCli: z.enum(["claude", "claude-internal", "codebuddy"]).optional(),
|
|
112
|
+
// Per-backend bin overrides. Missing entry → built-in default (`claude`,
|
|
113
|
+
// `claude-internal`, `codebuddy`). Example:
|
|
114
|
+
// cliBackends: { codebuddy: { bin: "/usr/local/bin/codebuddy" } }
|
|
115
|
+
cliBackends: z
|
|
116
|
+
.object({
|
|
117
|
+
claude: z.object({ bin: z.string().optional() }).optional(),
|
|
118
|
+
"claude-internal": z.object({ bin: z.string().optional() }).optional(),
|
|
119
|
+
codebuddy: z.object({ bin: z.string().optional() }).optional(),
|
|
120
|
+
})
|
|
121
|
+
.default({}),
|
|
122
|
+
cwd: z.string().default("~/.weclaude/workspace"),
|
|
123
|
+
sessionMapFile: z.string().default("~/.weclaude/sessions.json"),
|
|
124
|
+
extraArgs: z.array(z.string()).default([]),
|
|
125
|
+
mirror: Mirror.default({}),
|
|
126
|
+
// Mirror-only: tmux session name prefix for auto-spawn. Final name is
|
|
127
|
+
// `${prefix}-<short>`. Auto-spawn fires when an authorized inbound finds no
|
|
128
|
+
// mirror attached for that chat — allowFrom IS the authorization.
|
|
129
|
+
tmuxPrefix: z.string().default("weclaude"),
|
|
130
|
+
}).transform((v) => {
|
|
131
|
+
// Resolve the active backend once — honors defaultCli if set, otherwise
|
|
132
|
+
// infers from claudeBin basename (legacy back-compat). Pin the inferred
|
|
133
|
+
// name back into v.defaultCli so downstream readers see a concrete value
|
|
134
|
+
// and so logs / sync labels can report the effective backend.
|
|
135
|
+
const backend = resolveCliBackend(v);
|
|
136
|
+
if (!v.defaultCli)
|
|
137
|
+
v.defaultCli = backend.name;
|
|
138
|
+
// Sync claudeBin to the resolved backend's bin so every daemon file that
|
|
139
|
+
// reads cfg.wrc.claudeBin (cc-bridge, mirror-bridge, spawn-tmux, quota)
|
|
140
|
+
// spawns the correct binary — even when the user set only `defaultCli`
|
|
141
|
+
// without an explicit `claudeBin`. Without this, defaultCli:"codebuddy"
|
|
142
|
+
// + inherited claudeBin:"claude" would spawn the wrong CLI.
|
|
143
|
+
v.claudeBin = backend.bin;
|
|
144
|
+
// Auto-derive projectsDir from the resolved backend when not explicitly
|
|
145
|
+
// set. Replaces the old claudeBin-basename derivation — same result for the
|
|
146
|
+
// standard cases (claude → ~/.claude/projects, claude-internal →
|
|
147
|
+
// ~/.claude-internal/projects), plus now supports codebuddy →
|
|
148
|
+
// ~/.codebuddy/projects. Explicit projectsDir still wins.
|
|
149
|
+
if (!v.mirror.projectsDir) {
|
|
150
|
+
v.mirror.projectsDir = backend.projectsDir;
|
|
151
|
+
}
|
|
152
|
+
return v;
|
|
153
|
+
});
|
|
154
|
+
const Approval = z.object({
|
|
155
|
+
enabled: z.boolean().default(true),
|
|
156
|
+
matcher: z.string().default(".*"),
|
|
157
|
+
approvers: z.array(z.string()).default([]),
|
|
158
|
+
hookTimeoutSec: z.number().int().positive().default(43210),
|
|
159
|
+
longPollSec: z.number().int().positive().default(43200),
|
|
160
|
+
sessionCacheMinutes: z.number().int().nonnegative().default(30),
|
|
161
|
+
windowMinutes: z.number().int().nonnegative().default(600),
|
|
162
|
+
sensitiveArgRedact: z.boolean().default(true),
|
|
163
|
+
fallbackOnError: z.enum(["ask", "allow", "deny"]).default("ask"),
|
|
164
|
+
// 同 session 同 tool 的并发 PreToolUse 合流窗口: 第一次到达后等待这么久,
|
|
165
|
+
// 期间到的同类请求合成一张批量卡 (减少 N 张并发卡的轰炸)。0 = 关闭聚合,
|
|
166
|
+
// 每次立刻发卡 (旧行为)。单次到达走单卡路径, 仅多了一次性的延迟。
|
|
167
|
+
batchCoalesceMs: z.number().int().nonnegative().default(250),
|
|
168
|
+
// 拦截 model 主动调用的 EnterPlanMode (deny + reason),让 Claude 不要自动进
|
|
169
|
+
// plan mode、直接干活。用户仍可在本地 Shift+Tab 手动进 plan mode(那条路径
|
|
170
|
+
// 不过 hook)。默认 true。设 false 恢复原行为(允许模型自动进 plan mode)。
|
|
171
|
+
blockAutoPlanMode: z.boolean().default(true),
|
|
172
|
+
});
|
|
173
|
+
// sync.targets[].kind 的合法值。claude-internal / custom 等旧值自动 collapse
|
|
174
|
+
// 为 "claude" (preprocess) — settingsPath 已经表达了具体 fork, kind 只表达家族。
|
|
175
|
+
const SYNC_KIND_LEGACY_TO_CLAUDE = new Set(["claude-internal", "custom"]);
|
|
176
|
+
const normalizeSyncKind = (v) => typeof v === "string" && SYNC_KIND_LEGACY_TO_CLAUDE.has(v) ? "claude" : v;
|
|
177
|
+
const SyncTarget = z.object({
|
|
178
|
+
// CLI 家族标签: "claude" (含 claude-internal 等 fork) 或 "codebuddy"。
|
|
179
|
+
// 仅用于 sync 日志; 真正决定写入位置的是 settingsPath。
|
|
180
|
+
kind: z.preprocess(normalizeSyncKind, z.enum(["claude", "codebuddy"])).default("claude"),
|
|
181
|
+
settingsPath: z.string(),
|
|
182
|
+
scope: z.enum(["user", "project", "local"]).default("user"),
|
|
183
|
+
});
|
|
184
|
+
const Sync = z.object({
|
|
185
|
+
targets: z.array(SyncTarget).default([]),
|
|
186
|
+
});
|
|
187
|
+
// 独立 detail 中转服务 (weclaude svr) 的默认参数。CLI 参数 (--host/--port/…) 仍可覆盖,
|
|
188
|
+
// 空则退回硬编码默认 (0.0.0.0:17891)。放在 top-level 而不是塞进 daemon: svr 是独立进程,
|
|
189
|
+
// 与 daemon 生命周期解耦。
|
|
190
|
+
const Svr = z.object({
|
|
191
|
+
host: z.string().default("0.0.0.0"),
|
|
192
|
+
port: z.number().int().min(1).max(65535).default(17891),
|
|
193
|
+
stateDir: z.string().default("~/.weclaude/svr"),
|
|
194
|
+
tokenFile: z.string().default("~/.weclaude/svr-token"),
|
|
195
|
+
token: z.string().default(""),
|
|
196
|
+
logLevel: z.enum(["trace", "debug", "info", "warn", "error"]).default("info"),
|
|
197
|
+
});
|
|
198
|
+
// 事件订阅 / 广播。subs 是 topic → 目标id数组 (`user:xxx` / `chat:xxx`),
|
|
199
|
+
// 一个 topic 可以有多个订阅者; schedules 是「每天 HH:MM 触发某 topic」的定时任务。
|
|
200
|
+
// 订阅关系与定时通过 IM 命令 (订阅 / 每天HH:MM广播 …) 增删,写回 config.jsonc。
|
|
201
|
+
const Schedule = z.object({
|
|
202
|
+
topic: z.string(),
|
|
203
|
+
hour: z.number().int().min(0).max(23),
|
|
204
|
+
minute: z.number().int().min(0).max(59),
|
|
205
|
+
content: z.string(),
|
|
206
|
+
createdBy: z.string().default(""),
|
|
207
|
+
createdAt: z.number().default(0),
|
|
208
|
+
});
|
|
209
|
+
const Topics = z.object({
|
|
210
|
+
subs: z.record(z.string(), z.array(z.string())).default({}),
|
|
211
|
+
schedules: z.array(Schedule).default([]),
|
|
212
|
+
});
|
|
213
|
+
export const ConfigSchema = z.object({
|
|
214
|
+
bot: Bot,
|
|
215
|
+
defaultChat: z.string().default(""),
|
|
216
|
+
daemon: Daemon.default({}),
|
|
217
|
+
wrc: Wrc.default({}),
|
|
218
|
+
approval: Approval.default({}),
|
|
219
|
+
sync: Sync.default({ targets: [] }),
|
|
220
|
+
svr: Svr.default({}),
|
|
221
|
+
topics: Topics.default({ subs: {}, schedules: [] }),
|
|
222
|
+
});
|
|
223
|
+
// ── Loader ──────────────────────────────────────────────────────────
|
|
224
|
+
const DEFAULT_CONFIG_PATHS = [
|
|
225
|
+
"~/.weclaude/config.jsonc",
|
|
226
|
+
"~/.weclaude/config.json",
|
|
227
|
+
];
|
|
228
|
+
const SECRETS_PATH = "~/.weclaude/secrets.json";
|
|
229
|
+
const readJsoncIfExists = (p) => {
|
|
230
|
+
const abs = expandHome(p);
|
|
231
|
+
if (!existsSync(abs))
|
|
232
|
+
return undefined;
|
|
233
|
+
const text = readFileSync(abs, "utf8");
|
|
234
|
+
return parseJsonc(text);
|
|
235
|
+
};
|
|
236
|
+
const deepMerge = (a, b) => {
|
|
237
|
+
const out = { ...a };
|
|
238
|
+
for (const [k, v] of Object.entries(b ?? {})) {
|
|
239
|
+
const av = out[k];
|
|
240
|
+
if (v && typeof v === "object" && !Array.isArray(v) && av && typeof av === "object" && !Array.isArray(av)) {
|
|
241
|
+
out[k] = deepMerge(av, v);
|
|
242
|
+
}
|
|
243
|
+
else if (v !== undefined && v !== "") {
|
|
244
|
+
out[k] = v;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
};
|
|
249
|
+
/** Resolve config path: explicit > $WECLAUDE_CONFIG > defaults. */
|
|
250
|
+
const resolveConfigPath = (explicit) => {
|
|
251
|
+
if (explicit)
|
|
252
|
+
return explicit;
|
|
253
|
+
if (process.env.WECLAUDE_CONFIG)
|
|
254
|
+
return process.env.WECLAUDE_CONFIG;
|
|
255
|
+
for (const p of DEFAULT_CONFIG_PATHS) {
|
|
256
|
+
if (existsSync(expandHome(p)))
|
|
257
|
+
return p;
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
};
|
|
261
|
+
export const loadConfig = (explicitPath) => {
|
|
262
|
+
const sourcePath = resolveConfigPath(explicitPath);
|
|
263
|
+
if (!sourcePath) {
|
|
264
|
+
throw new Error(`weclaude config not found. Create ~/.weclaude/config.jsonc (see config.example.jsonc) or set $WECLAUDE_CONFIG.`);
|
|
265
|
+
}
|
|
266
|
+
const base = (readJsoncIfExists(sourcePath) ?? {});
|
|
267
|
+
const secrets = (readJsoncIfExists(SECRETS_PATH) ?? {});
|
|
268
|
+
const merged = deepMerge(base, secrets);
|
|
269
|
+
const parsed = ConfigSchema.safeParse(merged);
|
|
270
|
+
if (!parsed.success) {
|
|
271
|
+
const issues = parsed.error.issues
|
|
272
|
+
.map((i) => ` - ${i.path.join(".") || "<root>"}: ${i.message}`)
|
|
273
|
+
.join("\n");
|
|
274
|
+
throw new Error(`weclaude config invalid (${sourcePath}):\n${issues}`);
|
|
275
|
+
}
|
|
276
|
+
return { config: parsed.data, sourcePath: expandHome(sourcePath) };
|
|
277
|
+
};
|
|
278
|
+
//# sourceMappingURL=config.js.map
|