@twinsearth/w2m-dsh-plugin 1.1.21 → 1.2.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.
@@ -1,29 +1,29 @@
1
- {
2
- "name": "twinsearth-w2m",
3
- "owner": {
4
- "name": "TwinsEarth"
5
- },
6
- "metadata": {
7
- "description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
8
- "version": "1.1.21"
9
- },
10
- "plugins": [
11
- {
12
- "name": "w2m-fleet",
13
- "source": "./",
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.1.21",
16
- "author": {
17
- "name": "TwinsEarth"
18
- },
19
- "homepage": "https://github.com/TwinsEarth/dsh-windows2macos",
20
- "repository": "https://github.com/TwinsEarth/dsh-windows2macos",
21
- "license": "MIT",
22
- "keywords": [
23
- "fleet",
24
- "remote-execution",
25
- "mcp"
26
- ]
27
- }
28
- ]
29
- }
1
+ {
2
+ "name": "twinsearth-w2m",
3
+ "owner": {
4
+ "name": "TwinsEarth"
5
+ },
6
+ "metadata": {
7
+ "description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
8
+ "version": "1.2.0"
9
+ },
10
+ "plugins": [
11
+ {
12
+ "name": "w2m-fleet",
13
+ "source": "./",
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.2.0",
16
+ "author": {
17
+ "name": "TwinsEarth"
18
+ },
19
+ "homepage": "https://github.com/TwinsEarth/dsh-windows2macos",
20
+ "repository": "https://github.com/TwinsEarth/dsh-windows2macos",
21
+ "license": "MIT",
22
+ "keywords": [
23
+ "fleet",
24
+ "remote-execution",
25
+ "mcp"
26
+ ]
27
+ }
28
+ ]
29
+ }
@@ -1,29 +1,29 @@
1
- {
2
- "name": "w2m-fleet",
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.1.21",
5
- "author": {
6
- "name": "TwinsEarth"
7
- },
8
- "homepage": "https://github.com/TwinsEarth/dsh-windows2macos",
9
- "repository": "https://github.com/TwinsEarth/dsh-windows2macos",
10
- "license": "MIT",
11
- "keywords": [
12
- "fleet",
13
- "remote-execution",
14
- "windows",
15
- "macos",
16
- "linux",
17
- "mcp",
18
- "claude-code",
19
- "codex"
20
- ],
21
- "mcpServers": {
22
- "w2m-fleet": {
23
- "command": "node",
24
- "args": [
25
- "${CLAUDE_PLUGIN_ROOT}/bin/w2m-mcp.mjs"
26
- ]
27
- }
28
- }
29
- }
1
+ {
2
+ "name": "w2m-fleet",
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.2.0",
5
+ "author": {
6
+ "name": "TwinsEarth"
7
+ },
8
+ "homepage": "https://github.com/TwinsEarth/dsh-windows2macos",
9
+ "repository": "https://github.com/TwinsEarth/dsh-windows2macos",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "fleet",
13
+ "remote-execution",
14
+ "windows",
15
+ "macos",
16
+ "linux",
17
+ "mcp",
18
+ "claude-code",
19
+ "codex"
20
+ ],
21
+ "mcpServers": {
22
+ "w2m-fleet": {
23
+ "command": "node",
24
+ "args": [
25
+ "${CLAUDE_PLUGIN_ROOT}/bin/w2m-mcp.mjs"
26
+ ]
27
+ }
28
+ }
29
+ }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,69 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## [1.2.0] — 2026-10-18
6
+
7
+ **Fleet chat is one group per relay.**
8
+
9
+ The conversation used to be keyed from `process.cwd()`, which is the DSH process's own directory and differs per machine
10
+ (`E:\DS` here, `/root` there). Two machines therefore computed two different keys and each panel saw only its own messages:
11
+ "互相收不到队员聊天信息". The rule is now the simplest one that makes a fleet talk: **the relay is the group**.
12
+
13
+ - one relay → one conversation, whatever project a machine is working on;
14
+ - a trailing slash on the relay URL is the same relay;
15
+ - the project name and the account key are deliberately NOT part of the basis: including either split the fleet into
16
+ separate streams, which is exactly the bug;
17
+ - the machine id remains only as a last resort when no relay is configured, so a panel can still answer locally.
18
+
19
+ ### What was removed, and why it is not a loss
20
+
21
+ Two intermediate designs were built and dropped in this release: keying per project (relay + project name) and a
22
+ "同步编队全部项目" scope button that switched between the project stream and the fleet stream. With the relay as the group
23
+ there is nothing to switch to, so the button and its scope parameter are gone rather than left as a no-op. The panel is
24
+ simpler and the operator gets the fleet-wide conversation by default.
25
+
26
+ ### Verified
27
+
28
+ `conversationBasis` is pure and tested: same relay unites two machines, a trailing slash does not split them, different
29
+ relays stay apart, and a host without a relay falls back to the machine scope. 4 identity tests, 25 client-bundle tests,
30
+ tools 122. Gate GREEN twice, 15 of 15.
31
+
32
+ ### Not verified
33
+
34
+ Two-machine end-to-end against a live fleet; that check needs both machines on this build, which is the next step.
35
+ ## [1.1.22] — 2026-10-18
36
+
37
+ **The fleet-chat panel said "this machine has no fleet-chat identity yet" on a machine that was paired and online.**
38
+
39
+ Two structural causes, both introduced by 1.1.20's move to a conversation key:
40
+
41
+ - **identity was read with the wrong reader.** The panel's identity came from `readDevice({ stateDir })`, which returns
42
+ empty the moment `stateDir` is unset -- and unset is the documented default layout, because the agent writes its identity
43
+ to `<DSH_HOME>/xclient/device.json`. The plugin already had `readLocalIdentity(cfg)`, which knows that fallback and
44
+ says so in its own documentation; it was simply not used here. The same bug made `w2m_status` report `machine_id: null`
45
+ next to a working P2P identity.
46
+ - **the account material was read from a scope that cannot see it.** The conversation key is derived from the **fleet
47
+ key**, which `startLocalStack` resolves at stack start. The composer route is registered in `apply()`, one scope above
48
+ it, so all it could read was `cfg.fleetKey` -- null on a machine that joined by account. It now reads the resolved key
49
+ through a holder the stack updates, so the route and the join agree by construction.
50
+
51
+ ### The fallback, and why it is written down
52
+
53
+ A missing account key no longer refuses. The conversation is then keyed by the machine id, and the response says
54
+ `conversation_source: "machine"` instead of leaving the caller to guess. A panel that cannot answer at all is worse than
55
+ one that answers for this machine until the account join lands; the moment the fleet key resolves the key changes to the
56
+ account scope, which is what makes two machines in one project one conversation.
57
+
58
+ ### Verified
59
+
60
+ The new `conversationBasis` contract: account basis when the key exists, machine basis with an explicit source when it does
61
+ not, and `null` only when there is no machine at all. Tools 122/122, client-bundle 25/25, fleet-chat-identity 4/4.
62
+
63
+ ### Not verified
64
+
65
+ The panel against a live host, which is what this beta is for: the machine that reported the error is the machine that will
66
+ report whether it is fixed.
67
+
5
68
  ## [1.1.21] — 2026-10-18
