@twinsearth/w2m-dsh-plugin 0.57.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,67 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.58.0] — 2026-10-15
8
+
9
+ **The bridge now reports what it received instead of guessing what an image looks like.**
10
+
11
+ ### Why this is a probe and not an image branch
12
+
13
+ `ITEM_TYPE` declares exactly one member:
14
+
15
+ ```js
16
+ export const ITEM_TYPE = Object.freeze({ TEXT: 1 });
17
+ ```
18
+
19
+ **The shape of every other item type is unrecorded in this repository** — no `image_item`, no sample payload, no
20
+ fixture. `ilink-protocol.mjs` documents only the text form. **So a download path written today would be a guess
21
+ about field names, and it would fail at the moment a real photograph arrived** — which is the worst possible time
22
+ to discover a wrong guess, because the person is waiting for their picture to be read.
23
+
24
+ **This is the discovery step.** `describeItems()` reports **key paths and value types, never values**, so one
25
+ photograph sent by a person answers the question the source cannot.
26
+
27
+ ### Values are never recorded, and that is the point of the whole function
28
+
29
+ A message body is the user's content, and **a discovery aid that quietly wrote photographs into a log file would
30
+ be a worse defect than the one it exists to diagnose.** One assertion is dedicated to that: a distinctive
31
+ passphrase planted in a URL and a caption, and the check that neither appears in the output.
32
+
33
+ The walk is bounded — `maxKeys` and `maxDepth`, arrays described by length and first element, cycles reported
34
+ rather than followed — **so a surprising payload cannot bury the log it is being written to.**
35
+
36
+ ### Every wrong guess this session was corrected by printing the thing
37
+
38
+ A fabricated relay route (`/v1/stats`, `/v1/status`), a malformed PNG reported as a credential failure, a count
39
+ typed from memory onto a CHANGELOG entry — **each was fixed by reading rather than predicting.** This applies that
40
+ method *before* the guess instead of after it, which is the only difference between this and the four previous
41
+ occasions.
42
+
43
+ ### What is still not done, stated rather than implied
44
+
45
+ **The bridge still cannot read an image.** The `/v1`-style branch that downloads it does not exist, because the
46
+ field names are unknown. What exists now is the instrument that finds them: **send one photograph, and the log
47
+ says what the far end actually sends.**
48
+
49
+ **13 new tests, 0 failures**, and the existing bridge and tool-card suites still pass — the added `shape` field is
50
+ carried only on a non-text reply, so it means one thing. Full suite 2354 in 89 files. **Pre-flight GREEN.**
51
+
52
+ ## [0.57.1] — 2026-10-15
53
+
54
+ **The vision status line now appears only on a bridge that actually starts — which it already did, for a reason
55
+ worth writing down.**
56
+
57
+ `visionStatus()` sits at the top of `main()`, and the configuration refusal runs at **module scope, before `main()`
58
+ is ever called**. So a bridge that refuses to start prints nothing about vision.
59
+
60
+ **I first described that as a bug and set out to move the check "after the refusal block".** Reading the file
61
+ showed the ordering was already right: **the refusal already names what is wrong, and a second complaint about
62
+ vision underneath it would bury the first.** The comment was corrected to say what the code does rather than to
63
+ describe a fix that was never needed.
64
+
65
+ **A no-behaviour-change release**, and the only reason it exists is that the comment previously implied a
66
+ sequence the file does not have.
67
+
7
68
  ## [0.57.0] — 2026-10-15
8
69
 
9
70
  **An image path becomes text evidence: the vision subprocess, and the credential path that does not touch argv.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "0.57.0",
3
+ "version": "0.58.0",
4
4
  "description": "DSH: Windows2MacOS — drive the same project on online Windows and macOS machines from one DeepSeek account, across networks and regions. P2P hole punching is the default path (relay + shared server as rendezvous, ledger and fallback), with a bundled STUN responder, Tailscale/public-VPS/tunnel shapes, Rabbit relay + per-machine Localside + eight DSH tools (replicate / split), nine aggregate verdicts and signed requests.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -116,13 +116,13 @@
116
116
  }
117
117
  },
