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.
Files changed (117) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +14 -0
  3. package/CLAUDE.md +83 -0
  4. package/LICENSE +21 -0
  5. package/README.md +410 -0
  6. package/cli/weclaude.sh +236 -0
  7. package/commands/audit.md +7 -0
  8. package/commands/wrc.md +7 -0
  9. package/config.example.jsonc +102 -0
  10. package/dist/cli/audit.js +156 -0
  11. package/dist/cli/audit.js.map +1 -0
  12. package/dist/cli/init.js +311 -0
  13. package/dist/cli/init.js.map +1 -0
  14. package/dist/cli/sync.js +175 -0
  15. package/dist/cli/sync.js.map +1 -0
  16. package/dist/daemon/approval.js +1301 -0
  17. package/dist/daemon/approval.js.map +1 -0
  18. package/dist/daemon/ask.js +101 -0
  19. package/dist/daemon/ask.js.map +1 -0
  20. package/dist/daemon/audit.js +168 -0
  21. package/dist/daemon/audit.js.map +1 -0
  22. package/dist/daemon/cc-bridge.js +204 -0
  23. package/dist/daemon/cc-bridge.js.map +1 -0
  24. package/dist/daemon/cfg-sync.js +240 -0
  25. package/dist/daemon/cfg-sync.js.map +1 -0
  26. package/dist/daemon/claim.js +95 -0
  27. package/dist/daemon/claim.js.map +1 -0
  28. package/dist/daemon/detail.js +153 -0
  29. package/dist/daemon/detail.js.map +1 -0
  30. package/dist/daemon/graph.js +176 -0
  31. package/dist/daemon/graph.js.map +1 -0
  32. package/dist/daemon/http.js +83 -0
  33. package/dist/daemon/http.js.map +1 -0
  34. package/dist/daemon/inbound.js +919 -0
  35. package/dist/daemon/inbound.js.map +1 -0
  36. package/dist/daemon/index.js +569 -0
  37. package/dist/daemon/index.js.map +1 -0
  38. package/dist/daemon/last-response.js +59 -0
  39. package/dist/daemon/last-response.js.map +1 -0
  40. package/dist/daemon/mirror-bridge.js +3512 -0
  41. package/dist/daemon/mirror-bridge.js.map +1 -0
  42. package/dist/daemon/mirror-store.js +39 -0
  43. package/dist/daemon/mirror-store.js.map +1 -0
  44. package/dist/daemon/outbound.js +42 -0
  45. package/dist/daemon/outbound.js.map +1 -0
  46. package/dist/daemon/peers.js +146 -0
  47. package/dist/daemon/peers.js.map +1 -0
  48. package/dist/daemon/pending.js +90 -0
  49. package/dist/daemon/pending.js.map +1 -0
  50. package/dist/daemon/quota.js +164 -0
  51. package/dist/daemon/quota.js.map +1 -0
  52. package/dist/daemon/redact.js +27 -0
  53. package/dist/daemon/redact.js.map +1 -0
  54. package/dist/daemon/session-cache.js +149 -0
  55. package/dist/daemon/session-cache.js.map +1 -0
  56. package/dist/daemon/session-scan.js +273 -0
  57. package/dist/daemon/session-scan.js.map +1 -0
  58. package/dist/daemon/sessions.js +35 -0
  59. package/dist/daemon/sessions.js.map +1 -0
  60. package/dist/daemon/spawn-tmux.js +213 -0
  61. package/dist/daemon/spawn-tmux.js.map +1 -0
  62. package/dist/daemon/tips.js +25 -0
  63. package/dist/daemon/tips.js.map +1 -0
  64. package/dist/daemon/topics.js +197 -0
  65. package/dist/daemon/topics.js.map +1 -0
  66. package/dist/daemon/usage.js +358 -0
  67. package/dist/daemon/usage.js.map +1 -0
  68. package/dist/daemon/wedoc.js +210 -0
  69. package/dist/daemon/wedoc.js.map +1 -0
  70. package/dist/daemon/ws.js +67 -0
  71. package/dist/daemon/ws.js.map +1 -0
  72. package/dist/mcp/server.js +385 -0
  73. package/dist/mcp/server.js.map +1 -0
  74. package/dist/shared/ansi.js +134 -0
  75. package/dist/shared/ansi.js.map +1 -0
  76. package/dist/shared/chat-http.js +195 -0
  77. package/dist/shared/chat-http.js.map +1 -0
  78. package/dist/shared/chat-render.js +53 -0
  79. package/dist/shared/chat-render.js.map +1 -0
  80. package/dist/shared/chat-view.js +116 -0
  81. package/dist/shared/chat-view.js.map +1 -0
  82. package/dist/shared/cli-backends.js +304 -0
  83. package/dist/shared/cli-backends.js.map +1 -0
  84. package/dist/shared/config-writer.js +39 -0
  85. package/dist/shared/config-writer.js.map +1 -0
  86. package/dist/shared/config.js +278 -0
  87. package/dist/shared/config.js.map +1 -0
  88. package/dist/shared/detail-render.js +836 -0
  89. package/dist/shared/detail-render.js.map +1 -0
  90. package/dist/shared/detail-store.js +205 -0
  91. package/dist/shared/detail-store.js.map +1 -0
  92. package/dist/shared/highlight.js +154 -0
  93. package/dist/shared/highlight.js.map +1 -0
  94. package/dist/shared/lan-ip.js +35 -0
  95. package/dist/shared/lan-ip.js.map +1 -0
  96. package/dist/shared/log.js +18 -0
  97. package/dist/shared/log.js.map +1 -0
  98. package/dist/shared/md-chunk.js +99 -0
  99. package/dist/shared/md-chunk.js.map +1 -0
  100. package/dist/shared/paths.js +12 -0
  101. package/dist/shared/paths.js.map +1 -0
  102. package/dist/shared/session-label.js +67 -0
  103. package/dist/shared/session-label.js.map +1 -0
  104. package/dist/shared/web-assets.js +51 -0
  105. package/dist/shared/web-assets.js.map +1 -0
  106. package/dist/svr/index.js +246 -0
  107. package/dist/svr/index.js.map +1 -0
  108. package/hooks/hooks.json +16 -0
  109. package/hooks/pre-tool-use.sh +167 -0
  110. package/launchd/com.weclaude.daemon.plist.template +45 -0
  111. package/package.json +97 -0
  112. package/scripts/install.sh +73 -0
  113. package/scripts/postinstall.sh +12 -0
  114. package/scripts/uninstall.sh +44 -0
  115. package/systemd/weclaude.service.template +17 -0
  116. package/web/chat.css +171 -0
  117. package/web/chat.js +367 -0
