@twinsearth/w2m-dsh-plugin 1.2.1 → 1.2.2

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/README.md CHANGED
@@ -14,7 +14,7 @@ English | [中文](#中文说明)
14
14
 
15
15
  ## Install
16
16
 
17
- Five routes in. They all end with **12 tools registered** in your agent. Pick one.
17
+ Five routes in. They all end with **13 tools registered** in your agent. Pick one.
18
18
 
19
19
  ### 1. DSH plugin (recommended — zero configuration)
20
20
 
@@ -137,6 +137,7 @@ hand-maintained host list.
137
137
 
138
138
  `w2m_status` · `w2m_devices` · `w2m_run` · `w2m_wait` · `w2m_history` · `w2m_stats` · `w2m_report` ·
139
139
  `w2m_memory` · `w2m_approvals` · `w2m_council` · `w2m_chat` · `w2m_update`
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.
140
141
 
141
142
  ### Environment a dispatched command receives (1.1.15+)
142
143
 
@@ -260,7 +261,7 @@ MIT.
260
261
 
261
262
  ### 安装
262
263
 
263
- 五种方式,最终都会在你的 agent 里注册 **12 tools registered**。任选其一。
264
+ 五种方式,最终都会在你的 agent 里注册 **13 tools registered**。任选其一。
264
265
 
265
266
  #### 1. DSH 插件(推荐,零配置)
266
267
 
@@ -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.2.1", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.2.2", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
package/lib/client.js CHANGED
@@ -295,6 +295,11 @@ window.__ModuleLoader__.load({
295
295
  '.w2mChat_empty{color:var(--dsw-alias-label-tertiary)}',
296
296
  '.w2mChat_msg{display:flex;flex-direction:column;gap:2px}',
297
297
  '.w2mChat_meta{color:var(--dsw-alias-label-tertiary);font-size:11px}',
298
+ '.w2mChat_to{font:inherit;font-size:11px;max-width:120px;border-radius:6px;border:.5px solid var(--dsw-alias-border-default);background:var(--dsw-alias-surface-default);color:var(--dsw-alias-label-primary)}',
299
+ '.w2mChat_reply{display:flex;align-items:center;gap:6px;padding:4px 10px;font-size:11px;color:var(--dsw-alias-label-secondary);border-top:.5px solid var(--dsw-alias-border-default)}',
300
+ '.w2mChat_replyCancel{margin-left:auto;border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer}',
301
+ '.w2mChat_msg{cursor:pointer}',
302
+ '.w2mChat_thread{margin-left:6px;opacity:.7}',
298
303
  '.w2mChat_text{white-space:pre-wrap;word-break:break-word}',
299
304
  '.w2mChat_msg.is-session .w2mChat_text{color:var(--dsw-danger)}', '.w2mChat_msg.is-chat .w2mChat_text{color:var(--dsw-success)}', '.w2mChat_msg.is-remote .w2mChat_text{color:var(--dsw-text)}', '.w2mChat_avatar{display:inline-flex;width:16px;height:16px;margin-right:5px;vertical-align:-3px}',
300
305
  '.w2mChat_compose{display:flex;gap:8px;padding:8px 10px;border-top:.5px solid var(--dsw-alias-border-l2);align-items:flex-end}',
@@ -614,6 +619,8 @@ window.__ModuleLoader__.load({
614
619
  const [usage, setUsage] = React.useState(null);
615
620
  const [localMachineName, setLocalMachineName] = React.useState(null);
616
621
  const [roster, setRoster] = React.useState(null);
622
+ const [toRecipient, setToRecipient] = React.useState('all');
623
+ const [replyTo, setReplyTo] = React.useState(null);
617
624
  const [text, setText] = React.useState('');
618
625
  const [error, setError] = React.useState(null);
619
626
  const [busy, setBusy] = React.useState(false);
@@ -670,12 +677,18 @@ window.__ModuleLoader__.load({
670
677
  method: 'POST',
671
678
  credentials: 'same-origin',
672
679
  headers: { 'content-type': 'application/json', accept: 'application/json' },
673
- body: JSON.stringify({ session_id: sessionId, text: value }),
680
+ body: JSON.stringify({
681
+ session_id: sessionId,
682
+ text: value,
683
+ ...(toRecipient === 'all' ? {} : { to: toRecipient }),
684
+ ...(replyTo === null ? {} : { reply_to: replyTo.id, ...(replyTo.threadId === null ? {} : { thread_id: replyTo.threadId }) }),
685
+ }),
674
686
  },
675
687
  });
676
688
  setBusy(false);
677
689
  if (result.ok) {
678
690
  setText('');
691
+ setReplyTo(null);
679
692
  await load();
680
693
  } else {
681
694
  setError(result.error);
@@ -713,6 +726,15 @@ window.__ModuleLoader__.load({
713
726
  open
714
727
  ? h('div', { className: 'w2mChat_panel', role: 'dialog', 'aria-label': '编队聊天' },
715
728
  h('div', { className: 'w2mChat_head' },
729
+ h('select', {
730
+ className: 'w2mChat_to',
731
+ value: toRecipient,
732
+ title: '收件人',
733
+ onChange: (event) => setToRecipient(event.target.value),
734
+ }, [
735
+ h('option', { key: '__all__', value: 'all' }, '全部'),
736
+ ...(Array.isArray(roster?.machines) ? roster.machines.filter((machine) => machine.online === true && typeof machine.machine_id === 'string').map((machine) => h('option', { key: machine.machine_id, value: machine.machine_id }, machine.name ?? machine.machine_id)) : []),
737
+ ]),
716
738
  h('span', null, '编队聊天'),
717
739
  usageText === null ? null : h('span', { className: 'w2mChat_usage' }, usageText),
718
740
  h('button', { type: 'button', className: 'w2mChat_close', title: '关闭', onClick: () => setOpen(false) }, '×')),
@@ -722,13 +744,17 @@ window.__ModuleLoader__.load({
722
744
  ? (error === null ? h('div', { className: 'w2mChat_empty' }, '本会话还没有消息') : null)
723
745
  : messages.map((message) => {
724
746
  var kind = messageKind(message, localMachineId);
725
- return h('div', { key: message?.message_id ?? `${message?.machine_id}:${message?.at_ms}`, className: 'w2mChat_msg is-' + kind },
726
- h('div', { className: 'w2mChat_meta' }, h(WhaleAvatar, { machineId: message?.machine_id }), `${machineLabel(message)} · ${timeLabel(message)}`, kind === 'session' ? ' · 来自会话' : ''),
747
+ return h('div', { key: message?.message_id ?? `${message?.machine_id}:${message?.at_ms}`, className: 'w2mChat_msg is-' + kind, onClick: () => setReplyTo({ id: message?.message_id ?? null, threadId: message?.thread_id ?? null, text: typeof message?.text === 'string' ? message.text : '' }) },
748
+ h('div', { className: 'w2mChat_meta' }, h(WhaleAvatar, { machineId: message?.machine_id }), `${machineLabel(message)} · ${timeLabel(message)}`, kind === 'session' ? ' · 来自会话' : '', message?.thread_id ? h('span', { className: 'w2mChat_thread' }, '#' + message.thread_id) : null),
727
749
  h('div', { className: 'w2mChat_text' }, message?.text ?? ''));
728
750
  })),
729
751
 
730
752
 
731
753
  h('div', { className: 'w2mChat_compose' },
754
+ replyTo === null ? null : h('div', { className: 'w2mChat_reply' }, [
755
+ h('span', { key: 'label' }, '回复:' + (replyTo.text.length > 40 ? replyTo.text.slice(0, 40) + '…' : replyTo.text)),
756
+ h('button', { key: 'cancel', type: 'button', className: 'w2mChat_replyCancel', title: '取消回复', onClick: () => setReplyTo(null) }, '×'),
757
+ ]),
732
758
  h('textarea', {
733
759
  className: 'w2mChat_input',
734
760
  value: text,
package/package.json CHANGED
@@ -1,15 +1,8 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
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
- "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
6
  "exports": {
14
7
  ".": "./lib/index.js",
15
8
  "./tools": "./lib/tools.js",
@@ -20,6 +13,16 @@
20
13
  "./package.json": "./package.json",
21
14
  "./gpw": "./lib/gpw.js"
22
15
  },
16
+ "dsh": {
17
+ "plugin": true,
18
+ "kind": "server",
19
+ "bundle": {
20
+ "patch": "cordis.patch.yml"
21
+ },
22
+ "client": {
23
+ "platform": "web"
24
+ }
25
+ },
23
26
  "files": [
24
27
  "mcp.json",
25
28
  "plugin.json",
@@ -47,80 +50,10 @@
47
50
  ".claude-plugin",
48
51
  "codex"
49
52
  ],
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
53
  "engines": {
105
54
  "node": "\u003e=20.19.0"
106
55
  },
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
- },
56
+ "license": "MIT",
124
57
  "scripts": {
125
58
  "format": "prettier --write .",
126
59
  "format:check": "prettier --check .",
@@ -131,14 +64,14 @@
131
64
  "pack": "node scripts/pack.mjs",
132
65
  "reachability": "node scripts/reachability-check.mjs",
133
66
  "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",
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",
135
68
  "test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
136
69
  "test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
137
70
  "test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
138
71
  "test:admission": "node --test --test-force-exit test/admission.test.mjs",
139
72
  "test:admission-routes": "node --test --test-force-exit test/admission-routes.test.mjs",
140
73
  "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",
74
+ "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 test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs",
142
75
  "test:approval-gate": "node --test --test-force-exit test/approval-gate.test.mjs",
143
76
  "test:approval-index": "node --test --test-force-exit test/approval-index.test.mjs",
144
77
  "test:authority": "node --test --test-force-exit test/authority.test.mjs",
@@ -188,8 +121,30 @@
188
121
  "test:update-channels": "node --test --test-force-exit test/update-channels.test.mjs",
189
122
  "test:update-prerelease": "node --test --test-force-exit test/update-prerelease.test.mjs",
190
123
  "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"
124
+ "verify:join": "node scripts/verify-join.mjs",
125
+ "test:intents": "node --test --test-force-exit test/intents.test.mjs",
126
+ "test:dataflow": "node --test --test-force-exit test/dataflow.test.mjs",
127
+ "test:council-fleet": "node --test --test-force-exit test/council-fleet.test.mjs",
128
+ "test:fleet-memory": "node --test --test-force-exit test/fleet-memory.test.mjs",
129
+ "test:memory-view": "node --test --test-force-exit test/memory-view.test.mjs"
192
130
  },
131
+ "devDependencies": {
132
+ "@eslint/js": "^10.0.1",
133
+ "eslint": "^10.12.0",
134
+ "globals": "^17.13.0",
135
+ "prettier": "^3.9.9"
136
+ },
137
+ "publishConfig": {
138
+ "access": "public"
139
+ },
140
+ "repository": {
141
+ "type": "git",
142
+ "url": "git+https://github.com/TwinsEarth/dsh-windows2macos.git"
143
+ },
144
+ "homepage": "https://github.com/TwinsEarth/dsh-windows2macos",
145
+ "bugs": {
146
+ "url": "https://github.com/TwinsEarth/dsh-windows2macos/issues"
147
+ },
193
148
  "keywords": [
194
149
  "dsh-bundle",
195
150
  "dsh-plugin",
@@ -203,16 +158,66 @@
203
158
  "replicate",
204
159
  "sharding"
205
160
  ],
206
- "repository": {
207
- "type": "git",
208
- "url": "git+https://github.com/TwinsEarth/dsh-windows2macos.git"
161
+ "main": "lib/index.js",
162
+ "bin": {
163
+ "w2m-localside": "bin/w2m-localside.mjs",
164
+ "w2m-mcp": "bin/w2m-mcp.mjs",
165
+ "w2m-rabbit": "bin/w2m-rabbit.mjs",
166
+ "w2m-stun": "bin/w2m-stun.mjs"
167
+ },
168
+ "disclosure": {
169
+ "cloud": true,
170
+ "network": [
171
+ "http://202.182.123.154:8787",
172
+ "202.182.123.154:3478",
173
+ "stun.cloudflare.com:3478",
174
+ "stun.l.google.com:19302",
175
+ "stun.nextcloud.com:443",
176
+ "ilinkai.weixin.qq.com:443",
177
+ "weixin.qq.com:443",
178
+ "api.weixin.qq.com:443"
179
+ ],
180
+ "offlineMode": false,
181
+ "apiKeys": [
182
+ {
183
+ "env": "W2M_OPERATOR_TOKEN",
184
+ "storage": "profile-config"
185
+ },
186
+ {
187
+ "env": null,
188
+ "storage": "file-0600"
189
+ },
190
+ {
191
+ "env": null,
192
+ "storage": "profile-config"
193
+ },
194
+ {
195
+ "env": null,
196
+ "storage": "im-bot-token"
197
+ }
198
+ ],
199
+ "permissions": {
200
+ "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",
201
+ "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",
202
+ "env": [
203
+ "W2M_*"
204
+ ]
205
+ },
206
+ "jurisdiction": [
207
+ "CN",
208
+ "JP"
209
+ ],
210
+ "retention": "server"
209
211
  },
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"
212
+ "dependencies": {
213
+
214
+ },
215
+ "peerDependencies": {
216
+ "@deepseek-ai/dsh-tools": "*"
217
+ },
218
+ "peerDependenciesMeta": {
219
+ "@deepseek-ai/dsh-tools": {
220
+ "optional": true
221
+ }
222
+ }
218
223
  }
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.2.1",
4
+ "version": "1.2.2",
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": [
@@ -0,0 +1,221 @@
1
+ /**
2
+ * A council whose members are machines, not callers in one process.
3
+ *
4
+ * ## What is shared with the single-machine council, and what is not
5
+ *
6
+ * The RULE is shared: a vote's weight is a pure function of the numbers on its ballot (authority and reputation), so any
7
+ * machine that holds the same ballots computes the same outcome; and a tie is a REFUSAL, not a coin toss, because a split
8
+ * council is exactly the case a person has to look at.
9
+ *
10
+ * The TRANSPORT is different. A single-machine council holds its ballots in memory; a fleet council's ballots arrive as
11
+ * chat messages on a thread (\`council-<id>\`), which is what the 1.2.1 relay already supports. So this module is the state
12
+ * and the arithmetic, and {@link fleetCouncilRequest} / {@link fleetCouncilBallot} are the two messages it exchanges --
13
+ * plain JSON in text, because the fleet chat contract is "plain text" and inventing a binary frame would need a relay
14
+ * change for no benefit.
15
+ *
16
+ * ## Why late ballots are refused rather than ignored
17
+ *
18
+ * Silently dropping a ballot makes a voter believe their vote counted. Refusing it by name can be told to them.
19
+ */
20
+ export const FLEET_COUNCIL_REFUSAL = Object.freeze({
21
+ UNKNOWN: 'FLEET_COUNCIL_UNKNOWN',
22
+ ALREADY_OPEN: 'FLEET_COUNCIL_ALREADY_OPEN',
23
+ ALREADY_DECIDED: 'FLEET_COUNCIL_ALREADY_DECIDED',
24
+ NO_BALLOTS: 'FLEET_COUNCIL_NO_BALLOTS',
25
+ BAD_BALLOT: 'FLEET_COUNCIL_BAD_BALLOT',
26
+ DUPLICATE_VOTER: 'FLEET_COUNCIL_DUPLICATE_VOTER',
27
+ LATE_BALLOT: 'FLEET_COUNCIL_LATE_BALLOT',
28
+ TIED: 'FLEET_COUNCIL_TIED',
29
+ BAD_MESSAGE: 'FLEET_COUNCIL_BAD_MESSAGE',
30
+ });
31
+
32
+ /** The weight of one ballot. Deliberately the same formula as \`agent/council.mjs#weightOf\`, restated here because importing
33
+ * the single-machine module would drag its phase machine into a fleet decision that does not have phases. */
34
+ export function fleetWeightOf({ authority, reputation } = {}) {
35
+ const level = Number.isFinite(authority) ? authority : null;
36
+ if (level === null || level < 0 || level > 100) return null;
37
+ const rep = Number.isFinite(reputation) ? reputation : 0;
38
+ return level * (1 + rep / 100);
39
+ }
40
+
41
+ /**
42
+ * Count the stored ballots. Pure: same ballots, same outcome, on any machine.
43
+ *
44
+ * @param {Array<{choice: string, authority: number, reputation: number, machine_id?: string}>} ballots
45
+ * @returns {{ok: true, choice: string, weight: number, dissent: object[]} | {ok: false, code: string, detail?: object}}
46
+ */
47
+ export function tallyFleetBallots(ballots) {
48
+ const list = Array.isArray(ballots) ? ballots : [];
49
+ if (list.length === 0) return { ok: false, code: FLEET_COUNCIL_REFUSAL.NO_BALLOTS };
50
+ const totals = new Map();
51
+ for (const ballot of list) {
52
+ const choice = typeof ballot?.choice === 'string' ? ballot.choice.trim() : '';
53
+ if (choice === '') return { ok: false, code: FLEET_COUNCIL_REFUSAL.BAD_BALLOT, detail: { ballot } };
54
+ const weight = fleetWeightOf(ballot);
55
+ if (weight === null) return { ok: false, code: FLEET_COUNCIL_REFUSAL.BAD_BALLOT, detail: { authority: ballot?.authority } };
56
+ totals.set(choice, (totals.get(choice) ?? 0) + weight);
57
+ }
58
+ const ranked = [...totals.entries()].sort((a, b) => b[1] - a[1]);
59
+ const [topChoice, topWeight] = ranked[0];
60
+ const tied = ranked.slice(1).filter(([, weight]) => weight === topWeight);
61
+ if (tied.length > 0) {
62
+ return {
63
+ ok: false,
64
+ code: FLEET_COUNCIL_REFUSAL.TIED,
65
+ detail: { tied: [topChoice, ...tied.map(([choice]) => choice)], weight: topWeight },
66
+ };
67
+ }
68
+ return {
69
+ ok: true,
70
+ choice: topChoice,
71
+ weight: topWeight,
72
+ dissent: list.filter((ballot) => ballot.choice !== topChoice).map((ballot) => ({ ...ballot })),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * @param {{now?: () => number}} [options]
78
+ */
79
+ export function createFleetCouncil(options = {}) {
80
+ const now = typeof options.now === 'function' ? options.now : () => Date.now();
81
+ /** councilId -> { id, question, openedBy, atMs, decidedAtMs, ballots, decision } */
82
+ const councils = new Map();
83
+
84
+ function open({ councilId, question, openedByMachineId } = {}) {
85
+ const id = typeof councilId === 'string' && councilId.trim() !== '' ? councilId.trim() : null;
86
+ if (id === null) return { ok: false, code: FLEET_COUNCIL_REFUSAL.BAD_BALLOT, detail: { field: 'councilId' } };
87
+ const questionText = typeof question === 'string' ? question.trim() : '';
88
+ if (questionText === '') return { ok: false, code: FLEET_COUNCIL_REFUSAL.BAD_BALLOT, detail: { field: 'question' } };
89
+ const existing = councils.get(id);
90
+ if (existing !== undefined && existing.decision === null) return { ok: false, code: FLEET_COUNCIL_REFUSAL.ALREADY_OPEN };
91
+ const council = {
92
+ id,
93
+ question: questionText,
94
+ openedBy: typeof openedByMachineId === 'string' ? openedByMachineId : null,
95
+ atMs: now(),
96
+ decidedAtMs: null,
97
+ ballots: [],
98
+ decision: null,
99
+ };
100
+ councils.set(id, council);
101
+ return { ok: true, council: snapshot(council) };
102
+ }
103
+
104
+ function vote({ councilId, machineId, choice, reason = null, authority = null, reputation = null } = {}) {
105
+ const council = councils.get(typeof councilId === 'string' ? councilId.trim() : '');
106
+ if (council === undefined) return { ok: false, code: FLEET_COUNCIL_REFUSAL.UNKNOWN, detail: { councilId } };
107
+ if (council.decision !== null) return { ok: false, code: FLEET_COUNCIL_REFUSAL.LATE_BALLOT, detail: { decidedAtMs: council.decidedAtMs } };
108
+ const voter = typeof machineId === 'string' && machineId !== '' ? machineId : null;
109
+ if (voter === null) return { ok: false, code: FLEET_COUNCIL_REFUSAL.BAD_BALLOT, detail: { field: 'machineId' } };
110
+ if (council.ballots.some((ballot) => ballot.machine_id === voter)) {
111
+ return { ok: false, code: FLEET_COUNCIL_REFUSAL.DUPLICATE_VOTER, detail: { machineId: voter } };
112
+ }
113
+ const ballot = {
114
+ machine_id: voter,
115
+ choice: typeof choice === 'string' ? choice.trim() : '',
116
+ reason: typeof reason === 'string' ? reason : null,
117
+ authority: Number.isInteger(authority) && authority >= 0 && authority <= 100 ? authority : null,
118
+ reputation: Number.isFinite(reputation) ? reputation : null,
119
+ at_ms: now(),
120
+ };
121
+ if (ballot.choice === '' || ballot.authority === null) {
122
+ return { ok: false, code: FLEET_COUNCIL_REFUSAL.BAD_BALLOT, detail: { ballot } };
123
+ }
124
+ council.ballots.push(ballot);
125
+ return { ok: true, council: snapshot(council) };
126
+ }
127
+
128
+ function decide({ councilId } = {}) {
129
+ const council = councils.get(typeof councilId === 'string' ? councilId.trim() : '');
130
+ if (council === undefined) return { ok: false, code: FLEET_COUNCIL_REFUSAL.UNKNOWN, detail: { councilId } };
131
+ if (council.decision !== null) return { ok: true, council: snapshot(council), alreadyDecided: true };
132
+ const counted = tallyFleetBallots(council.ballots);
133
+ if (counted.ok !== true) return counted;
134
+ council.decision = {
135
+ choice: counted.choice,
136
+ weight: counted.weight,
137
+ dissent: counted.dissent,
138
+ ballots: council.ballots.length,
139
+ };
140
+ council.decidedAtMs = now();
141
+ return { ok: true, council: snapshot(council) };
142
+ }
143
+
144
+ function state({ councilId } = {}) {
145
+ const council = councils.get(typeof councilId === 'string' ? councilId.trim() : '');
146
+ if (council === undefined) return { ok: false, code: FLEET_COUNCIL_REFUSAL.UNKNOWN, detail: { councilId } };
147
+ return { ok: true, council: snapshot(council) };
148
+ }
149
+
150
+ function list() {
151
+ return { ok: true, councils: [...councils.values()].map(snapshot) };
152
+ }
153
+
154
+ function snapshot(council) {
155
+ return {
156
+ id: council.id,
157
+ question: council.question,
158
+ opened_by: council.openedBy,
159
+ open: council.decision === null,
160
+ at_ms: council.atMs,
161
+ decided_at_ms: council.decidedAtMs,
162
+ ballots: council.ballots.map((ballot) => ({ ...ballot })),
163
+ decision: council.decision === null ? null : { ...council.decision, dissent: council.decision.dissent.map((b) => ({ ...b })) },
164
+ };
165
+ }
166
+
167
+ return { open, vote, decide, state, list, _councils: councils };
168
+ }
169
+
170
+ /** The thread every message about one council travels on. */
171
+ export function fleetCouncilThread(councilId) {
172
+ return 'council-' + String(councilId ?? '').trim();
173
+ }
174
+
175
+ /** The message that opens a vote on the fleet chat. */
176
+ export function fleetCouncilRequest({ councilId, question, openedByMachineId = null } = {}) {
177
+ return JSON.stringify({
178
+ v: 1,
179
+ kind: 'council-request',
180
+ council_id: String(councilId ?? ''),
181
+ question: String(question ?? ''),
182
+ opened_by: openedByMachineId,
183
+ });
184
+ }
185
+
186
+ /** The message that carries one ballot. */
187
+ export function fleetCouncilBallot({ councilId, machineId, choice, reason = null, authority = null, reputation = null } = {}) {
188
+ return JSON.stringify({
189
+ v: 1,
190
+ kind: 'council-ballot',
191
+ council_id: String(councilId ?? ''),
192
+ machine_id: String(machineId ?? ''),
193
+ choice: String(choice ?? ''),
194
+ reason,
195
+ authority,
196
+ reputation,
197
+ });
198
+ }
199
+
200
+ /**
201
+ * Read one fleet-chat message: is it a council request, a ballot, or neither?
202
+ *
203
+ * @param {string} text
204
+ * @returns {{kind: 'request'|'ballot', payload: object} | {kind: 'none'} | {kind: 'bad', reason: string}}
205
+ */
206
+ export function parseFleetCouncilMessage(text) {
207
+ if (typeof text !== 'string' || text.trim() === '') return { kind: 'none' };
208
+ let parsed;
209
+ try { parsed = JSON.parse(text); } catch { return { kind: 'none' }; }
210
+ if (parsed === null || typeof parsed !== 'object' || parsed.v !== 1) return { kind: 'none' };
211
+ if (parsed.kind === 'council-request') {
212
+ if (typeof parsed.council_id !== 'string' || parsed.council_id.trim() === '') return { kind: 'bad', reason: 'council_id' };
213
+ return { kind: 'request', payload: parsed };
214
+ }
215
+ if (parsed.kind === 'council-ballot') {
216
+ if (typeof parsed.council_id !== 'string' || parsed.council_id.trim() === '') return { kind: 'bad', reason: 'council_id' };
217
+ if (typeof parsed.machine_id !== 'string' || parsed.machine_id.trim() === '') return { kind: 'bad', reason: 'machine_id' };
218
+ return { kind: 'ballot', payload: parsed };
219
+ }
220
+ return { kind: 'none' };
221
+ }