weflow-cli 1.8.2 → 1.9.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/CHANGELOG.md +342 -0
- package/OPERATIONS.md +79 -0
- package/README.en.md +9 -2
- package/README.md +12 -2
- package/bin/weflow-cli-electron.cjs +2 -0
- package/bin/weflow-cli.ts +648 -79
- package/dist/bin/weflow-cli.js +675 -78
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/mcp-server/http.d.ts +14 -0
- package/dist/mcp-server/http.d.ts.map +1 -0
- package/dist/mcp-server/http.js +84 -0
- package/dist/mcp-server/http.js.map +1 -0
- package/dist/mcp-server/httpConfig.d.ts +50 -0
- package/dist/mcp-server/httpConfig.d.ts.map +1 -0
- package/dist/mcp-server/httpConfig.js +104 -0
- package/dist/mcp-server/httpConfig.js.map +1 -0
- package/dist/mcp-server/index.d.ts +13 -1
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +39 -373
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/src/panel/server.d.ts.map +1 -1
- package/dist/src/panel/server.js +26 -1
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/services/assistantEval.d.ts +69 -0
- package/dist/src/services/assistantEval.d.ts.map +1 -1
- package/dist/src/services/assistantEval.js +270 -8
- package/dist/src/services/assistantEval.js.map +1 -1
- package/dist/src/services/assistantScenes.d.ts +81 -0
- package/dist/src/services/assistantScenes.d.ts.map +1 -0
- package/dist/src/services/assistantScenes.js +322 -0
- package/dist/src/services/assistantScenes.js.map +1 -0
- package/dist/src/services/assistantService.d.ts +14 -4
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +82 -6
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantSkills.d.ts +94 -0
- package/dist/src/services/assistantSkills.d.ts.map +1 -0
- package/dist/src/services/assistantSkills.js +330 -0
- package/dist/src/services/assistantSkills.js.map +1 -0
- package/dist/src/services/assistantTools.d.ts +7 -0
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +770 -61
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/assistantTrace.d.ts +4 -0
- package/dist/src/services/assistantTrace.d.ts.map +1 -1
- package/dist/src/services/assistantTrace.js +9 -0
- package/dist/src/services/assistantTrace.js.map +1 -1
- package/dist/src/services/configService.d.ts +7 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +4 -2
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/outboundSend.d.ts +53 -0
- package/dist/src/services/outboundSend.d.ts.map +1 -0
- package/dist/src/services/outboundSend.js +90 -0
- package/dist/src/services/outboundSend.js.map +1 -0
- package/dist/src/services/pythonBridge.d.ts.map +1 -1
- package/dist/src/services/pythonBridge.js +1 -0
- package/dist/src/services/pythonBridge.js.map +1 -1
- package/dist/src/utils/python.js +2 -2
- package/dist/src/utils/python.js.map +1 -1
- package/docs/BRANCHES.md +37 -0
- package/docs/DECISIONS.md +350 -0
- package/docs/EXTENDING.md +29 -3
- package/docs/MCP.md +56 -2
- package/docs/PROJECT_STATE.md +121 -9
- package/mcp-server/http.ts +108 -0
- package/mcp-server/httpConfig.ts +126 -0
- package/mcp-server/index.ts +344 -681
- package/package.json +1 -1
- package/resources/panel/ball-position.cjs +48 -2
- package/resources/panel/index.html +6 -4
- package/resources/panel/main.cjs +137 -5
- package/resources/panel/mascot-base.png +0 -0
- package/resources/panel/mascot-iris.png +0 -0
- package/resources/panel/mascot-peek.png +0 -0
- package/resources/panel/panel.css +95 -60
- package/resources/panel/preload.cjs +26 -0
- package/resources/panel/renderer.js +167 -10
- package/scripts/_utils.py +47 -1
- package/scripts/backfill_articles.py +202 -21
- package/scripts/biz_daily.py +8 -31
- package/scripts/bonds.py +331 -0
- package/scripts/chat_notes.py +551 -470
- package/scripts/chat_stats.py +8 -7
- package/scripts/compile_wiki.py +24 -2
- package/scripts/export_chat_html.py +3 -1
- package/scripts/mcp_bridge.py +6 -7
- package/scripts/wechat_voice.py +9 -0
- package/scripts/wiki_lint.py +30 -7
- package/src/panel/server.ts +25 -1
- package/src/services/assistantEval.ts +323 -12
- package/src/services/assistantScenes.ts +342 -0
- package/src/services/assistantService.ts +81 -6
- package/src/services/assistantSkills.ts +364 -0
- package/src/services/assistantTools.ts +738 -59
- package/src/services/assistantTrace.ts +9 -0
- package/src/services/configService.ts +412 -403
- package/src/services/outboundSend.ts +126 -0
- package/src/services/pythonBridge.ts +1 -0
- package/src/utils/python.ts +2 -2
package/docs/DECISIONS.md
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
> 编号说明:`D-009` 从未使用(D-008 直接跳到 D-010),保留为空号;
|
|
6
6
|
> 历史上曾有两条决策共用 `D-016`,其中「architecture visuals」一条改号为 `D-028`。
|
|
7
|
+
> `D-056`~`D-058` 原本写在**另一条分支** `feat/assistant-skills-scenes-tool-surface` 上(技能包/场景/工具面那三条),
|
|
8
|
+
> 而 `D-059`(悬浮球的眼睛跟随)写在 `feat/panel-eye-follow` 上。本分支当时为避免合并时撞号,直接从 `D-055` 跳到 `D-059`。
|
|
9
|
+
> 两条分支已在同一次合并里并入 `master`(2026-09-30),所以现在 `D-055`~`D-059` 五个号都在,没有空号。
|
|
7
10
|
|
|
8
11
|
## D-001: Local-first data handling
|
|
9
12
|
|
|
@@ -1400,6 +1403,353 @@ which is what the gate is for.
|
|
|
1400
1403
|
semantics are not reliable here), as is any automatic labelling: there is no gold standard for "is this
|
|
1401
1404
|
draft right", so the feature records what it judged and says so rather than claiming calibration.
|
|
1402
1405
|
|
|
1406
|
+
## D-063: Sending stays unreachable from the model, and becomes reachable from a human who names the candidate
|
|
1407
|
+
|
|
1408
|
+
**Status:** Active
|
|
1409
|
+
|
|
1410
|
+
**Decision.** `weflow-cli draft <talker>` already did the hard half - judge the intent, draft three deliberately
|
|
1411
|
+
different candidates, rank them with the decision model - and ended by saying the candidates are **only text** and that
|
|
1412
|
+
sending is the user's call. It now also carries that decision out, under three rules:
|
|
1413
|
+
|
|
1414
|
+
1. **`--send` requires `--pick N`.** There is no "draft three, let it choose, let it send" path. The human names the
|
|
1415
|
+
candidate; the model never selects what goes out.
|
|
1416
|
+
2. **Two-phase.** Without `--yes` there is no send: JSON mode returns `CONFIRMATION_REQUIRED` **with the text in the
|
|
1417
|
+
payload**, interactive mode asks "确定把第 N 条发给 X?". Both non-`--yes` paths are covered by tests that also assert
|
|
1418
|
+
**no send attempt reached the audit log**.
|
|
1419
|
+
3. **One send policy, not two.** Blacklist → whitelist → rate limit → audit now lives in `src/services/outboundSend.ts`
|
|
1420
|
+
and is called by both `send` and `draft --send`. Before this, that chain existed only inside the `send` command's
|
|
1421
|
+
action; a second copy is how a "send" path ends up quietly skipping the whitelist.
|
|
1422
|
+
|
|
1423
|
+
**Why the model stays out of it.** `docs/EXTENDING.md` states that sending is *structurally unreachable from a
|
|
1424
|
+
model-driven path*, and that is not a mood: the assistant and the MCP surface have no send tool, and
|
|
1425
|
+
`test/draft-send-cli.test.ts` now asserts that `send`, `draft`, `send_message` and `publish_article` are absent from
|
|
1426
|
+
`TOOL_DEFS` (while `draft_reply` stays present - drafting is not sending). What changes here is only the **human's**
|
|
1427
|
+
side: after reading the candidates, the user no longer has to copy one into another terminal.
|
|
1428
|
+
|
|
1429
|
+
**Constraints that fell out of the measurements.** (a) `--pick` out of range and `--send` without `--pick` are
|
|
1430
|
+
**usage errors, so they are reported before any model call** - the first version validated them after the confirmation
|
|
1431
|
+
gate, which meant the model ran, and was paid for, before the command said "there is no candidate 9". (b) `--dry-run`
|
|
1432
|
+
means **zero egress**, and candidates can only come from a model, so `--dry-run` cannot preview "what candidate 2 would
|
|
1433
|
+
send"; it reports the character count the script would send and says why, rather than pretending. (c) Sending still
|
|
1434
|
+
requires the other side's `context_token` - the channel needs a message from them first - so "send to anyone" was never
|
|
1435
|
+
available, and the whitelist remains the real boundary.
|
|
1436
|
+
|
|
1437
|
+
**Consequences.** The tests do not call a model and do not need one: refusing early is exactly what they assert. The
|
|
1438
|
+
audit file (`~/.weflow-cli/audit-send.log`) is what they read, and the positive control that proves that read is not
|
|
1439
|
+
vacuous does **not** come from a real send - with a temporary home the channel is not logged in, so `send --yes` exits
|
|
1440
|
+
at `MESSAGE_CHANNEL_NOT_LOGGED_IN` before the audit line. It uses the **blacklist** branch instead, which audits before
|
|
1441
|
+
that check. That detail cost one debugging round and is written here because the next person will hit it too.
|
|
1442
|
+
|
|
1443
|
+
## D-062: Relationship temperature stays on the command line, and its "who counts as a person" rule flags rather than drops
|
|
1444
|
+
|
|
1445
|
+
**Status:** Active
|
|
1446
|
+
|
|
1447
|
+
**Decision.** `scripts/bonds.py` reports three things from the local message database: relationships that are cooling
|
|
1448
|
+
(substantial history, then silence), relationships heating up (the last 7 days well above the long-run rate), and
|
|
1449
|
+
contacts whose history never mentions another channel (phone, email, meeting up) - i.e. the ones reachable *only*
|
|
1450
|
+
here. It is **read-only, local, and calls no model**. Two rules are pinned:
|
|
1451
|
+
|
|
1452
|
+
1. **It is not in the tool table.** It is not exposed to the assistant, to MCP, or to any cloud model - not as a tool
|
|
1453
|
+
and not as context. Everything else this project computes about the user is either explicitly shareable or has a
|
|
1454
|
+
confirmation gate; this one is neither. Its output is an **inference about third parties** (which relationships the
|
|
1455
|
+
user is neglecting, who they would lose), derived from private messages, and the person it could most embarrass is
|
|
1456
|
+
not the person running it.
|
|
1457
|
+
2. **"Who counts as a person" flags, it does not drop.** The rule is the **shape of the id** - anything containing
|
|
1458
|
+
`@` (`@chatroom` groups, `@openim` service accounts, `@weclaw` bots), `filehelper`, or a `gh_` official account, is
|
|
1459
|
+
not a person; everyone else is. Accounts that are structurally indistinguishable from a person but whose *name*
|
|
1460
|
+
looks like a shop or a hotline (普华口腔门诊部 is one on this machine, with `local_type = 1` and its own alias) are
|
|
1461
|
+
listed under "flagged, you decide", with a skip file (`output/bonds-skip.txt`) and `--skip` to act on it. And every
|
|
1462
|
+
account the run excluded is printed with the reason, so a filter can never quietly remove two friends.
|
|
1463
|
+
|
|
1464
|
+
**Reason.** Three measurements killed three simpler designs. (a) Ranking by message volume produced a leaderboard
|
|
1465
|
+
with no information - the people you message most are not news to you. (b) "Never mentions another channel" was
|
|
1466
|
+
**inverted**: it is trivially true of bots, so the first version of the irreplaceability ranking had 星巴克小助手,
|
|
1467
|
+
天天神券福利君, 串掌门(南村店)and 文件传输助手 at the top. (c) `flag` looked like the clean human/bot marker - real
|
|
1468
|
+
friends mostly carry `flag = 3` - until the distribution was read: of 38 people, three differ, and only one of those
|
|
1469
|
+
(`微信ClawBot`, 2049) is a bot; 彪弟 (2051) and 平凡的世界 (2563) are people whose bit was set by however they were
|
|
1470
|
+
added. Shipping `flag == 3` would have silently deleted two friends, which is the failure this project keeps meeting:
|
|
1471
|
+
a filter that is 95% right and reports nothing.
|
|
1472
|
+
|
|
1473
|
+
**Consequences.** The thresholds are constants with tests and mutation checks (`COOL_DAYS`, `HOT_RATIO`, the minimum
|
|
1474
|
+
history required for "cooling"), because they are the whole judgement. The skip file lives under `output/` (gitignored,
|
|
1475
|
+
never committed). Adding it as a command in `bin/weflow-cli.ts` is fine and expected; adding it to the assistant or MCP
|
|
1476
|
+
tool tables is not, and that is the part of this decision worth keeping written down.
|
|
1477
|
+
|
|
1478
|
+
## D-061: The MCP server can speak HTTP - so the boundary had to be written down, not just implemented
|
|
1479
|
+
|
|
1480
|
+
**Status:** Active
|
|
1481
|
+
|
|
1482
|
+
**Decision.** The MCP server serves its tools over **Streamable HTTP** as well as stdio, and the second transport is
|
|
1483
|
+
**off unless asked for** (`--http`, or `WEFLOW_MCP_HTTP`). The tool table is the same one (`MCP_TOOL_DEFS` derived from
|
|
1484
|
+
`TOOL_DEFS`), so the two transports cannot drift; only the carrying changes. Four rules are enforced in
|
|
1485
|
+
`mcp-server/httpConfig.ts` - pure functions with offline tests - rather than left to the caller:
|
|
1486
|
+
|
|
1487
|
+
1. **Off by default.** No flag means stdio, byte for byte as before.
|
|
1488
|
+
2. **A token is mandatory, with no default.** Missing or under 24 characters, the process refuses to start.
|
|
1489
|
+
3. **Loopback only.** `0.0.0.0` and every other non-loopback address are rejected with the tunnel command in the
|
|
1490
|
+
error message.
|
|
1491
|
+
4. **DNS-rebinding protection on.** The SDK's default is `false`; the realistic attack on a loopback server is a web
|
|
1492
|
+
page the user happens to visit reaching `127.0.0.1`, so the `Host` header must match and any `Origin` must be
|
|
1493
|
+
allowlisted.
|
|
1494
|
+
|
|
1495
|
+
**Why HTTP at all.** stdio can only be spoken by a client that is able to spawn a process. Serving HTTP is the norm
|
|
1496
|
+
in this ecosystem - `chatlog` is the de-facto standard and does exactly this - and it is what lets a resident
|
|
1497
|
+
service, a browser-side client, or several clients at once use the same query surface.
|
|
1498
|
+
|
|
1499
|
+
**Why the boundary is written down rather than only coded.** This is the first time this project opens a **listening
|
|
1500
|
+
socket that serves the user's private messages to whoever can reach it**. The failure mode is not a crash: it is a
|
|
1501
|
+
port that stays open, bound wider than the author intended, or opened with a token that looks configured and is not.
|
|
1502
|
+
So each rule is (a) in a pure function with its own test, (b) mutation-checked - disabling the rebinding protection,
|
|
1503
|
+
removing the token check, widening the loopback list, lowering the token floor, or making the check return `true`
|
|
1504
|
+
each turn the suite red - and (c) stated in `docs/MCP.md` where a user meets it.
|
|
1505
|
+
|
|
1506
|
+
**Why no remote bind.** The tempting convenience - `--host 0.0.0.0` so a laptop can reach the desktop - is exactly
|
|
1507
|
+
the change that moves private chat data onto the network. A tunnel is one command and keeps the exposure local; if
|
|
1508
|
+
someone later wants a real remote surface, that is a separate decision with its own threat model, not a flag here.
|
|
1509
|
+
|
|
1510
|
+
**Consequences.** `mcp-server/index.ts` now exports `buildServer()` and starts only when run as a script (importing
|
|
1511
|
+
it must not open a listener - the tests import it). The token is never logged. Requests are stateless (one server per
|
|
1512
|
+
request, no session id), so there is no cross-client state to leak. Nothing about the stdio path changed: an existing
|
|
1513
|
+
client configuration keeps working untouched.
|
|
1514
|
+
|
|
1515
|
+
## D-060: A backfilled day records which topics it holds, because "the file exists" is not "the day is done"
|
|
1516
|
+
|
|
1517
|
+
**Status:** Active
|
|
1518
|
+
|
|
1519
|
+
**Decision.** `backfill_articles.py` can fetch one topic instead of a whole day (`--topic AI`), and a day written
|
|
1520
|
+
that way is **partial and says so**. It uses the classifier the script already had - `_guess_topic`, which reads only
|
|
1521
|
+
the title and the account name and never the article body - so filtering first selects exactly the articles a full run
|
|
1522
|
+
would have filed under that topic. There is no second criterion. What is new is the bookkeeping: `.articles.json`
|
|
1523
|
+
carries `topicFilter`, the README states which topics are missing, and `day_done` refuses to call that day complete,
|
|
1524
|
+
so a later full run still covers it.
|
|
1525
|
+
|
|
1526
|
+
**Reason: one truncation prevented, and one found already live.** Without the marker, a day holding only its AI
|
|
1527
|
+
articles looks finished - the md files exist, `.articles.json` exists, and `day_done` asked nothing more than "is the
|
|
1528
|
+
list non-empty". The other five topics would never be fetched and **nothing would report it**, which is the failure
|
|
1529
|
+
shape this repository has already paid for once: `--vault-sync` was keyed off a `backfilled` marker that only one of
|
|
1530
|
+
its two writers sets, so 12 days were never copied *and* never reported as skipped (2026-09-28, 187 → 199 days).
|
|
1531
|
+
Checking for this one turned up the same defect already shipped in `--limit-per-day`: the day is sliced, written, and
|
|
1532
|
+
then reads as done, so everything past the limit was silently out of scope forever. It is recorded in the same marker
|
|
1533
|
+
(`truncated`) because it is the same mechanism, and it is a **behaviour change** - a limited day is now redone on the
|
|
1534
|
+
next run rather than skipped.
|
|
1535
|
+
|
|
1536
|
+
The filtering is applied **inside `write_day`** as well as at the call site. If only the caller filtered, a caller
|
|
1537
|
+
that forgot to would write all six topic directories and stamp "only AI" on them; a marker that lies is worse than no
|
|
1538
|
+
marker, and this is the only way to make the two structurally unable to disagree.
|
|
1539
|
+
|
|
1540
|
+
**Consequences.** `.articles.json` gains two optional keys; a full run's output is byte-identical to before, so no
|
|
1541
|
+
existing output needs migrating, and `day_done` reads a missing `topicFilter` as "complete" - which is also what the
|
|
1542
|
+
days already on disk mean. `--topic` changes no criterion: over 2025-09-05 ~ 2026-03-02 the script's own classifier
|
|
1543
|
+
puts **1,758 of 9,207** in-scope articles in `AI` (5-day sample: 300 → 66, the same share the daily pipeline reports
|
|
1544
|
+
over the 3,498 articles already backfilled). That figure is the tool's **keyword** view and not the daily line's model view -
|
|
1545
|
+
these articles have never been through a model, which is why the script can pre-filter at all, and why the count is
|
|
1546
|
+
labelled as the keyword's. The scope is official accounts only (`gh_%`), which is what the script queries - an earlier
|
|
1547
|
+
count over every session in `Name2Id` read 10,880 and was wrong by 1,673 articles that live in non-official
|
|
1548
|
+
conversations and that this path never fetches. Anything scripted against a partially backfilled day must read `topicFilter` rather than
|
|
1549
|
+
infer completeness from the presence of `.articles.json`. An unknown `--topic` is rejected before the database is
|
|
1550
|
+
opened, since a typo would otherwise select 0 articles and report "nothing to do" - indistinguishable from a day that
|
|
1551
|
+
genuinely had none.
|
|
1552
|
+
|
|
1553
|
+
## D-059: The ball's eyes follow the mouse - and that makes the cursor position a data flow, so it is written down
|
|
1554
|
+
|
|
1555
|
+
**Status:** Active
|
|
1556
|
+
|
|
1557
|
+
**Decision.** The mascot's irises track the cursor. The position comes from the **main process**
|
|
1558
|
+
(`screen.getCursorScreenPoint()`, sampled every 120 ms, pushed **only when the coordinates change**), because the
|
|
1559
|
+
renderer cannot see it: the ball is 96x96 and the pointer is outside the window almost all of the time, so
|
|
1560
|
+
`mousemove` never fires. The page eases toward the pushed point and **stops its rAF loop the moment it settles** -
|
|
1561
|
+
a permanently-running 60 fps transform would be a compositor cost paid for decoration, on a window that is always
|
|
1562
|
+
open.
|
|
1563
|
+
|
|
1564
|
+
**Why there are now two images instead of one.** The mascot PNG has its eyes **drawn into** it, so nothing in it can
|
|
1565
|
+
move. `mascot-base.png` is the same art with the iris region repainted in the sclera colour, and `mascot-iris.png` is
|
|
1566
|
+
the iris (with its pupil and highlight) as its own layer. Composited at zero offset the two are **pixel-identical**
|
|
1567
|
+
to `mascot.png` - a test decodes all three PNGs and asserts that pixel by pixel, because every weaker check (same
|
|
1568
|
+
size, present in the whitelist, present in `PANEL_FILES`) stays green if somebody re-cuts them one pixel off, and a
|
|
1569
|
+
one-pixel misalignment is visible to a human and to nothing else. `mascot.png` stays in the repo as the source the
|
|
1570
|
+
other four expressions are generated from, but it is **no longer what the ball shows** - pointing the ball's face at
|
|
1571
|
+
it while an iris layer moves on top would show two pupils, one of them stationary.
|
|
1572
|
+
|
|
1573
|
+
**The travel limits are measured, not chosen.** Scanning the source pixels along the iris: to its left there are
|
|
1574
|
+
about 7 px of sclera before the dark rim, and above it there is none at all (the upper lid sits on the iris). At the
|
|
1575
|
+
ball's 96 px that is roughly 2.6 px of usable travel sideways and essentially zero upward, so the clamp is an
|
|
1576
|
+
**ellipse** (2.6 sideways / 0.8 up / 2.6 down) rather than a circle. Past that the iris separates from the rim and
|
|
1577
|
+
the eye reads as a sticker sliding across the face. The constants are written for the 96 px ball and are scaled by
|
|
1578
|
+
the element's measured width, so changing `--ball-size` does not require re-deriving them.
|
|
1579
|
+
|
|
1580
|
+
**The eyes move only on the two faces that have any to move.** Four of the five faces have their eyes baked in;
|
|
1581
|
+
stacking a moving iris on those shows two pupils. The iris layer is therefore hidden for `busy` / `offline` /
|
|
1582
|
+
`quota` / `ball-happy`. That is deliberate, not a limitation: those are precisely the "not idle" states, and they
|
|
1583
|
+
already speak with the whole face.
|
|
1584
|
+
|
|
1585
|
+
**Why a data-flow entry.** Cursor coordinates are the panel's second input that does not come from the daemon (the
|
|
1586
|
+
first is the window geometry from `ball-position.cjs`). They travel **in-process only** - main process to this
|
|
1587
|
+
window's renderer - are narrowed in `preload.cjs` (two clamped integers; malformed payloads are dropped rather than
|
|
1588
|
+
handed to the page) along the same rule as every other channel, are never logged and never leave the machine, and
|
|
1589
|
+
the feed stops entirely while the window is hidden. "The app knows where your mouse is" is a data flow even when it
|
|
1590
|
+
never crosses the machine boundary, so it is recorded rather than left implicit.
|
|
1591
|
+
|
|
1592
|
+
**Not yet verified live.** The wiring (the feed, the clamp, the reduced-motion path, the pixel identity) is covered
|
|
1593
|
+
by the suite, but the effect itself has not been run on a real window - that window only exists on a machine with
|
|
1594
|
+
Electron, via `weflow-cli panel`. The amplitudes come from the pixel measurements above; whether 2.6 px reads as
|
|
1595
|
+
"looking at you" at 96 px is a judgement only a person in front of the screen can make.
|
|
1596
|
+
|
|
1597
|
+
## D-058: The assistant reaches the MCP ceiling minus the exclusions - and two of the new capabilities carry a residual risk that is written down here rather than discovered later
|
|
1598
|
+
|
|
1599
|
+
**Status:** Active
|
|
1600
|
+
|
|
1601
|
+
**Decision.** The assistant's tool table was brought up to the same ceiling the MCP surface already had, minus
|
|
1602
|
+
everything the project has decided stays out. Three surfaces had drifted apart: the CLI exposes ~93 command
|
|
1603
|
+
declarations, `TOOL_DEFS` had 21 tools, and `mcp-server/index.ts` additionally hand-wrote 11 tools that the chat path
|
|
1604
|
+
could not reach at all (`get_review`, `get_concepts`, `format_article`, `list_themes`, `fetch_article`,
|
|
1605
|
+
`search_public`, plus the merged `get_stats`). Ten tools were added - `list_contacts`, `get_review`, `get_concepts`,
|
|
1606
|
+
`fetch_article`, `search_public`, `format_article`, `list_themes`, `lint_wiki`, `check_skills`, `set_todo_status` -
|
|
1607
|
+
the seven that existed only on the MCP side were **moved** into `TOOL_DEFS` (same tool names, same argument names,
|
|
1608
|
+
single implementation, so the hand-written table shrank from 11 entries to 4), and four existing tools were widened
|
|
1609
|
+
rather than duplicated: `get_daily_report` gained `full` (the human-readable daily) and cross-date search (which is
|
|
1610
|
+
what the MCP-only `search_articles` did), `get_todos` gained the item id and `group_by`, `get_weread` gained five
|
|
1611
|
+
`mode`s, `get_stats` gained the knowledge-base half it had been missing while the MCP copy had it.
|
|
1612
|
+
|
|
1613
|
+
**The direction is safe because it is not a new kind of access.** Every capability added here was already reachable
|
|
1614
|
+
by an MCP client; the change removes an inconsistency rather than widening what the project exposes. What did *not*
|
|
1615
|
+
move is the ceiling itself: sending, publishing, configuration, access-list writes, `evidence-review`, `vault promote`,
|
|
1616
|
+
scene reads and writes, `login-wechat`/`logout`/`listen`/`assistant run`, `sns capture-key`, `decide` and `init`/`dbkey`
|
|
1617
|
+
all stay unreachable from a model-driven path, each with the decision or comment that says so.
|
|
1618
|
+
|
|
1619
|
+
**Two capabilities carry a residual risk, and both were chosen deliberately by the user rather than slipped in.**
|
|
1620
|
+
|
|
1621
|
+
*Network egress from the chat path.* `fetch_article` and `search_public` are now callable from a WeChat message. The
|
|
1622
|
+
user was shown the asymmetry - the MCP path is documented as a trusted local integration, while a chat message is
|
|
1623
|
+
untrusted input that can carry a prompt injection - and chose to expose them **without** a confirmation gate, matching
|
|
1624
|
+
MCP. What each actually is: `fetch_article` accepts only credential-free HTTPS URLs on the exact `mp.weixin.qq.com`
|
|
1625
|
+
host and **re-checks that allowlist on every redirect hop** (the default `follow` would validate only the entry URL;
|
|
1626
|
+
the test asserts the `redirect: 'manual'` option, because a stubbed fetch cannot observe undici's internal follow -
|
|
1627
|
+
that hole was found by a mutation check). `search_public` is the weaker one and is recorded as such: it scrapes a
|
|
1628
|
+
third-party search page, its URL is a constant so no allowlist applies, and **the model's query leaves the machine
|
|
1629
|
+
with no preview**. Anyone reviewing that decision should read it as: a chat-driven model can cause an arbitrary string
|
|
1630
|
+
to be sent to a search engine, and that is accepted. The mitigation that exists is not a gate but the shape of the
|
|
1631
|
+
data: the query is a search phrase, never chat content.
|
|
1632
|
+
|
|
1633
|
+
*A bounded local write.* `set_todo_status` flips one existing todo between `done` and `pending`. It **cannot create,
|
|
1634
|
+
delete or edit** anything else - there is no `todos rm`, no `daily add/remove`, no `todos extract` in the tool table,
|
|
1635
|
+
and a test pins that (`工具表里没有任何删除待办的工具`). It carries **no** `ctx.requiresConfirm` gate, following the
|
|
1636
|
+
`export_chat` precedent (a write tool with no gate, because the gate set is machine-checked by
|
|
1637
|
+
`test/assistant-tools.test.ts` to be exactly `callsCloudModels` - adding a gate to a tool that does not send user data
|
|
1638
|
+
would either break that equality or require writing a false claim into `capabilities --json`). The residual risk is
|
|
1639
|
+
therefore stated plainly: **an MCP client does not get this tool at all** (`MCP_EXCLUDED`, because "mutating todos
|
|
1640
|
+
stays out" of that surface), while the chat/panel path can flip a status with no preview. Ambiguity is refused rather
|
|
1641
|
+
than resolved: a task-text match must be unique, and the reason is that a wrong guess looks like success while
|
|
1642
|
+
changing the wrong row.
|
|
1643
|
+
|
|
1644
|
+
**Reason.** Adding tools is not free: it makes the model choose among more near-duplicates, which is a failure mode
|
|
1645
|
+
this project has already measured once (`get_reading_stats`'s description had to be rewritten after it stole WeRead's
|
|
1646
|
+
question space and answered a reading question with official-account push counts). So the widenings in this entry are
|
|
1647
|
+
mostly *replacements*: cross-date article search went into `get_daily_report` rather than a second article-finder,
|
|
1648
|
+
`todos remind`'s grouping went in as a parameter rather than a tool, per-book WeRead data went in as `mode` values
|
|
1649
|
+
rather than five tools, and `get_concept` was deliberately **not** added because `search_knowledge` already returns
|
|
1650
|
+
the page plus its neighbour subgraph and a second, weaker lookup is a net loss for routing.
|
|
1651
|
+
|
|
1652
|
+
**Consequences.** Two injection points were added - `WEFLOW_ASSISTANT_BIZ_DAILY_DIR` and
|
|
1653
|
+
`WEFLOW_ASSISTANT_REVIEWS_DIR` - following `WEFLOW_ASSISTANT_EXPORT_ROOT`, and both are read **per call** rather than
|
|
1654
|
+
frozen at import so a test can swap fixtures inside one process. That closed the last uncovered tool branch: the test
|
|
1655
|
+
suite's coverage note had said for months that `get_daily_report` was never executed because its directory was a
|
|
1656
|
+
module-level constant with no injection point, and that the only possible assertion was a "either there is data or
|
|
1657
|
+
there is not" shape check - which is not an assertion. It is now asserted against a fixture, with its three branches
|
|
1658
|
+
(date, cross-date search, prose). The coverage claim is therefore "**all 31 tools are executed**" and the note says
|
|
1659
|
+
where each group lives. Locally-held identifiers were the other thing to get right: `list_contacts` returns names
|
|
1660
|
+
only (remark / display name / nickname - never `username`, never the avatar URL), because keeping wxids out of the
|
|
1661
|
+
model's context is a standing rule of this repository.
|
|
1662
|
+
|
|
1663
|
+
**Added 2026-09-30.** A third injection point, `WEFLOW_ASSISTANT_VAULT_DIR`, was added the same way and for the
|
|
1664
|
+
same reason: `get_concepts` read a module-level constant, so in the behaviour eval's temporary home it could only
|
|
1665
|
+
ever report "the knowledge base has not been compiled yet" - there was no fixture that could change that. With the
|
|
1666
|
+
root injectable it has a real case, as does `get_review` (its variable already existed, but the eval harness had no
|
|
1667
|
+
way to set environment variables at all, so it too had no case). **"That closed the last uncovered tool branch"
|
|
1668
|
+
above was true of the deterministic test suite, not of the behaviour eval** - the eval still had ten tools with no
|
|
1669
|
+
case at all, and those are what the two 2026-09-30 passes went after.
|
|
1670
|
+
|
|
1671
|
+
## D-057: A scene is bound to a conversation by the one id that already exists, it is picked by three tiers that refuse to guess, and the model cannot write one
|
|
1672
|
+
|
|
1673
|
+
**Status:** Active
|
|
1674
|
+
|
|
1675
|
+
**Decision.** A scene is a named prompt preset - keywords, an extra instruction, an output spec, a required-skill list,
|
|
1676
|
+
an enabled flag - stored in its own versioned file (`~/.weflow-cli/assistant_scenes.json`, schema `weflow-scenes/v1`,
|
|
1677
|
+
unknown version quarantined rather than migrated, atomic write, same discipline as the memory file). A turn picks at
|
|
1678
|
+
most one scene through three tiers in order: an explicit **binding**, then a **keyword** hit in the incoming text, then
|
|
1679
|
+
the **last scene used** in that conversation. No tier matching means no scene section in the prompt - the default
|
|
1680
|
+
behaviour is byte-identical. When two scenes match a keyword of the same length the turn carries **no** scene and the
|
|
1681
|
+
trace records why, following `resolveUniqueTalker` and `resolvePanelUserId`: ambiguous input is refused, never guessed.
|
|
1682
|
+
|
|
1683
|
+
**The binding key is `userId`, which is the conversation on the path that has conversations.** On the WeChat path
|
|
1684
|
+
`resolveInboundRouting` sets `conversationId` to the `@chatroom` id for a group and the peer's wxid for a direct chat,
|
|
1685
|
+
and `assistantService` passes exactly that through as `userId`; so binding by `userId` *is* binding by conversation, and
|
|
1686
|
+
it also gives the panel path (one stable bucket) something coherent to bind to. The cost is written down rather than
|
|
1687
|
+
discovered later: the MCP path collapses every caller into the literal `'mcp'`, so **MCP has no conversation to bind a
|
|
1688
|
+
scene to** - the same collapse that already applies to memory. Keying scenes per `userId` inside the *memory* file was
|
|
1689
|
+
rejected outright: that file is rewritten by an LLM during compression and fact extraction, and it can be quarantined
|
|
1690
|
+
wholesale, neither of which is an acceptable fate for something the user authored by hand.
|
|
1691
|
+
|
|
1692
|
+
**The model cannot touch scenes, and that is the point.** There is no assistant tool that lists, reads, creates or
|
|
1693
|
+
binds them; scenes are administered by `weflow-cli scene …` and by one built-in chat command (`场景 <id>`, `场景 无`,
|
|
1694
|
+
`场景`). This is the same position the `隐私` built-in takes - a chat message must not be able to change configuration -
|
|
1695
|
+
and it is why `capabilities --json` reports `modelWritable: false` for the workflow. The built-in command is also the
|
|
1696
|
+
only prefix-matching command in the assistant, which is precisely the shape that silently eats ordinary sentences, so
|
|
1697
|
+
it is narrowed twice: the argument must name an existing scene id, and anything else (`场景切换怎么用`) falls through to
|
|
1698
|
+
the model untouched.
|
|
1699
|
+
|
|
1700
|
+
**Reason:** the roadmap's P3 item asks for per-conversation control of the assistant rather than one global
|
|
1701
|
+
personality, and the three tiers are ordered by how much the user meant them: a binding is an explicit statement, a
|
|
1702
|
+
keyword is an inference from one message, and "last used" is only a convenience that keeps a habit from decaying. The
|
|
1703
|
+
refusal to break a tie is the same rule the codebase already applies to names - a coin flip between two scenes looks
|
|
1704
|
+
like the feature working while producing an answer nobody asked for.
|
|
1705
|
+
|
|
1706
|
+
**Consequences:** `scene add`/`remove`/`bind`/`unbind`/`enable`/`disable` are two-phase (`--dry-run` then `--yes`),
|
|
1707
|
+
matching every other local mutation. The scene section enters the prompt wrapped by `frameLocalData`, because a
|
|
1708
|
+
scene's instruction is user-authored text that can contain `</weflow-local-data>`; scene ids are therefore validated
|
|
1709
|
+
against quotes and angle brackets before they reach the label, and sanitised again at render time. Verified end to end
|
|
1710
|
+
through the resident service with a stubbed model: the scene section appears in the system prompt, the trace carries a
|
|
1711
|
+
`note` step naming the scene and the tier that matched, a keyword tie produces no section plus an explanatory note, and
|
|
1712
|
+
a turn with no match produces no section at all.
|
|
1713
|
+
|
|
1714
|
+
## D-056: A skill is material, not a plugin - it is referenced by path, it grants no tools, and a broken one is reported rather than skipped
|
|
1715
|
+
|
|
1716
|
+
**Status:** Active
|
|
1717
|
+
|
|
1718
|
+
**Decision.** The assistant can use skill packages: directories containing a `SKILL.md` with `name`/`description`
|
|
1719
|
+
frontmatter, scanned from a configurable list of roots (`skillDirs`, default `~/.claude/skills` and
|
|
1720
|
+
`~/.weflow-cli/skills`, overridable per-process by `WEFLOW_ASSISTANT_SKILL_DIRS`). Two read-only tools expose them -
|
|
1721
|
+
`list_skills` returns the catalogue (id, name, one-line description, version, state) and `read_skill` returns one
|
|
1722
|
+
body wrapped in `frameLocalData` - and the system prompt carries the catalogue so the model knows what exists. Skills
|
|
1723
|
+
are **not installed anywhere**: the prompt tells the model to read the file at its path, which is what the reference
|
|
1724
|
+
implementation does and is far cheaper than writing an installer per target agent. A referenced skill that is missing,
|
|
1725
|
+
disabled or unreadable is rendered as exactly that state, and the instruction is to continue and say which part was
|
|
1726
|
+
not done - never to silently drop the reference, and never to invent the capability.
|
|
1727
|
+
|
|
1728
|
+
**This is the constrained form of the roadmap's plugin/adapter item (P4), and it is deliberately the smallest one that
|
|
1729
|
+
is still useful.** Its acceptance criteria were already written down - extensions must not read configuration,
|
|
1730
|
+
databases or arbitrary files, and must be individually disableable - and a prompt-time document satisfies all of them by
|
|
1731
|
+
construction: `read_skill` can only return a file named `SKILL.md` that a scan already produced, so an id like
|
|
1732
|
+
`../config.json` is not a path at all, and disabling is one config key (`skillDisabled`) or one line in the skill's own
|
|
1733
|
+
frontmatter (`enabled: false`). Nothing here executes skill content, and installing a skill does not add a tool to the
|
|
1734
|
+
model's table. D-048 stands: the only way to add a *capability* is still to edit this source tree.
|
|
1735
|
+
|
|
1736
|
+
**Two rules were taken from real data rather than from the specification.** First, the id rule is **wider than
|
|
1737
|
+
Anthropic's**: that spec allows only lowercase letters, digits and single hyphens, which would reject five of the 28
|
|
1738
|
+
skills on the development machine (`clz_docx_to_mp`, `agent-dialog_management`, `clz_wechat_mp_ops`,
|
|
1739
|
+
`ylx_onehub_usage_monitor`, `ylx_research_evidence_synthesis`). Those are skills the user actually uses, so a
|
|
1740
|
+
non-conforming id is **warned about and kept**, and only the quoting characters that could escape a prompt label are
|
|
1741
|
+
refused. Second, the parser reads **only the leading `---` block** and understands block scalars: the same 28 files
|
|
1742
|
+
carry `categories:`/`created:`/`tags:` inside their bodies, so a whole-file scan reads body text as fields, and
|
|
1743
|
+
`description: >-` with indented continuation lines is how several of them are actually written - a naive line reader
|
|
1744
|
+
returns the two characters `>-` as the description.
|
|
1745
|
+
|
|
1746
|
+
**Consequences:** symlinked skill directories are **followed** (a deliberate choice, not an oversight): developing a
|
|
1747
|
+
skill in a git checkout and linking it into `~/.claude/skills` is a normal workflow, the link is placed by the user and
|
|
1748
|
+
not by an extension, and the only file reachable through it is one named `SKILL.md`. A skill with no frontmatter block
|
|
1749
|
+
is reported as unreadable by `skill check` instead of being skipped - a skill that vanishes silently is indistinguishable
|
|
1750
|
+
from one that was never installed. Verified against the real 28 skills with `skill check`: 0 unreadable, 0 collisions, 5
|
|
1751
|
+
spec-deviation warnings, 1 self-disabled.
|
|
1752
|
+
|
|
1403
1753
|
## D-055: The Vault's copy of an article is cleaned, the fetched original is not - and "which days does the copy run cover" was being decided by a marker that only one writer sets
|
|
1404
1754
|
|
|
1405
1755
|
**Status:** Active
|
package/docs/EXTENDING.md
CHANGED
|
@@ -6,7 +6,7 @@ one of two things, and they are not equivalent:
|
|
|
6
6
|
|
|
7
7
|
1. **In-repo** - add the capability to this source tree (fork + PR). Every recipe below is about this path, and each
|
|
8
8
|
one lists the places you must touch **and the test that will catch you if you miss one**.
|
|
9
|
-
2. **Out-of-process** - call the project from a program you keep elsewhere, through **MCP** (`mcp-server/`,
|
|
9
|
+
2. **Out-of-process** - call the project from a program you keep elsewhere, through **MCP** (`mcp-server/`, **32 tools**: the 28 the assistant exposes minus the three it withholds from this path, plus four hand-written `wechat.*` ones - the table in `docs/MCP.md` is the list, and `test/tool-registry.test.ts` fails if it drifts)
|
|
10
10
|
or the CLI's `--json` surface. This is the only boundary that exists for code that does not live in this repository.
|
|
11
11
|
|
|
12
12
|
## Why there is no plugin loader
|
|
@@ -176,12 +176,38 @@ These hold for any extension, and the full statements live in `AGENTS.md` and `C
|
|
|
176
176
|
reporting a real change.
|
|
177
177
|
- Do not record a guess as a verified fact. When something has not been measured, say so where the claim is made.
|
|
178
178
|
|
|
179
|
+
## Skills are content, not an extension point
|
|
180
|
+
|
|
181
|
+
A skill is a directory containing `SKILL.md` (`name`/`description` frontmatter, optionally `version`, `triggers`,
|
|
182
|
+
`enabled`), scanned from `skillDirs` (default `~/.claude/skills`, `~/.weflow-cli/skills`). **It does not follow any
|
|
183
|
+
recipe above.** Nothing here is code that joins the process:
|
|
184
|
+
|
|
185
|
+
- **It adds no tool.** The model sees a catalogue in the system prompt and can call `read_skill` to read one body. The
|
|
186
|
+
tool table is unchanged, so installing a skill cannot widen what the assistant is able to do.
|
|
187
|
+
- **Its content is never executed.** No scripts, no dependencies, no installation step - the "installer" is the user
|
|
188
|
+
putting a directory where the scanner looks.
|
|
189
|
+
- **It cannot read anything.** `read_skill` only ever returns a file named `SKILL.md` that the directory scan already
|
|
190
|
+
produced; an id shaped like a path is not a path. That is the P4 acceptance criterion (extensions must not read
|
|
191
|
+
configuration, databases or arbitrary files) satisfied by construction rather than by a check.
|
|
192
|
+
- **It is disableable alone** (`config set skillDisabled <id>` or `enabled: false` in the file itself).
|
|
193
|
+
- Bodies enter the prompt through `frameLocalData`, exactly like memory and scene text, because a `SKILL.md` is
|
|
194
|
+
local material that can contain a closing frame tag.
|
|
195
|
+
|
|
196
|
+
The rules that *are* worth knowing when authoring one: read only the leading `---` block (bodies contain lines that look
|
|
197
|
+
like frontmatter), block scalars are supported (`description: >-` with indented lines), and ids should stay
|
|
198
|
+
lowercase-with-hyphens if the skill is meant to travel to another agent - this project keeps and warns about
|
|
199
|
+
non-conforming ids (`clz_docx_to_mp` is in use here) rather than refusing them. `weflow-cli skill check` reports
|
|
200
|
+
unreadable frontmatter, cross-root collisions, non-conforming ids and disabled skills.
|
|
201
|
+
|
|
179
202
|
## What does not exist yet
|
|
180
203
|
|
|
181
204
|
Named here so that the gaps are not mistaken for omissions:
|
|
182
205
|
|
|
183
|
-
- **A plugin loader / adapter interface.** Roadmap, not implemented (see above).
|
|
206
|
+
- **A plugin loader / adapter interface.** Roadmap, not implemented (see above). Skills exist and are deliberately
|
|
207
|
+
*not* it: they are prompt-time material with no code path into this process.
|
|
184
208
|
- **A public import surface.** No `exports`, `main` or `types`; the published package is a CLI, and `src/` is shipped
|
|
185
209
|
without a stability promise.
|
|
186
210
|
- **A versioned extension contract.** The versioned contracts that exist are data contracts (`weflow-message/v1`,
|
|
187
|
-
`weflow-sync/v1`), not code contracts.
|
|
211
|
+
`weflow-sync/v1`, `weflow-scenes/v1`), not code contracts.
|
|
212
|
+
- **A skill installer.** There is no install/update/uninstall path and no per-agent deployment; the roots are read,
|
|
213
|
+
never written.
|
package/docs/MCP.md
CHANGED
|
@@ -50,6 +50,52 @@ The generated server starts with Node.js and `tsx`:
|
|
|
50
50
|
|
|
51
51
|
Copy this entry into your MCP client's configuration and ensure `cwd` points to the cloned WeFlow CLI directory. Restart the client after saving the configuration.
|
|
52
52
|
|
|
53
|
+
## HTTP transport (opt-in)
|
|
54
|
+
|
|
55
|
+
The server speaks **stdio by default** and stays that way unless you ask for the other transport. Adding `--http`
|
|
56
|
+
(or `WEFLOW_MCP_HTTP=1`) serves the **same tools** over Streamable HTTP - the tool table is derived from one place
|
|
57
|
+
(`TOOL_DEFS`), so the two transports cannot drift apart:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" # make a token
|
|
61
|
+
npx tsx mcp-server/index.ts --http --token=<that token>
|
|
62
|
+
# WeFlow MCP 已监听 http://127.0.0.1:8790/mcp
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Point an HTTP-capable client at `http://127.0.0.1:8790/mcp` with `Authorization: Bearer <token>`.
|
|
66
|
+
|
|
67
|
+
Four boundaries, enforced in `mcp-server/httpConfig.ts` and `mcp-server/http.ts` (pure decisions, offline tests in
|
|
68
|
+
`test/mcp-http.test.ts`, each one mutation-checked):
|
|
69
|
+
|
|
70
|
+
| Boundary | Why |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| **Off unless asked** | No `--http` means stdio, byte for byte what it did before. |
|
|
73
|
+
| **A token is required, and there is no default** | A silently generated token looks configured. Without one the server refuses to start (exit 1) and tells you how to make one. |
|
|
74
|
+
| **Loopback only** (`127.0.0.1` / `localhost` / `::1`) | `0.0.0.0` is rejected on purpose. For another machine use a tunnel (`ssh -L 8790:127.0.0.1:8790 <host>`) or your own reverse proxy in front of it - widening the bind is not something this command should do for you. |
|
|
75
|
+
| **DNS-rebinding protection on** | The SDK defaults it to `false`. The realistic attack on a loopback server is a web page you happen to visit reaching `127.0.0.1`, so the `Host` header must match and any `Origin` must be allowlisted. |
|
|
76
|
+
|
|
77
|
+
Port defaults to 8790 (`--http=9100`, `--port=`, or `WEFLOW_MCP_HTTP_PORT`); `--http=0` lets the OS pick one, which
|
|
78
|
+
is what the tests do. Requests are stateless - one server per request, no session state to leak between clients -
|
|
79
|
+
and **the token is never written to a log**.
|
|
80
|
+
|
|
81
|
+
An HTTP-capable client points at the URL instead of a command. The server is one you start (and stop) yourself, so
|
|
82
|
+
the client never manages the process:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"weflow": {
|
|
88
|
+
"type": "http",
|
|
89
|
+
"url": "http://127.0.0.1:8790/mcp",
|
|
90
|
+
"headers": { "Authorization": "Bearer <the token you started it with>" }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Keep the token out of a file you commit: put it in an environment variable your client expands, or in a
|
|
97
|
+
client-local config that is not in version control.
|
|
98
|
+
|
|
53
99
|
## Available tools
|
|
54
100
|
|
|
55
101
|
| Tool | Purpose | Local data required |
|
|
@@ -81,6 +127,11 @@ Copy this entry into your MCP client's configuration and ensure `cwd` points to
|
|
|
81
127
|
| `wechat.export_chat` | Export one conversation to HTML, txt, json or excel. | Decrypted WeChat database, **writes under `output/exports/`** |
|
|
82
128
|
| `wechat.export_messages` | Return a conversation's local messages as `weflow-message/v1` JSON. Read-only; returns no database path, key or configuration. | Decrypted WeChat database |
|
|
83
129
|
| `wechat.get_reading_stats` | Which sources push most and what the daily processes. | `output/biz-daily/` |
|
|
130
|
+
| `wechat.list_skills` | List skill packages installed on this machine (`SKILL.md` bundles). | Skill directories (`skillDirs` config) |
|
|
131
|
+
| `wechat.read_skill` | Read one skill's `SKILL.md` body. Read-only; only files under the configured skill directories, never arbitrary paths. | Skill directories (`skillDirs` config) |
|
|
132
|
+
| `wechat.check_skills` | Report skill problems: unreadable frontmatter, cross-directory collisions, non-conforming ids, disabled skills. | Skill directories (`skillDirs` config) |
|
|
133
|
+
| `wechat.list_contacts` | List contact names from the local address book. **Names only** - no `wxid`, alias or other local identifiers. | Decrypted WeChat database |
|
|
134
|
+
| `wechat.lint_wiki` | Knowledge-base health check: broken links, orphan pages, empty pages, duplicate titles. Read-only; changes nothing. | Vault Wiki output |
|
|
84
135
|
|
|
85
136
|
This table lists the tools shipped today; `weflow-cli mcp-config` prints the authoritative list for the
|
|
86
137
|
installed version. **`wechat.look_at_image` is deliberately not on it**: that tool works by handing the image to
|
|
@@ -92,9 +143,9 @@ offered on it (the same rule `unavailableToolReason` applies elsewhere). The cha
|
|
|
92
143
|
|
|
93
144
|
- The server inherits the permissions of the MCP client. Only add it to a client you trust.
|
|
94
145
|
- Article and knowledge-base tools read files under the current project directory. Keep `cwd` scoped to your intended WeFlow CLI checkout.
|
|
95
|
-
- Chat-data tools (`list_sessions`, `get_messages`, `search_favorites`, `get_sns`, `get_todos`) read your locally decrypted WeChat database. Only run this MCP server on machines where that is acceptable, and never expose the stdio server over a network.
|
|
146
|
+
- Chat-data tools (`list_sessions`, `get_messages`, `search_favorites`, `get_sns`, `get_todos`, `list_contacts`) read your locally decrypted WeChat database. Only run this MCP server on machines where that is acceptable, and never expose the stdio server over a network.
|
|
96
147
|
- `read_favorite`, `fetch_article`, and `search_public` make network requests; `read_favorite` rejects private/loopback URLs.
|
|
97
|
-
- The MCP surface is **derived from the assistant's tool table minus `save_memory
|
|
148
|
+
- The MCP surface is **derived from the assistant's tool table minus the excluded tools** (`save_memory`, `look_at_image`, `set_todo_status`), not a hand-written read-only list - so it is *not* read-only, and this line used to claim it was. What it actually contains, declared in `capabilities --json` under `safety.mcpSurface`: one tool that writes (`export_chat`, new directories under `output/exports/` only) and four that send user data to cloud models (`who_owes_reply` and `search_chats` send chat text to the decision model, `search_semantic` sends the query for embedding and the hits for reranking, `draft_reply` sends a conversation to two models - and **all four** require `confirm: true`, not just `draft_reply`; that is what `safety.mcpSurface.requiresConfirm` lists, and it is now checked against the source rather than restated). Publishing, sending messages, mutating todos, changing configuration and writing assistant memory stay out.
|
|
98
149
|
- **Four tools on this surface send user data to cloud models, and each of them requires an explicit
|
|
99
150
|
confirmation from the machine caller.** They are `who_owes_reply` (every conversation's text, one request
|
|
100
151
|
each), `search_chats` (your question plus words extracted from the chats - not the message bodies),
|
|
@@ -110,5 +161,8 @@ offered on it (the same rule `unavailableToolReason` applies elsewhere). The cha
|
|
|
110
161
|
| Symptom | Check |
|
|
111
162
|
| --- | --- |
|
|
112
163
|
| Server fails to start | Run `npm install` in the configured `cwd`, then run `npx tsx mcp-server/index.ts`. |
|
|
164
|
+
| HTTP mode refuses to start | The message says why: no token (`--token=` / `WEFLOW_MCP_TOKEN`), a token under 24 characters, or a bind address that is not loopback. |
|
|
165
|
+
| HTTP client gets 401 | The `Authorization: Bearer <token>` header is missing or wrong - it must be the token the server was started with. |
|
|
166
|
+
| HTTP client cannot connect from another machine | Working as intended: the bind is loopback. Tunnel to it (`ssh -L 8790:127.0.0.1:8790 <host>`) rather than opening the port. |
|
|
113
167
|
| No articles found | Generate a daily collection first, then confirm `output/biz-daily/` exists under `cwd`. |
|
|
114
168
|
| Client cannot find `npx` | Configure an absolute Node.js command path or install Node.js 22.13+. |
|