@twinsearth/w2m-dsh-plugin 2.2.0 → 2.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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +23 -2
- package/README.md +1 -1
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/2.2.1/RELEASE-2210.json +31 -0
- package/docs/acceptance/2.2.2/RELEASE-2220.json +25 -0
- package/lib/client.js +478 -10
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/src/plugin/fleet-binding.mjs +9 -12
- package/src/plugin/fleet-config-route.mjs +130 -0
- package/src/plugin/fleet-config.mjs +183 -0
- package/src/plugin/fleet-home.mjs +263 -0
- package/src/plugin/tools.mjs +220 -28
package/lib/client.js
CHANGED
|
@@ -66,6 +66,37 @@ window.__ModuleLoader__.load({
|
|
|
66
66
|
|
|
67
67
|
const React = require('react');
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Render a popup under `document.body`.
|
|
71
|
+
*
|
|
72
|
+
* DSH 0.2.2-rc.1 puts `container-type: inline-size` on the composer row. Layout containment makes
|
|
73
|
+
* that row the containing block for `position: fixed` descendants, so a menu rendered inside the
|
|
74
|
+
* composer slot is positioned relative to the row (and can be clipped) instead of the viewport.
|
|
75
|
+
* A portal keeps the viewport coordinates the menus already compute. `react-dom` is a platform
|
|
76
|
+
* seed in the Web shell; the lazy require keeps this bundle loadable where only React is present,
|
|
77
|
+
* and the inline fallback preserves the old behavior if the portal cannot be created.
|
|
78
|
+
*
|
|
79
|
+
* @param {object|null} node - menu element.
|
|
80
|
+
* @returns {object|null} portal element, or the original node.
|
|
81
|
+
*/
|
|
82
|
+
let portalRuntime;
|
|
83
|
+
let portalUnavailable = false;
|
|
84
|
+
function portalNode(node) {
|
|
85
|
+
if (node === null || portalUnavailable) return node;
|
|
86
|
+
if (typeof document === 'undefined' || document.body === null || document.body === undefined) return node;
|
|
87
|
+
try {
|
|
88
|
+
if (portalRuntime === undefined) portalRuntime = require('react-dom');
|
|
89
|
+
if (typeof portalRuntime?.createPortal !== 'function') {
|
|
90
|
+
portalUnavailable = true;
|
|
91
|
+
return node;
|
|
92
|
+
}
|
|
93
|
+
return portalRuntime.createPortal(node, document.body);
|
|
94
|
+
} catch {
|
|
95
|
+
portalUnavailable = true;
|
|
96
|
+
return node;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
69
100
|
// -------------------------------------------------------------------------------------------
|
|
70
101
|
// Wire
|
|
71
102
|
// -------------------------------------------------------------------------------------------
|
|
@@ -103,6 +134,7 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
103
134
|
costAll: '本次会话的派发会送到所有在线机器',
|
|
104
135
|
costOne: '本次会话的派发会送到 {name}',
|
|
105
136
|
unconfirmed: '(未确认在线)',
|
|
137
|
+
mentionFailed: '目标已切换,但输入框未能同步 @ 标记;请稍后重新选择该机器',
|
|
106
138
|
targetLabel: '目标机器:{name}',
|
|
107
139
|
unreachable: '无法连接插件',
|
|
108
140
|
routeMissing: '这个 Harness 没有提供插件目标路由',
|
|
@@ -176,6 +208,7 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
176
208
|
costAll: 'Work from this session goes to every online machine',
|
|
177
209
|
costOne: 'Work from this session goes to {name}',
|
|
178
210
|
unconfirmed: ' (not confirmed online)',
|
|
211
|
+
mentionFailed: 'the target changed, but the composer could not add the @ handle; pick the machine again in a moment',
|
|
179
212
|
targetLabel: 'target machine: {name}',
|
|
180
213
|
unreachable: 'the plugin could not be reached',
|
|
181
214
|
routeMissing: 'this host does not serve the W2M target route',
|
|
@@ -1748,7 +1781,336 @@ function UpdateSettingsSection() {
|
|
|
1748
1781
|
]);
|
|
1749
1782
|
}
|
|
1750
1783
|
|
|
1751
|
-
|
|
1784
|
+
/**
|
|
1785
|
+
* The `@machine` token the target control writes into the composer.
|
|
1786
|
+
*
|
|
1787
|
+
* This is a *visible handle*, not the dispatch decision: the host-side selection store remains
|
|
1788
|
+
* the source of truth, and `w2m_run` reads it from the host process. The token exists so the
|
|
1789
|
+
* operator can see which machine the next turn is aimed at after the menu closes.
|
|
1790
|
+
*
|
|
1791
|
+
* A name that is not a single safe word is quoted, because DSH's own `@` trigger runs to the
|
|
1792
|
+
* next whitespace. A name containing a quote cannot be represented safely and yields null:
|
|
1793
|
+
* the selection still works, it simply gets no token.
|
|
1794
|
+
*
|
|
1795
|
+
* @param {string|null|undefined} name The machine's display name.
|
|
1796
|
+
* @returns {string|null} `@name`, `@"name with spaces"`, or null.
|
|
1797
|
+
*/
|
|
1798
|
+
function targetTokenForName(name) {
|
|
1799
|
+
if (typeof name !== 'string') return null;
|
|
1800
|
+
const trimmed = name.trim();
|
|
1801
|
+
if (trimmed === '' || /[\r\n"]/.test(trimmed)) return null;
|
|
1802
|
+
return /^[A-Za-z0-9_-]+$/.test(trimmed) ? `@${trimmed}` : `@"${trimmed}"`;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/** Occurrence offsets are clipboard offsets; sort them so both maps walk one order. */
|
|
1806
|
+
function sortedOccurrences(occurrences) {
|
|
1807
|
+
return (Array.isArray(occurrences) ? occurrences : [])
|
|
1808
|
+
.filter((entry) => entry !== null && typeof entry === 'object'
|
|
1809
|
+
&& Number.isInteger(entry.offset) && Number.isInteger(entry.length) && entry.length > 0)
|
|
1810
|
+
.slice()
|
|
1811
|
+
.sort((left, right) => left.offset - right.offset);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/**
|
|
1815
|
+
* Clipboard offset → editor (detect) offset.
|
|
1816
|
+
*
|
|
1817
|
+
* A reference chip is one U+FFFC in the editor but its whole clipboard text in the draft, so
|
|
1818
|
+
* the two coordinate spaces diverge after every chip. Positions inside a chip are clamped to
|
|
1819
|
+
* its editor position; a token boundary never lands there.
|
|
1820
|
+
*/
|
|
1821
|
+
function clipboardToDetect(offset, occurrences) {
|
|
1822
|
+
let shift = 0;
|
|
1823
|
+
for (const occurrence of sortedOccurrences(occurrences)) {
|
|
1824
|
+
if (offset <= occurrence.offset) break;
|
|
1825
|
+
if (offset < occurrence.offset + occurrence.length) return occurrence.offset - shift + 1;
|
|
1826
|
+
shift += occurrence.length - 1;
|
|
1827
|
+
}
|
|
1828
|
+
return offset - shift;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/** Editor (detect) offset → clipboard offset; the inverse of {@link clipboardToDetect}. */
|
|
1832
|
+
function detectToClipboard(offset, occurrences) {
|
|
1833
|
+
let shift = 0;
|
|
1834
|
+
for (const occurrence of sortedOccurrences(occurrences)) {
|
|
1835
|
+
const start = occurrence.offset - shift;
|
|
1836
|
+
if (offset <= start) break;
|
|
1837
|
+
if (offset < start + 1) return occurrence.offset;
|
|
1838
|
+
shift += occurrence.length - 1;
|
|
1839
|
+
}
|
|
1840
|
+
return offset + shift;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
/** Find a token in the draft, requiring a word boundary on both sides. */
|
|
1844
|
+
function targetTokenSpan(draft, token) {
|
|
1845
|
+
if (typeof draft !== 'string' || typeof token !== 'string' || token === '') return null;
|
|
1846
|
+
let from = 0;
|
|
1847
|
+
while (from <= draft.length) {
|
|
1848
|
+
const start = draft.indexOf(token, from);
|
|
1849
|
+
if (start < 0) return null;
|
|
1850
|
+
const before = start === 0 ? '' : draft.charAt(start - 1);
|
|
1851
|
+
const after = draft.charAt(start + token.length);
|
|
1852
|
+
const beforeOk = start === 0 || /\s/.test(before);
|
|
1853
|
+
const afterOk = after === '' || /\s/.test(after) || /[.,;:!?,。;:!?、]/.test(after);
|
|
1854
|
+
if (beforeOk && afterOk) return { start, end: start + token.length };
|
|
1855
|
+
from = start + 1;
|
|
1856
|
+
}
|
|
1857
|
+
return null;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* The editor span that removes one token without leaving a double word gap.
|
|
1862
|
+
*
|
|
1863
|
+
* Letting a token go and leaving its separating space turns "@alpha run" into " run", which is
|
|
1864
|
+
* visibly not the draft the operator typed. Only one ASCII space is eaten, and a newline is
|
|
1865
|
+
* never eaten: the plugin owns the token, not the paragraph around it.
|
|
1866
|
+
*/
|
|
1867
|
+
function targetTokenRemoval(draft, span, occurrences) {
|
|
1868
|
+
let clipboardEnd = span.end;
|
|
1869
|
+
if (draft.charAt(clipboardEnd) === ' ') clipboardEnd += 1;
|
|
1870
|
+
return {
|
|
1871
|
+
start: clipboardToDetect(span.start, occurrences),
|
|
1872
|
+
end: clipboardToDetect(clipboardEnd, occurrences),
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* Plan the one editor edit that keeps the visible token in step with a target change.
|
|
1878
|
+
*
|
|
1879
|
+
* Rules (all deliberately conservative):
|
|
1880
|
+
* · switching A → B replaces the old `@A` instead of leaving two targets in the draft;
|
|
1881
|
+
* · the new token is only inserted when the host accepted the selection;
|
|
1882
|
+
* · clearing the selection removes only the token this plugin inserted;
|
|
1883
|
+
* · an existing `@B` is never duplicated;
|
|
1884
|
+
* · a name that cannot be represented as a token is left alone rather than guessed.
|
|
1885
|
+
*
|
|
1886
|
+
* @returns {{kind: 'none'|'present'|'remove'|'replace'|'insert', text?: string,
|
|
1887
|
+
* start?: number, end?: number, reason?: string}}
|
|
1888
|
+
*/
|
|
1889
|
+
function planTargetMention(input = {}) {
|
|
1890
|
+
const draft = typeof input.draft === 'string' ? input.draft : '';
|
|
1891
|
+
const occurrences = sortedOccurrences(input.occurrences);
|
|
1892
|
+
const caret = input.caret !== null && typeof input.caret === 'object' ? input.caret : {};
|
|
1893
|
+
const nextToken = targetTokenForName(input.nextName);
|
|
1894
|
+
const previousToken = targetTokenForName(input.previousName);
|
|
1895
|
+
const previousSpan = previousToken === null ? null : targetTokenSpan(draft, previousToken);
|
|
1896
|
+
|
|
1897
|
+
if (nextToken === null) {
|
|
1898
|
+
if (previousSpan === null) return { kind: previousToken === null ? 'none' : 'present', reason: 'no-token' };
|
|
1899
|
+
return {
|
|
1900
|
+
kind: 'remove',
|
|
1901
|
+
...targetTokenRemoval(draft, previousSpan, occurrences),
|
|
1902
|
+
draftRev: Number.isInteger(caret.draftRev) ? caret.draftRev : 0,
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const nextSpan = targetTokenSpan(draft, nextToken);
|
|
1907
|
+
if (nextSpan !== null) {
|
|
1908
|
+
if (previousSpan !== null && previousToken !== nextToken) {
|
|
1909
|
+
return {
|
|
1910
|
+
kind: 'remove',
|
|
1911
|
+
...targetTokenRemoval(draft, previousSpan, occurrences),
|
|
1912
|
+
draftRev: Number.isInteger(caret.draftRev) ? caret.draftRev : 0,
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
return { kind: 'present' };
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
if (previousSpan !== null) {
|
|
1919
|
+
return {
|
|
1920
|
+
kind: 'replace',
|
|
1921
|
+
text: nextToken,
|
|
1922
|
+
start: clipboardToDetect(previousSpan.start, occurrences),
|
|
1923
|
+
end: clipboardToDetect(previousSpan.end, occurrences),
|
|
1924
|
+
draftRev: Number.isInteger(caret.draftRev) ? caret.draftRev : 0,
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
const start = Number.isInteger(caret.start) ? caret.start : 0;
|
|
1929
|
+
const end = Number.isInteger(caret.end) ? caret.end : start;
|
|
1930
|
+
// Inserting at a non-collapsed selection must not consume what the operator selected:
|
|
1931
|
+
// the selection is where their caret is, not text this control owns. Anchor at its end.
|
|
1932
|
+
const at = end;
|
|
1933
|
+
const clipboardAt = detectToClipboard(at, occurrences);
|
|
1934
|
+
const lead = clipboardAt > 0 && !/\s/.test(draft.charAt(clipboardAt - 1)) ? ' ' : '';
|
|
1935
|
+
const tail = /\s/.test(draft.charAt(clipboardAt)) ? '' : ' ';
|
|
1936
|
+
return {
|
|
1937
|
+
kind: 'insert',
|
|
1938
|
+
text: lead + nextToken + tail,
|
|
1939
|
+
start: at,
|
|
1940
|
+
end: at,
|
|
1941
|
+
draftRev: Number.isInteger(caret.draftRev) ? caret.draftRev : 0,
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
/** Put the caret back in the composer after the menu closes; best effort, never fatal. */
|
|
1946
|
+
function focusComposer() {
|
|
1947
|
+
if (typeof document === 'undefined') return;
|
|
1948
|
+
try {
|
|
1949
|
+
const node = document.querySelector('[data-composer-input]');
|
|
1950
|
+
if (node !== null && typeof node.focus === 'function') node.focus({ preventScroll: true });
|
|
1951
|
+
} catch { /* a detached or re-rendered editor is not an error */ }
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
/** A composition without the input seat keeps the control working, minus the token edit. */
|
|
1955
|
+
function useNoComposerInput() {
|
|
1956
|
+
return null;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/** Stable selector: the whole input snapshot, never a fresh derived object. */
|
|
1960
|
+
function selectComposerInput(snapshot) {
|
|
1961
|
+
return snapshot;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/**
|
|
1965
|
+
* v2.2.1: the settings-page fleet pin.
|
|
1966
|
+
*
|
|
1967
|
+
* The operator pastes an old fleet key, sees the fleet id it derives, compares that with the
|
|
1968
|
+
* expected id, and only then saves. The host route writes an independent `fleet-config.json`
|
|
1969
|
+
* with a backup; this component never edits `cordis.patch.yml` and never displays the key back.
|
|
1970
|
+
*/
|
|
1971
|
+
function FleetConfigSection() {
|
|
1972
|
+
const [state, setState] = React.useState(null);
|
|
1973
|
+
const [keyInput, setKeyInput] = React.useState('');
|
|
1974
|
+
const [expectedId, setExpectedId] = React.useState('');
|
|
1975
|
+
const [preview, setPreview] = React.useState(null);
|
|
1976
|
+
const [confirmed, setConfirmed] = React.useState(false);
|
|
1977
|
+
const [error, setError] = React.useState(null);
|
|
1978
|
+
const [busy, setBusy] = React.useState(false);
|
|
1979
|
+
const [saved, setSaved] = React.useState(null);
|
|
1980
|
+
|
|
1981
|
+
const call = async (init) => {
|
|
1982
|
+
try {
|
|
1983
|
+
const response = await fetch('/api/w2m/fleet-config', {
|
|
1984
|
+
credentials: 'same-origin',
|
|
1985
|
+
headers: { accept: 'application/json', ...(init?.body === undefined ? {} : { 'content-type': 'application/json' }) },
|
|
1986
|
+
...init,
|
|
1987
|
+
});
|
|
1988
|
+
let body = null;
|
|
1989
|
+
try { body = await response.json(); } catch { body = null; }
|
|
1990
|
+
if (response.status === 404) return { ok: false, routeMissing: true, error: 'this host does not serve the fleet-config route' };
|
|
1991
|
+
if (response.ok !== true || body?.ok === false) {
|
|
1992
|
+
return { ok: false, error: body?.error ?? body?.code ?? `HTTP ${response.status}`, body };
|
|
1993
|
+
}
|
|
1994
|
+
return { ok: true, body };
|
|
1995
|
+
} catch (err) {
|
|
1996
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
const load = React.useCallback(async () => {
|
|
2001
|
+
const result = await call({ method: 'GET' });
|
|
2002
|
+
if (result.ok) setState(result.body);
|
|
2003
|
+
else setError(result.routeMissing ? '当前 DSH 没有编队绑定路由(插件版本过旧或未挂载)' : (result.error ?? '读取失败'));
|
|
2004
|
+
}, []);
|
|
2005
|
+
React.useEffect(() => { load(); }, [load]);
|
|
2006
|
+
|
|
2007
|
+
const previewKey = async () => {
|
|
2008
|
+
const key = keyInput.trim();
|
|
2009
|
+
if (key === '') return;
|
|
2010
|
+
setBusy(true);
|
|
2011
|
+
setError(null);
|
|
2012
|
+
setSaved(null);
|
|
2013
|
+
setConfirmed(false);
|
|
2014
|
+
try {
|
|
2015
|
+
const result = await call({ method: 'POST', body: JSON.stringify({ action: 'preview', fleet_key: key, expected_fleet_id: expectedId.trim() }) });
|
|
2016
|
+
if (!result.ok) { setPreview(null); setError(result.error ?? '无法计算 fleet_id'); return; }
|
|
2017
|
+
setPreview(result.body);
|
|
2018
|
+
} finally { setBusy(false); }
|
|
2019
|
+
};
|
|
2020
|
+
|
|
2021
|
+
const save = async () => {
|
|
2022
|
+
const key = keyInput.trim();
|
|
2023
|
+
if (key === '' || preview === null || confirmed !== true) return;
|
|
2024
|
+
setBusy(true);
|
|
2025
|
+
setError(null);
|
|
2026
|
+
try {
|
|
2027
|
+
const result = await call({
|
|
2028
|
+
method: 'POST',
|
|
2029
|
+
body: JSON.stringify({ action: 'save', fleet_key: key, expected_fleet_id: preview.fleet_id }),
|
|
2030
|
+
});
|
|
2031
|
+
if (!result.ok) { setError(result.error ?? '保存失败'); return; }
|
|
2032
|
+
setSaved(result.body);
|
|
2033
|
+
setKeyInput('');
|
|
2034
|
+
setPreview(null);
|
|
2035
|
+
setConfirmed(false);
|
|
2036
|
+
await load();
|
|
2037
|
+
} finally { setBusy(false); }
|
|
2038
|
+
};
|
|
2039
|
+
|
|
2040
|
+
const expectedMismatch = preview !== null && expectedId.trim() !== ''
|
|
2041
|
+
&& String(preview.fleet_id).toLowerCase() !== expectedId.trim().toLowerCase();
|
|
2042
|
+
const mono = { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', wordBreak: 'break-all' };
|
|
2043
|
+
return h('div', { className: 'w2mGpw_admin' }, [
|
|
2044
|
+
h('div', { key: 'head', className: 'w2mGpw_adminHead' }, [
|
|
2045
|
+
h('h1', { key: 'h', className: 'w2mGpw_adminTitle' }, '编队绑定'),
|
|
2046
|
+
h('div', { key: 'sub', className: 'w2mGpw_adminSub' },
|
|
2047
|
+
'把本机固定到指定编队。保存到独立的 fleet-config.json(0600),不改 cordis.patch.yml;保存后重启 DSH 才生效。'),
|
|
2048
|
+
]),
|
|
2049
|
+
error === null ? null : h('div', { key: 'err', className: 'w2mGpw_adminNote' }, error),
|
|
2050
|
+
h('div', { key: 'current', className: 'w2mGpw_adminCard' }, [
|
|
2051
|
+
h('div', { key: 's', className: 'w2mGpw_adminSub' }, state === null
|
|
2052
|
+
? '读取中…'
|
|
2053
|
+
: state.configured === true
|
|
2054
|
+
? `已配置 · policy ${String(state.policy ?? '?')} · 指纹 ${String(state.fingerprint ?? '').slice(0, 12)}`
|
|
2055
|
+
: '未配置:当前按 fleetKeyPolicy 的默认策略绑定'),
|
|
2056
|
+
state?.fleet_id ? h('div', { key: 'id', style: mono }, 'fleet_id: ' + state.fleet_id) : null,
|
|
2057
|
+
state?.path ? h('div', { key: 'p', className: 'w2mGpw_adminSub' }, '文件:' + state.path) : null,
|
|
2058
|
+
]),
|
|
2059
|
+
h('div', { key: 'form', className: 'w2mGpw_adminCard' }, [
|
|
2060
|
+
h('div', { key: 'row', className: 'w2mGpw_adminToolbar' }, [
|
|
2061
|
+
h('input', {
|
|
2062
|
+
key: 'k',
|
|
2063
|
+
className: 'w2mGpw_adminInput',
|
|
2064
|
+
type: 'password',
|
|
2065
|
+
value: keyInput,
|
|
2066
|
+
placeholder: '旧 fleet key(保存后只显示指纹,不回显)',
|
|
2067
|
+
onChange: (event) => { setKeyInput(event.target.value); setPreview(null); setConfirmed(false); },
|
|
2068
|
+
}),
|
|
2069
|
+
h('button', { key: 'c', type: 'button', className: 'w2mGpw_adminBtn', disabled: busy || keyInput.trim() === '', onClick: previewKey }, '计算 fleet_id'),
|
|
2070
|
+
]),
|
|
2071
|
+
h('div', { key: 'row2', className: 'w2mGpw_adminToolbar', style: { marginTop: '8px' } }, [
|
|
2072
|
+
h('input', {
|
|
2073
|
+
key: 'e',
|
|
2074
|
+
className: 'w2mGpw_adminInput',
|
|
2075
|
+
value: expectedId,
|
|
2076
|
+
placeholder: '期望的 fleet_id(可选,例如 004945ed…)',
|
|
2077
|
+
onChange: (event) => { setExpectedId(event.target.value); setConfirmed(false); },
|
|
2078
|
+
}),
|
|
2079
|
+
]),
|
|
2080
|
+
preview === null ? null : h('div', { key: 'preview', style: { marginTop: '12px' } }, [
|
|
2081
|
+
h('div', { key: 'l', className: 'w2mGpw_adminSub' }, '这个 key 算出的 fleet_id:'),
|
|
2082
|
+
h('div', { key: 'id', style: { ...mono, margin: '4px 0 8px' } }, String(preview.fleet_id)),
|
|
2083
|
+
expectedMismatch
|
|
2084
|
+
? h('div', { key: 'bad', className: 'w2mGpw_adminNote' }, '与期望值不一致,不能保存。请核对 key 或期望值。')
|
|
2085
|
+
: h('label', { key: 'ok', className: 'w2mGpw_adminSub', style: { display: 'flex', gap: '8px', alignItems: 'center' } }, [
|
|
2086
|
+
h('input', {
|
|
2087
|
+
key: 'c',
|
|
2088
|
+
type: 'checkbox',
|
|
2089
|
+
checked: confirmed,
|
|
2090
|
+
disabled: expectedMismatch,
|
|
2091
|
+
onChange: (event) => setConfirmed(event.target.checked),
|
|
2092
|
+
}),
|
|
2093
|
+
h('span', { key: 't' }, '我确认这是目标编队,保存并固定'),
|
|
2094
|
+
]),
|
|
2095
|
+
h('button', {
|
|
2096
|
+
key: 'save',
|
|
2097
|
+
type: 'button',
|
|
2098
|
+
className: 'w2mGpw_adminBtn',
|
|
2099
|
+
style: { marginTop: '8px' },
|
|
2100
|
+
disabled: busy || confirmed !== true || expectedMismatch,
|
|
2101
|
+
onClick: save,
|
|
2102
|
+
}, '保存并固定到该编队'),
|
|
2103
|
+
]),
|
|
2104
|
+
saved === null ? null : h('div', { key: 'saved', className: 'w2mGpw_adminNote' }, [
|
|
2105
|
+
h('div', { key: 'a' }, '已保存。重启 DSH 后生效;本页不会自动重启。'),
|
|
2106
|
+
h('div', { key: 'b', style: mono }, 'fleet_id: ' + String(saved.fleet_id ?? '')),
|
|
2107
|
+
saved.backup ? h('div', { key: 'c' }, '备份:' + saved.backup) : null,
|
|
2108
|
+
]),
|
|
2109
|
+
]),
|
|
2110
|
+
]);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
function TargetSelect({ sessionId, locked: _locked, t, inputActions, useInput }) {
|
|
1752
2114
|
const [open, setOpen] = React.useState(false);
|
|
1753
2115
|
// v1.5.1: relay folding. The snapshot carries the pool; when it holds more than three nodes
|
|
1754
2116
|
// the menu shows a summary and this toggle reveals the folded ones.
|
|
@@ -1756,11 +2118,29 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
|
|
|
1756
2118
|
const [reloadSeq, setReloadSeq] = React.useState(0);
|
|
1757
2119
|
const [state, setState] = React.useState(null);
|
|
1758
2120
|
const [error, setError] = React.useState(null);
|
|
2121
|
+
// A write/edit refusal is not a fleet-read failure: the menu is closed when it happens, so
|
|
2122
|
+
// keeping it beside `error` (which a successful read clears) is what makes it survive the
|
|
2123
|
+
// reopen that shows it to the operator.
|
|
2124
|
+
const [notice, setNotice] = React.useState(null);
|
|
1759
2125
|
const [busy, setBusy] = React.useState(false);
|
|
1760
2126
|
const anchorRef = React.useRef(null);
|
|
1761
2127
|
const menuRef = React.useRef(null);
|
|
1762
2128
|
const [placement, setPlacement] = React.useState(null);
|
|
1763
2129
|
|
|
2130
|
+
// v2.3.0: the target control also writes a visible `@machine` token. `inputActions` and
|
|
2131
|
+
// `useInput` are standard session props (dsh-client-ui-conversation provides them to every
|
|
2132
|
+
// session slot); a host that does not carry them still gets the selector, just no token.
|
|
2133
|
+
const useComposerInput = typeof useInput === 'function' ? useInput : useNoComposerInput;
|
|
2134
|
+
const input = useComposerInput(selectComposerInput);
|
|
2135
|
+
const inputRef = React.useRef(input);
|
|
2136
|
+
inputRef.current = input;
|
|
2137
|
+
// Session switches are per mount in the shell, but a write can still land after the prop
|
|
2138
|
+
// changed under a reused component. The ref reads the current value at resolve time; the
|
|
2139
|
+
// sequence makes the last click win when two writes are in flight.
|
|
2140
|
+
const sessionRef = React.useRef(sessionId);
|
|
2141
|
+
sessionRef.current = sessionId;
|
|
2142
|
+
const writeSeq = React.useRef(0);
|
|
2143
|
+
|
|
1764
2144
|
// The host half is the source of truth, so every read replaces the mirror rather than merging
|
|
1765
2145
|
// into it: a stale "online" must not outlive the answer that disproved it.
|
|
1766
2146
|
React.useEffect(() => {
|
|
@@ -1832,7 +2212,7 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
|
|
|
1832
2212
|
const machines = Array.isArray(state?.machines) ? state.machines : [];
|
|
1833
2213
|
// v1.5.1: the relay pool the host probed, already folded. `null` means a single-relay host.
|
|
1834
2214
|
const relayFold = state?.relays ?? null;
|
|
1835
|
-
const problem = error ?? fleetProblem(state, t);
|
|
2215
|
+
const problem = notice ?? error ?? fleetProblem(state, t);
|
|
1836
2216
|
const disabled = typeof sessionId !== 'string' || sessionId === '';
|
|
1837
2217
|
const label = state === null ? (provisionalLabel(state) ?? t('machine')) : labelOf(state, t);
|
|
1838
2218
|
const selectedMachine = machines.find((machine) => machine.machine_id === chosen) ?? null;
|
|
@@ -1846,26 +2226,97 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
|
|
|
1846
2226
|
? t('costAll')
|
|
1847
2227
|
: `${t('costOne', { name: label })}${selectedOnline === true ? '' : t('unconfirmed')}`);
|
|
1848
2228
|
|
|
2229
|
+
/**
|
|
2230
|
+
* Apply the one editor edit that mirrors a target change.
|
|
2231
|
+
*
|
|
2232
|
+
* Ordering matters: the host write happens first, so a token is never left claiming a target
|
|
2233
|
+
* the host refused. The edit is best effort — a composer that is submitting, or a host
|
|
2234
|
+
* without `inputActions`, keeps the selection and simply loses the visible handle.
|
|
2235
|
+
*/
|
|
2236
|
+
const applyTargetMention = (nextName, previousName) => {
|
|
2237
|
+
if (inputActions === null || typeof inputActions !== 'object'
|
|
2238
|
+
|| typeof inputActions.insertText !== 'function'
|
|
2239
|
+
|| typeof inputActions.captureInsertion !== 'function') {
|
|
2240
|
+
focusComposer();
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
const capture = inputActions.captureInsertion();
|
|
2244
|
+
if (capture === null || typeof capture !== 'object') {
|
|
2245
|
+
focusComposer();
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
const current = inputRef.current ?? {};
|
|
2249
|
+
// `captureInsertion()` is the editor's current revision; `inputRef.current` is the last
|
|
2250
|
+
// published snapshot. If they disagree, the draft these offsets were computed from is not
|
|
2251
|
+
// the draft the editor holds, and `draftRev` would let a stale offset through.
|
|
2252
|
+
if (Number.isInteger(current.draftRev) && current.draftRev !== capture.draftRev) {
|
|
2253
|
+
focusComposer();
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
const plan = planTargetMention({
|
|
2257
|
+
draft: typeof current.draft === 'string' ? current.draft : '',
|
|
2258
|
+
occurrences: current.occurrences,
|
|
2259
|
+
caret: capture,
|
|
2260
|
+
nextName,
|
|
2261
|
+
previousName,
|
|
2262
|
+
});
|
|
2263
|
+
if (plan.kind === 'insert' || plan.kind === 'replace' || plan.kind === 'remove') {
|
|
2264
|
+
const text = plan.kind === 'remove' ? '' : plan.text;
|
|
2265
|
+
const applied = inputActions.insertText(text, {
|
|
2266
|
+
start: plan.start,
|
|
2267
|
+
end: plan.end,
|
|
2268
|
+
draftRev: capture.draftRev,
|
|
2269
|
+
});
|
|
2270
|
+
if (applied !== true) {
|
|
2271
|
+
setNotice(t('mentionFailed'));
|
|
2272
|
+
setOpen(true);
|
|
2273
|
+
} else {
|
|
2274
|
+
setNotice(null);
|
|
2275
|
+
}
|
|
2276
|
+
} else {
|
|
2277
|
+
setNotice(null);
|
|
2278
|
+
}
|
|
2279
|
+
focusComposer();
|
|
2280
|
+
};
|
|
2281
|
+
|
|
1849
2282
|
const choose = (machineId, machineName) => {
|
|
1850
2283
|
setOpen(false);
|
|
1851
|
-
|
|
2284
|
+
const previousName = selectedMachine?.name ?? state?.selected?.name ?? null;
|
|
2285
|
+
if (machineId === chosen) {
|
|
2286
|
+
// Clicking the current target restores a token the operator deleted, but never turns
|
|
2287
|
+
// the click into a destructive "clear" gesture: clearing is the All-machines row.
|
|
2288
|
+
setNotice(null);
|
|
2289
|
+
applyTargetMention(machineId === null ? null : previousName, null);
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
const seq = (writeSeq.current += 1);
|
|
2293
|
+
const at = sessionId;
|
|
1852
2294
|
setBusy(true);
|
|
1853
2295
|
writeSelection(sessionId, machineId, machineName).then((result) => {
|
|
2296
|
+
if (seq !== writeSeq.current) return; // a later click owns the answer
|
|
2297
|
+
if (at !== sessionRef.current) return; // the session changed while the write was in flight
|
|
1854
2298
|
setBusy(false);
|
|
1855
2299
|
if (result.ok) {
|
|
1856
2300
|
setState(result.state);
|
|
1857
2301
|
setError(null);
|
|
2302
|
+
setNotice(null);
|
|
2303
|
+
if (machineId === null) {
|
|
2304
|
+
applyTargetMention(null, previousName);
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
applyTargetMention(result.state?.selected?.name ?? machineName ?? null, previousName);
|
|
1858
2308
|
return;
|
|
1859
2309
|
}
|
|
1860
2310
|
// The write is refused for a reason the operator can act on (the machine went away, the
|
|
1861
|
-
// fleet could not be read)
|
|
1862
|
-
|
|
1863
|
-
|
|
2311
|
+
// fleet could not be read). Reopen the menu so `notice` is actually in front of them.
|
|
2312
|
+
setNotice(result.error);
|
|
2313
|
+
setOpen(true);
|
|
2314
|
+
setReloadSeq((reload) => reload + 1);
|
|
1864
2315
|
});
|
|
1865
2316
|
};
|
|
1866
2317
|
|
|
1867
2318
|
const menu = open && placement !== null
|
|
1868
|
-
? h('div', {
|
|
2319
|
+
? portalNode(h('div', {
|
|
1869
2320
|
className: cls('menu'),
|
|
1870
2321
|
role: 'listbox',
|
|
1871
2322
|
'aria-label': t('targetLabel', { name: label }),
|
|
@@ -1961,7 +2412,7 @@ function TargetSelect({ sessionId, locked: _locked, t }) {
|
|
|
1961
2412
|
? h('div', { key: 'hint', className: cls('relayHint') }, t('relayFold', { count: relayFold.hidden_count }))
|
|
1962
2413
|
: null,
|
|
1963
2414
|
]),
|
|
1964
|
-
])
|
|
2415
|
+
]))
|
|
1965
2416
|
: null;
|
|
1966
2417
|
|
|
1967
2418
|
return h(React.Fragment, null, [
|
|
@@ -2686,7 +3137,7 @@ const CHAT_EMOJIS = CHAT_COMMANDS;
|
|
|
2686
3137
|
};
|
|
2687
3138
|
|
|
2688
3139
|
const menu = open && placement !== null
|
|
2689
|
-
? h('div', {
|
|
3140
|
+
? portalNode(h('div', {
|
|
2690
3141
|
className: 'w2mCap_menu',
|
|
2691
3142
|
role: 'menu',
|
|
2692
3143
|
'aria-label': t('capMenuLabel'),
|
|
@@ -2698,7 +3149,7 @@ const CHAT_EMOJIS = CHAT_COMMANDS;
|
|
|
2698
3149
|
h('button', { key: 'temp', type: 'button', className: 'w2mCap_option', 'data-active': capability?.state === 'temporary' ? 'true' : 'false', onClick: () => choose('temporary', 24 * 60 * 60 * 1000) }, t('capTemporary')),
|
|
2699
3150
|
h('button', { key: 'perm', type: 'button', className: 'w2mCap_option', 'data-active': capability?.state === 'permanent' ? 'true' : 'false', onClick: () => choose('permanent') }, t('capPermanent')),
|
|
2700
3151
|
error === null ? null : h('div', { key: 'err', className: 'w2mCap_note', role: 'status' }, error),
|
|
2701
|
-
])
|
|
3152
|
+
]))
|
|
2702
3153
|
: null;
|
|
2703
3154
|
|
|
2704
3155
|
return h(React.Fragment, null, [
|
|
@@ -2848,6 +3299,20 @@ const CHAT_EMOJIS = CHAT_COMMANDS;
|
|
|
2848
3299
|
} catch (error) {
|
|
2849
3300
|
console.error('@twinsearth/w2m-dsh-plugin: the GPW settings tab was not mounted', error);
|
|
2850
3301
|
}
|
|
3302
|
+
// v2.2.1: the supported alternative to editing cordis.patch.yml. The section is its own
|
|
3303
|
+
// settings card so an operator can find "where do I put the old fleet key?" without
|
|
3304
|
+
// touching a profile file.
|
|
3305
|
+
try {
|
|
3306
|
+
slots.inject('settings.section', () => slots.register({
|
|
3307
|
+
name: 'settings.section',
|
|
3308
|
+
id: 'w2m-fleet',
|
|
3309
|
+
order: 11,
|
|
3310
|
+
label: () => '编队绑定',
|
|
3311
|
+
inject: () => ({ t }),
|
|
3312
|
+
}, FleetConfigSection));
|
|
3313
|
+
} catch (error) {
|
|
3314
|
+
console.error('@twinsearth/w2m-dsh-plugin: the fleet-binding settings section was not mounted', error);
|
|
3315
|
+
}
|
|
2851
3316
|
// v1.9.4: the update controls belong on the plugin's own page (same page as Uninstall),
|
|
2852
3317
|
// not only in the global settings list.
|
|
2853
3318
|
try {
|
|
@@ -3008,11 +3473,14 @@ const CHAT_EMOJIS = CHAT_COMMANDS;
|
|
|
3008
3473
|
exports.apply = apply;
|
|
3009
3474
|
exports.inject = inject;
|
|
3010
3475
|
exports.TargetSelect = TargetSelect;
|
|
3476
|
+
exports.targetTokenForName = targetTokenForName;
|
|
3477
|
+
exports.planTargetMention = planTargetMention;
|
|
3011
3478
|
exports.GpwLauncher = GpwLauncher;
|
|
3012
3479
|
exports.GpwPrompt = GpwPrompt;
|
|
3013
3480
|
exports.FleetChat = FleetChat;
|
|
3014
3481
|
exports.injectStyle = injectStyle;
|
|
3015
3482
|
exports.makeTranslator = makeTranslator;
|
|
3483
|
+
exports.portalNode = portalNode;
|
|
3016
3484
|
return module.exports;
|
|
3017
3485
|
},
|
|
3018
3486
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.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
6
|
"exports": {
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"pack": "node scripts/pack.mjs",
|
|
72
72
|
"reachability": "node scripts/reachability-check.mjs",
|
|
73
73
|
"reachability:classify": "node scripts/reachability-classify.mjs",
|
|
74
|
-
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs test/joint-r5-wiring.test.mjs test/p1-hardening.test.mjs test/joint-merge-engine.test.mjs test/artifact-ref-dir-sink.test.mjs test/chat-sticker-guard.test.mjs test/relay-task-progress.test.mjs test/gpw-modes.test.mjs test/joint-cli.test.mjs test/joint-command-hash.test.mjs test/joint-dispatch-audit.test.mjs test/tools-cancel-auth.test.mjs test/fleet-mirror.test.mjs test/sink-git-ssh.test.mjs test/agent-sse-watchdog.test.mjs test/version-gate.test.mjs test/joint-merge-cli.test.mjs test/joint-progress.test.mjs test/project-room-truncation.test.mjs test/chat-crypto-aad.test.mjs test/agent-artifact-delivery.test.mjs test/dataplane-sink-route.test.mjs test/artifact-sink-e2e.test.mjs test/project-room-persistence.test.mjs test/project-room-disk-persistence.test.mjs test/project-stream-route.test.mjs test/chat-v2-gate.test.mjs test/xiangqi.test.mjs test/xfer-foundation.test.mjs test/xfer-ferry.test.mjs test/xfer-publish.test.mjs test/xfer-want-block.test.mjs test/p2p-cipher-diag.test.mjs test/chacha-fallback.test.mjs test/fleet-binding.test.mjs test/xfer-channel.test.mjs test/p2p-race.test.mjs test/path-scheduler.test.mjs test/transport.test.mjs test/iroh-sidecar.test.mjs test/discovery.test.mjs test/p2p-rtt.test.mjs test/artifact-ref-v3.test.mjs test/xfer-recovery.test.mjs test/xfer-router.test.mjs test/remote-s2-capture.test.mjs test/view-capture.test.mjs test/view-frame-channel.test.mjs test/remote-view-route.test.mjs test/remote-media.test.mjs test/view-session-key.test.mjs test/remote-requests.test.mjs test/relay-target.test.mjs",
|
|
74
|
+
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs test/joint-r5-wiring.test.mjs test/p1-hardening.test.mjs test/joint-merge-engine.test.mjs test/artifact-ref-dir-sink.test.mjs test/chat-sticker-guard.test.mjs test/relay-task-progress.test.mjs test/gpw-modes.test.mjs test/joint-cli.test.mjs test/joint-command-hash.test.mjs test/joint-dispatch-audit.test.mjs test/tools-cancel-auth.test.mjs test/fleet-mirror.test.mjs test/sink-git-ssh.test.mjs test/agent-sse-watchdog.test.mjs test/version-gate.test.mjs test/joint-merge-cli.test.mjs test/joint-progress.test.mjs test/project-room-truncation.test.mjs test/chat-crypto-aad.test.mjs test/agent-artifact-delivery.test.mjs test/dataplane-sink-route.test.mjs test/artifact-sink-e2e.test.mjs test/project-room-persistence.test.mjs test/project-room-disk-persistence.test.mjs test/project-stream-route.test.mjs test/chat-v2-gate.test.mjs test/xiangqi.test.mjs test/xfer-foundation.test.mjs test/xfer-ferry.test.mjs test/xfer-publish.test.mjs test/xfer-want-block.test.mjs test/p2p-cipher-diag.test.mjs test/chacha-fallback.test.mjs test/fleet-binding.test.mjs test/fleet-home.test.mjs test/fleet-config.test.mjs test/fleet-config-route.test.mjs test/xfer-channel.test.mjs test/p2p-race.test.mjs test/path-scheduler.test.mjs test/transport.test.mjs test/iroh-sidecar.test.mjs test/discovery.test.mjs test/p2p-rtt.test.mjs test/artifact-ref-v3.test.mjs test/xfer-recovery.test.mjs test/xfer-router.test.mjs test/remote-s2-capture.test.mjs test/view-capture.test.mjs test/view-frame-channel.test.mjs test/remote-view-route.test.mjs test/remote-media.test.mjs test/view-session-key.test.mjs test/remote-requests.test.mjs test/relay-target.test.mjs",
|
|
75
75
|
"test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
|
|
76
76
|
"test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
|
|
77
77
|
"test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "twinsearth-w2m-dsh",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.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": [
|