@twinsearth/w2m-dsh-plugin 1.9.17 → 1.9.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +3 -3
- package/bin/w2m-joint.mjs +1 -1
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/1.9.19/RELEASE-1919.json +21 -0
- package/docs/acceptance/1.9.20/RELEASE-1920.json +22 -0
- package/docs/acceptance-ops/2026-10-28/AG-SMOKE.json +24 -0
- package/lib/client.js +240 -104
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/src/agent/agent.mjs +6 -1
- package/src/agent/sinks/dir-sink.mjs +59 -13
- package/src/plugin/fleet-chat-route.mjs +7 -1
- package/src/plugin/joint-dev/audit-file.mjs +70 -37
- package/src/plugin/joint-dev/merge-engine.mjs +102 -0
- package/src/plugin/joint-dev/relay-dispatch.mjs +103 -21
- package/src/plugin/tools-joint.mjs +97 -0
- package/src/plugin/tools.mjs +5 -2
- package/src/relay/fleet-messages.mjs +22 -0
- package/src/relay/report.mjs +35 -0
- package/src/relay/server.mjs +2043 -2038
- package/src/relay/state.mjs +11 -0
- package/src/util/artifact-ref.mjs +88 -0
- package/src/util/redline-core.mjs +2 -1
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
|
|
8
|
-
"version": "1.9.
|
|
8
|
+
"version": "1.9.20"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "w2m-fleet",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
|
|
15
|
-
"version": "1.9.
|
|
15
|
+
"version": "1.9.20",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -26,5 +26,5 @@
|
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
|
-
"version": "1.9.
|
|
29
|
+
"version": "1.9.20"
|
|
30
30
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "w2m-fleet",
|
|
3
3
|
"description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.20",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@
|
|
|
7
7
|
|
|
8
8
|
All notable changes to this project are documented here.
|
|
9
9
|
|
|
10
|
+
## [1.9.20] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
- 新增浮动滚动浮标(固定在主工作区右上角,不依赖 slot 布局):
|
|
13
|
+
· 「⇧3」:单击向上滚动约 3 轮对话;长按 3 秒直接回到第一轮;
|
|
14
|
+
· 「⤒」:一键到顶。
|
|
15
|
+
- 浮标挂载在插件生命周期内(ctx.effect 卸载时移除),失败只记录、不影响页面启动。
|
|
16
|
+
|
|
17
|
+
## [1.9.19] - 2026-10-28
|
|
18
|
+
|
|
19
|
+
- 对话右上角新增「⇧3」按钮:单击向上滚动约 3 轮对话,长按 3 秒直接回到第一轮。
|
|
20
|
+
- 全文搜索高亮修复:改为直接插入本地 mark 元素(黄色背景 rgba(255,235,59,.85) + 绿色文字 rgba(11,122,51,1)),并递归遍历 shadow DOM;不再依赖 CSS Custom Highlight API,命中项一定可见。
|
|
21
|
+
- 同版带上 v1.9.18 的聊天收紧:relay 只接收文字与 /微笑 /火箭 /烟花 指令(图片字段、data:image、Markdown 图片、base64 图片魔数一律 400 拒绝;加密消息按设计豁免);表情按钮改为指令按钮;长提示词镜像按 3500 B 截断,避免被 4096 B 上限静默丢弃。
|
|
22
|
+
|
|
10
23
|
## [1.9.17] - 2026-10-28
|
|
11
24
|
|
|
12
25
|
- 修复左侧栏 GPW 入口只剩小图标:恢复 🌐 GPW 文字(侧栏 label 仍为空,不会重复)。
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ English | [中文](#中文说明)
|
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
|
-
Five routes in. They all end with **
|
|
17
|
+
Five routes in. They all end with **18 tools registered** in your agent. Pick one.
|
|
18
18
|
|
|
19
19
|
### 1. DSH plugin (recommended — zero configuration)
|
|
20
20
|
|
|
@@ -136,7 +136,7 @@ hand-maintained host list.
|
|
|
136
136
|
### Tools
|
|
137
137
|
|
|
138
138
|
`w2m_status` · `w2m_devices` · `w2m_run` · `w2m_wait` · `w2m_history` · `w2m_stats` · `w2m_report` ·
|
|
139
|
-
`w2m_memory` · `w2m_approvals` · `w2m_cancel` · `w2m_council` · `w2m_chat` · `w2m_update` `w2m_tasks` · `w2m_reduce`
|
|
139
|
+
`w2m_memory` · `w2m_approvals` · `w2m_cancel` · `w2m_council` · `w2m_chat` · `w2m_update` `w2m_tasks` · `w2m_reduce` · `w2m_joint_plan` · `w2m_joint_status`
|
|
140
140
|
- `w2m_intent` — map a structured intent (run-tests, node-version, git-status, git-head, fleet-facts, shard-probe) onto an allow-listed command; unknown intents are refused by name, never guessed.
|
|
141
141
|
|
|
142
142
|
### Environment a dispatched command receives (1.1.15+)
|
|
@@ -261,7 +261,7 @@ MIT.
|
|
|
261
261
|
|
|
262
262
|
### 安装
|
|
263
263
|
|
|
264
|
-
五种方式,最终都会在你的 agent 里注册 **
|
|
264
|
+
五种方式,最终都会在你的 agent 里注册 **18 tools registered**。任选其一。
|
|
265
265
|
|
|
266
266
|
#### 1. DSH 插件(推荐,零配置)
|
|
267
267
|
|
package/bin/w2m-joint.mjs
CHANGED
|
@@ -163,7 +163,7 @@ if (command === 'dispatch') {
|
|
|
163
163
|
// decision 2: one approval authorises exactly one dispatch.
|
|
164
164
|
const consumed = consumeApproval({ stateDir, planSha: args.plan, approvalSha: gate.approval.approval.approval_sha256 });
|
|
165
165
|
if (consumed.ok !== true) fail(consumed.code ?? 'APPROVAL_CONSUME_FAILED', consumed.detail ?? null);
|
|
166
|
-
const sent = await dispatchPlanToRelay({ rabbitUrl: relayUrl, token, plan: gate.plan, approval: gate.approval.approval });
|
|
166
|
+
const sent = await dispatchPlanToRelay({ rabbitUrl: relayUrl, token, plan: gate.plan, approval: gate.approval.approval, auditFile: stateDir + '/joint-dev/audit.jsonl' });
|
|
167
167
|
print({ ...sent, plan_sha256: gate.plan.plan_sha256, relay: relayUrl });
|
|
168
168
|
process.exit(sent.ok === true ? 0 : 1);
|
|
169
169
|
}
|
package/codex/mcp-servers.toml
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.9.19",
|
|
3
|
+
"case": "RELEASE-1919",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-28T05:08:27.700Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"task_id": "pre-release-1919-1793164107702",
|
|
11
|
+
"requirement": "V1.9.19:对话向上滚动按钮(点击 3 轮/长按 3 秒到首轮)+ 搜索命中绿字黄底(DOM mark,含 shadow DOM)",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"scroll": "header ⇧3: click scrollBy 3 turns, hold 3s scrollTo top",
|
|
16
|
+
"search": "mark.w2mSearchMark with rgba yellow background + rgba green text, shadow-root aware",
|
|
17
|
+
"chat": "text-only guard retained from v1.9.18 work",
|
|
18
|
+
"tests": "client-bundle 26/26"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.9.20",
|
|
3
|
+
"case": "RELEASE-1920",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": [
|
|
6
|
+
"b53a37f4-1464-4512-9259-33a7df091e3e"
|
|
7
|
+
],
|
|
8
|
+
"executed_at": "2026-10-28T07:38:58.684Z",
|
|
9
|
+
"executor": "lead",
|
|
10
|
+
"task_id": "pre-release-1920-1793173138685",
|
|
11
|
+
"requirement": "V1.9.20:主工作区右上角浮动滚动浮标(⇧3 单击 3 轮/长按 3 秒到顶 + ⤒ 一键到顶)",
|
|
12
|
+
"result": {
|
|
13
|
+
"outcome": "ok",
|
|
14
|
+
"assertions": {
|
|
15
|
+
"float": "fixed top-right DOM controls installed from mount(), removed by ctx.effect",
|
|
16
|
+
"click": "scrollBy ~3 turns on the largest scrollable conversation surface",
|
|
17
|
+
"longpress": "pointerdown 3s -> scrollTo top",
|
|
18
|
+
"oneclick": "top button scrolls to first turn",
|
|
19
|
+
"tests": "client-bundle 26/26"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "ops-smoke",
|
|
3
|
+
"case": "AG-SMOKE",
|
|
4
|
+
"executed_at": "2026-10-28T05:02:36.913Z",
|
|
5
|
+
"executor": "lead",
|
|
6
|
+
"task_id": "01M60E2W38FFKXKRZS95JSVWFM",
|
|
7
|
+
"setup": "relay 43.142.74.71:8787 upgraded to 1.9.16; operator token used from ~/.w2m/operator-token; read via local device token",
|
|
8
|
+
"attempt": "broadcast executor_machine_id=ToFuture command_argv=[\"node\",\"-v\"] at 2.0.0-tree relay",
|
|
9
|
+
"observed": {
|
|
10
|
+
"dispatch": "POST /v1/task 200; executor lease on ToFuture=offered; DSH/macmini and 15 other devices=observing",
|
|
11
|
+
"final": "aggregate=refused; ToFuture lease=refused (agent allow-list), outcome=refused; observers did not execute"
|
|
12
|
+
},
|
|
13
|
+
"conclusions": {
|
|
14
|
+
"selection_routing": "PASS at relay level: only the named executor gets an executing lease; every other device is observing",
|
|
15
|
+
"execution": "NOT PROVEN: the target agent refused the command (allow-list)",
|
|
16
|
+
"ag_matrix": "PARTIAL: A/B/C/D routing smoke only; E/F/G and real execution blocked"
|
|
17
|
+
},
|
|
18
|
+
"blockers": [
|
|
19
|
+
"agent allow-list does not include node -v on ToFuture",
|
|
20
|
+
"running DSH process still predates the operator-token change (restart required)",
|
|
21
|
+
"macmini/DSH plugin versions unknown/unverified; same-version requirement not yet met",
|
|
22
|
+
"S4 data plane (agent putBlobBytes reroute + no-sink degradation) not implemented"
|
|
23
|
+
]
|
|
24
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -495,6 +495,8 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
495
495
|
// v1.6.10: the transcript is selectable text; screenshots get a preview and a copy affordance.
|
|
496
496
|
'.w2mChatTab{padding:4px 10px;border-radius:6px;border:1px solid rgba(127,127,127,.35);background:transparent;color:inherit;font:inherit;cursor:pointer}',
|
|
497
497
|
'.w2mChat_panelFull{position:fixed;inset:5vh 8vw;width:auto;max-height:90vh;z-index:70}',
|
|
498
|
+
'.w2mChat_sticker{font-size:16px;line-height:1}',
|
|
499
|
+
'.w2mChat_stickerBig{font-size:28px;line-height:1.1}',
|
|
498
500
|
'.w2mChat_emojiRow{display:flex;gap:4px;padding:4px 8px;flex-wrap:wrap}',
|
|
499
501
|
'.w2mChat_emoji{padding:2px 4px;border:0;background:transparent;font-size:16px;line-height:1;cursor:pointer}',
|
|
500
502
|
'.w2mFleetStatus{display:flex;gap:16px;align-items:center;padding:2px 12px;font-size:11px;opacity:.8;white-space:nowrap}',
|
|
@@ -504,6 +506,11 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
504
506
|
'.w2mSearchBox{display:flex;align-items:center;gap:4px;padding:2px 4px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-background-secondary)}',
|
|
505
507
|
'.w2mSearchInput{height:24px;min-width:220px;border:0;background:transparent;color:inherit;font:inherit;outline:none}',
|
|
506
508
|
'.w2mSearchCount{font-size:11px;opacity:.75;min-width:34px;text-align:center}',
|
|
509
|
+
'.w2mSearchControls{display:inline-flex;align-items:center;gap:4px}',
|
|
510
|
+
'.w2mScrollFloatWrap{position:fixed;right:18px;top:72px;z-index:2147483000;display:flex;flex-direction:column;gap:6px}',
|
|
511
|
+
'.w2mScrollFloatBtn{width:34px;height:34px;border-radius:50%;border:1px solid currentColor;background:transparent;color:inherit;opacity:.72;font-size:13px;line-height:1;cursor:pointer}',
|
|
512
|
+
'.w2mScrollFloatBtn:hover{opacity:1}',
|
|
513
|
+
'.w2mScrollUp{font-size:12px;padding:2px 6px;border:1px solid rgba(128,128,128,.4);border-radius:4px;background:transparent;color:inherit;cursor:pointer}',
|
|
507
514
|
'::highlight(w2m-conversation-search){background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1)}',
|
|
508
515
|
'.w2mSearchMark{background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1);border-radius:2px}',
|
|
509
516
|
'.w2mSearchBtn{height:24px;min-width:24px;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer}',
|
|
@@ -964,112 +971,216 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
964
971
|
* Custom Highlight API (no DOM mutation, so React's tree stays intact), then scrolls to the
|
|
965
972
|
* focused match. Matches stay yellow until the query changes or the box closes.
|
|
966
973
|
*/
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
974
|
+
function searchRoots() {
|
|
975
|
+
const roots = [];
|
|
976
|
+
const seen = new Set();
|
|
977
|
+
const visit = (node) => {
|
|
978
|
+
if (!node || seen.has(node)) return;
|
|
979
|
+
seen.add(node);
|
|
980
|
+
if (node.nodeType === 1 || node.nodeType === 11) {
|
|
981
|
+
if (node.shadowRoot) { roots.push(node.shadowRoot); visit(node.shadowRoot); }
|
|
982
|
+
const kids = node.children ?? [];
|
|
983
|
+
for (let i = 0; i < kids.length; i += 1) visit(kids[i]);
|
|
970
984
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
try {
|
|
1016
|
-
if (typeof CSS !== 'undefined' && CSS.highlights && typeof Highlight === 'function') {
|
|
1017
|
-
CSS.highlights.set(SEARCH_HIGHLIGHT, new Highlight(...ranges));
|
|
1018
|
-
}
|
|
1019
|
-
} catch { /* older engine: the count still helps */ }
|
|
1020
|
-
if (ranges.length > 0) {
|
|
1021
|
-
setCursor(0);
|
|
1022
|
-
try {
|
|
1023
|
-
const parent = ranges[0].startContainer.parentElement;
|
|
1024
|
-
if (parent && parent.scrollIntoView) parent.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1025
|
-
} catch { /* scroll is best effort */ }
|
|
1026
|
-
} else setCursor(-1);
|
|
1027
|
-
return ranges.length;
|
|
1028
|
-
}, [collect]);
|
|
1029
|
-
React.useEffect(() => {
|
|
1030
|
-
if (!open) { clearSearchHighlight(); setCount(0); setCursor(-1); return undefined; }
|
|
1031
|
-
paint();
|
|
1032
|
-
return () => clearSearchHighlight();
|
|
1033
|
-
}, [open, query, paint]);
|
|
1034
|
-
const step = (backwards) => {
|
|
1035
|
-
const ranges = rangesRef.current;
|
|
1036
|
-
if (ranges.length === 0) { paint(); return; }
|
|
1037
|
-
const next = (cursor + (backwards ? -1 : 1) + ranges.length) % ranges.length;
|
|
1038
|
-
setCursor(next);
|
|
985
|
+
};
|
|
986
|
+
if (typeof document !== 'undefined' && document.body) { roots.push(document.body); visit(document.body); }
|
|
987
|
+
return roots;
|
|
988
|
+
}
|
|
989
|
+
function clearSearchMarks() {
|
|
990
|
+
if (typeof document === 'undefined') return;
|
|
991
|
+
const marks = document.querySelectorAll('mark.w2mSearchMark');
|
|
992
|
+
for (const mark of marks) {
|
|
993
|
+
const parent = mark.parentNode;
|
|
994
|
+
if (parent === null) continue;
|
|
995
|
+
parent.replaceChild(document.createTextNode(mark.textContent ?? ''), mark);
|
|
996
|
+
if (parent.normalize) parent.normalize();
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function paintSearchMarks(query) {
|
|
1000
|
+
clearSearchMarks();
|
|
1001
|
+
if (typeof document === 'undefined' || query === '') return 0;
|
|
1002
|
+
const needle = query.toLowerCase();
|
|
1003
|
+
let total = 0;
|
|
1004
|
+
for (const root of searchRoots()) {
|
|
1005
|
+
let walker;
|
|
1006
|
+
try {
|
|
1007
|
+
walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
1008
|
+
acceptNode(node) {
|
|
1009
|
+
const parent = node.parentElement;
|
|
1010
|
+
if (parent === null) return NodeFilter.FILTER_REJECT;
|
|
1011
|
+
const tag = parent.tagName;
|
|
1012
|
+
if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'TEXTAREA' || tag === 'INPUT') return NodeFilter.FILTER_REJECT;
|
|
1013
|
+
if (parent.closest && parent.closest('[data-w2m-search]') !== null) return NodeFilter.FILTER_REJECT;
|
|
1014
|
+
if (parent.closest && parent.closest('mark.w2mSearchMark') !== null) return NodeFilter.FILTER_REJECT;
|
|
1015
|
+
return node.nodeValue && node.nodeValue.trim() !== '' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
|
1016
|
+
},
|
|
1017
|
+
});
|
|
1018
|
+
} catch { continue; }
|
|
1019
|
+
const nodes = [];
|
|
1020
|
+
let node = walker.nextNode();
|
|
1021
|
+
while (node !== null) { nodes.push(node); node = walker.nextNode(); }
|
|
1022
|
+
for (const textNode of nodes) {
|
|
1023
|
+
const text = textNode.nodeValue ?? '';
|
|
1024
|
+
const lower = text.toLowerCase();
|
|
1025
|
+
const hits = [];
|
|
1026
|
+
let index = lower.indexOf(needle);
|
|
1027
|
+
while (index >= 0) { hits.push(index); index = lower.indexOf(needle, index + query.length); }
|
|
1028
|
+
for (let k = hits.length - 1; k >= 0; k -= 1) {
|
|
1039
1029
|
try {
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
'aria-label': '搜索本对话',
|
|
1050
|
-
onClick: () => setOpen(true),
|
|
1051
|
-
}, '🔍');
|
|
1030
|
+
const range = document.createRange();
|
|
1031
|
+
range.setStart(textNode, hits[k]);
|
|
1032
|
+
range.setEnd(textNode, hits[k] + query.length);
|
|
1033
|
+
const mark = document.createElement('mark');
|
|
1034
|
+
mark.className = 'w2mSearchMark';
|
|
1035
|
+
mark.setAttribute('style', 'background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1)');
|
|
1036
|
+
range.surroundContents(mark);
|
|
1037
|
+
total += 1;
|
|
1038
|
+
} catch { /* a range that crosses elements cannot be wrapped; skip it */ }
|
|
1052
1039
|
}
|
|
1053
|
-
return h('div', { className: 'w2mSearchBox', 'data-w2m-search': 'box' }, [
|
|
1054
|
-
h('input', {
|
|
1055
|
-
key: 'q',
|
|
1056
|
-
ref: inputRef,
|
|
1057
|
-
className: 'w2mSearchInput',
|
|
1058
|
-
value: query,
|
|
1059
|
-
placeholder: '搜索本对话全部内容',
|
|
1060
|
-
onChange: (event) => setQuery(event.target.value),
|
|
1061
|
-
onKeyDown: (event) => {
|
|
1062
|
-
if (event.key === 'Enter') { event.preventDefault(); step(event.shiftKey); }
|
|
1063
|
-
if (event.key === 'Escape') { setOpen(false); setQuery(''); }
|
|
1064
|
-
},
|
|
1065
|
-
}),
|
|
1066
|
-
h('span', { key: 'count', className: 'w2mSearchCount' }, count === 0 ? (query === '' ? '' : '0') : whenCount(count, cursor)),
|
|
1067
|
-
h('button', { key: 'prev', type: 'button', className: 'w2mSearchBtn', title: '上一个', onClick: () => step(true) }, '↑'),
|
|
1068
|
-
h('button', { key: 'next', type: 'button', className: 'w2mSearchBtn', title: '下一个', onClick: () => step(false) }, '↓'),
|
|
1069
|
-
h('button', { key: 'close', type: 'button', className: 'w2mSearchBtn', title: '关闭搜索', onClick: () => { setOpen(false); setQuery(''); clearSearchHighlight(); } }, '×'),
|
|
1070
|
-
]);
|
|
1071
1040
|
}
|
|
1072
|
-
|
|
1041
|
+
}
|
|
1042
|
+
return total;
|
|
1043
|
+
}
|
|
1044
|
+
function findScrollSurface() {
|
|
1045
|
+
if (typeof document === 'undefined') return null;
|
|
1046
|
+
let best = null;
|
|
1047
|
+
for (const root of searchRoots()) {
|
|
1048
|
+
let all = [];
|
|
1049
|
+
try { all = root.querySelectorAll ? [...root.querySelectorAll('*')] : []; } catch { continue; }
|
|
1050
|
+
for (const el of all) {
|
|
1051
|
+
if (el.scrollHeight <= el.clientHeight + 40) continue;
|
|
1052
|
+
const style = typeof getComputedStyle === 'function' ? getComputedStyle(el) : null;
|
|
1053
|
+
const overflow = style === null ? '' : style.overflowY;
|
|
1054
|
+
if (overflow !== 'auto' && overflow !== 'scroll') continue;
|
|
1055
|
+
if (best === null || el.scrollHeight > best.scrollHeight) best = el;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return best;
|
|
1059
|
+
}
|
|
1060
|
+
function scrollChatTurns() {
|
|
1061
|
+
const surface = findScrollSurface();
|
|
1062
|
+
if (surface !== null) surface.scrollBy({ top: -Math.round(Math.max(240, surface.clientHeight * 1.8)), behavior: 'smooth' });
|
|
1063
|
+
else if (typeof window !== 'undefined') window.scrollBy({ top: -Math.round(window.innerHeight * 1.8), behavior: 'smooth' });
|
|
1064
|
+
}
|
|
1065
|
+
function scrollChatTop() {
|
|
1066
|
+
const surface = findScrollSurface();
|
|
1067
|
+
if (surface !== null) surface.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1068
|
+
else if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1069
|
+
}
|
|
1070
|
+
/** Floating scroll controls: fixed top-right of the workspace, independent of any slot layout. */
|
|
1071
|
+
function installFloatingScrollButtons() {
|
|
1072
|
+
if (typeof document === 'undefined' || document.body === null) return null;
|
|
1073
|
+
const wrap = document.createElement('div');
|
|
1074
|
+
wrap.className = 'w2mScrollFloatWrap';
|
|
1075
|
+
wrap.setAttribute('data-w2m-scroll-float', 'true');
|
|
1076
|
+
const makeButton = (label, title) => {
|
|
1077
|
+
const button = document.createElement('button');
|
|
1078
|
+
button.type = 'button';
|
|
1079
|
+
button.className = 'w2mScrollFloatBtn';
|
|
1080
|
+
button.textContent = label;
|
|
1081
|
+
button.title = title;
|
|
1082
|
+
button.setAttribute('aria-label', title);
|
|
1083
|
+
wrap.appendChild(button);
|
|
1084
|
+
return button;
|
|
1085
|
+
};
|
|
1086
|
+
const up = makeButton('\u21E73', '向上滚动 3 轮(长按 3 秒到第一轮)');
|
|
1087
|
+
const top = makeButton('\u2912', '一键到顶');
|
|
1088
|
+
let holdTimer = null;
|
|
1089
|
+
let longFired = false;
|
|
1090
|
+
const clearHold = () => { if (holdTimer !== null) { clearTimeout(holdTimer); holdTimer = null; } };
|
|
1091
|
+
up.addEventListener('pointerdown', (event) => {
|
|
1092
|
+
if (event && typeof event.preventDefault === 'function') event.preventDefault();
|
|
1093
|
+
longFired = false;
|
|
1094
|
+
clearHold();
|
|
1095
|
+
holdTimer = setTimeout(() => { longFired = true; scrollChatTop(); }, 3000);
|
|
1096
|
+
});
|
|
1097
|
+
for (const type of ['pointerup', 'pointerleave', 'pointercancel']) up.addEventListener(type, clearHold);
|
|
1098
|
+
up.addEventListener('click', () => { if (longFired) { longFired = false; return; } scrollChatTurns(); });
|
|
1099
|
+
top.addEventListener('click', () => scrollChatTop());
|
|
1100
|
+
document.body.appendChild(wrap);
|
|
1101
|
+
return () => { clearHold(); wrap.remove(); };
|
|
1102
|
+
}
|
|
1103
|
+
function ConversationSearch() {
|
|
1104
|
+
const [open, setOpen] = React.useState(false);
|
|
1105
|
+
const [query, setQuery] = React.useState('');
|
|
1106
|
+
const [count, setCount] = React.useState(0);
|
|
1107
|
+
const [cursor, setCursor] = React.useState(-1);
|
|
1108
|
+
const inputRef = React.useRef(null);
|
|
1109
|
+
const holdRef = React.useRef(null);
|
|
1110
|
+
const longFiredRef = React.useRef(false);
|
|
1111
|
+
React.useEffect(() => { if (open && inputRef.current && inputRef.current.focus) inputRef.current.focus(); }, [open]);
|
|
1112
|
+
React.useEffect(() => {
|
|
1113
|
+
if (!open) { clearSearchMarks(); setCount(0); setCursor(-1); return undefined; }
|
|
1114
|
+
const total = paintSearchMarks(query);
|
|
1115
|
+
setCount(total);
|
|
1116
|
+
setCursor(total > 0 ? 0 : -1);
|
|
1117
|
+
const first = document.querySelector('mark.w2mSearchMark');
|
|
1118
|
+
if (first && first.scrollIntoView) first.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1119
|
+
return () => clearSearchMarks();
|
|
1120
|
+
}, [open, query]);
|
|
1121
|
+
const step = (backwards) => {
|
|
1122
|
+
const marks = [...document.querySelectorAll('mark.w2mSearchMark')];
|
|
1123
|
+
if (marks.length === 0) { paintSearchMarks(query); return; }
|
|
1124
|
+
const next = (cursor + (backwards ? -1 : 1) + marks.length) % marks.length;
|
|
1125
|
+
setCursor(next);
|
|
1126
|
+
if (marks[next] && marks[next].scrollIntoView) marks[next].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1127
|
+
};
|
|
1128
|
+
const scrollTurns = () => {
|
|
1129
|
+
const surface = findScrollSurface();
|
|
1130
|
+
if (surface !== null) surface.scrollBy({ top: -Math.round(Math.max(240, surface.clientHeight * 1.8)), behavior: 'smooth' });
|
|
1131
|
+
else if (typeof window !== 'undefined') window.scrollBy({ top: -Math.round(window.innerHeight * 1.8), behavior: 'smooth' });
|
|
1132
|
+
};
|
|
1133
|
+
const scrollFirstTurn = () => {
|
|
1134
|
+
const surface = findScrollSurface();
|
|
1135
|
+
if (surface !== null) surface.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1136
|
+
else if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1137
|
+
};
|
|
1138
|
+
if (!open) {
|
|
1139
|
+
return h('span', { className: 'w2mSearchControls' }, [
|
|
1140
|
+
h('button', {
|
|
1141
|
+
key: 'up',
|
|
1142
|
+
type: 'button',
|
|
1143
|
+
className: 'w2mScrollUp',
|
|
1144
|
+
title: '向上滚动 3 轮(长按 3 秒到第一轮)',
|
|
1145
|
+
'aria-label': '向上滚动 3 轮,长按 3 秒到第一轮',
|
|
1146
|
+
onMouseDown: () => {
|
|
1147
|
+
longFiredRef.current = false;
|
|
1148
|
+
holdRef.current = setTimeout(() => { longFiredRef.current = true; scrollFirstTurn(); }, 3000);
|
|
1149
|
+
},
|
|
1150
|
+
onMouseUp: () => { if (holdRef.current) { clearTimeout(holdRef.current); holdRef.current = null; } },
|
|
1151
|
+
onMouseLeave: () => { if (holdRef.current) { clearTimeout(holdRef.current); holdRef.current = null; } },
|
|
1152
|
+
onClick: () => { if (longFiredRef.current) { longFiredRef.current = false; return; } scrollTurns(); },
|
|
1153
|
+
}, '⇧3'),
|
|
1154
|
+
h('button', {
|
|
1155
|
+
key: 'find',
|
|
1156
|
+
type: 'button',
|
|
1157
|
+
className: 'w2mSearchToggle',
|
|
1158
|
+
title: '搜索本对话',
|
|
1159
|
+
'aria-label': '搜索本对话',
|
|
1160
|
+
onClick: () => setOpen(true),
|
|
1161
|
+
}, '🔍'),
|
|
1162
|
+
]);
|
|
1163
|
+
}
|
|
1164
|
+
return h('div', { className: 'w2mSearchBox', 'data-w2m-search': 'box' }, [
|
|
1165
|
+
h('input', {
|
|
1166
|
+
key: 'q',
|
|
1167
|
+
ref: inputRef,
|
|
1168
|
+
className: 'w2mSearchInput',
|
|
1169
|
+
value: query,
|
|
1170
|
+
placeholder: '搜索本对话全部内容',
|
|
1171
|
+
onChange: (event) => setQuery(event.target.value),
|
|
1172
|
+
onKeyDown: (event) => {
|
|
1173
|
+
if (event.key === 'Enter') { event.preventDefault(); step(event.shiftKey); }
|
|
1174
|
+
if (event.key === 'Escape') { setOpen(false); setQuery(''); }
|
|
1175
|
+
},
|
|
1176
|
+
}),
|
|
1177
|
+
h('span', { key: 'count', className: 'w2mSearchCount' }, count === 0 ? (query === '' ? '' : '0') : whenCount(count, cursor)),
|
|
1178
|
+
h('button', { key: 'prev', type: 'button', className: 'w2mSearchBtn', title: '上一个', onClick: () => step(true) }, '↑'),
|
|
1179
|
+
h('button', { key: 'next', type: 'button', className: 'w2mSearchBtn', title: '下一个', onClick: () => step(false) }, '↓'),
|
|
1180
|
+
h('button', { key: 'close', type: 'button', className: 'w2mSearchBtn', title: '关闭搜索', onClick: () => { setOpen(false); setQuery(''); clearSearchMarks(); } }, '×'),
|
|
1181
|
+
]);
|
|
1182
|
+
}
|
|
1183
|
+
function whenCount(total, at) {
|
|
1073
1184
|
return (at >= 0 ? at + 1 : 0) + '/' + total;
|
|
1074
1185
|
}
|
|
1075
1186
|
|
|
@@ -1667,7 +1778,22 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
|
|
|
1667
1778
|
}
|
|
1668
1779
|
|
|
1669
1780
|
function FleetChat({ sessionId, viewMode }) {
|
|
1670
|
-
|
|
1781
|
+
// v1.9.18: chat emotes are local slash commands, never image bytes. The relay only sees text.
|
|
1782
|
+
const CHAT_COMMANDS = ['/微笑', '/火箭', '/烟花'];
|
|
1783
|
+
// v1.9.18: the sticker pack is local to the plugin. Commands travel as text; rendering never requests bytes.
|
|
1784
|
+
const CHAT_STICKER_GLYPHS = { '/微笑': '\u{1F60A}', '/火箭': '\u{1F680}', '/烟花': '\u{1F386}' };
|
|
1785
|
+
const CHAT_STICKER_RE = /(\/微笑|\/火箭|\/烟花)/g;
|
|
1786
|
+
function renderChatText(text) {
|
|
1787
|
+
const raw = String(text ?? '');
|
|
1788
|
+
const whole = CHAT_STICKER_GLYPHS[raw.trim()];
|
|
1789
|
+
if (whole !== undefined) return h('span', { className: 'w2mChat_stickerBig', 'data-sticker': raw.trim(), title: raw.trim() }, whole);
|
|
1790
|
+
const parts = raw.split(CHAT_STICKER_RE);
|
|
1791
|
+
if (parts.length === 1) return raw;
|
|
1792
|
+
return parts.map((part, index) => (CHAT_STICKER_GLYPHS[part] !== undefined
|
|
1793
|
+
? h('span', { key: 'st' + index, className: 'w2mChat_sticker', 'data-sticker': part, title: part }, CHAT_STICKER_GLYPHS[part])
|
|
1794
|
+
: part));
|
|
1795
|
+
}
|
|
1796
|
+
const CHAT_EMOJIS = CHAT_COMMANDS;
|
|
1671
1797
|
const [open, setOpen] = React.useState(false);
|
|
1672
1798
|
const [messages, setMessages] = React.useState([]);
|
|
1673
1799
|
const [usage, setUsage] = React.useState(null);
|
|
@@ -1973,7 +2099,7 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
|
|
|
1973
2099
|
var kind = messageKind(message, localMachineId);
|
|
1974
2100
|
return h('div', { key: message?.message_id ?? `${message?.machine_id}:${message?.at_ms}`, className: 'w2mChat_msg is-' + kind, onClick: () => { const selection = typeof window.getSelection === 'function' ? String(window.getSelection() ?? '') : ''; if (selection !== '') return; setReplyTo({ id: message?.message_id ?? null, threadId: message?.thread_id ?? null, text: typeof message?.text === 'string' ? message.text : '' }); } },
|
|
1975
2101
|
h('div', { className: 'w2mChat_meta' }, h(WhaleAvatar, { machineId: message?.machine_id }), `${machineLabel(message)} · ${timeLabel(message)}`, kind === 'session' ? ' · 来自会话' : '', h('button', { key: 'copy', type: 'button', className: 'w2mChat_copy', title: '复制这条消息', onClick: (event) => { event.stopPropagation(); copyText(String(message?.text ?? '')); } }, '⧉'), message?.thread_id ? h('span', { className: 'w2mChat_thread' }, '#' + message.thread_id) : null),
|
|
1976
|
-
h('div', { className: 'w2mChat_text' }, message?.text ?? ''));
|
|
2102
|
+
h('div', { className: 'w2mChat_text' }, renderChatText(message?.text ?? '')));
|
|
1977
2103
|
}),
|
|
1978
2104
|
].filter((node) => node !== null))),
|
|
1979
2105
|
|
|
@@ -2288,6 +2414,16 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
|
|
|
2288
2414
|
} catch (error) {
|
|
2289
2415
|
console.error('@twinsearth/w2m-dsh-plugin: the conversation search control was not mounted', error);
|
|
2290
2416
|
}
|
|
2417
|
+
// v1.9.20: floating scroll controls, fixed top-right of the workspace. DOM nodes, not slot
|
|
2418
|
+
// entries, so they appear whichever conversation view is active; ctx.effect disposes them.
|
|
2419
|
+
try {
|
|
2420
|
+
const disposeFloat = installFloatingScrollButtons();
|
|
2421
|
+
if (typeof disposeFloat === 'function' && typeof ctx.effect === 'function') {
|
|
2422
|
+
ctx.effect(() => disposeFloat, 'w2m: floating scroll buttons');
|
|
2423
|
+
}
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
console.error('@twinsearth/w2m-dsh-plugin: the floating scroll controls were not mounted', error);
|
|
2426
|
+
}
|
|
2291
2427
|
// v1.9.6: fleet chat as a real conversation View tab (after Chat and Rules), rendering the
|
|
2292
2428
|
// full chat page in the main workspace.
|
|
2293
2429
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.20",
|
|
4
4
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"pack": "node scripts/pack.mjs",
|
|
65
65
|
"reachability": "node scripts/reachability-check.mjs",
|
|
66
66
|
"reachability:classify": "node scripts/reachability-classify.mjs",
|
|
67
|
-
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/data-plane.test.mjs test/sealed-object.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs",
|
|
67
|
+
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/data-plane.test.mjs test/sealed-object.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs test/joint-r5-wiring.test.mjs test/p1-hardening.test.mjs test/joint-merge-engine.test.mjs test/artifact-ref-dir-sink.test.mjs test/chat-sticker-guard.test.mjs test/relay-task-progress.test.mjs",
|
|
68
68
|
"test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
|
|
69
69
|
"test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
|
|
70
70
|
"test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "twinsearth-w2m-dsh",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.20",
|
|
5
5
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 12 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|