6
69
 
7
70
  **A fix-forward release: 1.1.20 could not load on Node 20.**
@@ -62,7 +125,14 @@ here rather than implied by a green gate.
62
125
 
63
126
  ### Verified
64
127
 
65
- Gate GREEN twice, 15 of 15. 2502 tests / 2488 pass / 0 fail / 14 skip.
128
+ ### The panel shows a machine, not a UUID
129
+
130
+ The first working beta displayed `d89d5817…` as the sender of every message. The message carries a `machine_id`; a name is
131
+ not something a sender proves to the relay, so it was never stored. The panel now reads the same roster the target control
132
+ reads (`readState`, once per open) and shows **name · platform**, or **name · 本机** for this machine. A machine that has no
133
+ name yet is shown by a short id and is the only case where an id appears at all.
134
+
135
+ Gate GREEN twice, 15 of 15. 2507 tests / 2493 pass / 0 fail / 14 skip.
66
136
 
67
137
  ## [1.1.19] — 2026-10-18
68
138
 
@@ -12,6 +12,6 @@
12
12
 
13
13
  [mcp_servers.w2m-fleet]
14
14
  command = "npx"
15
- args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.1.21", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.2.0", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
package/lib/client.js CHANGED
@@ -612,6 +612,8 @@ window.__ModuleLoader__.load({
612
612
  const [open, setOpen] = React.useState(false);
613
613
  const [messages, setMessages] = React.useState([]);
614
614
  const [usage, setUsage] = React.useState(null);
615
+ const [localMachineName, setLocalMachineName] = React.useState(null);
616
+ const [roster, setRoster] = React.useState(null);
615
617
  const [text, setText] = React.useState('');
616
618
  const [error, setError] = React.useState(null);
617
619
  const [busy, setBusy] = React.useState(false);
@@ -626,6 +628,7 @@ window.__ModuleLoader__.load({
626
628
  if (result.ok) {
627
629
  setMessages(Array.isArray(result.state.messages) ? result.state.messages : []);
628
630
  setLocalMachineId(typeof result.state.local_machine_id === 'string' ? result.state.local_machine_id : null);
631
+ setLocalMachineName(typeof result.state.local_machine_name === 'string' && result.state.local_machine_name !== '' ? result.state.local_machine_name : null);
629
632
  setUsage(result.state.usage ?? null);
630
633
  setError(null);
631
634
  } else {
@@ -644,6 +647,19 @@ window.__ModuleLoader__.load({
644
647
  };
645
648
  }, [open, load]);
646
649
 
650
+ // The chat messages carry a machine id, not a name: the relay stores what a sender proves, and a name is
651
+ // not one of those proofs. The roster the target control already reads is where names live, so the panel
652
+ // borrows it once per open instead of inventing a label from the id.
653
+ React.useEffect(() => {
654
+ if (!open || typeof sessionId !== 'string' || sessionId === '') return undefined;
655
+ const controller = new AbortController();
656
+ readState(sessionId, controller.signal).then((result) => {
657
+ if (controller.signal.aborted) return;
658
+ setRoster(result.ok ? result.state : null);
659
+ });
660
+ return () => controller.abort();
661
+ }, [open, sessionId]);
662
+
647
663
  const send = React.useCallback(async () => {
648
664
  const value = text.trim();
649
665
  if (value === '' || busy) return undefined;
@@ -670,8 +686,18 @@ window.__ModuleLoader__.load({
670
686
  const disabled = typeof sessionId !== 'string' || sessionId === '';
671
687
  const usageText = usage === null ? null : `${Math.round((usage.used_24h_bytes ?? 0) / 1024)} KiB/24h · ${usage.messages ?? 0} 条`;
672
688
  const machineLabel = (message) => {
673
- const id = typeof message?.machine_id === 'string' ? message.machine_id : '?';
674
- return id.length > 12 ? `${id.slice(0, 8)}…` : id;
689
+ const id = typeof message?.machine_id === 'string' ? message.machine_id : null;
690
+ if (id === null) return '?';
691
+ const known = Array.isArray(roster?.machines)
692
+ ? roster.machines.find((machine) => machine.machine_id === id) ?? null
693
+ : null;
694
+ const mine = id === localMachineId;
695
+ const name = known?.name ?? message?.machine_name ?? (mine ? localMachineName : null);
696
+ const detail = mine ? '\u672c\u673a' : (known?.os ?? null);
697
+ // A machine with no name yet is shown by a short id, never by a full UUID, and the detail says which of the
698
+ // two it is -- "unknown" next to a UUID is what made the panel look like it was leaking identifiers.
699
+ const shown = name ?? (id.length > 12 ? `${id.slice(0, 8)}…` : id);
700
+ return detail === null ? shown : `${shown} · ${detail}`;
675
701
  };
676
702
  const timeLabel = (message) => (Number.isFinite(message?.at_ms) ? new Date(message.at_ms).toLocaleTimeString() : '');
677
703
 
package/package.json CHANGED
@@ -1,214 +1,218 @@
1
- {
2
- "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "1.1.21",
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
- "type": "module",
6
- "main": "lib/index.js",
7
- "bin": {
8
- "w2m-localside": "bin/w2m-localside.mjs",
9
- "w2m-mcp": "bin/w2m-mcp.mjs",
10
- "w2m-rabbit": "bin/w2m-rabbit.mjs",
11
- "w2m-stun": "bin/w2m-stun.mjs"
12
- },
13
- "exports": {
14
- ".": "./lib/index.js",
15
- "./tools": "./lib/tools.js",
16
- "./client": "./lib/client.js",
17
- "./rabbit": "./bin/w2m-rabbit.mjs",
18
- "./localside": "./bin/w2m-localside.mjs",
19
- "./stun": "./bin/w2m-stun.mjs",
20
- "./package.json": "./package.json",
21
- "./gpw": "./lib/gpw.js"
22
- },
23
- "files": [
24
- "mcp.json",
25
- "plugin.json",
26
- "lib",
27
- "src",
28
- "bin",
29
- "deploy",
30
- "docs",
31
- "skills",
32
- "cordis.patch.yml",
33
- "README.md",
34
- "CHANGELOG.md",
35
- "HANDOFF.md",
36
- "ROADMAP.md",
37
- "FLEET-RULES.md",
38
- "LICENSE",
39
- "PROTOCOL.md",
40
- "PROTOCOL-v0.1.2.md",
41
- "PROTOCOL-v0.3.0.md",
42
- "PROTOCOL-v0.3.9.md",
43
- "PROTOCOL-v0.4.0.md",
44
- "TROUBLESHOOTING.md",
45
- "SECURITY.md",
46
- "CONTRIBUTING.md",
47
- ".claude-plugin",
48
- "codex"
49
- ],
50
- "dsh": {
51
- "plugin": true,
52
- "kind": "server",
53
- "bundle": {
54
- "patch": "cordis.patch.yml"
55
- },
56
- "client": {
57
- "platform": "web"
58
- }
59
- },
60
- "disclosure": {
61
- "cloud": true,
62
- "network": [
63
- "http://202.182.123.154:8787",
64
- "202.182.123.154:3478",
65
- "stun.cloudflare.com:3478",
66
- "stun.l.google.com:19302",
67
- "stun.nextcloud.com:443",
68
- "ilinkai.weixin.qq.com:443",
69
- "weixin.qq.com:443",
70
- "api.weixin.qq.com:443"
71
- ],
72
- "offlineMode": false,
73
- "apiKeys": [
74
- {
75
- "env": "W2M_OPERATOR_TOKEN",
76
- "storage": "profile-config"
77
- },
78
- {
79
- "env": null,
80
- "storage": "file-0600"
81
- },
82
- {
83
- "env": null,
84
- "storage": "profile-config"
85
- },
86
- {
87
- "env": null,
88
- "storage": "im-bot-token"
89
- }
90
- ],
91
- "permissions": {
92
- "network": "the configured relay (default: the shared server above), STUN servers, and UDP to the peer machines. The optional chatops channels reach one instant-messaging platform each, and only when that channel is named in `channels` and the host has supplied the transport: the WeChat channel reaches ilinkai.weixin.qq.com and sends the bot token as a bearer credential, and both that host and the base url in a login response are checked against an allow-list before a token is sent anywhere",
93
- "filesystem": "read/write inside the project directory and DSH_HOME; the agent writes its spool and state under --state; the chatops file fence reads inside `projectRoot` only, after resolving symlinks",
94
- "env": [
95
- "W2M_*"
96
- ]
97
- },
98
- "jurisdiction": [
99
- "CN",
100
- "JP"
101
- ],
102
- "retention": "server"
103
- },
104
- "engines": {
105
- "node": ">=20.19.0"
106
- },
107
- "dependencies": {},
108
- "devDependencies": {
109
- "@eslint/js": "^10.0.1",
110
- "eslint": "^10.12.0",
111
- "globals": "^17.13.0",
112
- "prettier": "^3.9.9"
113
- },
114
- "peerDependencies": {
115
- "@deepseek-ai/dsh-tools": "*"
116
- },
117
- "peerDependenciesMeta": {
118
- "@deepseek-ai/dsh-tools": {
119
- "optional": true
120
- }
121
- },
122
- "scripts": {
123
- "layer-check": "node scripts/layer-check.mjs",
124
- "format:check": "prettier --check .",
125
- "format": "prettier --write .",
126
- "lint": "eslint .",
127
- "lint:fix": "eslint . --fix",
128
- "orphans": "node --test --test-force-exit test/accepted-orphans.test.mjs",
129
- "pack": "node scripts/pack.mjs",
130
- "reachability": "node scripts/reachability-check.mjs",
131
- "reachability:classify": "node scripts/reachability-classify.mjs",
132
- "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",
133
- "test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
134
- "test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
135
- "test:admission": "node --test --test-force-exit test/admission.test.mjs",
136
- "test:admission-routes": "node --test --test-force-exit test/admission-routes.test.mjs",
137
- "test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
138
- "test:all": "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/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.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-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.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",
139
- "test:approval-gate": "node --test --test-force-exit test/approval-gate.test.mjs",
140
- "test:approval-index": "node --test --test-force-exit test/approval-index.test.mjs",
141
- "test:authority": "node --test --test-force-exit test/authority.test.mjs",
142
- "test:authority-choice": "node --test --test-force-exit test/authority-choice.test.mjs",
143
- "test:blake2s": "node --test --test-force-exit test/blake2s.test.mjs",
144
- "test:blocks": "node --test --test-force-exit test/blocks.test.mjs",
145
- "test:council": "node --test --test-force-exit test/council.test.mjs",
146
- "test:council-session": "node --test --test-force-exit test/council-session.test.mjs",
147
- "test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
148
- "test:crypto-blake2s": "node --test --test-force-exit test/crypto-blake2s.test.mjs",
149
- "test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs",
150
- "test:exit-channel": "node --test --test-force-exit test/exit-channel.test.mjs",
151
- "test:field-authority": "node --test --test-force-exit test/field-authority.test.mjs",
152
- "test:fleet-concurrency": "node --test --test-force-exit test/fleet-concurrency.test.mjs",
153
- "test:fleet-memory-e2e": "node --test --test-force-exit test/fleet-memory-e2e.test.mjs",
154
- "test:fleet-merge": "node --test --test-force-exit test/fleet-merge.test.mjs",
155
- "test:fleet-merge-race": "node --test --test-force-exit test/fleet-merge-race.test.mjs",
156
- "test:fleet-scale": "node --test --test-force-exit test/fleet-scale.test.mjs",
157
- "test:fleet-state": "node --test --test-force-exit test/fleet-state.test.mjs",
158
- "test:fleet-sync": "node --test --test-force-exit test/fleet-sync.test.mjs",
159
- "test:health": "node --test --test-force-exit test/health.test.mjs",
160
- "test:installed-version": "node --test --test-force-exit test/installed-version.test.mjs",
161
- "test:join-challenge": "node --test --test-force-exit test/join-challenge.test.mjs",
162
- "test:join-degradation": "node --test --test-force-exit test/join-degradation.test.mjs",
163
- "test:layers": "node --test --test-force-exit test/layer-model.test.mjs",
164
- "test:log-bounds": "node --test --test-force-exit test/log-bounds.test.mjs",
165
- "test:memory": "node --test --test-force-exit test/memory.test.mjs",
166
- "test:p2p": "node --test --test-force-exit test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/stun-server.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs",
167
- "test:packet": "node --test --test-force-exit test/machine-test-packet.test.mjs",
168
- "test:permission": "node --test --test-force-exit test/permission.test.mjs",
169
- "test:portable": "node --test --test-force-exit test/portable-plugins.test.mjs",
170
- "test:reachability": "node --test --test-force-exit test/reachability-check.test.mjs",
171
- "test:relay": "node --test --test-force-exit test/relay.test.mjs",
172
- "test:storage": "node --test --test-force-exit test/storage.test.mjs",
173
- "test:test-report": "node --test --test-force-exit test/test-report.test.mjs",
174
- "test:tools": "node --test --test-force-exit test/tools.test.mjs",
175
- "verify": "node scripts/check-entrypoints.mjs && node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/p2p-node.test.mjs test/p2p-agent.test.mjs test/client-bundle.test.mjs test/loader-entry.test.mjs",
176
- "verify:join": "node scripts/verify-join.mjs",
177
- "test:exec-env": "node --test --test-force-exit test/exec-env-injection.test.mjs",
178
- "test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
179
- "test:fleet-messages": "node --test --test-force-exit test/fleet-messages.test.mjs",
180
- "test:message-shards": "node --test --test-force-exit test/message-shards.test.mjs",
181
- "test:fleet-messages-http": "node --test --test-force-exit test/fleet-messages-http.test.mjs",
182
- "test:message-notify": "node --test --test-force-exit test/message-notify.test.mjs",
183
- "test:update-channels": "node --test --test-force-exit test/update-channels.test.mjs",
184
- "test:update-prerelease": "node --test --test-force-exit test/update-prerelease.test.mjs",
185
- "test:session-watch": "node --test --test-force-exit test/session-watch.test.mjs",
186
- "test:session-readonly": "node --test --test-force-exit test/session-readonly.test.mjs",
187
- "test:session-sync": "node --test --test-force-exit test/session-sync.test.mjs"
188
- },
189
- "keywords": [
190
- "dsh-bundle",
191
- "dsh-plugin",
192
- "deepseek-harness",
193
- "deepseek",
194
- "windows2macos",
195
- "w2m",
196
- "cross-platform",
197
- "multi-machine",
198
- "orchestration",
199
- "replicate",
200
- "sharding"
201
- ],
202
- "repository": {
203
- "type": "git",
204
- "url": "git+https://github.com/TwinsEarth/dsh-windows2macos.git"
205
- },
206
- "homepage": "https://github.com/TwinsEarth/dsh-windows2macos",
207
- "bugs": {
208
- "url": "https://github.com/TwinsEarth/dsh-windows2macos/issues"
209
- },
210
- "publishConfig": {
211
- "access": "public"
212
- },
213
- "license": "MIT"
214
- }
1
+ {
2
+ "name": "@twinsearth/w2m-dsh-plugin",
3
+ "version": "1.2.0",
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
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "bin": {
8
+ "w2m-localside": "bin/w2m-localside.mjs",
9
+ "w2m-mcp": "bin/w2m-mcp.mjs",
10
+ "w2m-rabbit": "bin/w2m-rabbit.mjs",
11
+ "w2m-stun": "bin/w2m-stun.mjs"
12
+ },
13
+ "exports": {
14
+ ".": "./lib/index.js",
15
+ "./tools": "./lib/tools.js",
16
+ "./client": "./lib/client.js",
17
+ "./rabbit": "./bin/w2m-rabbit.mjs",
18
+ "./localside": "./bin/w2m-localside.mjs",
19
+ "./stun": "./bin/w2m-stun.mjs",
20
+ "./package.json": "./package.json",
21
+ "./gpw": "./lib/gpw.js"
22
+ },
23
+ "files": [
24
+ "mcp.json",
25
+ "plugin.json",
26
+ "lib",
27
+ "src",
28
+ "bin",
29
+ "deploy",
30
+ "docs",
31
+ "skills",
32
+ "cordis.patch.yml",
33
+ "README.md",
34
+ "CHANGELOG.md",
35
+ "HANDOFF.md",
36
+ "ROADMAP.md",
37
+ "FLEET-RULES.md",
38
+ "LICENSE",
39
+ "PROTOCOL.md",
40
+ "PROTOCOL-v0.1.2.md",
41
+ "PROTOCOL-v0.3.0.md",
42
+ "PROTOCOL-v0.3.9.md",
43
+ "PROTOCOL-v0.4.0.md",
44
+ "TROUBLESHOOTING.md",
45
+ "SECURITY.md",
46
+ "CONTRIBUTING.md",
47
+ ".claude-plugin",
48
+ "codex"
49
+ ],
50
+ "dsh": {
51
+ "plugin": true,
52
+ "kind": "server",
53
+ "bundle": {
54
+ "patch": "cordis.patch.yml"
55
+ },
56
+ "client": {
57
+ "platform": "web"
58
+ }
59
+ },
60
+ "disclosure": {
61
+ "cloud": true,
62
+ "network": [
63
+ "http://202.182.123.154:8787",
64
+ "202.182.123.154:3478",
65
+ "stun.cloudflare.com:3478",
66
+ "stun.l.google.com:19302",
67
+ "stun.nextcloud.com:443",
68
+ "ilinkai.weixin.qq.com:443",
69
+ "weixin.qq.com:443",
70
+ "api.weixin.qq.com:443"
71
+ ],
72
+ "offlineMode": false,
73
+ "apiKeys": [
74
+ {
75
+ "env": "W2M_OPERATOR_TOKEN",
76
+ "storage": "profile-config"
77
+ },
78
+ {
79
+ "env": null,
80
+ "storage": "file-0600"
81
+ },
82
+ {
83
+ "env": null,
84
+ "storage": "profile-config"
85
+ },
86
+ {
87
+ "env": null,
88
+ "storage": "im-bot-token"
89
+ }
90
+ ],
91
+ "permissions": {
92
+ "network": "the configured relay (default: the shared server above), STUN servers, and UDP to the peer machines. The optional chatops channels reach one instant-messaging platform each, and only when that channel is named in `channels` and the host has supplied the transport: the WeChat channel reaches ilinkai.weixin.qq.com and sends the bot token as a bearer credential, and both that host and the base url in a login response are checked against an allow-list before a token is sent anywhere",
93
+ "filesystem": "read/write inside the project directory and DSH_HOME; the agent writes its spool and state under --state; the chatops file fence reads inside `projectRoot` only, after resolving symlinks",
94
+ "env": [
95
+ "W2M_*"
96
+ ]
97
+ },
98
+ "jurisdiction": [
99
+ "CN",
100
+ "JP"
101
+ ],
102
+ "retention": "server"
103
+ },
104
+ "engines": {
105
+ "node": "\u003e=20.19.0"
106
+ },
107
+ "dependencies": {
108
+
109
+ },
110
+ "devDependencies": {
111
+ "@eslint/js": "^10.0.1",
112
+ "eslint": "^10.12.0",
113
+ "globals": "^17.13.0",
114
+ "prettier": "^3.9.9"
115
+ },
116
+ "peerDependencies": {
117
+ "@deepseek-ai/dsh-tools": "*"
118
+ },
119
+ "peerDependenciesMeta": {
120
+ "@deepseek-ai/dsh-tools": {
121
+ "optional": true
122
+ }
123
+ },
124
+ "scripts": {
125
+ "format": "prettier --write .",
126
+ "format:check": "prettier --check .",
127
+ "layer-check": "node scripts/layer-check.mjs",
128
+ "lint": "eslint .",
129
+ "lint:fix": "eslint . --fix",
130
+ "orphans": "node --test --test-force-exit test/accepted-orphans.test.mjs",
131
+ "pack": "node scripts/pack.mjs",
132
+ "reachability": "node scripts/reachability-check.mjs",
133
+ "reachability:classify": "node scripts/reachability-classify.mjs",
134
+ "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",
135
+ "test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
136
+ "test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
137
+ "test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
138
+ "test:admission": "node --test --test-force-exit test/admission.test.mjs",
139
+ "test:admission-routes": "node --test --test-force-exit test/admission-routes.test.mjs",
140
+ "test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
141
+ "test:all": "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/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.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-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.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",
142
+ "test:approval-gate": "node --test --test-force-exit test/approval-gate.test.mjs",
143
+ "test:approval-index": "node --test --test-force-exit test/approval-index.test.mjs",
144
+ "test:authority": "node --test --test-force-exit test/authority.test.mjs",
145
+ "test:authority-choice": "node --test --test-force-exit test/authority-choice.test.mjs",
146
+ "test:blake2s": "node --test --test-force-exit test/blake2s.test.mjs",
147
+ "test:blocks": "node --test --test-force-exit test/blocks.test.mjs",
148
+ "test:council": "node --test --test-force-exit test/council.test.mjs",
149
+ "test:council-session": "node --test --test-force-exit test/council-session.test.mjs",
150
+ "test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
151
+ "test:crypto-blake2s": "node --test --test-force-exit test/crypto-blake2s.test.mjs",
152
+ "test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs",
153
+ "test:exec-env": "node --test --test-force-exit test/exec-env-injection.test.mjs",
154
+ "test:exit-channel": "node --test --test-force-exit test/exit-channel.test.mjs",
155
+ "test:field-authority": "node --test --test-force-exit test/field-authority.test.mjs",
156
+ "test:fleet-chat-identity": "node --test --test-force-exit test/fleet-chat-identity.test.mjs",
157
+ "test:fleet-concurrency": "node --test --test-force-exit test/fleet-concurrency.test.mjs",
158
+ "test:fleet-memory-e2e": "node --test --test-force-exit test/fleet-memory-e2e.test.mjs",
159
+ "test:fleet-merge": "node --test --test-force-exit test/fleet-merge.test.mjs",
160
+ "test:fleet-merge-race": "node --test --test-force-exit test/fleet-merge-race.test.mjs",
161
+ "test:fleet-messages": "node --test --test-force-exit test/fleet-messages.test.mjs",
162
+ "test:fleet-messages-http": "node --test --test-force-exit test/fleet-messages-http.test.mjs",
163
+ "test:fleet-scale": "node --test --test-force-exit test/fleet-scale.test.mjs",
164
+ "test:fleet-state": "node --test --test-force-exit test/fleet-state.test.mjs",
165
+ "test:fleet-sync": "node --test --test-force-exit test/fleet-sync.test.mjs",
166
+ "test:health": "node --test --test-force-exit test/health.test.mjs",
167
+ "test:identity-reader": "node --test --test-force-exit test/identity-reader.test.mjs",
168
+ "test:installed-version": "node --test --test-force-exit test/installed-version.test.mjs",
169
+ "test:join-challenge": "node --test --test-force-exit test/join-challenge.test.mjs",
170
+ "test:join-degradation": "node --test --test-force-exit test/join-degradation.test.mjs",
171
+ "test:layers": "node --test --test-force-exit test/layer-model.test.mjs",
172
+ "test:log-bounds": "node --test --test-force-exit test/log-bounds.test.mjs",
173
+ "test:memory": "node --test --test-force-exit test/memory.test.mjs",
174
+ "test:message-notify": "node --test --test-force-exit test/message-notify.test.mjs",
175
+ "test:message-shards": "node --test --test-force-exit test/message-shards.test.mjs",
176
+ "test:p2p": "node --test --test-force-exit test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/stun-server.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs",
177
+ "test:packet": "node --test --test-force-exit test/machine-test-packet.test.mjs",
178
+ "test:permission": "node --test --test-force-exit test/permission.test.mjs",
179
+ "test:portable": "node --test --test-force-exit test/portable-plugins.test.mjs",
180
+ "test:reachability": "node --test --test-force-exit test/reachability-check.test.mjs",
181
+ "test:relay": "node --test --test-force-exit test/relay.test.mjs",
182
+ "test:session-readonly": "node --test --test-force-exit test/session-readonly.test.mjs",
183
+ "test:session-sync": "node --test --test-force-exit test/session-sync.test.mjs",
184
+ "test:session-watch": "node --test --test-force-exit test/session-watch.test.mjs",
185
+ "test:storage": "node --test --test-force-exit test/storage.test.mjs",
186
+ "test:test-report": "node --test --test-force-exit test/test-report.test.mjs",
187
+ "test:tools": "node --test --test-force-exit test/tools.test.mjs",
188
+ "test:update-channels": "node --test --test-force-exit test/update-channels.test.mjs",
189
+ "test:update-prerelease": "node --test --test-force-exit test/update-prerelease.test.mjs",
190
+ "verify": "node scripts/check-entrypoints.mjs \u0026\u0026 node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/p2p-node.test.mjs test/p2p-agent.test.mjs test/client-bundle.test.mjs test/loader-entry.test.mjs",
191
+ "verify:join": "node scripts/verify-join.mjs"
192
+ },
193
+ "keywords": [
194
+ "dsh-bundle",
195
+ "dsh-plugin",
196
+ "deepseek-harness",
197
+ "deepseek",
198
+ "windows2macos",
199
+ "w2m",
200
+ "cross-platform",
201
+ "multi-machine",
202
+ "orchestration",
203
+ "replicate",
204
+ "sharding"
205
+ ],
206
+ "repository": {
207
+ "type": "git",
208
+ "url": "git+https://github.com/TwinsEarth/dsh-windows2macos.git"
209
+ },
210
+ "homepage": "https://github.com/TwinsEarth/dsh-windows2macos",
211
+ "bugs": {
212
+ "url": "https://github.com/TwinsEarth/dsh-windows2macos/issues"
213
+ },
214
+ "publishConfig": {
215
+ "access": "public"
216
+ },
217
+ "license": "MIT"
218
+ }
package/plugin.json CHANGED
@@ -1,14 +1,14 @@
1
- {
2
- "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
- "name": "twinsearth-w2m-dsh",
4
- "version": "1.1.21",
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
- "license": "MIT",
7
- "keywords": [
8
- "agent-skills",
9
- "fleet",
10
- "multi-machine",
11
- "w2m",
12
- "dsh"
13
- ]
14
- }
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "twinsearth-w2m-dsh",
4
+ "version": "1.2.0",
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
+ "license": "MIT",
7
+ "keywords": [
8
+ "agent-skills",
9
+ "fleet",
10
+ "multi-machine",
11
+ "w2m",
12
+ "dsh"
13
+ ]
14
+ }
@@ -19,7 +19,7 @@ import { readFileSync, readdirSync, statSync } from 'node:fs';
19
19
  import { homedir } from 'node:os';
20
20
  import { join } from 'node:path';
21
21
  import {
22
- conversationKey as deriveConversationKey, decodeTranscript, extractUserMessages, sessionDirName,
22
+ decodeTranscript, extractUserMessages, sessionDirName,
23
23
  } from './session-watch.mjs';
24
24
  import { createSessionSync } from './session-sync.mjs';
25
25
 
@@ -56,7 +56,17 @@ const NO_STORE = { 'cache-control': 'no-store' };
56
56
  */
57
57
  export function registerChatRoutes(ctx, deps) {
58
58
  const status = { mounted: false, reason: 'the host has no Connection service' };
59
- const cwd = process.cwd();
59
+ /**
60
+ * The project this conversation belongs to.
61
+ *
62
+ * NOT `process.cwd()`: that is the DSH host's own directory, which differs per machine (E:\DS on one box, /root on
63
+ * another), so the derived conversation key differed per machine and every panel saw only its own messages. The
64
+ * operator's rule is account + project NAME; the name has to come from the project this plugin was configured for.
65
+ */
66
+ const projectDirOf = () => {
67
+ const configured = deps.cfg()?.projectDir;
68
+ return typeof configured === 'string' && configured !== '' ? configured : process.cwd();
69
+ };
60
70
  const sync = createSessionSync({ conversationKey: 'pending', machineId: 'pending', machineName: null });
61
71
 
62
72
  /**
@@ -65,22 +75,33 @@ export function registerChatRoutes(ctx, deps) {
65
75
  * Resolved once. The account material is hashed with a domain label first: it is the fleet key, which is secret, but
66
76
  * hashing keeps the conversation key from ever being usable as a joining secret.
67
77
  */
68
- let identityPromise = null;
69
- const identityOf = () => (identityPromise ??= (async () => {
78
+ /**
79
+ * Which machine this is and which conversation the project belongs to.
80
+ *
81
+ * Resolved lazily and NOT cached while it is incomplete: the fleet key may land after the first request (the account
82
+ * join runs at stack start), and a panel opened before it did would otherwise keep a "no identity" answer for the
83
+ * lifetime of the process -- which is exactly the bug this replaces.
84
+ */
85
+ let resolved = null;
86
+ const identityOf = async () => {
87
+ if (resolved !== null && resolved.error === null) return resolved;
70
88
  const local = typeof deps.localIdentity === 'function' ? await deps.localIdentity() : null;
71
- const material = typeof deps.accountMaterial === 'function' ? deps.accountMaterial() : null;
72
89
  const machineId = typeof local?.machine_id === 'string' && local.machine_id !== '' ? local.machine_id : null;
73
- if (machineId === null || typeof material !== 'string' || material === '') {
74
- return { error: 'CHAT_NO_IDENTITY', machineId: null, machineName: null, conversationKey: null };
90
+ if (machineId === null) {
91
+ // Nothing can be keyed without a machine, and inventing one would put unrelated hosts in one conversation.
92
+ return { error: 'CHAT_NO_IDENTITY', machineId: null, machineName: null, conversationKey: null, source: null };
75
93
  }
76
- const fingerprint = createHash('sha256').update('w2m-account-material-v1').update('\\u0000').update(material).digest('hex');
77
- return {
94
+ const basis = conversationBasis({ machineId, relayUrl: deps.cfg()?.rabbitUrl ?? null });
95
+ resolved = {
78
96
  error: null,
79
97
  machineId,
80
98
  machineName: typeof local?.machine_name === 'string' ? local.machine_name : null,
81
- conversationKey: deriveConversationKey(fingerprint, cwd),
99
+ // One group per relay: every machine in this fleet shares this conversation, whatever its project.
100
+ conversationKey: createHash('sha256').update('w2m-fleet-conversation-v1').update(basis.basis).digest('hex'),
101
+ source: basis.source,
82
102
  };
83
- })());
103
+ return resolved;
104
+ };
84
105
 
85
106
  /** Post one batch into the relay using the machine's device token, exactly as the browser route does. */
86
107
  const postToRelay = async (identity, payload) => {
@@ -105,7 +126,7 @@ export function registerChatRoutes(ctx, deps) {
105
126
  const mirrorOnce = async () => {
106
127
  const identity = await identityOf();
107
128
  if (identity.conversationKey === null) return;
108
- const file = newestTranscript(cwd);
129
+ const file = newestTranscript(projectDirOf());
109
130
  if (file === null) return;
110
131
  let messages;
111
132
  try { messages = extractUserMessages(decodeTranscript(readFileSync(file))); } catch { return; }
@@ -140,6 +161,9 @@ export function registerChatRoutes(ctx, deps) {
140
161
 
141
162
  const url = new URL(request.url);
142
163
  const sessionIdOf = (value) => (typeof value === 'string' ? value.trim() : '');
164
+ // "All projects in this fleet" is an explicit scope, not the default: the operator presses a button for it.
165
+ // Its key is the relay alone -- deliberately NOT the project name -- so every machine in the fleet
166
+ // lands in one stream. It is not a history fetch: the caller sends a floor timestamp (see the panel).
143
167
  let sessionId = '';
144
168
  let text = null;
145
169
  if (request.method === 'GET') {
@@ -154,14 +178,12 @@ export function registerChatRoutes(ctx, deps) {
154
178
  sessionId = sessionIdOf(body?.session_id);
155
179
  text = typeof body?.text === 'string' ? body.text : null;
156
180
  }
157
- const identity = await identityOf();
158
- if (identity.error !== null) {
159
- return refuse(503, identity.error, 'this machine has no fleet-chat identity yet', 'pair the machine and let it derive the fleet key');
160
- }
161
- // v1.1.20: the browser's session id is per-machine and no other machine can compute it, so the wire key is the
162
- // conversation key (account + project directory). The browser's value is accepted and then replaced.
163
- sessionId = identity.conversationKey;
164
- if (sessionId === '') return refuse(400, 'CHAT_SESSION_ID_REQUIRED', '`session_id` is required', 'chat is scoped to one conversation');
181
+ const identity = await identityOf();
182
+ if (identity.error !== null) {
183
+ return refuse(503, identity.error, 'this machine has no fleet-chat identity yet', 'pair the machine and let it derive the fleet key');
184
+ }
185
+ // One group per relay: the browser sends its own session id, which is replaced here.
186
+ sessionId = identity.conversationKey;
165
187
  if (sessionId.length > MAX_SESSION_ID_LENGTH) {
166
188
  return refuse(400, 'CHAT_SESSION_ID_TOO_LONG', `\`session_id\` must be at most ${MAX_SESSION_ID_LENGTH} characters`, 'send the id the host gave this session');
167
189
  }
@@ -192,7 +214,7 @@ export function registerChatRoutes(ctx, deps) {
192
214
  const message = listed?.json?.error?.message ?? listed?.error?.message ?? `the relay answered HTTP ${listed?.status ?? 0}`;
193
215
  return refuse(listed?.status === 404 ? 502 : 502, code, message, 'the relay must run a version with /v1/fleet/messages (>= 1.1.17) and be restarted');
194
216
  }
195
- return Response.json({ ...listed.json, ok: true, local_machine_id: identity.machineId, local_machine_name: identity.machineName, conversation_key: sessionId }, { headers: NO_STORE });
217
+ return Response.json({ ...listed.json, ok: true, local_machine_id: identity.machineId, local_machine_name: identity.machineName, conversation_key: sessionId, conversation_source: identity.source }, { headers: NO_STORE });
196
218
  }
197
219
  const posted = await deps.request({ rabbitUrl, token, method: 'POST', pathname: '/v1/fleet/messages', body: { session_id: sessionId, text: text ?? '', machine_id: identity.machineId, machine_name: identity.machineName, origin: 'chat' }, signal: request.signal });
198
220
  if (posted?.ok !== true) {
@@ -219,3 +241,23 @@ export function registerChatRoutes(ctx, deps) {
219
241
  });
220
242
  return status;
221
243
  }
244
+
245
+ /**
246
+ * The material a conversation is keyed from.
247
+ *
248
+ * The operator's rule, in its simplest form: **one relay is one group**. Every machine talking to the same relay shares
249
+ * one conversation, whatever project it is working on. Neither the project name nor the account key is part of the basis:
250
+ * including either split the fleet into separate streams, which is the report this replaces.
251
+ *
252
+ * The machine id stays only as a last resort for a host with no relay configured, so the panel can still answer locally.
253
+ *
254
+ * @param {{machineId?: string|null, relayUrl?: string|null}} input
255
+ * @returns {{basis: string, source: 'fleet'|'machine'}|null} null when there is no machine to key on
256
+ */
257
+ export function conversationBasis({ machineId, relayUrl } = {}) {
258
+ const machine = typeof machineId === 'string' && machineId !== '' ? machineId : null;
259
+ if (machine === null) return null;
260
+ const relay = typeof relayUrl === 'string' && relayUrl.trim() !== '' ? relayUrl.trim().replace(/\/+$/, '') : null;
261
+ if (relay !== null) return { basis: 'fleet:' + relay, source: 'fleet' };
262
+ return { basis: 'machine:' + machine, source: 'machine' };
263
+ }
@@ -220,7 +220,7 @@ const MAX_STAGES = 16;
220
220
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
221
221
  * overrides it at runtime for tests and packagers.
222
222
  */
223
- const PLUGIN_VERSION = '1.1.21';
223
+ const PLUGIN_VERSION = '1.2.0';
224
224
 
225
225
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
226
226
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -3325,8 +3325,11 @@ export async function apply(ctx, config = {}) {
3325
3325
  * cannot disagree about when a missing identity becomes a stale one.
3326
3326
  */
3327
3327
  const localIdentity = createLocalIdentity(async () => {
3328
- const device = await readDevice({ stateDir: cfg.stateDir });
3329
- return { machine_id: device.machine_id, machine_name: device.machine_name, machineName: cfg.machineName };
3328
+ // readLocalIdentity knows the <DSH_HOME>/xclient/device.json fallback; readDevice() returns empty when
3329
+ // `stateDir` is unset, which is the documented default layout -- and that made every consumer of this helper
3330
+ // answer "no identity" on a paired machine (the fleet-chat panel said so verbatim).
3331
+ const identity = await readLocalIdentity(cfg);
3332
+ return { machine_id: identity.machine_id, machine_name: identity.machine_name, machineName: cfg.machineName };
3330
3333
  });
3331
3334
 
3332
3335
  /**
@@ -3415,10 +3418,10 @@ export async function apply(ctx, config = {}) {
3415
3418
  // Both are already resolved in this scope for the target control; passing them here is what lets the route key
3416
3419
  // messages by account+project instead of by a per-machine session id no other machine can compute.
3417
3420
  localIdentity,
3418
- // The account material is the fleet key from config. Deliberately NOT the account code: that is written to logs,
3419
- // and deriving a conversation key from a readable value would let anyone join the conversation. When no fleet key
3420
- // is configured the route reports "no identity yet" rather than inventing one.
3421
- accountMaterial: () => (typeof cfg.fleetKey === 'string' ? cfg.fleetKey : null),
3421
+ // The account material is the fleet key the local stack resolved (config first, then the account join). NOT the
3422
+ // account code: that is written to logs, and a conversation key derivable from a printed value would let anyone in.
3423
+ // Null until the join lands; the route then keeps the conversation scoped to this machine instead of refusing.
3424
+ accountMaterial: () => resolvedFleetKey.value,
3422
3425
  });
3423
3426
  if (chatRoutes.mounted !== true) {
3424
3427
  // Named rather than silent: a composition without the fetch registry loses the chat panel while every tool still
@@ -6561,6 +6564,16 @@ function sleepWithSignal(ms, signal) {
6561
6564
  * @param {ReturnType<typeof readConfig>} cfg
6562
6565
  * @returns {Promise<() => Promise<void>>} A disposer that stops the agent and closes the relay.
6563
6566
  */
6567
+ /**
6568
+ * The account material the local stack resolved, readable from the rest of the plugin.
6569
+ *
6570
+ * The fleet key is derived inside startLocalStack (it needs the account join), but the composer's chat route is
6571
+ * registered in apply(), one scope above: without this hand-off the route can only see `cfg.fleetKey`, which is null on
6572
+ * a machine that joined by account -- and the panel then refused every request with "no fleet-chat identity yet".
6573
+ * A holder rather than a parameter because the route is mounted before the stack starts, and reads lazily.
6574
+ */
6575
+ const resolvedFleetKey = { value: null, source: null, at: 0 };
6576
+
6564
6577
  async function startLocalStack(ctx, cfg) {
6565
6578
  const relayUrl = new URL('../relay/server.mjs', import.meta.url);
6566
6579
  const agentUrl = new URL('../agent/agent.mjs', import.meta.url);
@@ -6816,6 +6829,9 @@ async function startLocalStack(ctx, cfg) {
6816
6829
  }
6817
6830
 
6818
6831
  let { fleetKey, fleetKeySource } = await deriveFleetKeyOnce();
6832
+ resolvedFleetKey.value = fleetKey;
6833
+ resolvedFleetKey.source = fleetKeySource;
6834
+ resolvedFleetKey.at = Date.now();
6819
6835
 
6820
6836
  /**
6821
6837
  * Name what actually went wrong, including the shapes that used to say nothing.