118
118
  "scripts": {
119
- "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.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/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/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/chatops-fleet-join.test.mjs",
119
+ "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.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/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/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",
120
120
  "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",
121
121
  "test:relay": "node --test --test-force-exit test/relay.test.mjs",
122
122
  "test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
123
123
  "test:tools": "node --test --test-force-exit test/tools.test.mjs",
124
124
  "test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
125
- "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.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/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/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/chatops-fleet-join.test.mjs",
125
+ "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.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/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/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",
126
126
  "pack": "node scripts/pack.mjs",
127
127
  "verify:join": "node scripts/verify-join.mjs",
128
128
  "lint": "eslint .",
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": "0.57.0",
4
+ "version": "0.58.0",
5
5
  "description": "Cross-platform fleet execution between Windows and macOS: one DeepSeek Harness account, a Rabbit relay, and signed device identity. Ships four Agent Skills describing fleet dispatch, field-level memory acceptance, session references, and resource scheduling.",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -113,6 +113,57 @@ export function textOf(message) {
113
113
  return null;
114
114
  }
115
115
 
116
+ /**
117
+ * Describe the SHAPE of a message's items without recording any of their content.
118
+ *
119
+ * ## Why this exists rather than an image branch
120
+ *
121
+ * `ITEM_TYPE` declares exactly one member, `TEXT: 1`. **The shape of every other item type is unrecorded in this
122
+ * repository** -- no `image_item`, no sample payload, no fixture. So a download path written now would be a guess
123
+ * about field names, and **it would fail at the moment a real person's photograph arrived**, which is the worst
124
+ * possible time to discover it.
125
+ *
126
+ * This is the discovery step instead. It reports **key paths and value types, never values**, so an operator can
127
+ * send one photograph and the log then says what the far end actually sends. **Every wrong guess this session
128
+ * was corrected by printing the thing instead of predicting it** -- a fabricated relay route, a malformed PNG, a
129
+ * count on a CHANGELOG entry -- and this is that method applied before the guess rather than after.
130
+ *
131
+ * Values are never included because a message body is the user's content, and **a discovery aid that quietly
132
+ * logs photographs to a file would be a worse defect than the one it is diagnosing.**
133
+ *
134
+ * @param {object} message one `msgs[]` entry
135
+ * @param {{maxKeys?: number, maxDepth?: number}} [options]
136
+ * @returns {string} a compact, content-free description
137
+ */
138
+ export function describeItems(message, { maxKeys = 24, maxDepth = 3 } = {}) {
139
+ const items = Array.isArray(message?.item_list) ? message.item_list : [];
140
+ if (items.length === 0) return 'item_list is empty or absent';
141
+
142
+ /**
143
+ * A bounded walk: enough to answer "which fields exist and what kind are they", and short enough that a
144
+ * surprising payload cannot bury the log. Arrays are described by their first element, because a caption list
145
+ * and a sticker list need the same answer.
146
+ */
147
+ const seen = new Set();
148
+ const walk = (value, depth) => {
149
+ if (value === null) return 'null';
150
+ if (Array.isArray(value)) return value.length === 0 ? 'array[0]' : `array[${value.length}] of ${walk(value[0], depth)}`;
151
+ const kind = typeof value;
152
+ if (kind !== 'object') return kind;
153
+ if (depth >= maxDepth) return 'object{…}';
154
+ if (seen.has(value)) return 'object{cyclic}';
155
+ seen.add(value);
156
+ const keys = Object.keys(value).slice(0, maxKeys);
157
+ const parts = keys.map((k) => `${k}:${walk(value[k], depth + 1)}`);
158
+ const more = Object.keys(value).length > maxKeys ? `,+${Object.keys(value).length - maxKeys}` : '';
159
+ return `{${parts.join(',')}${more}}`;
160
+ };
161
+
162
+ return items
163
+ .map((item, i) => `item[${i}].type=${JSON.stringify(item?.type)} ${walk(item, 1)}`)
164
+ .join(' | ');
165
+ }
166
+
116
167
  /**
117
168
  * The reply envelope.
118
169
  *
@@ -230,7 +281,22 @@ export function planMessage({ message, authorize }) {
230
281
  // A non-text message is answered rather than dropped: "every sentence must get a reply" includes the
231
282
  // sentences the bridge cannot read.
232
283
  if (text === null) {
233
- return { act: 'reply', to: from, contextToken, text: helpText({ reason: '这条消息不是文字(语音/图片/文件暂不支持)' }) };
284
+ const shape = describeItems(message);
285
+ return {
286
+ act: 'reply',
287
+ to: from,
288
+ contextToken,
289
+ text: helpText({ reason: '这条消息不是文字(语音/图片/文件暂不支持)' }),
290
+ /**
291
+ * Carried out of the plan so the caller can log it.
292
+ *
293
+ * **The shape, never the content.** This field exists so that one photograph sent by a person answers the
294
+ * question this file cannot answer from its own source -- which fields iLink actually puts on an image --
295
+ * and it is reported rather than acted on, because acting on a guessed shape is how a download path ends up
296
+ * failing on the first real photograph.
297
+ */
298
+ shape,
299
+ };
234
300
  }
235
301
 
236
302
  const trimmed = text.trim();
@@ -208,7 +208,7 @@ const MAX_STAGES = 16;
208
208
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
209
209
  * overrides it at runtime for tests and packagers.
210
210
  */
211
- const PLUGIN_VERSION = '0.57.0';
211
+ const PLUGIN_VERSION = '0.58.0';
212
212
 
213
213
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
214
214
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;