weclaude 0.0.4 → 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.
- package/LICENSE +1 -1
- package/README.md +34 -22
- package/cli/{wrc.sh → weclaude.sh} +34 -18
- package/commands/wrc.md +4 -4
- package/config.example.jsonc +6 -6
- package/dist/cli/init.js +10 -10
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/sync.js +35 -18
- package/dist/cli/sync.js.map +1 -1
- package/dist/daemon/approval.js +480 -36
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/cc-bridge.js +37 -20
- package/dist/daemon/cc-bridge.js.map +1 -1
- package/dist/daemon/claim.js +1 -1
- package/dist/daemon/claim.js.map +1 -1
- package/dist/daemon/detail.js +500 -0
- package/dist/daemon/detail.js.map +1 -0
- package/dist/daemon/http.js +2 -1
- package/dist/daemon/http.js.map +1 -1
- package/dist/daemon/inbound.js +115 -21
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +24 -7
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +972 -151
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/mirror-store.js +39 -0
- package/dist/daemon/mirror-store.js.map +1 -0
- package/dist/daemon/pending.js +46 -0
- package/dist/daemon/pending.js.map +1 -1
- package/dist/daemon/session-cache.js +71 -3
- package/dist/daemon/session-cache.js.map +1 -1
- package/dist/daemon/spawn-tmux.js +132 -0
- package/dist/daemon/spawn-tmux.js.map +1 -0
- package/dist/mcp/server.js +104 -65
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/config-writer.js +1 -1
- package/dist/shared/config.js +34 -20
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/paths.js +6 -0
- package/dist/shared/paths.js.map +1 -1
- package/docs/DESIGN-INIT.md +6 -6
- package/docs/ONBOARDING.md +25 -25
- package/hooks/pre-tool-use.sh +32 -7
- package/launchd/{com.cc-wecom.daemon.plist.template → com.weclaude.daemon.plist.template} +3 -3
- package/package.json +10 -11
- package/scripts/install.sh +6 -6
- package/scripts/uninstall.sh +3 -3
- package/systemd/{cc-wecom.service.template → weclaude.service.template} +3 -3
package/dist/mcp/server.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
// MCP server `
|
|
2
|
-
//
|
|
1
|
+
// MCP server `weclaude`. Stdio transport. Single tool `wrc` = "wecom remote
|
|
2
|
+
// control": attaches the *current* Claude session for WeCom mirror — session
|
|
3
|
+
// resolved via CLAUDE_CODE_SESSION_ID env (Claude Code populates this for
|
|
4
|
+
// every child process), so multiple windows can each /wrc without trampling.
|
|
3
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
9
|
+
import { join, basename } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
5
11
|
import { z } from "zod";
|
|
6
|
-
const DAEMON_BASE = process.env.
|
|
7
|
-
const post = async (path, body) => {
|
|
8
|
-
const r = await fetch(`${DAEMON_BASE}${path}`, {
|
|
9
|
-
method: "POST",
|
|
10
|
-
headers: { "content-type": "application/json" },
|
|
11
|
-
body: JSON.stringify(body),
|
|
12
|
-
});
|
|
13
|
-
return (await r.json().catch(() => ({})));
|
|
14
|
-
};
|
|
12
|
+
const DAEMON_BASE = process.env.WECLAUDE_DAEMON_BASE ?? "http://127.0.0.1:17890";
|
|
15
13
|
const ok = (data) => ({
|
|
16
14
|
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
17
15
|
});
|
|
@@ -19,63 +17,104 @@ const fail = (msg) => ({
|
|
|
19
17
|
isError: true,
|
|
20
18
|
content: [{ type: "text", text: msg }],
|
|
21
19
|
});
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
20
|
+
// claude encodes a project's cwd into a dir name by replacing each `/` with `-`
|
|
21
|
+
const encodeProjectDir = (absCwd) => absCwd.replace(/\//g, "-");
|
|
22
|
+
const findProjectDir = (cwd) => {
|
|
23
|
+
const enc = encodeProjectDir(cwd);
|
|
24
|
+
return [
|
|
25
|
+
join(homedir(), ".claude-internal", "projects", enc),
|
|
26
|
+
join(homedir(), ".claude", "projects", enc),
|
|
27
|
+
].find((p) => existsSync(p));
|
|
28
|
+
};
|
|
29
|
+
const latestJsonlByMtime = (projectDir) => {
|
|
30
|
+
const files = readdirSync(projectDir).filter((n) => n.endsWith(".jsonl"));
|
|
31
|
+
if (files.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
return files
|
|
34
|
+
.map((n) => ({ p: join(projectDir, n), m: statSync(join(projectDir, n)).mtimeMs }))
|
|
35
|
+
.reduce((a, b) => (b.m > a.m ? b : a)).p;
|
|
36
|
+
};
|
|
37
|
+
const resolveCallerSession = () => {
|
|
38
|
+
const cwd = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
39
|
+
const projectDir = findProjectDir(cwd);
|
|
40
|
+
if (!projectDir)
|
|
41
|
+
return { error: `no claude project dir for cwd ${cwd}` };
|
|
42
|
+
// Primary: env tells us exactly which session invoked us.
|
|
43
|
+
const envSid = process.env.CLAUDE_CODE_SESSION_ID ?? process.env.CLAUDE_SESSION_ID;
|
|
44
|
+
if (envSid) {
|
|
45
|
+
const p = join(projectDir, `${envSid}.jsonl`);
|
|
46
|
+
if (existsSync(p))
|
|
47
|
+
return { sessionId: envSid, jsonlPath: p };
|
|
48
|
+
}
|
|
49
|
+
// Fallback: most-recently-written jsonl. Imperfect when other Claude windows
|
|
50
|
+
// share the cwd, but the env-var path catches that case.
|
|
51
|
+
const jsonlPath = latestJsonlByMtime(projectDir);
|
|
52
|
+
if (!jsonlPath)
|
|
53
|
+
return { error: `no .jsonl under ${projectDir}` };
|
|
54
|
+
return { sessionId: basename(jsonlPath, ".jsonl"), jsonlPath };
|
|
55
|
+
};
|
|
56
|
+
// Resolve the current pane's tmux session name. `tmux display-message -p` runs
|
|
57
|
+
// against the tmux server pointed at by $TMUX (set in every process running
|
|
58
|
+
// inside tmux), so it returns the session containing *this* pane without us
|
|
59
|
+
// needing to pass a target. Returns undefined if not in tmux or query failed.
|
|
60
|
+
const detectTmuxSession = () => new Promise((resolve) => {
|
|
61
|
+
if (!process.env.TMUX)
|
|
62
|
+
return resolve(undefined);
|
|
63
|
+
const p = spawn("tmux", ["display-message", "-p", "#{session_name}"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
64
|
+
let out = "";
|
|
65
|
+
p.stdout?.on("data", (c) => (out += c.toString("utf8")));
|
|
66
|
+
p.on("error", () => resolve(undefined));
|
|
67
|
+
p.on("close", (code) => resolve(code === 0 ? out.trim() || undefined : undefined));
|
|
46
68
|
});
|
|
47
|
-
server
|
|
48
|
-
|
|
49
|
-
|
|
69
|
+
const server = new McpServer({ name: "weclaude", version: "0.0.1" }, { capabilities: { tools: {} } });
|
|
70
|
+
// Accept user-friendly prefixes from the LLM: vid:<id> → user:<id>, chatid:<id> → chat:<id>.
|
|
71
|
+
// Pass anything else (already user:/chat:/group:, or empty) through unchanged.
|
|
72
|
+
const normalizeTarget = (raw) => {
|
|
73
|
+
if (!raw)
|
|
74
|
+
return undefined;
|
|
75
|
+
if (raw.startsWith("vid:"))
|
|
76
|
+
return `user:${raw.slice(4)}`;
|
|
77
|
+
if (raw.startsWith("chatid:"))
|
|
78
|
+
return `chat:${raw.slice(7)}`;
|
|
79
|
+
return raw;
|
|
80
|
+
};
|
|
81
|
+
server.registerTool("wrc", {
|
|
82
|
+
title: "WeCom remote control",
|
|
83
|
+
description: "wecom remote control — attach current Claude session to a WeCom chat for live mirror push",
|
|
50
84
|
inputSchema: {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.array(z.object({ key: z.string(), label: z.string(), style: z.number().int().optional() }))
|
|
56
|
-
.min(1)
|
|
57
|
-
.max(6),
|
|
58
|
-
timeoutSec: z.number().int().positive().default(600),
|
|
85
|
+
target: z
|
|
86
|
+
.string()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('Optional push target. Accepts "vid:<userid>" (DM), "chatid:<chatid>" (group), or raw "user:<id>"/"chat:<id>". Empty → use config defaultChat / mirror.pushChat.'),
|
|
59
89
|
},
|
|
60
|
-
}, async ({
|
|
61
|
-
const r =
|
|
62
|
-
if (r
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
}, async ({ target }) => {
|
|
91
|
+
const r = resolveCallerSession();
|
|
92
|
+
if ("error" in r)
|
|
93
|
+
return fail(r.error);
|
|
94
|
+
const normalizedTarget = normalizeTarget(target);
|
|
95
|
+
// tmux sets $TMUX_PANE for every process inside a pane (e.g. `%5`); we
|
|
96
|
+
// inherit it through claude → MCP child, so each /wrc auto-picks its own
|
|
97
|
+
// pane without the user touching config. Pane ids are not stable across
|
|
98
|
+
// tmux server restarts, so we also capture the session name — the daemon
|
|
99
|
+
// uses it to re-derive a fresh paneId after reload, and as the "user wants
|
|
100
|
+
// a tmux pane" signal that drives respawn when their pane dies.
|
|
101
|
+
const tmuxPane = process.env.TMUX_PANE?.trim();
|
|
102
|
+
const tmuxSession = tmuxPane ? await detectTmuxSession() : undefined;
|
|
103
|
+
const resp = await fetch(`${DAEMON_BASE}/mirror/attach`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "content-type": "application/json" },
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
sessionId: r.sessionId,
|
|
108
|
+
jsonlPath: r.jsonlPath,
|
|
109
|
+
...(normalizedTarget ? { target: normalizedTarget } : {}),
|
|
110
|
+
...(tmuxPane ? { tmuxPane } : {}),
|
|
111
|
+
...(tmuxSession ? { tmuxSession } : {}),
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
const j = (await resp.json().catch(() => ({})));
|
|
115
|
+
return j.ok
|
|
116
|
+
? ok({ ok: true, sessionId: r.sessionId, target: j.target })
|
|
117
|
+
: fail(`attach failed: ${j.reason ?? "unknown"}`);
|
|
79
118
|
});
|
|
80
119
|
const transport = new StdioServerTransport();
|
|
81
120
|
await server.connect(transport);
|
package/dist/mcp/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../mcp/server.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../mcp/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Surgical edits to ~/.
|
|
1
|
+
// Surgical edits to ~/.weclaude/config.jsonc and secrets.json — preserves
|
|
2
2
|
// comments + formatting via jsonc-parser's `modify` patches.
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname } from "node:path";
|
package/dist/shared/config.js
CHANGED
|
@@ -12,9 +12,14 @@ const Bot = z.object({
|
|
|
12
12
|
const Daemon = z.object({
|
|
13
13
|
host: z.string().default("127.0.0.1"),
|
|
14
14
|
port: z.number().int().min(1).max(65535).default(17890),
|
|
15
|
-
stateDir: z.string().default("~/.
|
|
16
|
-
logFile: z.string().default("~/.
|
|
15
|
+
stateDir: z.string().default("~/.weclaude/state"),
|
|
16
|
+
logFile: z.string().default("~/.weclaude/daemon.log"),
|
|
17
17
|
logLevel: z.enum(["trace", "debug", "info", "warn", "error"]).default("info"),
|
|
18
|
+
// 工具调用 / 授权详情页 URL。空则用 http://<host>:<port> (回环)。
|
|
19
|
+
// 想让手机 WeCom 也能点开, 需要在反向代理后填外网地址。桌面端用回环即可。
|
|
20
|
+
detailPublicBase: z.string().default(""),
|
|
21
|
+
// 镜像消息里给每个 tool_use 行包成 markdown 链接, 点开本地 HTML 详情页。
|
|
22
|
+
detailLinksInMirror: z.boolean().default(true),
|
|
18
23
|
});
|
|
19
24
|
const Mirror = z.object({
|
|
20
25
|
// Where Claude Code writes per-project transcripts. Forks like `claude-internal`
|
|
@@ -24,17 +29,14 @@ const Mirror = z.object({
|
|
|
24
29
|
// Pin a specific Claude session to mirror. Empty → auto-pick latest .jsonl
|
|
25
30
|
// under `<projectsDir>/<encoded(wrc.cwd)>/` by mtime.
|
|
26
31
|
sessionId: z.string().default(""),
|
|
27
|
-
// Inbound (WeCom→CLI) inject target. Empty → spawn `claude --resume -p`
|
|
28
|
-
// (response lands in jsonl but NOT in the live TTY). Set to a tmux target
|
|
29
|
-
// (e.g. "_cc-wecom" or "_cc-wecom:0.0") to paste into the live interactive
|
|
30
|
-
// claude — the only way to make WeCom messages visible in the CLI window.
|
|
31
|
-
tmuxTarget: z.string().default(""),
|
|
32
32
|
// Where to push live assistant output. Empty → fall back to defaultChat.
|
|
33
33
|
pushChat: z.string().default(""),
|
|
34
34
|
// Cap a single push payload (WeCom markdown ~2048 limit). Long replies are split.
|
|
35
35
|
chunkChars: z.number().int().positive().default(1800),
|
|
36
|
-
// Mirror the user's CLI prompts (type:"user" with string content).
|
|
37
|
-
|
|
36
|
+
// Mirror the user's CLI prompts (type:"user" with string content). Off by
|
|
37
|
+
// default: WeCom-sourced inbounds get dedup'd anyway, and local CLI typing
|
|
38
|
+
// is rare in the bot-driven flow — keeping it on mostly produced echo noise.
|
|
39
|
+
includeUser: z.boolean().default(false),
|
|
38
40
|
// Mirror assistant tool_use blocks (Bash/Edit/Read/...).
|
|
39
41
|
includeTools: z.boolean().default(true),
|
|
40
42
|
// Mirror tool_result blocks. Off by default — usually noisy.
|
|
@@ -43,16 +45,28 @@ const Mirror = z.object({
|
|
|
43
45
|
toolResultMaxChars: z.number().int().positive().default(400),
|
|
44
46
|
// Where inbound images/files from WeCom get saved before being pasted into
|
|
45
47
|
// the live TTY. Files persist — claude reads them by absolute path.
|
|
46
|
-
inboxDir: z.string().default("~/.
|
|
48
|
+
inboxDir: z.string().default("~/.weclaude/inbox"),
|
|
49
|
+
// Persisted mirror attachments (principal → sessionId/jsonl/tmux). Restored
|
|
50
|
+
// on daemon boot + lazily on first inbound after reload — so reloading the
|
|
51
|
+
// daemon doesn't re-spawn a fresh claude for an already-bound chat.
|
|
52
|
+
attachmentsFile: z.string().default("~/.weclaude/mirror-attachments.json"),
|
|
53
|
+
// Standalone fallback 路径(liveStream 已 closed/dead/capped) 上的防抖聚合窗口
|
|
54
|
+
// (ms)。窗口内同一 attachment 的多个 item 合并为一条 markdown, 抑制连续工具
|
|
55
|
+
// 调用刷屏。liveStream 仍活时不受影响——直接走 typewriter。0 = 关闭。
|
|
56
|
+
standaloneDebounceMs: z.number().int().nonnegative().default(3000),
|
|
47
57
|
});
|
|
48
58
|
const Wrc = z.object({
|
|
49
59
|
allowFrom: z.array(z.string()).default([]),
|
|
50
60
|
mode: z.enum(["headless", "mirror"]).default("headless"),
|
|
51
61
|
claudeBin: z.string().default("claude"),
|
|
52
|
-
cwd: z.string().default("
|
|
53
|
-
sessionMapFile: z.string().default("~/.
|
|
62
|
+
cwd: z.string().default("~/.weclaude/workspace"),
|
|
63
|
+
sessionMapFile: z.string().default("~/.weclaude/sessions.json"),
|
|
54
64
|
extraArgs: z.array(z.string()).default([]),
|
|
55
65
|
mirror: Mirror.default({}),
|
|
66
|
+
// Mirror-only: tmux session name prefix for auto-spawn. Final name is
|
|
67
|
+
// `${prefix}-<short>`. Auto-spawn fires when an authorized inbound finds no
|
|
68
|
+
// mirror attached for that chat — allowFrom IS the authorization.
|
|
69
|
+
tmuxPrefix: z.string().default("weclaude"),
|
|
56
70
|
});
|
|
57
71
|
const Approval = z.object({
|
|
58
72
|
enabled: z.boolean().default(true),
|
|
@@ -83,10 +97,10 @@ export const ConfigSchema = z.object({
|
|
|
83
97
|
});
|
|
84
98
|
// ── Loader ──────────────────────────────────────────────────────────
|
|
85
99
|
const DEFAULT_CONFIG_PATHS = [
|
|
86
|
-
"~/.
|
|
87
|
-
"~/.
|
|
100
|
+
"~/.weclaude/config.jsonc",
|
|
101
|
+
"~/.weclaude/config.json",
|
|
88
102
|
];
|
|
89
|
-
const SECRETS_PATH = "~/.
|
|
103
|
+
const SECRETS_PATH = "~/.weclaude/secrets.json";
|
|
90
104
|
const readJsoncIfExists = (p) => {
|
|
91
105
|
const abs = expandHome(p);
|
|
92
106
|
if (!existsSync(abs))
|
|
@@ -107,12 +121,12 @@ const deepMerge = (a, b) => {
|
|
|
107
121
|
}
|
|
108
122
|
return out;
|
|
109
123
|
};
|
|
110
|
-
/** Resolve config path: explicit > $
|
|
124
|
+
/** Resolve config path: explicit > $WECLAUDE_CONFIG > defaults. */
|
|
111
125
|
const resolveConfigPath = (explicit) => {
|
|
112
126
|
if (explicit)
|
|
113
127
|
return explicit;
|
|
114
|
-
if (process.env.
|
|
115
|
-
return process.env.
|
|
128
|
+
if (process.env.WECLAUDE_CONFIG)
|
|
129
|
+
return process.env.WECLAUDE_CONFIG;
|
|
116
130
|
for (const p of DEFAULT_CONFIG_PATHS) {
|
|
117
131
|
if (existsSync(expandHome(p)))
|
|
118
132
|
return p;
|
|
@@ -122,7 +136,7 @@ const resolveConfigPath = (explicit) => {
|
|
|
122
136
|
export const loadConfig = (explicitPath) => {
|
|
123
137
|
const sourcePath = resolveConfigPath(explicitPath);
|
|
124
138
|
if (!sourcePath) {
|
|
125
|
-
throw new Error(`
|
|
139
|
+
throw new Error(`weclaude config not found. Create ~/.weclaude/config.jsonc (see config.example.jsonc) or set $WECLAUDE_CONFIG.`);
|
|
126
140
|
}
|
|
127
141
|
const base = (readJsoncIfExists(sourcePath) ?? {});
|
|
128
142
|
const secrets = (readJsoncIfExists(SECRETS_PATH) ?? {});
|
|
@@ -132,7 +146,7 @@ export const loadConfig = (explicitPath) => {
|
|
|
132
146
|
const issues = parsed.error.issues
|
|
133
147
|
.map((i) => ` - ${i.path.join(".") || "<root>"}: ${i.message}`)
|
|
134
148
|
.join("\n");
|
|
135
|
-
throw new Error(`
|
|
149
|
+
throw new Error(`weclaude config invalid (${sourcePath}):\n${issues}`);
|
|
136
150
|
}
|
|
137
151
|
return { config: parsed.data, sourcePath: expandHome(sourcePath) };
|
|
138
152
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../shared/config.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,KAAK,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,uEAAuE;AACvE,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC;IACnB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACxB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACzB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,iCAAiC,CAAC;CAC1E,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;IACtB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC;IACrC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IACvD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,mBAAmB,CAAC;IACjD,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,wBAAwB,CAAC;IACrD,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../shared/config.ts"],"names":[],"mappings":"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"}
|
package/dist/shared/paths.js
CHANGED
|
@@ -3,4 +3,10 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
/** Expand leading `~` and normalize. */
|
|
5
5
|
export const expandHome = (p) => p.startsWith("~") ? resolve(homedir(), p.slice(p.startsWith("~/") ? 2 : 1)) : resolve(p);
|
|
6
|
+
// IM principals ("user:xxx" / "chat:xxx") get pasted around — copy-paste from
|
|
7
|
+
// WeCom UI sometimes drags along zero-width / invisible-separator chars
|
|
8
|
+
// (U+200B-U+200D, U+2060-U+206F, U+FEFF). They render identical but break
|
|
9
|
+
// `===` matches between allowFrom and the wire-side principal.
|
|
10
|
+
const INVISIBLE = /[--]/g;
|
|
11
|
+
export const sanitizeId = (s) => s.replace(INVISIBLE, "").trim();
|
|
6
12
|
//# sourceMappingURL=paths.js.map
|
package/dist/shared/paths.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../shared/paths.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,wCAAwC;AACxC,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAS,EAAU,EAAE,CAC9C,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../shared/paths.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,wCAAwC;AACxC,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAS,EAAU,EAAE,CAC9C,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAE3F,8EAA8E;AAC9E,wEAAwE;AACxE,0EAA0E;AAC1E,+DAA+D;AAC/D,MAAM,SAAS,GAAG,YAAY,CAAC;AAC/B,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC"}
|
package/docs/DESIGN-INIT.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
如果 `config.jsonc` 整个塞进 dotfile,`secret` 就会进 git。
|
|
26
26
|
|
|
27
|
-
**方案**:复用 `loadConfig()` 里早就有的 `secrets.json` deep-merge 钩子。init 时把 `botId/secret` 写到 `~/.
|
|
27
|
+
**方案**:复用 `loadConfig()` 里早就有的 `secrets.json` deep-merge 钩子。init 时把 `botId/secret` 写到 `~/.weclaude/secrets.json`,其他写 `config.jsonc`。两份文件运行时合并。`config.jsonc` 可入 dotfiles,`secrets.json` 留本机。
|
|
28
28
|
|
|
29
29
|
### 3. 配置写入要保留注释
|
|
30
30
|
|
|
@@ -91,7 +91,7 @@ CLAIMED (cleared) (cleared)
|
|
|
91
91
|
## 演示选什么 prompt
|
|
92
92
|
|
|
93
93
|
```
|
|
94
|
-
1. Bash: echo hello world from
|
|
94
|
+
1. Bash: echo hello world from weclaude
|
|
95
95
|
2. Bash: sleep 3
|
|
96
96
|
3. wecom__send_markdown chat="<defaultChat>" content="✅ ..."
|
|
97
97
|
```
|
|
@@ -109,7 +109,7 @@ prompt 里**显式拼出 chat id**而不是依赖 LLM 推断「default chat」
|
|
|
109
109
|
| 场景 | init 行为 | 状态可恢复性 |
|
|
110
110
|
| --- | --- | --- |
|
|
111
111
|
| 用户 Ctrl-C 在 prompt 中 | 进程退出,未写任何文件 | ✅ 完全干净 |
|
|
112
|
-
| 写完 config 但 daemon 起不来 | 抛 `daemon did not become ready` | ✅ config 保留,下次 `
|
|
112
|
+
| 写完 config 但 daemon 起不来 | 抛 `daemon did not become ready` | ✅ config 保留,下次 `weclaude reload` 即可 |
|
|
113
113
|
| Claim 超时 | 报「超时未收到消息」并退出 | ✅ daemon 还在跑,可手填 config 或重跑 init |
|
|
114
114
|
| 演示 spawn `claude` 失败 | 红字打印 spawn 错误,正常退出 | ✅ 前两步成果都保留 |
|
|
115
115
|
|
|
@@ -119,7 +119,7 @@ prompt 里**显式拼出 chat id**而不是依赖 LLM 推断「default chat」
|
|
|
119
119
|
|
|
120
120
|
不在本次范围,但留给未来:
|
|
121
121
|
|
|
122
|
-
- `
|
|
122
|
+
- `weclaude init --reset` 一键清空(unsync + uninstall daemon + 删 ~/.weclaude)
|
|
123
123
|
- claim 暗号支持自定义(防多人共用同一台 daemon 的暗号冲突)
|
|
124
|
-
- 演示步骤后做一次 `
|
|
125
|
-
- ink 化的 daemon dashboard(独立工具,`
|
|
124
|
+
- 演示步骤后做一次 `weclaude status` 的彩色断言,给用户一个「绿色对勾合集」
|
|
125
|
+
- ink 化的 daemon dashboard(独立工具,`weclaude tui`)
|
package/docs/ONBOARDING.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# weclaude 上手指南
|
|
2
2
|
|
|
3
3
|
3 步在一台新机器上把「IM 授权转发 + 远程 CC 控制」跑通。
|
|
4
4
|
|
|
@@ -13,20 +13,20 @@
|
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
# 方式 A:npm 全局安装(推荐)
|
|
16
|
-
npm install -g
|
|
17
|
-
|
|
16
|
+
npm install -g weclaude
|
|
17
|
+
weclaude init
|
|
18
18
|
|
|
19
19
|
# 方式 B:免安装一次性试用
|
|
20
|
-
npx
|
|
20
|
+
npx weclaude init
|
|
21
21
|
|
|
22
22
|
# 方式 C:本地开发
|
|
23
|
-
git clone <repo> && cd
|
|
23
|
+
git clone <repo> && cd weclaude
|
|
24
24
|
npm install && npm run build
|
|
25
|
-
./cli/
|
|
25
|
+
./cli/weclaude.sh init
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
> ⚠️ 卸载时**先**跑 `
|
|
29
|
-
> `~/.
|
|
28
|
+
> ⚠️ 卸载时**先**跑 `weclaude uninstall`(unsync settings.json + 卸载 daemon),**再** `npm uninstall -g weclaude`。否则 launchd/systemd 会反复尝试启动已删除的二进制。
|
|
29
|
+
> `~/.weclaude/` 下的 config/secrets/state 不会被清掉,二次安装可无缝复用。
|
|
30
30
|
|
|
31
31
|
### [1/3] 采集配置
|
|
32
32
|
|
|
@@ -34,10 +34,10 @@ npm install && npm run build
|
|
|
34
34
|
|
|
35
35
|
| 字段 | 写到 |
|
|
36
36
|
| --- | --- |
|
|
37
|
-
| botId | `~/.
|
|
38
|
-
| secret | `~/.
|
|
39
|
-
| Claude agent | `~/.
|
|
40
|
-
| 是否启用 PreToolUse hook | `~/.
|
|
37
|
+
| botId | `~/.weclaude/secrets.json` |
|
|
38
|
+
| secret | `~/.weclaude/secrets.json` |
|
|
39
|
+
| Claude agent | `~/.weclaude/config.jsonc` (`wrc.claudeBin` + `sync.targets[0].settingsPath`) |
|
|
40
|
+
| 是否启用 PreToolUse hook | `~/.weclaude/config.jsonc` (`approval.enabled`) |
|
|
41
41
|
|
|
42
42
|
Agent 选项:
|
|
43
43
|
|
|
@@ -49,7 +49,7 @@ Agent 选项:
|
|
|
49
49
|
|
|
50
50
|
完成后自动:
|
|
51
51
|
1. `npx tsc` 编译 dist
|
|
52
|
-
2. `
|
|
52
|
+
2. `weclaude sync` 把 hook / MCP / env 写进选定的 settings.json
|
|
53
53
|
3. 安装常驻 daemon(macOS launchd / Linux systemd --user)
|
|
54
54
|
4. 等 WebSocket 鉴权通过
|
|
55
55
|
|
|
@@ -75,7 +75,7 @@ Daemon 在内存里临时打开 claim 窗口(10 分钟)。这条消息精确
|
|
|
75
75
|
CLI 自动 `spawn claude -p` 跑一条三步指令:
|
|
76
76
|
|
|
77
77
|
```
|
|
78
|
-
1. Bash: echo hello world from
|
|
78
|
+
1. Bash: echo hello world from weclaude ← 触发 PreToolUse hook
|
|
79
79
|
2. Bash: sleep 3 ← 再触发一次
|
|
80
80
|
3. wecom__send_markdown ← 通过 MCP 主动推送结果
|
|
81
81
|
```
|
|
@@ -85,34 +85,34 @@ CLI 自动 `spawn claude -p` 跑一条三步指令:
|
|
|
85
85
|
- IM 收到按钮卡片:`授权请求: Bash`
|
|
86
86
|
- 点 ✅ → 卡片就地刷新成 `✅ Bash · 已允许`
|
|
87
87
|
- 3 秒后再来一张卡片(或被 5 分钟自动窗口短路)
|
|
88
|
-
- 最终收到 `✅
|
|
88
|
+
- 最终收到 `✅ weclaude 演示完成:hello world`
|
|
89
89
|
|
|
90
90
|
如果第 1 步选「不开 hook」,会跳过演示。
|
|
91
91
|
|
|
92
92
|
## 完成后
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
weclaude status # daemon + WS 健康
|
|
96
|
+
weclaude logs -f # 实时日志
|
|
97
|
+
weclaude send <chat> <text> # 主动推消息
|
|
98
|
+
weclaude unsync # 卸载 hook/MCP(保留 daemon)
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
## 排错
|
|
102
102
|
|
|
103
103
|
| 现象 | 处理 |
|
|
104
104
|
| --- | --- |
|
|
105
|
-
| `daemon: down` | `
|
|
106
|
-
| 发完暗号 daemon 没反应 | 检查 `
|
|
107
|
-
| Hook 不触发 | `cat ~/.claude/settings.json | jq .hooks.PreToolUse`;`
|
|
105
|
+
| `daemon: down` | `weclaude reload`;看 `~/.weclaude/daemon.stderr.log` |
|
|
106
|
+
| 发完暗号 daemon 没反应 | 检查 `weclaude status` 的 `wsConnected` 是不是 true;机器人 `secret` 错会卡在 auth |
|
|
107
|
+
| Hook 不触发 | `cat ~/.claude/settings.json | jq .hooks.PreToolUse`;`weclaude sync` 重写 |
|
|
108
108
|
| 卡片点了没反应 | 5 秒内才能就地更新;超时是正常情况,决策仍然生效 |
|
|
109
|
-
| MCP 调用 404 | `mcpServers.wecom._managedBy=="
|
|
109
|
+
| MCP 调用 404 | `mcpServers.wecom._managedBy=="weclaude"` 应在 settings.json;`weclaude sync` 修复 |
|
|
110
110
|
|
|
111
111
|
## 多机部署
|
|
112
112
|
|
|
113
113
|
`config.jsonc` 可以纳入 dotfiles;`secrets.json` 每台机器独立填。`init` 跑过一次后,第二台机器:
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
|
-
cp ~/dotfiles/
|
|
117
|
-
./cli/
|
|
116
|
+
cp ~/dotfiles/weclaude-config.jsonc ~/.weclaude/config.jsonc
|
|
117
|
+
./cli/weclaude.sh init # 跳过覆盖提示,但仍走 claim 步骤拿到本机的 IM principal
|
|
118
118
|
```
|
package/hooks/pre-tool-use.sh
CHANGED
|
@@ -1,18 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
2
|
+
# weclaude PreToolUse hook: forward to local daemon → long-poll → emit decision.
|
|
3
3
|
# Any failure → ask (never break workflow).
|
|
4
4
|
set -uo pipefail
|
|
5
5
|
|
|
6
|
-
DAEMON_URL="${
|
|
7
|
-
HOOK_TIMEOUT="${
|
|
6
|
+
DAEMON_URL="${WECLAUDE_DAEMON_URL:-http://127.0.0.1:17890/approve}"
|
|
7
|
+
HOOK_TIMEOUT="${WECLAUDE_HOOK_TIMEOUT:-1810}"
|
|
8
|
+
STATE_DIR="${WECLAUDE_STATE_DIR:-$HOME/.weclaude/state}"
|
|
9
|
+
# Fallback policy when the daemon is unreachable / replies garbage. ask|allow|deny.
|
|
10
|
+
# Default keeps the safe behavior; set to `allow` in trusted local-only setups.
|
|
11
|
+
FALLBACK="${WECLAUDE_HOOK_FALLBACK:-ask}"
|
|
8
12
|
|
|
9
13
|
emit() {
|
|
10
14
|
local decision="$1" reason="${2:-}"
|
|
11
|
-
printf '%s\n' "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"${decision}\",\"permissionDecisionReason\":\"
|
|
15
|
+
printf '%s\n' "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"${decision}\",\"permissionDecisionReason\":\"weclaude: ${reason}\"}}"
|
|
12
16
|
exit 0
|
|
13
17
|
}
|
|
14
18
|
ask() { emit "ask" "${1:-bridge unreachable}"; }
|
|
15
19
|
|
|
20
|
+
# Daemon-down fallback. Consults the persisted auto-approve window first so a
|
|
21
|
+
# session-level "allow N min" survives daemon restart / outage; otherwise falls
|
|
22
|
+
# back to FALLBACK. SESSION_ID must already be parsed.
|
|
23
|
+
bridge_down() {
|
|
24
|
+
local reason="$1"
|
|
25
|
+
local sf="$STATE_DIR/auto-windows.json"
|
|
26
|
+
if [[ -n "${SESSION_ID:-}" && -r "$sf" ]] && command -v jq >/dev/null 2>&1; then
|
|
27
|
+
local active
|
|
28
|
+
active=$(jq -r --arg s "$SESSION_ID" \
|
|
29
|
+
'if (.windows[$s].until // 0) > (now * 1000) then "1" else "0" end' \
|
|
30
|
+
"$sf" 2>/dev/null) || active=""
|
|
31
|
+
if [[ "$active" == "1" ]]; then
|
|
32
|
+
emit "allow" "auto-window (offline): $reason"
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
case "$FALLBACK" in
|
|
36
|
+
allow|deny|ask) emit "$FALLBACK" "$reason" ;;
|
|
37
|
+
*) emit "ask" "$reason" ;;
|
|
38
|
+
esac
|
|
39
|
+
}
|
|
40
|
+
|
|
16
41
|
PAYLOAD=$(cat) || ask "stdin read failed"
|
|
17
42
|
command -v jq >/dev/null 2>&1 || ask "jq missing"
|
|
18
43
|
|
|
@@ -70,12 +95,12 @@ BODY=$(jq -nc \
|
|
|
70
95
|
RESP=$(curl -sS --max-time "$HOOK_TIMEOUT" \
|
|
71
96
|
-H 'Content-Type: application/json' \
|
|
72
97
|
-d "$BODY" \
|
|
73
|
-
"$DAEMON_URL" 2>/dev/null) ||
|
|
98
|
+
"$DAEMON_URL" 2>/dev/null) || bridge_down "daemon curl failed"
|
|
74
99
|
|
|
75
|
-
DECISION=$(printf '%s' "$RESP" | jq -r '.decision // "ask"' 2>/dev/null) ||
|
|
100
|
+
DECISION=$(printf '%s' "$RESP" | jq -r '.decision // "ask"' 2>/dev/null) || bridge_down "bad daemon response"
|
|
76
101
|
REASON=$(printf '%s' "$RESP" | jq -r '.reason // ""' 2>/dev/null)
|
|
77
102
|
|
|
78
103
|
case "$DECISION" in
|
|
79
104
|
allow|deny|ask) emit "$DECISION" "$REASON" ;;
|
|
80
|
-
*)
|
|
105
|
+
*) bridge_down "unknown decision: $DECISION" ;;
|
|
81
106
|
esac
|