@twinsearth/w2m-dsh-plugin 1.1.18 → 1.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +68 -0
- package/README.md +49 -8
- package/codex/mcp-servers.toml +1 -1
- package/lib/client.js +850 -640
- package/lib/client.js.bak2 +640 -0
- package/package.json +9 -6
- package/plugin.json +2 -2
- package/src/plugin/fleet-chat-route.mjs +221 -0
- package/src/plugin/fleet-target.mjs.bak-arch2 +812 -0
- package/src/plugin/fleet-target.mjs.bak-archive +812 -0
- package/src/plugin/session-sync.mjs +102 -0
- package/src/plugin/session-watch.mjs +127 -0
- package/src/plugin/tools.mjs +7148 -7119
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.20",
|
|
4
4
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -120,22 +120,22 @@
|
|
|
120
120
|
}
|
|
121
121
|
},
|
|
122
122
|
"scripts": {
|
|
123
|
-
"format": "prettier --write .",
|
|
124
|
-
"format:check": "prettier --check .",
|
|
125
123
|
"layer-check": "node scripts/layer-check.mjs",
|
|
124
|
+
"format:check": "prettier --check .",
|
|
125
|
+
"format": "prettier --write .",
|
|
126
126
|
"lint": "eslint .",
|
|
127
127
|
"lint:fix": "eslint . --fix",
|
|
128
128
|
"orphans": "node --test --test-force-exit test/accepted-orphans.test.mjs",
|
|
129
129
|
"pack": "node scripts/pack.mjs",
|
|
130
130
|
"reachability": "node scripts/reachability-check.mjs",
|
|
131
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",
|
|
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
133
|
"test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
|
|
134
134
|
"test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
|
|
135
135
|
"test:admission": "node --test --test-force-exit test/admission.test.mjs",
|
|
136
136
|
"test:admission-routes": "node --test --test-force-exit test/admission-routes.test.mjs",
|
|
137
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",
|
|
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
139
|
"test:approval-gate": "node --test --test-force-exit test/approval-gate.test.mjs",
|
|
140
140
|
"test:approval-index": "node --test --test-force-exit test/approval-index.test.mjs",
|
|
141
141
|
"test:authority": "node --test --test-force-exit test/authority.test.mjs",
|
|
@@ -181,7 +181,10 @@
|
|
|
181
181
|
"test:fleet-messages-http": "node --test --test-force-exit test/fleet-messages-http.test.mjs",
|
|
182
182
|
"test:message-notify": "node --test --test-force-exit test/message-notify.test.mjs",
|
|
183
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"
|
|
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"
|
|
185
188
|
},
|
|
186
189
|
"keywords": [
|
|
187
190
|
"dsh-bundle",
|
package/plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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.1.
|
|
5
|
-
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through
|
|
4
|
+
"version": "1.1.20",
|
|
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": [
|
|
8
8
|
"agent-skills",
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/api/w2m/chat` — the browser-facing half of fleet chat (v1.1.17 shipped the tool only; this
|
|
3
|
+
* module is the missing host route the composer's chat panel calls).
|
|
4
|
+
*
|
|
5
|
+
* The route proxies the relay's device-gated `/v1/fleet/messages` endpoints with this machine's
|
|
6
|
+
* device token. The browser never sees the token: it calls this same-origin route, which is inside
|
|
7
|
+
* the host's authentication fence, exactly like `/api/w2m/target`.
|
|
8
|
+
*
|
|
9
|
+
* Wire:
|
|
10
|
+
* GET /api/w2m/chat?session_id=…&since_ms=…&limit=… -> relay list shape passed through
|
|
11
|
+
* POST /api/w2m/chat { session_id, text } -> relay send shape passed through
|
|
12
|
+
*
|
|
13
|
+
* Failure is a value here: `{ok:false, error, reason_code, detail}` with a non-2xx status, so the
|
|
14
|
+
* panel can print the host's own words. A relay that predates the fleet-messages routes answers
|
|
15
|
+
* 404, which is reported as such rather than as an empty conversation.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import {
|
|
22
|
+
conversationKey as deriveConversationKey, decodeTranscript, extractUserMessages, sessionDirName,
|
|
23
|
+
} from './session-watch.mjs';
|
|
24
|
+
import { createSessionSync } from './session-sync.mjs';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The newest transcript for a cwd, or null. Reads directory listings and stat only -- this module never opens a
|
|
28
|
+
* session file for writing, and \ est/session-readonly.test.mjs\ enforces that.
|
|
29
|
+
*/
|
|
30
|
+
function newestTranscript(cwd) {
|
|
31
|
+
const dir = join(homedir(), '.dsh', 'sessions', sessionDirName(cwd));
|
|
32
|
+
let entries;
|
|
33
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return null; }
|
|
34
|
+
let best = null;
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry.isDirectory() || !entry.name.startsWith('session-')) continue;
|
|
37
|
+
const file = join(dir, entry.name, 'session.v4.jsonl.zstd');
|
|
38
|
+
let stat;
|
|
39
|
+
try { stat = statSync(file); } catch { continue; }
|
|
40
|
+
if (best === null || stat.mtimeMs > best.mtimeMs) best = { file, mtimeMs: stat.mtimeMs };
|
|
41
|
+
}
|
|
42
|
+
return best === null ? null : best.file;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const CHAT_ROUTE_PATH = '/api/w2m/chat';
|
|
46
|
+
export const MAX_SESSION_ID_LENGTH = 200;
|
|
47
|
+
|
|
48
|
+
const NO_STORE = { 'cache-control': 'no-store' };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Attach the route to one host's fetch registry, recording the outcome on `status`.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} ctx the plugin context (a Cordis fiber)
|
|
54
|
+
* @param {{cfg: () => object, request: Function, resolveToken: () => Promise<string|null>}} deps
|
|
55
|
+
* @returns {{mounted: boolean, reason: string|null}}
|
|
56
|
+
*/
|
|
57
|
+
export function registerChatRoutes(ctx, deps) {
|
|
58
|
+
const status = { mounted: false, reason: 'the host has no Connection service' };
|
|
59
|
+
const cwd = process.cwd();
|
|
60
|
+
const sync = createSessionSync({ conversationKey: 'pending', machineId: 'pending', machineName: null });
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Which machine this is and which conversation the project belongs to.
|
|
64
|
+
*
|
|
65
|
+
* Resolved once. The account material is hashed with a domain label first: it is the fleet key, which is secret, but
|
|
66
|
+
* hashing keeps the conversation key from ever being usable as a joining secret.
|
|
67
|
+
*/
|
|
68
|
+
let identityPromise = null;
|
|
69
|
+
const identityOf = () => (identityPromise ??= (async () => {
|
|
70
|
+
const local = typeof deps.localIdentity === 'function' ? await deps.localIdentity() : null;
|
|
71
|
+
const material = typeof deps.accountMaterial === 'function' ? deps.accountMaterial() : null;
|
|
72
|
+
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 };
|
|
75
|
+
}
|
|
76
|
+
const fingerprint = createHash('sha256').update('w2m-account-material-v1').update('\\u0000').update(material).digest('hex');
|
|
77
|
+
return {
|
|
78
|
+
error: null,
|
|
79
|
+
machineId,
|
|
80
|
+
machineName: typeof local?.machine_name === 'string' ? local.machine_name : null,
|
|
81
|
+
conversationKey: deriveConversationKey(fingerprint, cwd),
|
|
82
|
+
};
|
|
83
|
+
})());
|
|
84
|
+
|
|
85
|
+
/** Post one batch into the relay using the machine's device token, exactly as the browser route does. */
|
|
86
|
+
const postToRelay = async (identity, payload) => {
|
|
87
|
+
const cfg = deps.cfg();
|
|
88
|
+
const rabbitUrl = cfg?.rabbitUrl ?? null;
|
|
89
|
+
if (rabbitUrl === null) return { ok: false };
|
|
90
|
+
let token;
|
|
91
|
+
try { token = await deps.resolveToken(); } catch { return { ok: false }; }
|
|
92
|
+
if (typeof token !== 'string' || token === '') return { ok: false };
|
|
93
|
+
return deps.request({
|
|
94
|
+
rabbitUrl, token, method: 'POST', pathname: '/v1/fleet/messages',
|
|
95
|
+
body: { session_id: identity.conversationKey, ...payload },
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* v1.1.20: mirror this machine's own prompts into the fleet conversation -- READ ONLY.
|
|
101
|
+
*
|
|
102
|
+
* The session files are never written and nothing is injected into DSH: the transcript is read, the user's messages
|
|
103
|
+
* are extracted, and only the new ones are published. A failure here must not disturb the panel or the route.
|
|
104
|
+
*/
|
|
105
|
+
const mirrorOnce = async () => {
|
|
106
|
+
const identity = await identityOf();
|
|
107
|
+
if (identity.conversationKey === null) return;
|
|
108
|
+
const file = newestTranscript(cwd);
|
|
109
|
+
if (file === null) return;
|
|
110
|
+
let messages;
|
|
111
|
+
try { messages = extractUserMessages(decodeTranscript(readFileSync(file))); } catch { return; }
|
|
112
|
+
for (const payload of sync.pending(messages)) {
|
|
113
|
+
await postToRelay(identity, {
|
|
114
|
+
text: payload.text,
|
|
115
|
+
machine_id: identity.machineId,
|
|
116
|
+
machine_name: identity.machineName,
|
|
117
|
+
origin: 'session',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
if (typeof ctx?.inject !== 'function') return status;
|
|
122
|
+
ctx.inject(['connection'], (webCtx) => {
|
|
123
|
+
const registry = webCtx?.connection?.fetch;
|
|
124
|
+
if (registry === undefined || registry === null || typeof registry.register !== 'function') {
|
|
125
|
+
status.reason = 'the connection service carries no fetch registry';
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
registry.register({
|
|
130
|
+
path: CHAT_ROUTE_PATH,
|
|
131
|
+
methods: ['GET', 'POST'],
|
|
132
|
+
requestBody: 'buffered',
|
|
133
|
+
fetch: async (request) => {
|
|
134
|
+
const refuse = (httpStatus, reasonCode, error, detail) =>
|
|
135
|
+
Response.json({ ok: false, error, reason_code: reasonCode, detail }, { status: httpStatus, headers: NO_STORE });
|
|
136
|
+
|
|
137
|
+
if (request.method !== 'GET' && request.method !== 'POST') {
|
|
138
|
+
return refuse(405, 'CHAT_METHOD_NOT_ALLOWED', `${request.method} is not answered here`, 'GET lists a session; POST sends a message');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const url = new URL(request.url);
|
|
142
|
+
const sessionIdOf = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
143
|
+
let sessionId = '';
|
|
144
|
+
let text = null;
|
|
145
|
+
if (request.method === 'GET') {
|
|
146
|
+
sessionId = sessionIdOf(url.searchParams.get('session_id'));
|
|
147
|
+
} else {
|
|
148
|
+
let body;
|
|
149
|
+
try {
|
|
150
|
+
body = await request.json();
|
|
151
|
+
} catch {
|
|
152
|
+
return refuse(400, 'CHAT_BODY_NOT_JSON', 'body is not JSON', 'send {"session_id":"…","text":"…"}');
|
|
153
|
+
}
|
|
154
|
+
sessionId = sessionIdOf(body?.session_id);
|
|
155
|
+
text = typeof body?.text === 'string' ? body.text : null;
|
|
156
|
+
}
|
|
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');
|
|
165
|
+
if (sessionId.length > MAX_SESSION_ID_LENGTH) {
|
|
166
|
+
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
|
+
}
|
|
168
|
+
|
|
169
|
+
const cfg = deps.cfg();
|
|
170
|
+
const rabbitUrl = cfg?.rabbitUrl ?? null;
|
|
171
|
+
if (rabbitUrl === null) return refuse(503, 'CHAT_NO_RELAY', 'no relay is configured', 'set rabbitUrl in the profile patch');
|
|
172
|
+
let token;
|
|
173
|
+
try {
|
|
174
|
+
token = await deps.resolveToken();
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return refuse(503, 'CHAT_UNPAIRED', `this machine has no device token: ${error instanceof Error ? error.message : String(error)}`, 'pair the machine first (autoStartAgent or a pairing code)');
|
|
177
|
+
}
|
|
178
|
+
if (typeof token !== 'string' || token === '') {
|
|
179
|
+
return refuse(503, 'CHAT_UNPAIRED', 'this machine has no device token', 'pair the machine first (autoStartAgent or a pairing code)');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
if (request.method === 'GET') {
|
|
184
|
+
const query = { session_id: sessionId };
|
|
185
|
+
const sinceMs = url.searchParams.get('since_ms');
|
|
186
|
+
const limit = url.searchParams.get('limit');
|
|
187
|
+
if (sinceMs !== null && /^\d+$/.test(sinceMs)) query.since_ms = sinceMs;
|
|
188
|
+
if (limit !== null && /^\d+$/.test(limit)) query.limit = limit;
|
|
189
|
+
const listed = await deps.request({ rabbitUrl, token, method: 'GET', pathname: '/v1/fleet/messages', query, signal: request.signal });
|
|
190
|
+
if (listed?.ok !== true) {
|
|
191
|
+
const code = listed?.json?.error?.code ?? listed?.error?.code ?? 'CHAT_LIST_REFUSED';
|
|
192
|
+
const message = listed?.json?.error?.message ?? listed?.error?.message ?? `the relay answered HTTP ${listed?.status ?? 0}`;
|
|
193
|
+
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
|
+
}
|
|
195
|
+
return Response.json({ ...listed.json, ok: true, local_machine_id: identity.machineId, local_machine_name: identity.machineName, conversation_key: sessionId }, { headers: NO_STORE });
|
|
196
|
+
}
|
|
197
|
+
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
|
+
if (posted?.ok !== true) {
|
|
199
|
+
const code = posted?.json?.error?.code ?? posted?.error?.code ?? 'CHAT_SEND_REFUSED';
|
|
200
|
+
const message = posted?.json?.error?.message ?? posted?.error?.message ?? `the relay answered HTTP ${posted?.status ?? 0}`;
|
|
201
|
+
return refuse(posted?.status === 404 ? 502 : 400, code, message, 'quota, size and non-text are each refused by name; 404 means the relay predates fleet chat');
|
|
202
|
+
}
|
|
203
|
+
return Response.json({ ...posted.json, ok: true }, { headers: NO_STORE });
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return refuse(502, 'CHAT_RELAY_ERROR', `the relay call failed: ${error instanceof Error ? error.message : String(error)}`, 'check the relay and this machine token');
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
status.mounted = true;
|
|
210
|
+
status.reason = null;
|
|
211
|
+
const timer = setInterval(() => { mirrorOnce().catch(() => { /* one bad tick must not stop the next */ }); }, 2000);
|
|
212
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
213
|
+
if (typeof ctx?.effect === 'function') ctx.effect(() => () => clearInterval(timer), 'w2m: fleet chat session mirror');
|
|
214
|
+
else globalThis.__w2mChatMirrorTimer = timer;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
status.reason = error instanceof Error ? error.message : String(error);
|
|
217
|
+
ctx?.logger?.warn?.(`w2m: the fleet chat route is inactive — ${status.reason}`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return status;
|
|
221
|
+
}
|