@@ -0,0 +1,12 @@
1
+ // Path utilities — kept tiny and pure.
2
+ import { homedir } from "node:os";
3
+ import { resolve } from "node:path";
4
+ /** Expand leading `~` and normalize. */
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();
12
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +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;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"}
@@ -0,0 +1,67 @@
1
+ // Stable per-session visual tag. Hash a sessionId to a fixed animal emoji so
2
+ // the same Claude session always shows the same icon — on approval cards and
3
+ // in the /sessions list — letting the user tell sibling sessions apart when
4
+ // several un-mirrored sessions all fall back to the same WeCom chat.
5
+ //
6
+ // Stateless + deterministic: same sessionId → same emoji across daemon
7
+ // restarts, no persistence needed.
8
+ const ANIMALS = [
9
+ "🦊", "🐬", "🦄", "🐙", "🦉", "🐢", "🦋", "🐝",
10
+ "🐳", "🦁", "🐯", "🐰", "🦝", "🐼", "🐨", "🦓",
11
+ "🦔", "🦇", "🐧", "🦜", "🦩", "🐸", "🐺", "🦅",
12
+ "🐡", "🦞", "🦗", "🐌", "🦚", "🐲",
13
+ ];
14
+ // FNV-1a — small, fast, good spread for short ascii ids.
15
+ const hash = (s) => {
16
+ let h = 0x811c9dc5;
17
+ for (let i = 0; i < s.length; i++) {
18
+ h ^= s.charCodeAt(i);
19
+ h = Math.imul(h, 0x01000193);
20
+ }
21
+ return h >>> 0;
22
+ };
23
+ export const labelFor = (sessionId) => {
24
+ if (!sessionId)
25
+ return "❔";
26
+ return ANIMALS[hash(sessionId) % ANIMALS.length] ?? "❔";
27
+ };
28
+ // ── Target-key tagging ──────────────────────────────────────────────────
29
+ // Daemon-internal target keys are `user:xxx[#tag]` / `chat:xxx[#tag]`. Every
30
+ // user-visible artifact bound to a TAGGED session must carry the same visual
31
+ // so a chat hosting parallel sessions stays disambiguated:
32
+ // • markdown bubbles → `emoji \`#tag\`` header line (withTagHeader)
33
+ // • card titles → `emoji ` badge (tagBadge)
34
+ // Untagged (default session) passes through unchanged — single-session users
35
+ // see no extra glyphs. Emoji is keyed on the TAG STRING, not the sessionId,
36
+ // so it survives `/clear` rotations.
37
+ /** `#tag` suffix of a target key, "" when untagged. */
38
+ export const tagOfKey = (target) => {
39
+ if (!target)
40
+ return "";
41
+ const h = target.indexOf("#");
42
+ return h >= 0 ? target.slice(h + 1) : "";
43
+ };
44
+ /** Drop the `#tag` suffix — collapses a tagged session key to the chat-scoped
45
+ * base principal (`user:xxx#foo` → `user:xxx`). Everything shared across a
46
+ * chat's sessions (cwd, peer discovery, graph runs) keys off this. */
47
+ export const baseOfKey = (target) => {
48
+ const h = target.indexOf("#");
49
+ return h >= 0 ? target.slice(0, h) : target;
50
+ };
51
+ /** Compose a session key from a base principal and a tag ("" → default session). */
52
+ export const keyOf = (base, tag) => (tag ? `${base}#${tag}` : base);
53
+ /** Trailing-space emoji badge for card titles; "" for untagged targets. */
54
+ export const tagBadge = (target) => {
55
+ const tag = tagOfKey(target);
56
+ return tag ? `${labelFor(tag)} ` : "";
57
+ };
58
+ /** Prefix markdown content with the `emoji \`#tag\`` header; identity when untagged.
59
+ * `seq` ("2/5") marks one piece of a split push — it rides in the same header
60
+ * line so every chunk of a long reply is attributable on its own, and shows
61
+ * alone when the session is untagged. */
62
+ export const withTagHeader = (target, content, seq) => {
63
+ const tag = tagOfKey(target);
64
+ const head = [tag ? `${labelFor(tag)} \`#${tag}\`` : "", seq ? `\`${seq}\`` : ""].filter(Boolean).join(" ");
65
+ return head ? `${head}\n\n${content}` : content;
66
+ };
67
+ //# sourceMappingURL=session-label.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-label.js","sourceRoot":"","sources":["../../shared/session-label.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ // Static web assets (web/*.css, web/*.js) — the front end lives in real files
2
+ // instead of template literals inside TS, so it gets syntax highlighting, no
3
+ // backslash escaping, and edits that don't need a `tsc` pass.
4
+ //
5
+ // 解析路径要同时命中三种布局: 源码跑 (tsx: shared/ → ../web)、编译后跑
6
+ // (dist/shared/ → ../../web)、npm 包安装 (同后者, 见 package.json files[])。
7
+ // 内容按 mtime 缓存: 命中时零 IO, 改文件即时生效 (daemon 不必重启)。
8
+ import { existsSync, readFileSync, statSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ const HERE = dirname(fileURLToPath(import.meta.url));
12
+ const WEB_DIR = [join(HERE, "..", "web"), join(HERE, "..", "..", "web")]
13
+ .find((p) => existsSync(join(p, "chat.js")));
14
+ const TYPES = {
15
+ css: "text/css; charset=utf-8",
16
+ js: "text/javascript; charset=utf-8",
17
+ html: "text/html; charset=utf-8",
18
+ };
19
+ const cache = new Map();
20
+ /** 读一个 web/ 下的资源; 文件缺失 (打包遗漏) 返回 undefined 而不是抛 —— 页面
21
+ * 少个样式表也好过整个 daemon 起不来。 */
22
+ export const readAsset = (name) => {
23
+ if (!WEB_DIR || /[/\\]/.test(name))
24
+ return undefined;
25
+ const path = join(WEB_DIR, name);
26
+ let mtimeMs;
27
+ try {
28
+ mtimeMs = statSync(path).mtimeMs;
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ const hit = cache.get(name);
34
+ if (hit && hit.mtimeMs === mtimeMs)
35
+ return hit.asset;
36
+ try {
37
+ const body = readFileSync(path, "utf8");
38
+ const ext = name.slice(name.lastIndexOf(".") + 1);
39
+ const asset = {
40
+ body,
41
+ type: TYPES[ext] ?? "application/octet-stream",
42
+ etag: `W/"${mtimeMs.toString(36)}-${body.length.toString(36)}"`,
43
+ };
44
+ cache.set(name, { mtimeMs, asset });
45
+ return asset;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ };
51
+ //# sourceMappingURL=web-assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-assets.js","sourceRoot":"","sources":["../../shared/web-assets.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,6EAA6E;AAC7E,8DAA8D;AAC9D,EAAE;AACF,kDAAkD;AAClD,oEAAoE;AACpE,gDAAgD;AAChD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAErD,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;KACrE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;AAI/C,MAAM,KAAK,GAA2B;IACpC,GAAG,EAAE,yBAAyB;IAC9B,EAAE,EAAE,gCAAgC;IACpC,IAAI,EAAE,0BAA0B;CACjC,CAAC;AAEF,MAAM,KAAK,GAAG,IAAI,GAAG,EAA6C,CAAC;AAEnE;6BAC6B;AAC7B,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAY,EAAqB,EAAE;IAC3D,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACrD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACjC,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QAAC,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,SAAS,CAAC;IAAC,CAAC;IACrE,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,KAAK,OAAO;QAAE,OAAO,GAAG,CAAC,KAAK,CAAC;IACrD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAClD,MAAM,KAAK,GAAU;YACnB,IAAI;YACJ,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,0BAA0B;YAC9C,IAAI,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG;SAChE,CAAC;QACF,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACpC,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC,CAAC"}
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+ // weclaude svr — 独立的 detail 中转服务。部署到 chat + cli 都可达的网络机器上,
3
+ // cli/daemon 端把 tool/approval 详情 POST 过来, chat 用户点卡片链接直连本机浏览。
4
+ //
5
+ // 只有两条业务路由:
6
+ // • POST /d bearer 鉴权, body = DetailRecord JSON → 存入 store
7
+ // • GET /detail?id= 从 store 取, 用 shared/detail-render 渲染 HTML
8
+ // 加上 GET /healthz 便于反代/监控探活。
9
+ //
10
+ // 存储直接复用 daemon 的 createDetailStore (append-only JSONL + LRU + 24h TTL)。
11
+ // 信任模型: token 相同 = 可写; 读端不签名 (拿到 id 即可读)。id 是 uuid, 不可枚举。
12
+ import { createServer } from "node:http";
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
14
+ import { randomBytes } from "node:crypto";
15
+ import { dirname } from "node:path";
16
+ import pino from "pino";
17
+ import { expandHome } from "../shared/paths.js";
18
+ import { createDetailStore } from "../shared/detail-store.js";
19
+ import { renderDetailPage, renderNotFound } from "../shared/detail-render.js";
20
+ import { createChatRoutes, chatRouteTable } from "../shared/chat-http.js";
21
+ import { resolvePublicHost } from "../shared/lan-ip.js";
22
+ import { loadConfig } from "../shared/config.js";
23
+ // Defaults: config.svr.* > 硬编码兜底。CLI 参数在 parseArgs 里再覆盖上去。
24
+ // Config 加载失败 (缺 bot.botId 之类) 不该阻塞 svr 启动 —— svr 与 daemon 解耦。
25
+ const buildDefaults = () => {
26
+ const fallback = {
27
+ host: "0.0.0.0",
28
+ port: 17891,
29
+ stateDir: "~/.weclaude/svr",
30
+ tokenFile: "~/.weclaude/svr-token",
31
+ logLevel: "info",
32
+ };
33
+ try {
34
+ const { config } = loadConfig();
35
+ const s = config.svr;
36
+ return {
37
+ host: s.host || fallback.host,
38
+ port: s.port || fallback.port,
39
+ stateDir: s.stateDir || fallback.stateDir,
40
+ tokenFile: s.tokenFile || fallback.tokenFile,
41
+ token: s.token || undefined,
42
+ logLevel: s.logLevel || fallback.logLevel,
43
+ };
44
+ }
45
+ catch {
46
+ return fallback;
47
+ }
48
+ };
49
+ const parseArgs = (argv) => {
50
+ const a = buildDefaults();
51
+ for (let i = 0; i < argv.length; i++) {
52
+ const k = argv[i];
53
+ const v = argv[i + 1];
54
+ switch (k) {
55
+ case "--host":
56
+ a.host = v;
57
+ i++;
58
+ break;
59
+ case "--port":
60
+ a.port = Number(v);
61
+ i++;
62
+ break;
63
+ case "--state":
64
+ a.stateDir = v;
65
+ i++;
66
+ break;
67
+ case "--token":
68
+ a.token = v;
69
+ i++;
70
+ break;
71
+ case "--token-file":
72
+ a.tokenFile = v;
73
+ i++;
74
+ break;
75
+ case "--log-level":
76
+ a.logLevel = v;
77
+ i++;
78
+ break;
79
+ case "--help":
80
+ case "-h":
81
+ printHelp();
82
+ process.exit(0);
83
+ }
84
+ }
85
+ return a;
86
+ };
87
+ const printHelp = () => {
88
+ // eslint-disable-next-line no-console
89
+ console.log(`weclaude svr — standalone detail relay
90
+
91
+ Defaults come from ~/.weclaude/config.jsonc (svr.*) when present; CLI overrides.
92
+
93
+ --host <ip> bind address (default 0.0.0.0)
94
+ --port <n> listen port (default 17891)
95
+ --state <dir> state dir (default ~/.weclaude/svr)
96
+ --token <t> bearer token (default: generate + persist to --token-file)
97
+ --token-file <p> token persist path (default ~/.weclaude/svr-token)
98
+ --log-level <lvl> pino level (default info)
99
+ `);
100
+ };
101
+ const loadOrCreateToken = (explicit, tokenFile) => {
102
+ if (explicit && explicit.length > 0)
103
+ return explicit;
104
+ const abs = expandHome(tokenFile);
105
+ if (existsSync(abs)) {
106
+ const t = readFileSync(abs, "utf8").trim();
107
+ if (t.length > 0)
108
+ return t;
109
+ }
110
+ const t = randomBytes(24).toString("hex");
111
+ mkdirSync(dirname(abs), { recursive: true });
112
+ writeFileSync(abs, `${t}\n`, { mode: 0o600 });
113
+ return t;
114
+ };
115
+ const readBody = async (req) => {
116
+ const chunks = [];
117
+ for await (const c of req)
118
+ chunks.push(c);
119
+ return Buffer.concat(chunks);
120
+ };
121
+ const json = (res, status, body) => {
122
+ res.statusCode = status;
123
+ res.setHeader("content-type", "application/json");
124
+ res.end(JSON.stringify(body));
125
+ };
126
+ const isDetailRecord = (v) => {
127
+ if (!v || typeof v !== "object")
128
+ return false;
129
+ const r = v;
130
+ if (typeof r.id !== "string" || !r.id)
131
+ return false;
132
+ if (r.kind !== "tool" && r.kind !== "approval" && r.kind !== "turn")
133
+ return false;
134
+ if (typeof r.createdAt !== "number")
135
+ return false;
136
+ return true;
137
+ };
138
+ const main = async () => {
139
+ const args = parseArgs(process.argv.slice(2));
140
+ const log = pino({ level: args.logLevel, name: "weclaude-svr" });
141
+ const stateDir = expandHome(args.stateDir);
142
+ mkdirSync(stateDir, { recursive: true });
143
+ const token = loadOrCreateToken(args.token, args.tokenFile);
144
+ const store = createDetailStore({ stateDir, log });
145
+ // Chat 视图 (SPA + JSON API + SSE) 与 daemon 完全同源 —— svr 侧的记录是 POST /d
146
+ // 推过来的, store.subscribe 一样会触发, 所以远端浏览也是实时的。
147
+ const chat = chatRouteTable(createChatRoutes(store));
148
+ const server = createServer(async (req, res) => {
149
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
150
+ try {
151
+ if (req.method === "GET" && url.pathname === "/healthz") {
152
+ json(res, 200, { ok: true });
153
+ return;
154
+ }
155
+ const chatHandler = chat[`${req.method ?? "GET"} ${url.pathname}`];
156
+ if (chatHandler) {
157
+ chatHandler(req, res, url);
158
+ return;
159
+ }
160
+ if (req.method === "GET" && url.pathname === "/detail") {
161
+ const id = url.searchParams.get("id") ?? "";
162
+ if (!id) {
163
+ res.statusCode = 400;
164
+ res.setHeader("content-type", "text/plain; charset=utf-8");
165
+ res.end("missing ?id=");
166
+ return;
167
+ }
168
+ const rec = store.get(id);
169
+ if (!rec) {
170
+ res.statusCode = 404;
171
+ res.setHeader("content-type", "text/html; charset=utf-8");
172
+ res.end(renderNotFound(id));
173
+ return;
174
+ }
175
+ res.statusCode = 200;
176
+ res.setHeader("content-type", "text/html; charset=utf-8");
177
+ res.setHeader("cache-control", "no-store");
178
+ res.end(renderDetailPage(rec));
179
+ return;
180
+ }
181
+ if (req.method === "POST" && url.pathname === "/d") {
182
+ const auth = req.headers.authorization ?? "";
183
+ if (!auth.startsWith("Bearer ") || auth.slice(7) !== token) {
184
+ json(res, 401, { ok: false, error: "unauthorized" });
185
+ return;
186
+ }
187
+ const body = await readBody(req);
188
+ let parsed;
189
+ try {
190
+ parsed = JSON.parse(body.toString("utf8"));
191
+ }
192
+ catch {
193
+ json(res, 400, { ok: false, error: "invalid json" });
194
+ return;
195
+ }
196
+ if (!isDetailRecord(parsed)) {
197
+ json(res, 400, { ok: false, error: "invalid detail record" });
198
+ return;
199
+ }
200
+ store.put(parsed);
201
+ log.debug({ id: parsed.id, kind: parsed.kind }, "record stored");
202
+ json(res, 200, { ok: true, id: parsed.id });
203
+ return;
204
+ }
205
+ res.statusCode = 404;
206
+ res.end("not found");
207
+ }
208
+ catch (e) {
209
+ log.error({ err: e.message }, "handler failed");
210
+ try {
211
+ json(res, 500, { ok: false, error: e.message });
212
+ }
213
+ catch { /* ignore */ }
214
+ }
215
+ });
216
+ server.listen(args.port, args.host, () => {
217
+ const displayHost = resolvePublicHost(args.host);
218
+ const base = `http://${displayHost}:${args.port}`;
219
+ // eslint-disable-next-line no-console
220
+ console.log(`[weclaude-svr] listening on ${args.host}:${args.port}`);
221
+ // eslint-disable-next-line no-console
222
+ console.log(`[weclaude-svr] base URL: ${base}`);
223
+ // eslint-disable-next-line no-console
224
+ console.log(`[weclaude-svr] token: ${token}`);
225
+ // eslint-disable-next-line no-console
226
+ console.log(`\nAdd to ~/.weclaude/config.jsonc on every daemon host:
227
+ "daemon": {
228
+ "detailRemoteBase": "${base}",
229
+ "detailRemoteToken": "${token}"
230
+ }
231
+ `);
232
+ });
233
+ const shutdown = (sig) => {
234
+ log.info({ sig }, "shutdown");
235
+ server.close(() => process.exit(0));
236
+ setTimeout(() => process.exit(0), 3000).unref();
237
+ };
238
+ process.on("SIGINT", () => shutdown("SIGINT"));
239
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
240
+ };
241
+ main().catch((e) => {
242
+ // eslint-disable-next-line no-console
243
+ console.error("[weclaude-svr] fatal:", e);
244
+ process.exit(1);
245
+ });
246
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../svr/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": ".*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/hooks/pre-tool-use.sh",
10
+ "timeout": 43220
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env bash
2
+ # weclaude PreToolUse hook: forward to local daemon → long-poll → emit decision.
3
+ # Any failure → ask (never break workflow).
4
+ set -uo pipefail
5
+
6
+ DAEMON_URL="${WECLAUDE_DAEMON_URL:-http://127.0.0.1:17890/approve}"
7
+ # 必须严格大于 daemon 的 approval.longPollSec (默认 43200) 且小于 hooks.json
8
+ # 的 timeout, 否则卡在 WeCom 里挂 >30min 后 curl 先死, 用户的点击写进死 socket。
9
+ HOOK_TIMEOUT="${WECLAUDE_HOOK_TIMEOUT:-43210}"
10
+ STATE_DIR="${WECLAUDE_STATE_DIR:-$HOME/.weclaude/state}"
11
+ # Fallback policy when the daemon is unreachable / replies garbage. ask|allow|deny.
12
+ # Default keeps the safe behavior; set to `allow` in trusted local-only setups.
13
+ FALLBACK="${WECLAUDE_HOOK_FALLBACK:-ask}"
14
+
15
+ emit() {
16
+ local decision="$1" reason="${2:-}"
17
+ # 用 jq 拼 JSON: reason 可能含字面引号(askq deny 把答案塞进 reason),
18
+ # printf 直接拼会漏出内层 " 把 JSON 撕烂, Claude Code 解析失败 fallback
19
+ # 弹原生 picker → askq 失效。
20
+ if command -v jq >/dev/null 2>&1; then
21
+ jq -cn --arg d "$decision" --arg r "weclaude: $reason" \
22
+ '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$d,permissionDecisionReason:$r}}'
23
+ else
24
+ # jq 缺失兜底: 手动转义 \ 和 " (足够覆盖 reason 里的字面引号)。
25
+ local esc="${reason//\\/\\\\}"; esc="${esc//\"/\\\"}"
26
+ printf '%s\n' "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"${decision}\",\"permissionDecisionReason\":\"weclaude: ${esc}\"}}"
27
+ fi
28
+ exit 0
29
+ }
30
+ ask() { emit "ask" "${1:-bridge unreachable}"; }
31
+
32
+ # Daemon-down fallback. Consults the persisted auto-approve window first so a
33
+ # session-level "allow N min" survives daemon restart / outage; otherwise falls
34
+ # back to FALLBACK. SESSION_ID must already be parsed.
35
+ bridge_down() {
36
+ local reason="$1"
37
+ local sf="$STATE_DIR/auto-windows.json"
38
+ if [[ -n "${SESSION_ID:-}" && -r "$sf" ]] && command -v jq >/dev/null 2>&1; then
39
+ local active
40
+ active=$(jq -r --arg s "$SESSION_ID" \
41
+ 'if (.windows[$s].until // 0) > (now * 1000) then "1" else "0" end' \
42
+ "$sf" 2>/dev/null) || active=""
43
+ if [[ "$active" == "1" ]]; then
44
+ emit "allow" "auto-window (offline): $reason"
45
+ fi
46
+ fi
47
+ case "$FALLBACK" in
48
+ allow|deny|ask) emit "$FALLBACK" "$reason" ;;
49
+ *) emit "ask" "$reason" ;;
50
+ esac
51
+ }
52
+
53
+ PAYLOAD=$(cat) || ask "stdin read failed"
54
+ command -v jq >/dev/null 2>&1 || ask "jq missing"
55
+
56
+ SESSION_ID=$(printf '%s' "$PAYLOAD" | jq -r '.session_id // empty')
57
+ TOOL_NAME=$(printf '%s' "$PAYLOAD" | jq -r '.tool_name // empty')
58
+ TOOL_INPUT=$(printf '%s' "$PAYLOAD" | jq -c '.tool_input // {}')
59
+ CWD=$(printf '%s' "$PAYLOAD" | jq -r '.cwd // ""')
60
+ TRANSCRIPT_PATH=$(printf '%s' "$PAYLOAD" | jq -r '.transcript_path // ""')
61
+ PERMISSION_MODE=$(printf '%s' "$PAYLOAD" | jq -r '.permission_mode // ""')
62
+
63
+ # 权限模式短路: 只对用户主动选的"完全跳过"模式放掉, 其它一律走 daemon 发 IM
64
+ # 卡, 让 weclaude 远端用户能在 IM 上点决策。
65
+ # - bypassPermissions: 用户已经主动选了"全跳", emit allow 直接放, 不打 daemon。
66
+ # - auto: 不在此短路。第一次发卡用户点 10min 后, daemon 的 isAutoWindowActive
67
+ # 会接管, 后续 tool 自动放行不再发卡, 既不丢 IM 控制权也不会变成"卡刷屏"。
68
+ # 若 emit "ask" 让 Claude 自己处理, 不安全 tool 会弹本地 CLI picker, 远端用户
69
+ # 完全看不见也答不了 — 那是 v0.1.35 的回退原因。
70
+ # - dontAsk: Claude 默认拒, 但 weclaude 用户可能希望从 IM 端覆盖, 不在此短路。
71
+ # acceptEdits / default / plan 不在此列, 维持 daemon 流程。AskUserQuestion 始终
72
+ # 走卡 (远端用户的主动决策), 故任何 mode 都不在此短路。
73
+ # WECLAUDE_HONOR_AUTO_MODE=0 关掉本段对齐行为。
74
+ if [[ "$TOOL_NAME" != "AskUserQuestion" ]] && [[ "${WECLAUDE_HONOR_AUTO_MODE:-1}" != "0" ]]; then
75
+ case "$PERMISSION_MODE" in
76
+ bypassPermissions)
77
+ emit "allow" "bypass-mode passthrough" ;;
78
+ esac
79
+ fi
80
+
81
+ # ExitPlanMode 只是把 plan mode 的产出交给用户确认, 本地 CLI 会再弹一次原生
82
+ # 确认框, 走 IM 审批纯属多此一举, 直接放行。
83
+ if [[ "$TOOL_NAME" == "ExitPlanMode" ]]; then
84
+ emit "allow" "ExitPlanMode passthrough"
85
+ fi
86
+
87
+ # weclaude 自家 MCP 工具全部走 loopback 到本 daemon, 自审会把 /wrc 首次绑定卡死
88
+ # (还没 defaultChat, 卡片无处可推 → 鸡生蛋), 直接放行。
89
+ # 命名两条路径:
90
+ # 1. cli/sync.ts (legacy claude-internal): mcp__weclaude__<tool>
91
+ # 2. .claude-plugin/plugin.json: mcp__plugin_weclaude_weclaude__<tool>
92
+ # 用 *weclaude__* 同时覆盖两种前缀。
93
+ if [[ "$TOOL_NAME" == mcp__*weclaude__* ]]; then
94
+ emit "allow" "weclaude mcp self-call bypass"
95
+ fi
96
+
97
+ # weclaude 自家 Skill (slash commands like /wrc) 也是本地插件代码, 无须审批。
98
+ # tool_input.skill 形如 "weclaude:wrc"; plugin 命名空间用冒号分隔。
99
+ if [[ "$TOOL_NAME" == "Skill" ]]; then
100
+ SKILL_NAME=$(printf '%s' "$TOOL_INPUT" | jq -r '.skill // ""')
101
+ if [[ "$SKILL_NAME" == weclaude:* ]]; then
102
+ emit "allow" "weclaude skill self-call bypass"
103
+ fi
104
+ fi
105
+
106
+ # Bash read-only fast-path: bypass cards for grep / rg etc.
107
+ if [[ "$TOOL_NAME" == "Bash" ]]; then
108
+ CMD=$(printf '%s' "$TOOL_INPUT" | jq -r '.command // ""')
109
+ if [[ "$CMD" =~ ^[[:space:]]*(grep|egrep|fgrep|rg|ls|cat|head|tail|wc|file)([[:space:]]|$) ]] \
110
+ && [[ ! "$CMD" =~ [\;\|\&\>\<\`\$\(] ]]; then
111
+ emit "allow" "read-only bypass"
112
+ fi
113
+ # weclaude CLI 同理: /wrc /cd 这些 slash command 的 ! bash 都打到本 daemon,
114
+ # 自己审自己没意义, 也避免首次绑定时无处推卡。
115
+ if [[ "$CMD" =~ (^|/|[[:space:]])weclaude(\.sh)?([[:space:]]|$) ]]; then
116
+ emit "allow" "weclaude self-call bypass"
117
+ fi
118
+ fi
119
+
120
+ # Tail of recent user messages for context on the card.
121
+ # 注意 .message.content 可能是 string 或 content blocks 数组;过滤 tool_result 与
122
+ # Claude Code 注入的 <system-reminder>/<command-*>/<local-command-*> 包裹标签。
123
+ TRANSCRIPT_TAIL=""
124
+ if [[ -n "$TRANSCRIPT_PATH" && -r "$TRANSCRIPT_PATH" ]]; then
125
+ TRANSCRIPT_TAIL=$(tail -n 200 "$TRANSCRIPT_PATH" 2>/dev/null \
126
+ | jq -r '
127
+ select(.type == "user" or .role == "user")
128
+ | select((.isMeta // false) == false)
129
+ | (.message.content // .content) as $c
130
+ | ( if ($c | type) == "string" then $c
131
+ elif ($c | type) == "array" then
132
+ ([ $c[]? | select(.type == "text") | .text ] | join("\n"))
133
+ else "" end )
134
+ | gsub("(?s)<system-reminder>.*?</system-reminder>"; "")
135
+ | gsub("(?s)<command-name>.*?</command-name>"; "")
136
+ | gsub("(?s)<command-message>.*?</command-message>"; "")
137
+ | gsub("(?s)<command-args>.*?</command-args>"; "")
138
+ | gsub("(?s)<local-command-stdout>.*?</local-command-stdout>"; "")
139
+ | gsub("(?s)<local-command-caveat>.*?</local-command-caveat>"; "")
140
+ | gsub("\\s+"; " ")
141
+ | sub("^\\s+"; "") | sub("\\s+$"; "")
142
+ | select(length > 0)
143
+ ' 2>/dev/null \
144
+ | tail -n 3 \
145
+ | head -c 800 || true)
146
+ fi
147
+
148
+ BODY=$(jq -nc \
149
+ --arg sid "$SESSION_ID" \
150
+ --arg tn "$TOOL_NAME" \
151
+ --argjson ti "$TOOL_INPUT" \
152
+ --arg cwd "$CWD" \
153
+ --arg tail "$TRANSCRIPT_TAIL" \
154
+ '{session_id:$sid,tool_name:$tn,tool_input:$ti,cwd:$cwd,transcript_tail:$tail}')
155
+
156
+ RESP=$(curl -sS --max-time "$HOOK_TIMEOUT" \
157
+ -H 'Content-Type: application/json' \
158
+ -d "$BODY" \
159
+ "$DAEMON_URL" 2>/dev/null) || bridge_down "daemon curl failed"
160
+
161
+ DECISION=$(printf '%s' "$RESP" | jq -r '.decision // "ask"' 2>/dev/null) || bridge_down "bad daemon response"
162
+ REASON=$(printf '%s' "$RESP" | jq -r '.reason // ""' 2>/dev/null)
163
+
164
+ case "$DECISION" in
165
+ allow|deny|ask) emit "$DECISION" "$REASON" ;;
166
+ *) bridge_down "unknown decision: $DECISION" ;;
167
+ esac
@@ -0,0 +1,45 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <!-- Generated template — `scripts/install-launchd.sh` materializes the
4
+ real plist into ~/Library/LaunchAgents with absolute paths. -->
5
+ <plist version="1.0">
6
+ <dict>
7
+ <key>Label</key>
8
+ <string>com.weclaude.daemon</string>
9
+
10
+ <key>ProgramArguments</key>
11
+ <array>
12
+ <string>__NODE__</string>
13
+ <string>__REPO__/dist/daemon/index.js</string>
14
+ </array>
15
+
16
+ <key>WorkingDirectory</key>
17
+ <string>__REPO__</string>
18
+
19
+ <key>EnvironmentVariables</key>
20
+ <dict>
21
+ <key>PATH</key>
22
+ <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
23
+ <key>NODE_ENV</key>
24
+ <string>production</string>
25
+ </dict>
26
+
27
+ <key>RunAtLoad</key>
28
+ <true/>
29
+ <key>KeepAlive</key>
30
+ <dict>
31
+ <key>SuccessfulExit</key>
32
+ <false/>
33
+ <key>Crashed</key>
34
+ <true/>
35
+ </dict>
36
+
37
+ <key>ThrottleInterval</key>
38
+ <integer>10</integer>
39
+
40
+ <key>StandardOutPath</key>
41
+ <string>__HOME__/.weclaude/daemon.stdout.log</string>
42
+ <key>StandardErrorPath</key>
43
+ <string>__HOME__/.weclaude/daemon.stderr.log</string>
44
+ </dict>
45
+ </plist>