@deepseek-insight/dsi 0.10.0 → 0.10.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/.agents/skills/glass-dsi/scripts/_connect.mjs +68 -9
- package/.agents/skills/glass-dsi/scripts/glass.mjs +6 -5
- package/.agents/skills/glass-url/scripts/_connect.mjs +59 -10
- package/.agents/skills/glass-url/scripts/glass-url.mjs +5 -4
- package/README.md +44 -15
- package/bin/dsi.mjs +44 -7
- package/bin/lib/dev-mode.mjs +8 -5
- package/bin/lib/glass-browser-bootstrap.mjs +132 -0
- package/bin/lib/mcp-banner.mjs +26 -0
- package/bin/lib/zai-patch.mjs +3 -1
- package/build/client/_app/immutable/assets/0.B2rul4Mk.css +2 -0
- package/build/client/_app/immutable/assets/2.glyfXGzi.css +1 -0
- package/build/client/_app/immutable/chunks/{Bu2owQS4.js → BodLd2is.js} +1 -1
- package/build/client/_app/immutable/chunks/Cy4hfhN-.js +1 -0
- package/build/client/_app/immutable/chunks/D17XAauH.js +2 -0
- package/build/client/_app/immutable/chunks/FY34R_78.js +1 -0
- package/build/client/_app/immutable/entry/{app.DjLAWmJH.js → app.ClaEF2lH.js} +2 -2
- package/build/client/_app/immutable/entry/start.CU6iOT39.js +1 -0
- package/build/client/_app/immutable/nodes/{1.w6Hz6-dJ.js → 1.CSTMQTZ7.js} +1 -1
- package/build/client/_app/immutable/nodes/{2.CFmqEODJ.js → 2.CdbJFHUF.js} +197 -199
- package/build/client/_app/version.json +1 -1
- package/build/handler.js +5 -5
- package/build/index.js +5 -5
- package/build/server/chunks/chunks/{db.js-f2PmBjyD.js → db.js-BGSxIVb3.js} +2 -2
- package/build/server/chunks/chunks/{db2.js-DkwY64WO.js → db2.js-vQNFcVO7.js} +2 -2
- package/build/server/chunks/chunks/{dsh-connection.js-BW07Aicl.js → dsh-connection.js-Dcdxqp52.js} +27 -7
- package/build/server/chunks/chunks/{exports.js-DH_tjtdv.js → exports.js-D5zA1U7A.js} +42 -4
- package/build/server/chunks/chunks/{git-probe.js-CQqTV7XX.js → git-probe.js-BIieYqLU.js} +2 -2
- package/build/server/chunks/chunks/{insight-config.js-C5-Rhh5A.js → insight-config.js-ylYA2CXm.js} +2 -2
- package/build/server/chunks/chunks/{internal2.js-DRFhzPda.js → internal2.js-XxZ8wzRE.js} +3 -3
- package/build/server/chunks/chunks/{registry.js-CWnRQ4vx.js → registry.js-ClhrzR6O.js} +145 -56
- package/build/server/chunks/chunks/{registry2.js-CFzpvj9c.js → registry2.js-BZfrwsOy.js} +20 -1
- package/build/server/chunks/chunks/{settings-document.js-DxKURfc_.js → settings-document.js-BIxaivJ1.js} +23 -8
- package/build/server/chunks/chunks/{state.js-kZHonU4N.js → state.js-D5zsYmjz.js} +3 -3
- package/build/server/chunks/chunks/{watcher.js-DvlNn8X0.js → watcher.js-Bcm29AuA.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/a2a/_id_/{_server.ts.js-DItDXV23.js → _server.ts.js-BbIEpZB5.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/a2a/{_server.ts.js-Hor8QZb1.js → _server.ts.js-Daho1I-l.js} +6 -6
- package/build/server/chunks/entries/endpoints/api/a2a/register/{_server.ts.js-CEvvsa8B.js → _server.ts.js-C5X77Uef.js} +6 -6
- package/build/server/chunks/entries/endpoints/api/browser/_id_/activate/{_server.ts.js-sXqf-okw.js → _server.ts.js-B-Ns5pyR.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/_id_/close/{_server.ts.js-Byj9ApJh.js → _server.ts.js-CzXe-sTr.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/_id_/closetab/{_server.ts.js-DOIsrzvg.js → _server.ts.js-Ct465_rt.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/_id_/element/{_server.ts.js-Cq3gmRfE.js → _server.ts.js-6mdytkvM.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/_id_/history/{_server.ts.js-BGSLIV0t.js → _server.ts.js-BwUPVQGx.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/_id_/input/{_server.ts.js-8slca75-.js → _server.ts.js-8Nq3JxSo.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/_id_/mcp/{_server.ts.js-CpbdTYds.js → _server.ts.js-B9eKtj1-.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/_id_/navigate/{_server.ts.js-Bls9LtLY.js → _server.ts.js-CHJs9Qbl.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/_id_/opentab/{_server.ts.js-DqNi_iW_.js → _server.ts.js-B32Ow0Y2.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/_id_/page/{_server.ts.js-DuXWwo7i.js → _server.ts.js-Cpv4BolN.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/_id_/stream/{_server.ts.js-DXfqBDEw.js → _server.ts.js-DJqJImSI.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/_id_/tabs/{_server.ts.js-Bgnl8lYs.js → _server.ts.js-Bm6IAZF6.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/_id_/viewport/{_server.ts.js-FHmQVbrv.js → _server.ts.js-B1VVzfCk.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/_id_/zoom/{_server.ts.js-BV4cBtSA.js → _server.ts.js-CoUpqHeq.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/browser/{_server.ts.js-4vgkwh7v.js → _server.ts.js-D9hK0CG8.js} +8 -5
- package/build/server/chunks/entries/endpoints/api/config/{_server.ts.js-C_9kwANR.js → _server.ts.js-DqPIv-gn.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/directory/{_server.ts.js-h1OTHcL4.js → _server.ts.js-DzFVVhO_.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/jobs/{_server.ts.js-B6nih9Sd.js → _server.ts.js-DvuZUgbK.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/jobs/follow/{_server.ts.js-Cf17SVmD.js → _server.ts.js-D6brRlYZ.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/pick-directory/{_server.ts.js-DjNwMbdS.js → _server.ts.js-8nNlcikK.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/presets/{_server.ts.js-Cz5aaEhC.js → _server.ts.js-C8AZBt63.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/attachment/{_server.ts.js-BFBy5O9Y.js → _server.ts.js-BFfr6uJh.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/cancel/{_server.ts.js-CgEdCo3T.js → _server.ts.js-DSSgbD87.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/catalog/{_server.ts.js-Bo_aEnHh.js → _server.ts.js-DYtrYC2P.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/changes/{_server.ts.js-Bd2G_GaH.js → _server.ts.js-HxI5M4jg.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/command/{_server.ts.js-DL64FyRw.js → _server.ts.js-Ch0dMm33.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/events/{_server.ts.js-MhOGB66D.js → _server.ts.js-BG5C--Ok.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/fork/{_server.ts.js-MjPgXgGO.js → _server.ts.js-B64YyaFI.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/goal/{_server.ts.js-BUjDae5H.js → _server.ts.js-0RtL-9Zr.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/history/{_server.ts.js-dTKd7Hsv.js → _server.ts.js-C4k-RTIw.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/models/{_server.ts.js-B82rvKoI.js → _server.ts.js-CU38DxMt.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/path/{_server.ts.js-Cqijsi-c.js → _server.ts.js-BXphf5ux.js} +3 -3
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/permission/{_server.ts.js-CL_vVv2p.js → _server.ts.js-DQ5FG9Pc.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/prompt/{_server.ts.js-WS4W9Ly4.js → _server.ts.js-In3bXhUO.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/rename/{_server.ts.js-B72KGRGh.js → _server.ts.js-Bd0KF8Qm.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/respond/{_server.ts.js-DK5xb2b5.js → _server.ts.js-4Y-KAw2r.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/select-model/{_server.ts.js-lhT87EQu.js → _server.ts.js-zXYA1AJD.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/sessions/{_server.ts.js-CMhfhbkN.js → _server.ts.js-vh7lFV0n.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/subagents/{_server.ts.js-BLvcFsg6.js → _server.ts.js-DH7OEWm0.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/workspace-file/{_server.ts.js-D_rw2Uq6.js → _server.ts.js-CDwgBp7D.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/workspace-tree/{_server.ts.js-DfXpMMpw.js → _server.ts.js-CtFxC_yV.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/workspaces/{_server.ts.js-2mJCJVxk.js → _server.ts.js-DRNzGdAj.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/workspaces/_workspaceId_/delete/{_server.ts.js-BCx0Lzjf.js → _server.ts.js-DIjNfN0d.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/dsh/workspaces/_workspaceId_/rename/{_server.ts.js-DECaSjwp.js → _server.ts.js-CtV7JhrK.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/prompts/_id_/{_server.ts.js-i_ciJzlb.js → _server.ts.js-Cg9zHbUP.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/prompts/{_server.ts.js-BO1pScn7.js → _server.ts.js-DOHmlKvl.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/prompts/use/{_server.ts.js-Buhpf7WE.js → _server.ts.js-COVyy224.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/settings/{_server.ts.js-DngUQzcT.js → _server.ts.js-HjpfRAaT.js} +3 -3
- package/build/server/chunks/entries/endpoints/api/terminal/_id_/delete/_server.ts.js-D9UU6xR6.js +42 -0
- package/build/server/chunks/entries/endpoints/api/terminal/_id_/output/{_server.ts.js-DLETP4yQ.js → _server.ts.js-BqJRMRsb.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/terminal/_id_/reattach/{_server.ts.js-uWl6oxPS.js → _server.ts.js-D6bMXeOk.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/terminal/_id_/send/{_server.ts.js-DYNmXaAd.js → _server.ts.js-BRgsqPFM.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/terminal/_id_/stream/{_server.ts.js-C5m3Ktho.js → _server.ts.js-EWo4sSmg.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/terminal/{_server.ts.js-DoTNvcOa.js → _server.ts.js-v1ixRdpN.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/terminal/stream/_server.ts.js-BQOQyT7o.js +185 -0
- package/build/server/chunks/entries/endpoints/api/workspace/file-write/{_server.ts.js-BzYOJSkI.js → _server.ts.js-CjU6bLOf.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/workspace/git-events/{_server.ts.js-xcNesei9.js → _server.ts.js-CYU0btiH.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/workspace/git-file-head/{_server.ts.js-DsJWYdxt.js → _server.ts.js-Du9YcwLw.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/workspace/git-map/{_server.ts.js-CNCku9BR.js → _server.ts.js-BdV_KDjb.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/workspace/git-status/{_server.ts.js-nLsNjOxj.js → _server.ts.js-C-C84Yzu.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/workspace/write-gate/{_server.ts.js-urQk2-ds.js → _server.ts.js-D9uFvZya.js} +5 -5
- package/build/server/chunks/entries/{hooks.server.js-CU2RE5vs.js → hooks.server.js-YJQlamhk.js} +2 -2
- package/build/server/chunks/entries/pages/{_error.svelte.js-BKhpJygh.js → _error.svelte.js-TrOSxb0e.js} +4 -4
- package/build/server/chunks/entries/pages/{_page.server.ts.js-BK3Y6eff.js → _page.server.ts.js-BFDPRDEm.js} +2 -2
- package/build/server/chunks/entries/pages/{_page.svelte.js-BlbGdGej.js → _page.svelte.js-BUuXoaje.js} +706 -158
- package/build/server/chunks/{handler-BtG6aVza.js → handler-BOqY82LA.js} +3 -3
- package/build/server/chunks/{index.js-rGfUdWOY.js → index.js-M1GginY2.js} +3 -44
- package/build/server/chunks/{manifest.js-DwQ7BEt1.js → manifest.js-BEmwisii.js} +81 -67
- package/build/server/chunks/nodes/{0.js-Cum-Y-us.js → 0.js-Do4ZKaKe.js} +3 -3
- package/build/server/chunks/nodes/1.js-CRbdbfof.js +9 -0
- package/build/server/chunks/nodes/{2.js-ButkUErW.js → 2.js-BFX_zPfJ.js} +8 -8
- package/package.json +1 -1
- package/skills/dsi-drive-terminal/SKILL.md +23 -1
- package/skills/dsi-spec/SKILL.md +8 -0
- package/skills/dsi-task/SKILL.md +34 -0
- package/skills/dsi-token-spent/SKILL.md +122 -0
- package/template/.dsi/prompts.sqlite +0 -0
- package/template/.dsi/prompts.sqlite-shm +0 -0
- package/template/.dsi/prompts.sqlite-wal +0 -0
- package/build/client/_app/immutable/assets/0.uhYTRapi.css +0 -2
- package/build/client/_app/immutable/assets/2.D0QOh44D.css +0 -1
- package/build/client/_app/immutable/chunks/0Uk2rkmL.js +0 -1
- package/build/client/_app/immutable/chunks/BaH6C3xD.js +0 -2
- package/build/client/_app/immutable/chunks/DXba8Dom.js +0 -1
- package/build/client/_app/immutable/entry/start.D-t6KuoK.js +0 -1
- package/build/server/chunks/nodes/1.js-D0EwPqp_.js +0 -9
- /package/build/client/_app/immutable/nodes/{0.DeDuCaDD.js → 0.CYP0vr1_.js} +0 -0
|
@@ -26,13 +26,74 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { chromium } from 'playwright-core';
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} GlassSession
|
|
31
|
+
* @property {string} id
|
|
32
|
+
* @property {number} [port]
|
|
33
|
+
* @property {string|null} [ownerSessionId]
|
|
34
|
+
* @property {string} [createdAt]
|
|
35
|
+
* @typedef {Object} PickResult
|
|
36
|
+
* @property {GlassSession|null|undefined} session
|
|
37
|
+
* @property {boolean} [degraded]
|
|
38
|
+
* @property {string} [note]
|
|
39
|
+
* @property {string} [error]
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// ── The Floor Seam (ADR "The Floor Seam" 2026-11-05, D1-D3) ────────────────
|
|
43
|
+
// DSI_UI env verbatim when set; else parallel probe of both known floor
|
|
44
|
+
// bases (5175 dev, 5174 build) with the D7 identity tiebreak. Tab identity
|
|
45
|
+
// is an origin compare against the RESOLVED base — never a port literal.
|
|
46
|
+
|
|
47
|
+
const DEFAULT_FLOOR_BASES = ['http://127.0.0.1:5175', 'http://127.0.0.1:5174'];
|
|
48
|
+
|
|
49
|
+
/** Candidate bases: env seam wins; else dev first, build second. */
|
|
50
|
+
export function floorCandidates(env = process.env) {
|
|
51
|
+
const ui = env.DSI_UI;
|
|
52
|
+
if (ui && /^http:\/\//.test(ui)) return [ui.replace(/\/$/, '')];
|
|
53
|
+
return [...DEFAULT_FLOOR_BASES];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Resolve the live floor — one answer wins; both → pickSession per floor,
|
|
57
|
+
* my-glass floor wins, double-claim dies loud; none → die naming both. */
|
|
58
|
+
/** @param {{ env?: Record<string, string|undefined>, fetchImpl?: typeof fetch, identity?: string|undefined, pickSessionFn?: (s: GlassSession[]|null|undefined, id: string|undefined) => PickResult, dieFn?: (msg: string) => never, timeoutMs?: number }} [opts] @returns {Promise<{ base: string, reg: { sessions: GlassSession[] } }>} — never resolves undefined: every fall-through path dies. */
|
|
59
|
+
export async function resolveDsiBase({ env = process.env, fetchImpl = fetch, identity = undefined, pickSessionFn = pickSession, dieFn = die, timeoutMs = 3000 } = {}) {
|
|
60
|
+
const candidates = floorCandidates(env);
|
|
61
|
+
const probes = await Promise.all(candidates.map(async (base) => {
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetchImpl(base + '/api/browser', { signal: AbortSignal.timeout(timeoutMs) });
|
|
64
|
+
if (!res.ok) return { base, reg: null };
|
|
65
|
+
return { base, reg: await res.json() };
|
|
66
|
+
} catch { return { base, reg: null }; }
|
|
67
|
+
}));
|
|
68
|
+
const answered = probes.filter((p) => p.reg?.sessions);
|
|
69
|
+
if (answered.length === 0) {
|
|
70
|
+
dieFn('no DSI floor answers — probed ' + candidates.join(', ') +
|
|
71
|
+
' (dev: dsi dsh --sync --dev --playwright-mcp · build: dsi dsh --sync · or set DSI_UI=<floor url>)');
|
|
72
|
+
}
|
|
73
|
+
if (answered.length === 1) return answered[0];
|
|
74
|
+
const claims = answered.map((p) => ({ ...p, verdict: pickSessionFn(p.reg.sessions, identity) }));
|
|
75
|
+
const mine = claims.filter((c) => c.verdict.session);
|
|
76
|
+
if (mine.length === 1) return mine[0];
|
|
77
|
+
if (mine.length > 1) {
|
|
78
|
+
dieFn('a glass owned by you lives on BOTH floors (' + mine.map((m) => m.base).join(', ') +
|
|
79
|
+
') — set DSI_UI=<floor url> to name the floor; refusing to guess (D7)');
|
|
80
|
+
}
|
|
81
|
+
return dieFn('both floors answer but neither holds YOUR glass (' + answered.map((m) => m.base).join(', ') +
|
|
82
|
+
') — click the Browser chip in THIS conversation');
|
|
83
|
+
}
|
|
30
84
|
|
|
85
|
+
/** @param {string} url @param {string} base @returns {boolean} True when a tab url's ORIGIN is the resolved floor's origin (D2). */
|
|
86
|
+
export function isFloorTab(url, base) {
|
|
87
|
+
try { return new URL(url).origin === new URL(base).origin; } catch { return false; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @param {string} msg @returns {never} */
|
|
31
91
|
export function die(msg) {
|
|
32
92
|
console.error(JSON.stringify({ error: msg, fallback: 'SKILL.md Step 1 — ask the OPERATOR to click the Browser chip IN THE RIGHT CONVERSATION, then catch the session via GET /api/browser. If DSH_SESSION_ID is absent from env, pass --owner-session <id> (the session id you already know from context).' }));
|
|
33
93
|
process.exit(1);
|
|
34
94
|
}
|
|
35
95
|
|
|
96
|
+
/** @param {string[]} argv @returns {string|undefined} */
|
|
36
97
|
export function identityFrom(argv) {
|
|
37
98
|
const flag = argv.indexOf('--owner-session');
|
|
38
99
|
if (flag !== -1 && argv[flag + 1]) return argv[flag + 1];
|
|
@@ -41,6 +102,7 @@ export function identityFrom(argv) {
|
|
|
41
102
|
return undefined;
|
|
42
103
|
}
|
|
43
104
|
|
|
105
|
+
/** @param {GlassSession[]|null|undefined} sessions @param {string|undefined} envId @returns {PickResult} */
|
|
44
106
|
export function pickSession(sessions, envId) {
|
|
45
107
|
const list = sessions ?? [];
|
|
46
108
|
if (envId) {
|
|
@@ -61,21 +123,18 @@ export function pickSession(sessions, envId) {
|
|
|
61
123
|
return { session: null, error: 'multiple live glasses and no explicit identity — pass --owner-session <id> (or GLASS_OWNER_SESSION) from the session id you already know; refusing to guess (D7)' };
|
|
62
124
|
}
|
|
63
125
|
|
|
126
|
+
/** @returns {Promise<{ session: GlassSession, browser: import('playwright-core').Browser, ctx: import('playwright-core').BrowserContext, base: string }>} */
|
|
64
127
|
export async function connect() {
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
reg = await (await fetch(DSI + '/api/browser')).json();
|
|
68
|
-
} catch {
|
|
69
|
-
die('DSI dev server not reachable on 5175 — floor not up (dsi dsh --sync --dev --playwright-mcp)');
|
|
70
|
-
}
|
|
128
|
+
const { base, reg } = await resolveDsiBase({ env: process.env, identity: identityFrom(process.argv) });
|
|
71
129
|
const { session, degraded, note, error } = pickSession(reg.sessions, identityFrom(process.argv));
|
|
72
130
|
if (!session) die(error ?? 'no glass session resolved — operator must click the Browser chip');
|
|
73
131
|
if (degraded || note) console.error(JSON.stringify({ warn: note ?? 'degraded', sessionId: session.id }));
|
|
74
132
|
const browser = await chromium.connectOverCDP('http://127.0.0.1:' + session.port).catch(() => null);
|
|
75
133
|
if (!browser) die('CDP connect failed on port ' + session.port);
|
|
76
|
-
return { session, browser, ctx: browser.contexts()[0] };
|
|
134
|
+
return { session, browser, ctx: browser.contexts()[0], base };
|
|
77
135
|
}
|
|
78
136
|
|
|
137
|
+
/** @param {import('playwright-core').BrowserContext} ctx @param {string} substr @returns {import('playwright-core').Page|undefined} */
|
|
79
138
|
export function pick(ctx, substr) {
|
|
80
139
|
return ctx.pages().find((p) => p.url().includes(substr));
|
|
81
|
-
}
|
|
140
|
+
}
|
|
@@ -20,18 +20,19 @@
|
|
|
20
20
|
* On any failure it prints the manual fallback hint (SKILL.md Step 1 —
|
|
21
21
|
* the operator clicks the Browser chip; agents never open sessions).
|
|
22
22
|
*/
|
|
23
|
-
import { connect, die } from './_connect.mjs';
|
|
23
|
+
import { connect, die, isFloorTab } from './_connect.mjs';
|
|
24
24
|
|
|
25
|
-
const DSI = 'http://127.0.0.1:5175';
|
|
26
25
|
const [cmd, ...args] = process.argv.slice(2);
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
// The Floor Seam (ADR 2026-11-05): connect resolves the live floor (DSI_UI
|
|
28
|
+
// env or probe); the DSI tab is its own ORIGIN — never a port literal.
|
|
29
|
+
const { session, browser, ctx, base } = await connect();
|
|
29
30
|
|
|
30
|
-
let page = ctx.pages().find((p) => p.url()
|
|
31
|
+
let page = ctx.pages().find((p) => isFloorTab(p.url(), base));
|
|
31
32
|
if (cmd === 'open') {
|
|
32
33
|
if (!page) {
|
|
33
34
|
page = await ctx.newPage();
|
|
34
|
-
await page.goto(args[0] ??
|
|
35
|
+
await page.goto(args[0] ?? base, { waitUntil: 'domcontentloaded' }).catch(() => {});
|
|
35
36
|
} else if (args[0] && args[0] !== page.url()) {
|
|
36
37
|
await page.goto(args[0], { waitUntil: 'domcontentloaded' }).catch(() => {});
|
|
37
38
|
}
|
|
@@ -39,7 +39,61 @@ import { chromium } from 'playwright-core';
|
|
|
39
39
|
* @property {string} [error]
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// ── The Floor Seam (ADR "The Floor Seam" 2026-11-05, D1-D3) ────────────────
|
|
43
|
+
// The drivers dial the floor they SEE: DSI_UI env verbatim when set, else a
|
|
44
|
+
// parallel probe of the two known floor bases (5175 dev, 5174 build) with the
|
|
45
|
+
// D7 identity resolver as tiebreak. No port literal may survive in tab
|
|
46
|
+
// filtering — isFloorTab compares origins against the RESOLVED base (D2).
|
|
47
|
+
|
|
48
|
+
const DEFAULT_FLOOR_BASES = ['http://127.0.0.1:5175', 'http://127.0.0.1:5174'];
|
|
49
|
+
|
|
50
|
+
/** Candidate bases: the env seam wins verbatim (validated http), else both
|
|
51
|
+
* known floor bases — dev first so a dev-only machine keeps today's shape. */
|
|
52
|
+
export function floorCandidates(env = process.env) {
|
|
53
|
+
const ui = env.DSI_UI;
|
|
54
|
+
if (ui && /^http:\/\//.test(ui)) return [ui.replace(/\/$/, '')];
|
|
55
|
+
return [...DEFAULT_FLOOR_BASES];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resolve the live floor: fetch /api/browser on every candidate in
|
|
59
|
+
* PARALLEL (3 s timeout each), then — one answer wins; both answer →
|
|
60
|
+
* pickSession per floor with the caller's identity, the floor holding the
|
|
61
|
+
* caller's OWN glass wins, a double-claim dies loud (D7 stands); none
|
|
62
|
+
* answer → die naming BOTH ports and BOTH launch commands. */
|
|
63
|
+
/** @param {{ env?: Record<string, string|undefined>, fetchImpl?: typeof fetch, identity?: string|undefined, pickSessionFn?: (s: GlassSession[]|null|undefined, id: string|undefined) => PickResult, dieFn?: (msg: string) => never, timeoutMs?: number }} [opts] @returns {Promise<{ base: string, reg: { sessions: GlassSession[] } }>} — never resolves undefined: every fall-through path dies. */
|
|
64
|
+
export async function resolveDsiBase({ env = process.env, fetchImpl = fetch, identity = undefined, pickSessionFn = pickSession, dieFn = die, timeoutMs = 3000 } = {}) {
|
|
65
|
+
const candidates = floorCandidates(env);
|
|
66
|
+
const probes = await Promise.all(candidates.map(async (base) => {
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetchImpl(base + '/api/browser', { signal: AbortSignal.timeout(timeoutMs) });
|
|
69
|
+
if (!res.ok) return { base, reg: null };
|
|
70
|
+
return { base, reg: await res.json() };
|
|
71
|
+
} catch { return { base, reg: null }; }
|
|
72
|
+
}));
|
|
73
|
+
const answered = probes.filter((p) => p.reg?.sessions);
|
|
74
|
+
if (answered.length === 0) {
|
|
75
|
+
dieFn('no DSI floor answers — probed ' + candidates.join(', ') +
|
|
76
|
+
' (dev: dsi dsh --sync --dev --playwright-mcp · build: dsi dsh --sync · or set DSI_UI=<floor url>)');
|
|
77
|
+
}
|
|
78
|
+
if (answered.length === 1) return answered[0];
|
|
79
|
+
// Both floors live — the caller's own glass picks the floor (D7 tiebreak).
|
|
80
|
+
const claims = answered.map((p) => ({ ...p, verdict: pickSessionFn(p.reg.sessions, identity) }));
|
|
81
|
+
const mine = claims.filter((c) => c.verdict.session);
|
|
82
|
+
if (mine.length === 1) return mine[0];
|
|
83
|
+
if (mine.length > 1) {
|
|
84
|
+
dieFn('a glass owned by you lives on BOTH floors (' + mine.map((m) => m.base).join(', ') +
|
|
85
|
+
') — set DSI_UI=<floor url> to name the floor; refusing to guess (D7)');
|
|
86
|
+
}
|
|
87
|
+
return dieFn('both floors answer but neither holds YOUR glass (' + answered.map((m) => m.base).join(', ') +
|
|
88
|
+
') — click the Browser chip in THIS conversation');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** True when a tab url's ORIGIN is the resolved floor's origin (D2): the
|
|
92
|
+
* DSI tab is the floor's own page, whatever port it landed on. */
|
|
93
|
+
/** @param {string} url @param {string} base @returns {boolean} */
|
|
94
|
+
export function isFloorTab(url, base) {
|
|
95
|
+
try { return new URL(url).origin === new URL(base).origin; } catch { return false; }
|
|
96
|
+
}
|
|
43
97
|
|
|
44
98
|
/** @param {string} msg @returns {never} */
|
|
45
99
|
export function die(msg) {
|
|
@@ -77,23 +131,18 @@ export function pickSession(sessions, envId) {
|
|
|
77
131
|
return { session: null, error: 'multiple live glasses and no explicit identity — pass --owner-session <id> (or GLASS_OWNER_SESSION) from the session id you already know; refusing to guess (D7)' };
|
|
78
132
|
}
|
|
79
133
|
|
|
80
|
-
/** @returns {Promise<{ session: GlassSession, browser: import('playwright-core').Browser, ctx: import('playwright-core').BrowserContext }>} */
|
|
134
|
+
/** @returns {Promise<{ session: GlassSession, browser: import('playwright-core').Browser, ctx: import('playwright-core').BrowserContext, base: string }>} */
|
|
81
135
|
export async function connect() {
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
reg = await (await fetch(DSI + '/api/browser')).json();
|
|
85
|
-
} catch {
|
|
86
|
-
die('DSI dev server not reachable on 5175 — floor not up (dsi dsh --sync --dev --playwright-mcp)');
|
|
87
|
-
}
|
|
136
|
+
const { base, reg } = await resolveDsiBase({ env: process.env, identity: identityFrom(process.argv) });
|
|
88
137
|
const { session, degraded, note, error } = pickSession(reg.sessions, identityFrom(process.argv));
|
|
89
138
|
if (!session) die(error ?? 'no glass session resolved — operator must click the Browser chip');
|
|
90
139
|
if (degraded || note) console.error(JSON.stringify({ warn: note ?? 'degraded', sessionId: session.id }));
|
|
91
140
|
const browser = await chromium.connectOverCDP('http://127.0.0.1:' + session.port).catch(() => null);
|
|
92
141
|
if (!browser) die('CDP connect failed on port ' + session.port);
|
|
93
|
-
return { session, browser, ctx: browser.contexts()[0] };
|
|
142
|
+
return { session, browser, ctx: browser.contexts()[0], base };
|
|
94
143
|
}
|
|
95
144
|
|
|
96
145
|
/** @param {import('playwright-core').BrowserContext} ctx @param {string} substr @returns {import('playwright-core').Page|undefined} */
|
|
97
146
|
export function pick(ctx, substr) {
|
|
98
147
|
return ctx.pages().find((p) => p.url().includes(substr));
|
|
99
|
-
}
|
|
148
|
+
}
|
|
@@ -23,22 +23,23 @@
|
|
|
23
23
|
* EXIT DISCIPLINE: this script always process.exit()s — a driver that
|
|
24
24
|
* played media once hung its caller for a full timeout (RCA trap 5).
|
|
25
25
|
*/
|
|
26
|
-
import { connect, die, pick } from './_connect.mjs';
|
|
26
|
+
import { connect, die, pick, isFloorTab } from './_connect.mjs';
|
|
27
27
|
import { norm, parseEvalArgs, matchAllTerms } from './_args.mjs';
|
|
28
28
|
|
|
29
29
|
const argv = process.argv.slice(2);
|
|
30
|
-
const { session, browser, ctx } = await connect();
|
|
30
|
+
const { session, browser, ctx, base } = await connect();
|
|
31
31
|
const out = (v) => { console.log(JSON.stringify(v, null, 1)); return process.exit(0); };
|
|
32
32
|
|
|
33
33
|
// target tab: url-substring when given; else last http(s) page that is
|
|
34
|
-
// neither the glass placeholder nor
|
|
34
|
+
// neither the glass placeholder nor the FLOOR's own page — origin compare
|
|
35
|
+
// against the resolved base (The Floor Seam ADR D2; never a port literal).
|
|
35
36
|
const target = (substr) => {
|
|
36
37
|
if (substr) {
|
|
37
38
|
const p = pick(ctx, substr);
|
|
38
39
|
if (!p) die('no tab matching "' + substr + '" — run: pages');
|
|
39
40
|
return p;
|
|
40
41
|
}
|
|
41
|
-
const pages = ctx.pages().filter((p) => p.url().startsWith('http') && !p.url()
|
|
42
|
+
const pages = ctx.pages().filter((p) => p.url().startsWith('http') && !isFloorTab(p.url(), base));
|
|
42
43
|
if (!pages.length) die('no non-DSI tab to drive — run: glass-url.mjs open <url>');
|
|
43
44
|
return pages[pages.length - 1];
|
|
44
45
|
};
|
package/README.md
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/mitmplay/dsh-insight/refs/heads/main/docs/images/dsi-insight-pich.png"></img>
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
>
|
|
5
|
+
<p align="center">
|
|
6
|
+
A different style opinionate <strong>DSH web client</strong> - aka <strong>DSI</strong>
|
|
7
|
+
<p>
|
|
8
|
+
<p align="center">
|
|
9
|
+
Screenshot: Sidebar, Workspace-explorer, Agent, Glass-browser, Subagent
|
|
10
|
+
<p>
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
#### Where you can see mulltiple **agent** / **sub-agent**, plus+ non conversation panels:
|
|
13
|
+
- keys panel: `Agent`, `Subagent`, `Workspace Explorer`, `Glass Browser`, `Terminal`,
|
|
14
|
+
- management panel: `Prompt`, `Skill`, `Plugins` and `Configs` Manager`
|
|
6
15
|
|
|
7
|
-
>
|
|
16
|
+
> [!NOTE]
|
|
17
|
+
> ["in a nutshell" - DSI](https://github.com/mitmplay/dsh-insight) make `Harness` present multiple conversations (`agent` or `sub-agent`), `keys` and `management` panels **side-by-side** `INSIDE Container` that can be **re-size** or **scale**.
|
|
8
18
|
|
|
9
|
-
##
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @deepseek-insight/dsi@latest dsh --sync --zai
|
|
23
|
+
# Open http://localhost:5174
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Argument | What it does |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `dsh` | starts **DSH** (`dsh web`) and **DSI** together, as one pair and runs it |
|
|
29
|
+
| `--sync` | copies DSH token to DSI config — **one window, one Ctrl-C stops both** |
|
|
30
|
+
| `--zai` | **optional.** It bootstraps the GLM (Z.AI) provider into the harness settings (`~/.dsh/settings.yaml`). **If you do not have a GLM account, simply leave it off** — DSI will use whatever provider your harness is already configured with |
|
|
31
|
+
|
|
32
|
+
When it is up, your browser opens the page by itself (default `http://127.0.0.1:5174`).
|
|
33
|
+
|
|
34
|
+
## Various operations can be Enter in Composer:
|
|
10
35
|
- `/dsi-terminal` — Open a terminal session
|
|
11
36
|
- `/dsi-prompts` — Open prompt manager
|
|
12
37
|
- `/dsi-plugins` — Open plugin manager
|
|
@@ -48,22 +73,26 @@ The workspace explorer is DSI's window into the folder your agents are working i
|
|
|
48
73
|
|
|
49
74
|
---
|
|
50
75
|
|
|
51
|
-
|
|
76
|
+
### Ports & the browser brain, in plain words
|
|
52
77
|
|
|
53
|
-
|
|
54
|
-
npx @deepseek-insight/dsi@latest dsh --sync --zai
|
|
55
|
-
# Open http://localhost:5174
|
|
56
|
-
```
|
|
78
|
+
One "floor" is a pair: **DSH** (the harness host) and **DSI** (the page you watch). Each sits on its own port, and each port can be moved — handy when you want a second floor beside one that is already running.
|
|
57
79
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
| What | Default | How to move it | Plain meaning |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| DSH host | `3080` | `--port <n>` | the harness brain of the floor |
|
|
83
|
+
| DSI page | `5174` | `PORT=<n>` env var | the page you open in the browser |
|
|
84
|
+
| Browser brain | `9515` | `DSI_MCP_PORT=<n>` env var | the MCP endpoint agents use to drive the glass browser |
|
|
62
85
|
|
|
86
|
+
**The browser brain.** By default the glass browser's assistant (the MCP brain that lets an agent click and type in the browser) runs as its **own separate helper process** — if it fails, you lose only the glass panel. Adding `--playwright-mcp` moves that brain **inside the DSI page server**: one less background process and a faster start, but a brain failure can now touch the page server itself. DSI prints a banner saying so whenever you use the flag — drop the flag to go back to the helper process.
|
|
63
87
|
|
|
88
|
+
**Example — a second, self-contained floor beside a live one** (nothing on the running floor is touched):
|
|
64
89
|
|
|
90
|
+
```bash
|
|
91
|
+
PORT=5176 DSI_MCP_PORT=19515 npx @deepseek-insight/dsi@latest dsh --sync --playwright-mcp --port 3081
|
|
92
|
+
# Page: http://localhost:5176 · host: 3081 · brain: http://127.0.0.1:19515/sse
|
|
93
|
+
```
|
|
65
94
|
|
|
66
|
-
|
|
95
|
+
**Second floors are fully drivable.** At boot, a floor stamps its own addresses (`DSI_UI`, `DSI_MCP_URL`) into everything it starts — so agents in *that* floor's conversations drive *that* floor's glass automatically, and the preset brain entry follows the stamped port. Nothing on the primary floor changes: without a stamp (a bare `dsh web` boot), the brain falls back to `9515` and the drivers probe `5175` → `5174` exactly as before. One conversation = one glass owner; the driver refuses to touch another conversation's glass, and a session busy in the live floor shows `writer-held` if you try to prompt it from the second window (watch it there, prompt it on its own floor).
|
|
67
96
|
|
|
68
97
|
## License
|
|
69
98
|
|
package/bin/dsi.mjs
CHANGED
|
@@ -33,7 +33,9 @@ import { parse, stringify } from 'yaml';
|
|
|
33
33
|
import { mirrorSkills } from './lib/mirror-skills.mjs';
|
|
34
34
|
import { dshLauncherCmd, parsePluginArgs, takeoverFloorPorts, hostBounceDecision, floorUiPort, parseDevFloorPids, launchUrl, tokenFromSettings } from './lib/plugin-chain.mjs';
|
|
35
35
|
import { parseDevArg, devModeEnv, modeFromBoolean, resolveBounceMode, extractInspectArgs, parseInspectArgsFromEnv } from './lib/dev-mode.mjs';
|
|
36
|
+
import { brainBannerLines } from './lib/mcp-banner.mjs';
|
|
36
37
|
import { addZaiProvider as addZaiPatch, zaiPatchPath } from './lib/zai-patch.mjs';
|
|
38
|
+
import { ensureGlassBrowser } from './lib/glass-browser-bootstrap.mjs';
|
|
37
39
|
|
|
38
40
|
// Layman-facing help: wrapped near 76 columns, continuation lines indented,
|
|
39
41
|
// the recommended one-liner first. --zai is optional on purpose — no GLM
|
|
@@ -177,15 +179,32 @@ if (command === 'dsh') {
|
|
|
177
179
|
const rest = process.argv.slice(4);
|
|
178
180
|
const withZai = rest.includes('--zai');
|
|
179
181
|
const withOv = rest.includes('--ov');
|
|
180
|
-
// The Glass Browser (ADR-0029 D7): --playwright-mcp
|
|
181
|
-
// into the in-process MCP brain — a dsi-side flag,
|
|
182
|
-
// host spawn (dsh web would refuse it) and carried
|
|
183
|
-
//
|
|
182
|
+
// The Glass Browser (ADR-0029 D7, amendment A6 D7c): --playwright-mcp
|
|
183
|
+
// opts ANY floor into the in-process MCP brain — a dsi-side flag,
|
|
184
|
+
// stripped before the host spawn (dsh web would refuse it) and carried
|
|
185
|
+
// to the server as an env marker (DSI_MCP_IN_PROCESS, dev-mode.mjs).
|
|
184
186
|
const withPlaywrightMcp = rest.includes('--playwright-mcp');
|
|
185
187
|
const restNoMcp = withPlaywrightMcp ? rest.filter((a) => a !== '--playwright-mcp') : rest;
|
|
188
|
+
// A6 D7d: say out loud what the flag chose, in plain language, BEFORE
|
|
189
|
+
// any child spawns — an unannounced behavior change is a silent trap.
|
|
190
|
+
if (withPlaywrightMcp) {
|
|
191
|
+
const { mode: bannerMode } = parseDevArg(restNoMcp);
|
|
192
|
+
for (const line of brainBannerLines(bannerMode !== 'build')) console.log(line);
|
|
193
|
+
}
|
|
186
194
|
if (withZai) addZaiProvider();
|
|
187
195
|
if (withOv) runOvPlugin();
|
|
188
196
|
runSkillsMirror();
|
|
197
|
+
// The Glass Bootstrap (ADR 2026-11-05, D1): probe the glass browser
|
|
198
|
+
// BEFORE the pair spawns — system Chrome first, cached chromium second,
|
|
199
|
+
// a one-time pinned fetch last (soft failure; DSI_SKIP_BROWSER_INSTALL=1
|
|
200
|
+
// skips). The bounce chain never fetches — only this first-run branch.
|
|
201
|
+
const glass = ensureGlassBrowser({
|
|
202
|
+
packageRoot,
|
|
203
|
+
spawnSync,
|
|
204
|
+
env: process.env,
|
|
205
|
+
log: (line) => console.log(line)
|
|
206
|
+
});
|
|
207
|
+
if (glass.action === 'fetched') console.log(`dsi dsh: chromium ${glass.reason} — the glass browser is ready`);
|
|
189
208
|
// --dev parse (D4): 'in-process' | 'pair' consumed as a value; bare
|
|
190
209
|
// --dev stays 'pair'. dsi-side flags stripped before the host spawn.
|
|
191
210
|
const { mode, rest: hostArgs0 } = parseDevArg(restNoMcp);
|
|
@@ -365,6 +384,18 @@ function setDshAuthToken(token) {
|
|
|
365
384
|
* falls back to the registry's latest tag). An exact pin, not a range: a
|
|
366
385
|
* range drifts to whatever the registry resolves, and the pin exists
|
|
367
386
|
* precisely because latest can change the API contract under DSI. */
|
|
387
|
+
/** The Stamped Floor (ADR 2026-11-05, D1): every floor announces its own
|
|
388
|
+
* addresses into the env it gives its children — the UI base (the Floor
|
|
389
|
+
* Seam D1a seam, finally with a writer) and the brain URL (the preset's
|
|
390
|
+
* !!js expression reads DSI_MCP_URL with a 9515 bare-boot fallback). The
|
|
391
|
+
* launcher is the only party that knows both ports at spawn time. */
|
|
392
|
+
function stampedFloorEnv(uiPort) {
|
|
393
|
+
return {
|
|
394
|
+
DSI_UI: 'http://127.0.0.1:' + uiPort,
|
|
395
|
+
DSI_MCP_URL: 'http://127.0.0.1:' + (process.env.DSI_MCP_PORT ?? 9515) + '/sse'
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
368
399
|
function dshWebVersion() {
|
|
369
400
|
const doc = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
|
|
370
401
|
const version = doc?.dsh?.webVersion;
|
|
@@ -487,9 +518,9 @@ async function runSyncedPair(extraArgs, dev = 'build', execArgv = [], opts = {})
|
|
|
487
518
|
// host lifecycle (vite close hook stops it; one Ctrl-C, no orphan).
|
|
488
519
|
const host = inProcess ? null : spawn('/bin/sh', [script, ...extraArgs], {
|
|
489
520
|
stdio: 'inherit',
|
|
490
|
-
env: { ...process.env, DSH_WEB_VERSION: dshWebVersion() }
|
|
521
|
+
env: { ...process.env, DSH_WEB_VERSION: dshWebVersion(), ...stampedFloorEnv(uiPort) }
|
|
491
522
|
});
|
|
492
|
-
const ui = mode === 'build' ? startDsiServer(uiPort, dshBase) : startDsiDevServer(uiPort, dshBase, mode, execArgv, opts);
|
|
523
|
+
const ui = mode === 'build' ? startDsiServer(uiPort, dshBase, opts) : startDsiDevServer(uiPort, dshBase, mode, execArgv, opts);
|
|
493
524
|
// The browser follows the UI: the built page (5174) — or, in --dev mode,
|
|
494
525
|
// the dev server (5175). Gated on the HOST actually listening (bug
|
|
495
526
|
// 2026-10-17): the fixed 2s timer lost every race the host booted slower
|
|
@@ -754,6 +785,7 @@ function startDsiDevServer(defaultPort = '5175', dshBase = undefined, mode = 'pa
|
|
|
754
785
|
DSI_DEV: '1',
|
|
755
786
|
// D4/D6: the mode rides as env, never past the CLI to the host.
|
|
756
787
|
...devModeEnv(mode, { playwrightMcp: opts.playwrightMcp === true }),
|
|
788
|
+
...stampedFloorEnv(process.env.PORT ?? defaultPort),
|
|
757
789
|
// The Faithful Bounce (ADR 2026-10-13, D1): the inspector flags are
|
|
758
790
|
// part of the launch record — stamped so every restorer (the bounce
|
|
759
791
|
// boot via ps -E, the delayed re-serve via inherited env) can put
|
|
@@ -764,7 +796,7 @@ function startDsiDevServer(defaultPort = '5175', dshBase = undefined, mode = 'pa
|
|
|
764
796
|
});
|
|
765
797
|
}
|
|
766
798
|
|
|
767
|
-
function startDsiServer(defaultPort = '5174', dshBase = undefined) {
|
|
799
|
+
function startDsiServer(defaultPort = '5174', dshBase = undefined, opts = {}) {
|
|
768
800
|
const serverEntry = path.join(packageRoot, 'build', 'index.js');
|
|
769
801
|
if (!existsSync(serverEntry)) {
|
|
770
802
|
console.error(`dsi: no build found at ${serverEntry} — run \`pnpm run build\` first`);
|
|
@@ -779,6 +811,11 @@ function startDsiServer(defaultPort = '5174', dshBase = undefined) {
|
|
|
779
811
|
// First-start bootstrap seam: the server clones template/.dsi to ~/.dsi
|
|
780
812
|
// when the operator home is absent (src/lib/server/settings-document.ts).
|
|
781
813
|
DSI_TEMPLATE_PATH: process.env.DSI_TEMPLATE_PATH ?? path.join(packageRoot, 'template', '.dsi'),
|
|
814
|
+
// A6 D7c: the brain marker rides EVERY floor, build included — the
|
|
815
|
+
// vite child got it via startDsiDevServer's devModeEnv spread; the
|
|
816
|
+
// built server was silently dropping it (caught by the live gate).
|
|
817
|
+
...devModeEnv('build', { playwrightMcp: opts.playwrightMcp === true }),
|
|
818
|
+
...stampedFloorEnv(process.env.PORT ?? defaultPort),
|
|
782
819
|
...(dshBase !== undefined ? { DSH_BASE_URL: dshBase } : {})
|
|
783
820
|
}
|
|
784
821
|
});
|
package/bin/lib/dev-mode.mjs
CHANGED
|
@@ -54,9 +54,15 @@ export function parseDevArg(rest) {
|
|
|
54
54
|
* @returns {Record<string, string>}
|
|
55
55
|
*/
|
|
56
56
|
export function devModeEnv(mode, opts = {}) {
|
|
57
|
-
if (mode === 'build') return {};
|
|
58
57
|
/** @type {Record<string, string>} */
|
|
59
|
-
const env = {
|
|
58
|
+
const env = {};
|
|
59
|
+
// ADR-0029 amendment A6 D7c: the glass brain's placement is the operator's
|
|
60
|
+
// choice on EVERY floor — build included. DSI_DEV/DSI_DEV_MODE stay
|
|
61
|
+
// strictly dev-only (the bounce chain probes them as dev-floor markers).
|
|
62
|
+
if (opts.playwrightMcp) env.DSI_MCP_IN_PROCESS = '1';
|
|
63
|
+
if (mode === 'build') return env;
|
|
64
|
+
env.DSI_DEV = '1';
|
|
65
|
+
env.DSI_DEV_MODE = mode;
|
|
60
66
|
if (mode === 'in-process') {
|
|
61
67
|
env.DSI_DEV_IN_PROCESS = '1';
|
|
62
68
|
// ADR-0025 Wave 3 (real-floor gate finding, 2026-10-14): the bounce
|
|
@@ -67,9 +73,6 @@ export function devModeEnv(mode, opts = {}) {
|
|
|
67
73
|
// that carries the handle, so in-process floors default to it.
|
|
68
74
|
env.DSH_HOST_INTROSPECT = '1';
|
|
69
75
|
}
|
|
70
|
-
// ADR-0029 D7: the glass browser's MCP brain runs in-process (opt-in,
|
|
71
|
-
// dev floor only — the tandem/production floors keep the stdio child)
|
|
72
|
-
if (opts.playwrightMcp) env.DSI_MCP_IN_PROCESS = '1';
|
|
73
76
|
return env;
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glass browser bootstrap (ADR "The Glass Bootstrap" 2026-11-05, D1-D4):
|
|
3
|
+
* decide whether the floor's glass browser has a binary it can launch, and
|
|
4
|
+
* fetch the pinned chromium once when it does not. Pure decision logic —
|
|
5
|
+
* every effect (spawnSync, env, log, platform) is injected, so the unit
|
|
6
|
+
* pins exercise the full decision tree without a network or a real cache.
|
|
7
|
+
*
|
|
8
|
+
* Probe order IS the launch order (D2): registry.ts tries system Google
|
|
9
|
+
* Chrome (channel: 'chrome') FIRST and falls back to the bundled chromium,
|
|
10
|
+
* so a machine with Chrome must never pay for a chromium download.
|
|
11
|
+
*
|
|
12
|
+
* Soft failure is the contract (D4): a failed or skipped fetch warns and
|
|
13
|
+
* returns — the floor boots; only the glass is affected, and the launch
|
|
14
|
+
* site's honest error (D5) names the cure.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
/** Chrome candidates probed on PATH (Linux shape). macOS has no google-chrome
|
|
21
|
+
* on PATH — the .app bundle is the truth (checked separately). */
|
|
22
|
+
const CHROME_PATH_CANDIDATES = ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser'];
|
|
23
|
+
|
|
24
|
+
/** The pinned playwright family, derived from this package's own
|
|
25
|
+
* playwright-core dependency (D3: no second hardcoded pin may exist).
|
|
26
|
+
* Returns null when the pin cannot be read — the caller then warns and
|
|
27
|
+
* skips the fetch; a null pin never spawns npx. */
|
|
28
|
+
/** @param {string} packageRoot @returns {string | null} */
|
|
29
|
+
export function pinnedPlaywrightVersion(packageRoot) {
|
|
30
|
+
try {
|
|
31
|
+
const doc = /** @type {any} */ (JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf-8')));
|
|
32
|
+
const version = doc?.dependencies?.['playwright-core'];
|
|
33
|
+
return typeof version === 'string' && /^\d+\.\d+\.\d+/.test(version) ? version : null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** True when a system Google/Chromium browser is resolvable: PATH probes on
|
|
40
|
+
* Linux, the .app bundle on macOS (where PATH probing is always blind). */
|
|
41
|
+
/** @param {(cmd: string, args?: string[], opts?: { stdio?: 'pipe' | 'inherit'; timeout?: number }) => { status: number | null }} spawnSync
|
|
42
|
+
* @param {string} [platform]
|
|
43
|
+
* @param {(p: string) => boolean} [exists]
|
|
44
|
+
* @returns {boolean} */
|
|
45
|
+
export function findSystemChrome(spawnSync, platform = process.platform, exists = existsSync) {
|
|
46
|
+
if (platform === 'darwin') {
|
|
47
|
+
return exists('/Applications/Google Chrome.app');
|
|
48
|
+
}
|
|
49
|
+
for (const name of CHROME_PATH_CANDIDATES) {
|
|
50
|
+
try {
|
|
51
|
+
if (spawnSync('which', [name], { stdio: 'pipe' }).status === 0) return true;
|
|
52
|
+
} catch {
|
|
53
|
+
// no which (or no sh) — the next candidate still probes
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True when ANY chromium-family build sits in the playwright cache (either
|
|
60
|
+
* platform root). The npx fetch is the revision authority — this probe is
|
|
61
|
+
* the cheap hint that avoids re-downloading what is already present. */
|
|
62
|
+
/** @param {Record<string, string | undefined>} [env]
|
|
63
|
+
* @param {(p: string) => string[]} [readdir]
|
|
64
|
+
* @returns {boolean} */
|
|
65
|
+
export function playwrightCacheHasChromium(env = process.env, readdir = readdirSync) {
|
|
66
|
+
const home = env.HOME ?? homedir();
|
|
67
|
+
const roots = [
|
|
68
|
+
path.join(home, 'Library', 'Caches', 'ms-playwright'),
|
|
69
|
+
path.join(home, '.cache', 'ms-playwright')
|
|
70
|
+
];
|
|
71
|
+
for (const root of roots) {
|
|
72
|
+
try {
|
|
73
|
+
if (readdir(root).some((entry) => entry.startsWith('chromium'))) return true;
|
|
74
|
+
} catch {
|
|
75
|
+
// unreadable/absent root — the next root still probes
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The D2 decision tree. Sync by construction: every step is a stat, a
|
|
83
|
+
* readdir, or one spawnSync child. Returns a verdict the launcher logs:
|
|
84
|
+
* { action: 'skip', reason } — a browser exists (or the hatch fired)
|
|
85
|
+
* { action: 'fetched', reason } — the pinned chromium landed in the cache
|
|
86
|
+
* { action: 'warned', reason } — no browser and the fetch failed/skipped;
|
|
87
|
+
* the floor still boots (D4)
|
|
88
|
+
*/
|
|
89
|
+
/** @param {{
|
|
90
|
+
* packageRoot: string,
|
|
91
|
+
* spawnSync: (cmd: string, args?: string[], opts?: { stdio?: 'pipe' | 'inherit'; timeout?: number }) => { status: number | null },
|
|
92
|
+
* env?: Record<string, string | undefined>,
|
|
93
|
+
* log?: (msg: string) => void,
|
|
94
|
+
* platform?: string
|
|
95
|
+
* }} options
|
|
96
|
+
* @returns {{ action: 'skip' | 'fetched' | 'warned', reason: string }} */
|
|
97
|
+
export function ensureGlassBrowser({ packageRoot, spawnSync, env = process.env, log = () => {}, platform = process.platform }) {
|
|
98
|
+
if (env.DSI_SKIP_BROWSER_INSTALL === '1') {
|
|
99
|
+
log('dsi dsh: glass browser check skipped (DSI_SKIP_BROWSER_INSTALL=1)');
|
|
100
|
+
return { action: 'skip', reason: 'env-escape-hatch' };
|
|
101
|
+
}
|
|
102
|
+
if (findSystemChrome(spawnSync, platform)) {
|
|
103
|
+
return { action: 'skip', reason: 'system-chrome' };
|
|
104
|
+
}
|
|
105
|
+
if (playwrightCacheHasChromium(env)) {
|
|
106
|
+
return { action: 'skip', reason: 'cached-chromium' };
|
|
107
|
+
}
|
|
108
|
+
const pin = pinnedPlaywrightVersion(packageRoot);
|
|
109
|
+
if (pin === null) {
|
|
110
|
+
log('dsi dsh: glass browser — no playwright-core pin readable in package.json, skipping the chromium fetch');
|
|
111
|
+
return { action: 'warned', reason: 'no-pin' };
|
|
112
|
+
}
|
|
113
|
+
// The loud line comes BEFORE the fetch child (D1): the silent-minutes
|
|
114
|
+
// bug (2026-11-06) must never repeat on this stage.
|
|
115
|
+
log(`dsi dsh: glass browser missing — downloading chromium (~150 MB, one-time, playwright@${pin})…`);
|
|
116
|
+
let result;
|
|
117
|
+
try {
|
|
118
|
+
result = spawnSync('npx', ['--yes', `playwright@${pin}`, 'install', 'chromium'], {
|
|
119
|
+
stdio: 'inherit',
|
|
120
|
+
timeout: 300000
|
|
121
|
+
});
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const message = /** @type {any} */ (error)?.message ?? error;
|
|
124
|
+
log(`dsi dsh: chromium fetch failed (${message}) — the floor still boots; the glass needs: npx playwright@${pin} install chromium`);
|
|
125
|
+
return { action: 'warned', reason: 'fetch-error' };
|
|
126
|
+
}
|
|
127
|
+
if ((result?.status ?? 1) === 0) {
|
|
128
|
+
return { action: 'fetched', reason: `playwright@${pin}` };
|
|
129
|
+
}
|
|
130
|
+
log(`dsi dsh: chromium fetch failed (exit ${result?.status}) — the floor still boots; the glass needs: npx playwright@${pin} install chromium`);
|
|
131
|
+
return { action: 'warned', reason: 'fetch-failed' };
|
|
132
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// mcp-banner — A6 D7d: when --playwright-mcp is present, say out loud what
|
|
2
|
+
// the operator just chose, in plain language, BEFORE any child spawns.
|
|
3
|
+
// Extracted as a module so the exact text is unit-pinned (T3-T).
|
|
4
|
+
|
|
5
|
+
/** @param {boolean} devFloor - true when a --dev mode was chosen
|
|
6
|
+
* @returns {string[]} banner lines (never empty; the caller prints each) */
|
|
7
|
+
export function brainBannerLines(devFloor) {
|
|
8
|
+
const blast = devFloor
|
|
9
|
+
? 'a brain failure can now touch this dev floor server.'
|
|
10
|
+
: 'a brain failure can now touch this page server — not just the glass.';
|
|
11
|
+
return [
|
|
12
|
+
'',
|
|
13
|
+
'────────── playwright-mcp: in-process browser brain ──────────',
|
|
14
|
+
'You asked for the browser-assist brain to run INSIDE the DSI page',
|
|
15
|
+
'server instead of as its own separate background helper.',
|
|
16
|
+
'',
|
|
17
|
+
'What that means:',
|
|
18
|
+
' + one less background process on your machine, faster start',
|
|
19
|
+
' - ' + blast,
|
|
20
|
+
' Before, it could only lose the glass panel, never the server.',
|
|
21
|
+
'',
|
|
22
|
+
'Drop the --playwright-mcp flag to go back to the old helper process.',
|
|
23
|
+
'───────────────────────────────────────────────────────────────',
|
|
24
|
+
''
|
|
25
|
+
];
|
|
26
|
+
}
|