@deepseek-insight/dsi 0.11.3 → 0.11.5

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.
Files changed (57) hide show
  1. package/.agents/skills/dsi-plugin-rack/SKILL.md +5 -0
  2. package/.agents/skills/dsi-skill-shelf/SKILL.md +5 -0
  3. package/.agents/skills/glass-dsh/DESIGN.md +36 -0
  4. package/.agents/skills/glass-dsh/SKILL.md +126 -0
  5. package/.agents/skills/glass-dsh/scripts/_connect.mjs +156 -0
  6. package/.agents/skills/glass-dsh/scripts/gesture.mjs +170 -0
  7. package/.agents/skills/glass-dsh/scripts/glass-dsh.mjs +63 -0
  8. package/.agents/skills/glass-dsh/scripts/probe-dsh.mjs +59 -0
  9. package/README.md +7 -1
  10. package/bin/dsi-chassis.mjs +40 -6
  11. package/build/client/_app/immutable/assets/0.DtmjX3F5.css +2 -0
  12. package/build/client/_app/immutable/chunks/B_-jo8TU.js +1 -0
  13. package/build/client/_app/immutable/chunks/D4hWXjx0.js +1 -0
  14. package/build/client/_app/immutable/chunks/{ClnxU8JH.js → VN40mceS.js} +2 -2
  15. package/build/client/_app/immutable/chunks/{DAFsxYyZ.js → m-9mXueE.js} +1 -1
  16. package/build/client/_app/immutable/entry/{app.B3Jpw67t.js → app.CMOM1S0-.js} +2 -2
  17. package/build/client/_app/immutable/entry/start.BSTSdw9I.js +1 -0
  18. package/build/client/_app/immutable/nodes/{1.DbxOgdbH.js → 1.D89b4952.js} +1 -1
  19. package/build/client/_app/immutable/nodes/{2.bJkyxVXf.js → 2.BTTHruTq.js} +2 -2
  20. package/build/client/_app/version.json +1 -1
  21. package/build/handler.js +4 -4
  22. package/build/index.js +4 -4
  23. package/build/server/chunks/chunks/{internal2.js-B5x__0aq.js → internal2.js-Bu378t5u.js} +2 -2
  24. package/build/server/chunks/chunks/{registry.js-Cnmd1vW3.js → registry.js-BuEw-96E.js} +2 -2
  25. package/build/server/chunks/chunks/{state.js-DsQO24cN.js → state.js-DjKlX2y2.js} +2 -2
  26. package/build/server/chunks/entries/endpoints/api/browser/_id_/activate/{_server.ts.js-DlbbO3RF.js → _server.ts.js-BWbEzlCm.js} +2 -2
  27. package/build/server/chunks/entries/endpoints/api/browser/_id_/close/{_server.ts.js-B85bLw4w.js → _server.ts.js-BRhxONLL.js} +2 -2
  28. package/build/server/chunks/entries/endpoints/api/browser/_id_/closetab/{_server.ts.js-Dt5Bxv2-.js → _server.ts.js-DcafQOUs.js} +2 -2
  29. package/build/server/chunks/entries/endpoints/api/browser/_id_/element/{_server.ts.js-uJ5BnGUA.js → _server.ts.js-BBSIIDAx.js} +2 -2
  30. package/build/server/chunks/entries/endpoints/api/browser/_id_/history/{_server.ts.js-CVCoWk2n.js → _server.ts.js-BDAujEcp.js} +2 -2
  31. package/build/server/chunks/entries/endpoints/api/browser/_id_/input/{_server.ts.js-CmhQ7zGO.js → _server.ts.js-PP6mc-7m.js} +2 -2
  32. package/build/server/chunks/entries/endpoints/api/browser/_id_/mcp/{_server.ts.js-CFtpmSCd.js → _server.ts.js-N1WWoP-W.js} +2 -2
  33. package/build/server/chunks/entries/endpoints/api/browser/_id_/navigate/{_server.ts.js-BH8MNt9W.js → _server.ts.js-7iuI6c8E.js} +2 -2
  34. package/build/server/chunks/entries/endpoints/api/browser/_id_/opentab/{_server.ts.js-uIrK0T8h.js → _server.ts.js-CsF_LSmK.js} +2 -2
  35. package/build/server/chunks/entries/endpoints/api/browser/_id_/page/{_server.ts.js-Bg5yHS9F.js → _server.ts.js-CshBvfm4.js} +2 -2
  36. package/build/server/chunks/entries/endpoints/api/browser/_id_/stream/{_server.ts.js-DG6xvUsm.js → _server.ts.js-CAtPWIGT.js} +2 -2
  37. package/build/server/chunks/entries/endpoints/api/browser/_id_/tabs/{_server.ts.js-RiXP45mV.js → _server.ts.js-Cuqk6xWI.js} +2 -2
  38. package/build/server/chunks/entries/endpoints/api/browser/_id_/viewport/{_server.ts.js-w9MNQddQ.js → _server.ts.js-CstasxV_.js} +2 -2
  39. package/build/server/chunks/entries/endpoints/api/browser/_id_/zoom/{_server.ts.js-BoXDoshV.js → _server.ts.js-BAe1WUX-.js} +2 -2
  40. package/build/server/chunks/entries/endpoints/api/browser/{_server.ts.js-pQmJm_Io.js → _server.ts.js-CkzA7KWV.js} +2 -2
  41. package/build/server/chunks/entries/pages/{_error.svelte.js-DImBUaQF.js → _error.svelte.js-BQo1hDr-.js} +3 -3
  42. package/build/server/chunks/entries/pages/{_page.svelte.js-CO5I0GZ2.js → _page.svelte.js-DKmRrZt2.js} +4 -4
  43. package/build/server/chunks/{handler-veh0A37V.js → handler-BPITMesY.js} +3 -3
  44. package/build/server/chunks/{index.js-D63-CxtE.js → index.js-DB5oAaYI.js} +2 -2
  45. package/build/server/chunks/{manifest.js-DJgcuJpZ.js → manifest.js-DnTfr66C.js} +20 -20
  46. package/build/server/chunks/nodes/{0.js-B8ciwPSu.js → 0.js-tNBtSaEq.js} +3 -3
  47. package/build/server/chunks/nodes/1.js-DCnyPlh8.js +9 -0
  48. package/build/server/chunks/nodes/{2.js-BoWRQ_XI.js → 2.js-2Lw9Q9N5.js} +3 -3
  49. package/package.json +1 -1
  50. package/template/.dsi/prompts.sqlite-shm +0 -0
  51. package/template/.dsi/prompts.sqlite-wal +0 -0
  52. package/build/client/_app/immutable/assets/0.Bjw4vcZl.css +0 -2
  53. package/build/client/_app/immutable/chunks/B5lb1z82.js +0 -1
  54. package/build/client/_app/immutable/chunks/CWAff5t1.js +0 -1
  55. package/build/client/_app/immutable/entry/start.BEEYbYm7.js +0 -1
  56. package/build/server/chunks/nodes/1.js-BPYsraeg.js +0 -9
  57. /package/build/client/_app/immutable/nodes/{0.DopnsZUT.js → 0.IJdZxujs.js} +0 -0
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: dsi-plugin-rack
3
+ description: Use when asked to install or uninstall a dsh plugin from the plugins reference (~/.dsi/resources/plugins-reff.md) — dsh-delegated apply via rack.mjs, snapshot-first cache, manifest-recorded removes per the Plugin Rack ADR. Reserved keyword /dsi-plugin-rack.
4
+ ---
5
+
1
6
  # dsi-plugin-rack
2
7
 
3
8
  Install and uninstall dsh plugins from the plugins reference (~/.dsi/resources/plugins-reff.md), cached at ~/.dsi/resources/pgr-cache.json. Implements the ADR "The Plugin Rack" (dev/architectural-decission/2026-09/, 2026-09-27).
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: dsi-skill-shelf
3
+ description: Use when asked to install or uninstall agent skills from the SKR reference (~/.dsi/resources/skills-reff.md) — signed installs and signature-gated uninstalls via shelf.mjs, unstable tier opt-in, collision abort, per the Skill Shelf ADR. Reserved keyword /dsi-skill-shelf.
4
+ ---
5
+
1
6
  # dsi-skill-shelf
2
7
 
3
8
  Install and uninstall agent skills from the SKR reference (`~/.dsi/resources/skills-reff.md`). Implements the ADR 'The Skill Shelf' (dev/architectural-decission/2026-09/, 2026-09-20).
@@ -0,0 +1,36 @@
1
+ # glass-dsh — short design (approved 2026-11-09)
2
+
3
+ **Intent.** One skill that owns the creator-mode verification loop for a new DSH
4
+ web-UI plugin: install → probe → real gestures → persistence → restore, ending in
5
+ a gate report the operator can trust. Plugin craft stays in the harness's
6
+ `cordis-plugin-development` skill.
7
+
8
+ **Approaches considered** (operator decision 2026-11-09, recorded in ADR-0045
9
+ Amendment 6 D12–D15):
10
+ - *Full creator loop* (chosen) vs verification-only half — the operator picked the loop.
11
+ - *Own driver scripts* (chosen) vs reusing glass-dsi's `_connect.mjs` — decoupling beats
12
+ shared-drift risk at this size (accepted duplication, D1/D12 give-up).
13
+ - *Repo `skills/` folder* (chosen, operator override) + `~/.agents/skills/` mirror for
14
+ the slash menu — same pattern as every DSI skill.
15
+ - *Probe-then-env port resolution* (chosen, D13): `--dsh-url`/`--dsh-port` argument >
16
+ `DSH_PORT` env > default `3080`; CLI `--port` applies at boot, so the env/arg the agent
17
+ sees is authoritative — stated first in SKILL.md.
18
+
19
+ **Goals.** Five machine-checkable gates (console baseline, origin tab pick, real
20
+ gestures with control assertions, localStorage persistence, state restore) runnable
21
+ against any slot-rendering DSH plugin with plugin-supplied selectors.
22
+
23
+ **Non-goals.** No plugin authoring; no DSI-surface verification (glass-dsi); no
24
+ foreign URLs (glass-url); no changes to sibling glass skills; no synthetic-event
25
+ "testing" (D15); no state files (the floor registry is the only truth, ADR-0030).
26
+
27
+ **Constraints.** playwright-core resolves only from the DSI repo root; the glass
28
+ session is claimed by harness-session identity or refused loudly (D7); a failing
29
+ gate ends the run with a report (D11 stop-loss).
30
+
31
+ **Left to the builder.** Per-plugin selectors and store keys come from the plugin's
32
+ own client module; the gesture composition (which verbs, which order) is the
33
+ calling agent's judgment inside the gates.
34
+
35
+ **Status:** built and gate-verified on the live sticky-notes plugin 2026-11-09 —
36
+ see completion notes in the spec folder.
@@ -0,0 +1,126 @@
1
+ ---
2
+ name: glass-dsh
3
+ description: "Use when a newly created DSH web-UI plugin (plugin_manager returned application=applied) must be verified live on the DSH page of the operator's glass browser — probe, real gestures, persistence and restore gates per ADR-0045. NOT for DSI's own workspace surface (that is glass-dsi) or arbitrary foreign URLs (that is glass-url). Reserved keyword /glass-dsh."
4
+ ---
5
+
6
+ # glass-dsh — verify a new DSH web-UI plugin on the operator's glass, creator mode
7
+
8
+ Use when you just created and installed a **DSH web-UI plugin** (`plugin_manager`
9
+ returned `application: applied`) and must prove it live on the glass browser —
10
+ the surface at the DSH GUI (`http://127.0.0.1:3080/` by default). This skill owns
11
+ the full creator loop's verification half: the plugin CRAFT itself (manifest,
12
+ locale, icon, slot choice) belongs to the harness's `cordis-plugin-development`
13
+ skill — defer to it first, then run THIS skill.
14
+
15
+ **Dispatch rule (ADR-0045 Amendment 6, the three hands):**
16
+ - Verifying DSI's own workspace surface → `glass-dsi` (its driver DIES without a
17
+ DSI tab; it cannot verify DSH work — observed live 2026-11-09).
18
+ - Driving an arbitrary foreign URL → `glass-url` (opens a NEW tab; never touches
19
+ the DSI tab).
20
+ - Verifying a newly created **DSH plugin** on the DSH page → **this skill**.
21
+
22
+ ## The law (every gate cites ADR-0045)
23
+
24
+ 1. **The registry is the only way in.** Never launch your own browser; never
25
+ POST your own glass session (orphans can never be adopted — the operator must
26
+ click the Browser chip). ADR-0030.
27
+ 2. **The glass is claimed by identity, never by index.** `_connect.mjs`
28
+ resolves `--owner-session` flag > `GLASS_OWNER_SESSION` > `DSH_SESSION_ID`;
29
+ multiple glasses with no match = loud refusal. D7.
30
+ 3. **The DSH tab is picked by ORIGIN compare, never a port substring.** D13.
31
+ 4. **Console-error baseline (Gate 0) before any gesture.** A
32
+ `slot entry crashed in '<slot>'` naming your plugin is an automatic FAIL with
33
+ the stack in the report — element presence alone verifies nothing (observed
34
+ live: the only signal of a dead plugin was the console crash). D14.
35
+ 5. **Real gestures only; every read gates on a control.** `page.mouse` /
36
+ `page.keyboard` with stepped moves; synthetic `dispatchEvent` is not
37
+ verification. Assert the gesture moved what it should before reading
38
+ counters. D15. Failure branches live in §gates below.
39
+ 6. **A refusal is an answer; stop-loss.** A failing gate ENDS the run with a
40
+ report — no improvised fixes beyond the plugin under test. D11.
41
+ 7. **Restore user state.** Clear and label anything you seeded. Gate 5.
42
+
43
+ ## DSH base resolution (probe then env; D13)
44
+
45
+ Order: explicit argument > environment > default.
46
+
47
+ 1. `--dsh-url <url>` or `--dsh-port <n>` argument (highest).
48
+ 2. `DSH_PORT` env — note: when the floor was booted with the CLI `--port` flag
49
+ it wins over the env at boot time, so if the operator booted with `--port`
50
+ but exported no `DSH_PORT`, ONLY the explicit argument form can find the
51
+ tab; state which source resolved the base in the report.
52
+ 3. Default `http://127.0.0.1:3080/`.
53
+
54
+ The floor registry (glass sessions) is resolved separately: `DSI_UI` env when
55
+ set, else probe `5175` then `5174` (`/api/browser`). A floor stamps its own
56
+ addresses at boot — agents in a floor's conversations drive that floor's glass.
57
+
58
+ ## Environment contract (D6, amended by D16 2026-11-09)
59
+
60
+ - Plain node ESM, no build step. The driver resolves `playwright-core`
61
+ **itself** (D16): script-relative `node_modules` → the cwd's `package.json`
62
+ tree → `$DSI_REPO_ROOT` → loud death naming the remedy. Running from the DSI
63
+ repo root still works (the cwd fallback), but is no longer required; there is
64
+ NO symlink step. If the skill directory is read-only (harness workspace-write
65
+ mode), copying `scripts/` into the workspace is the sanctioned deployment —
66
+ the copy bootstraps itself.
67
+ - Every verb prints ONE JSON verdict on stdout and exits 0/1; failures print
68
+ `{"error": ...}` via `die()` — a refusal is an answer, quote it.
69
+ - No state files between runs: every run re-derives session and tab from the
70
+ registry and the resolved origin.
71
+
72
+ ## The loop (creator mode)
73
+
74
+ 1. **Install** — via the harness `plugin_manager` (`install_bundle`; replacing
75
+ an installed package = `remove_bundle` then `install_bundle` again —
76
+ observed 2026-11-09). This skill starts when `application: applied`.
77
+ 2. **Probe (Gates 0–2)** —
78
+ `node skills/glass-dsh/scripts/probe-dsh.mjs --expect '<selector>' --store '<localStorageKey>'`
79
+ → reloads the DSH tab, arms the console listener, emits
80
+ `{launcherProbe, consoleErrors, crashedSlots, moduleLoader}`.
81
+ 3. **Gestures (Gates 3–4)** — one verb per process, composition in your hands:
82
+ `glass-dsh.mjs eval --b64` for reads, `gesture.mjs click|type|drag|control|persist`
83
+ for real input. Each verb prints `{before, after, moved}` — quote the pair.
84
+ For gesture SEQUENCES prefer the D17 composite (one process, one CDP session,
85
+ one verdict per step):
86
+ `node gesture.mjs run steps.json` where steps.json is an array like
87
+ `[{"verb":"click","selector":"…","label":"open"},{"verb":"drag","selector":"…","dx":-433,"dy":-303},{"verb":"type","selector":"…","text":"…"},{"verb":"control","expr":"…"}]`
88
+ → `{"verb":"run","steps":4,"executed":N,"failedAt":null,"ok":true,"results":[…per-step verdicts…]}`
89
+ and exit 1 if any step failed. The composite STOPS at the first failed gate
90
+ (control `pass:false`, gesture `moved:false`, any `die`) and reports the
91
+ completed prefix — quote the prefix, do not improvise onward (D11). Single
92
+ verbs stay for one-off reads and re-runs of a single failed step.
93
+ 4. **Restore (Gate 5)** — clear seeded state via the page's own store, reload,
94
+ re-probe: launcher present, console clean, store empty.
95
+ 5. **Report** — table of before/after numbers, the gesture that produced them,
96
+ gates passed/failed, what was seeded and cleared.
97
+
98
+ ## Gates (machine-checkable)
99
+
100
+ | Gate | Asserts | Verb |
101
+ |---|---|---|
102
+ | 0 | glass session exists, owned by you | `probe-dsh.mjs` (dies loudly otherwise) |
103
+ | 1 | DSH tab found by origin | `glass-dsh.mjs pages` (origin classification) |
104
+ | 2 | console baseline clean of the plugin's slot crash | `probe-dsh.mjs` (`crashedSlots`) |
105
+ | 3 | real gesture moved its control | `gesture.mjs` (`moved: true` + control expr) |
106
+ | 4 | persistence via the plugin's own store | `gesture.mjs persist` before/after reload |
107
+ | 5 | state restored | `probe-dsh.mjs` re-run after cleanup |
108
+
109
+ ## Failure branches (the 2026-11-09 sticky-notes incidents)
110
+
111
+ | Symptom | Root cause | Remedy |
112
+ |---|---|---|
113
+ | Plugin entry absent, console has `slot entry crashed in '<slot>': TypeError ...` | a thrown factory/component blanks the slot entry | Gate 2 FAIL → fix the plugin module, reinstall, re-probe |
114
+ | Click on element X times out: `X ... subtree intercepts pointer events` | another plugin element covers the target (stacked notes) | drag the covering element aside first, or click the TOPMOST instance (`.last()`); then report the overlap as a plugin defect |
115
+ | Click "succeeds" but the button's handler never fires | an ancestor's `setPointerCapture` on pointerdown retargets the derived click | plugin defect: skip capture when `e.target.closest('button')` |
116
+ | Drag moved nothing but gates "pass" | geometry lied or the drag clamped at viewport bounds | control gate (`moved: true`) is mandatory before reading counters; fix coordinates inside the container |
117
+ | `no glass ...` / multiple glasses | no panel, or foreign ownership | operator clicks the Browser chip in THIS conversation; `--owner-session <id>` if env lacks identity |## Reserved keys
118
+
119
+ Plugin selectors and localStorage keys are plugin-owned (read them from the
120
+ plugin's client module — e.g. sticky-notes uses `.sticky-notes-launcher` and
121
+ `dsh-sticky-notes:v1`). This skill hardcodes none.
122
+
123
+ ---
124
+ Driver scripts: `scripts/_connect.mjs` (header lists exports), `scripts/glass-dsh.mjs`,
125
+ `scripts/probe-dsh.mjs`, `scripts/gesture.mjs`.
126
+ Companions: ADR-0045 Amendment 6; spec `dev/specs/2026-11/2026-11-09 - Glass DSH Skill/`.
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * _connect.mjs — shared glass connection for the glass-dsh driver (skills/glass-dsh).
4
+ * Dated copy 2026-11-09; derived from glass-dsi's helper (ADR-0045 D1/D12 —
5
+ * accepted duplication, each copy dated and attributed).
6
+ *
7
+ * EXPORTS (do not guess — this header is the contract):
8
+ * die(msg) — print a JSON error and exit 1.
9
+ * identityFrom(argv) — caller identity, first-wins: --owner-session flag
10
+ * > GLASS_OWNER_SESSION > DSH_SESSION_ID env.
11
+ * pickSession(sessions, envId)
12
+ * — PURE D7 resolver: match identity to ownerSessionId;
13
+ * NEVER a registry index. { session } | { session:null, error }.
14
+ * resolveDsiBase({env, fetchImpl, timeoutMs})
15
+ * — the floor that owns the registry: DSI_UI env wins,
16
+ * else probe http://127.0.0.1:5175 then :5174 (/api/browser).
17
+ * Returns { base, reg }; dies loudly when no floor answers.
18
+ * resolveDshBase(argv, env)
19
+ * — the DSH surface to verify (D13, probe-then-env):
20
+ * --dsh-url / --dsh-port argument > DSH_PORT env >
21
+ * default http://127.0.0.1:3080. Returns origin string.
22
+ * connect(argv) — resolveDsiBase → pickSession → chromium.connectOverCDP
23
+ * → { session, browser, ctx, floorBase, dshOrigin }.
24
+ * Dies loudly on ambiguity (D7) or CDP failure.
25
+ * pickByOrigin(ctx, origin) — the page whose URL ORIGIN equals `origin`
26
+ * (never a port substring, never positional). Dies when
27
+ * absent, naming the resolved origin.
28
+ */
29
+ import { createRequire } from 'node:module';
30
+ import { join } from 'node:path';
31
+
32
+ // D16 (ADR-0045 Amendment 7) — the driver resolves its own engine; the cwd is
33
+ // never the contract. Resolution order: script-relative node_modules → the
34
+ // cwd's package.json tree (the documented "run from the repo root" case) →
35
+ // the stamped default repo (~/agentic-ai/deepseek-insight) or $DSI_REPO_ROOT →
36
+ // die loudly with the exact remedy. ESM bare imports resolve from the script's
37
+ // own directory, which is why cwd alone was never enough.
38
+ function resolveChromium() {
39
+ const attempts = [];
40
+ const repoCandidates = [];
41
+ if (process.env.DSI_REPO_ROOT) repoCandidates.push(process.env.DSI_REPO_ROOT);
42
+ if (process.env.HOME) repoCandidates.push(join(process.env.HOME, 'agentic-ai', 'deepseek-insight'));
43
+ const tryRequire = (label, anchor) => {
44
+ try {
45
+ return createRequire(anchor)('playwright-core');
46
+ } catch (e) {
47
+ attempts.push(label + ': ' + (e?.code ?? e?.message ?? String(e)));
48
+ return null;
49
+ }
50
+ };
51
+ let mod = tryRequire('script-relative', import.meta.url);
52
+ if (!mod) mod = tryRequire('cwd', join(process.cwd(), 'package.json'));
53
+ for (const root of repoCandidates) {
54
+ if (mod) break;
55
+ mod = tryRequire('repo:' + root, join(root, 'package.json'));
56
+ }
57
+ if (!mod?.chromium) {
58
+ die('playwright-core not resolvable — tried: ' + attempts.join(' | ') +
59
+ '. Remedy: run from the DSI repo root (cwd with a package.json), copy these ' +
60
+ 'scripts next to a node_modules that has playwright-core, export DSI_REPO_ROOT=<repo path>, ' +
61
+ 'or clone the DSI repo to ~/agentic-ai/deepseek-insight.');
62
+ }
63
+ return mod.chromium;
64
+ }
65
+ const chromium = resolveChromium();
66
+
67
+ const DEFAULT_FLOOR_BASES = ['http://127.0.0.1:5175', 'http://127.0.0.1:5174'];
68
+
69
+ export function die(msg) {
70
+ console.error(JSON.stringify({ error: msg }));
71
+ process.exit(1);
72
+ }
73
+
74
+ export function identityFrom(argv) {
75
+ const flag = argv.indexOf('--owner-session');
76
+ if (flag !== -1 && argv[flag + 1]) return argv[flag + 1];
77
+ if (process.env.GLASS_OWNER_SESSION) return process.env.GLASS_OWNER_SESSION;
78
+ if (process.env.DSH_SESSION_ID) return process.env.DSH_SESSION_ID;
79
+ return undefined;
80
+ }
81
+
82
+ export function pickSession(sessions, envId) {
83
+ const list = sessions ?? [];
84
+ if (envId) {
85
+ const mine = list.filter((s) => (s.ownerSessionId ?? null) === envId);
86
+ if (mine.length === 1) return { session: mine[0], degraded: false };
87
+ if (mine.length > 1) {
88
+ const newest = [...mine]
89
+ .sort((a, b) => String(a.createdAt ?? '').localeCompare(String(b.createdAt ?? '')))
90
+ .at(-1);
91
+ return { session: newest, degraded: false, note: 'multiple glasses owned by ' + envId + ' — newest wins' };
92
+ }
93
+ if (list.length === 1 && (list[0].ownerSessionId ?? null) === null) {
94
+ return { session: list[0], degraded: true, note: 'legacy session without ownerSessionId — degraded match' };
95
+ }
96
+ return { session: null, error: 'no glass owned by ' + envId + ' — your glass is dead or never summoned (the operator clicks the Browser chip in THIS conversation); live glasses belong to other conversations and are untouchable (D7)' };
97
+ }
98
+ if (list.length === 1) {
99
+ return { session: list[0], degraded: true, note: 'no explicit identity in env — degraded single-session resolution' };
100
+ }
101
+ return { session: null, error: 'multiple live glasses and no explicit identity — pass --owner-session <id>; refusing to guess (D7)' };
102
+ }
103
+
104
+ export async function resolveDsiBase({ env = process.env, fetchImpl = fetch, timeoutMs = 3000 } = {}) {
105
+ const candidates = env.DSI_UI && /^http:\/\//.test(env.DSI_UI)
106
+ ? [env.DSI_UI.replace(/\/$/, '')]
107
+ : [...DEFAULT_FLOOR_BASES];
108
+ const probes = await Promise.all(candidates.map(async (base) => {
109
+ try {
110
+ const res = await fetchImpl(base + '/api/browser', { signal: AbortSignal.timeout(timeoutMs) });
111
+ if (!res.ok) return { base, reg: null };
112
+ return { base, reg: await res.json() };
113
+ } catch { return { base, reg: null }; }
114
+ }));
115
+ const answered = probes.filter((p) => p.reg?.sessions);
116
+ if (answered.length === 0) {
117
+ die('no DSI floor answers — probed ' + candidates.join(', ') +
118
+ ' (the glass session registry lives on the DSI floor; open the glass from the floor panel)');
119
+ }
120
+ return answered[0];
121
+ }
122
+
123
+ /** D13 — explicit argument > DSH_PORT env > default 3080; returns an origin string. */
124
+ export function resolveDshBase(argv = [], env = process.env) {
125
+ const urlFlag = argv.indexOf('--dsh-url');
126
+ if (urlFlag !== -1 && argv[urlFlag + 1]) return new URL(argv[urlFlag + 1]).origin;
127
+ const portFlag = argv.indexOf('--dsh-port');
128
+ if (portFlag !== -1 && argv[portFlag + 1]) return new URL('http://127.0.0.1:' + argv[portFlag + 1]).origin;
129
+ if (env.DSH_PORT) return new URL('http://127.0.0.1:' + env.DSH_PORT).origin;
130
+ return new URL('http://127.0.0.1:3080').origin;
131
+ }
132
+
133
+ export function originOf(url) {
134
+ try { return new URL(url).origin; } catch { return null; }
135
+ }
136
+
137
+ export async function connect(argv = []) {
138
+ const { base, reg } = await resolveDsiBase({ env: process.env });
139
+ const { session, degraded, note, error } = pickSession(reg.sessions, identityFrom(argv));
140
+ if (!session) die(error ?? 'no glass session resolved — the operator clicks the Browser chip');
141
+ if (degraded || note) console.error(JSON.stringify({ warn: note ?? 'degraded', sessionId: session.id }));
142
+ const browser = await chromium.connectOverCDP('http://127.0.0.1:' + session.port).catch(() => null);
143
+ if (!browser) die('CDP connect failed on port ' + session.port);
144
+ const ctx = browser.contexts()[0];
145
+ const dshOrigin = resolveDshBase(argv, process.env);
146
+ return { session, browser, ctx, floorBase: base, dshOrigin };
147
+ }
148
+
149
+ export function pickByOrigin(ctx, origin) {
150
+ const page = ctx.pages().find((p) => originOf(p.url()) === origin);
151
+ if (!page) {
152
+ die('no DSH tab at origin ' + origin + ' in the glass — open ' + origin +
153
+ ' (or pass --dsh-url/--dsh-port for a second floor); refusing positional guesses (D13)');
154
+ }
155
+ return page;
156
+ }
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gesture.mjs — real-gesture verbs for the glass-dsh skill (Gates 3–5, D15).
4
+ * Real mouse/keyboard only: synthetic dispatchEvent is not verification.
5
+ * One verb per process; each prints {before, after, moved|delta} JSON.
6
+ * Run from any cwd (D16: the driver resolves its own playwright-core).
7
+ *
8
+ * Verbs:
9
+ * click <selector> real click (raises interception as an error, never force-clicks)
10
+ * type <selector> <text> real click into the element + page.keyboard.type
11
+ * drag <selector> <dx> <dy> real mouse down → 10 stepped moves with waits → up, on the element's box
12
+ * control <js-expr> control gate: evaluate an assertion expression → {pass, result}
13
+ * persist <localStorageKey> read a store key from the DSH tab → {raw}
14
+ * run <steps.json> D17 composite: ONE process + ONE CDP session executes a JSON
15
+ * array of steps ({verb, …args, label?}) in order, printing one
16
+ * verdict per step (same shapes as the single verbs) and stopping
17
+ * at the first failed gate — control pass:false, a moved:false on
18
+ * a gesture, or any die() — reporting the completed prefix.
19
+ * Exit 1 when a step failed, 0 otherwise (D11 stop-loss, batched).
20
+ *
21
+ * Flags: --dsh-url|--dsh-port|--owner-session (see _connect.mjs); --substr <s> selects a tab by URL
22
+ * substring for reads (never positional, D10); input verbs always target the DSH-origin tab.
23
+ */
24
+ import { readFileSync } from 'node:fs';
25
+ import { connect, pickByOrigin, originOf, die } from './_connect.mjs';
26
+
27
+ const argv = process.argv.slice(2);
28
+ const verb = argv[0];
29
+ function opt(name) {
30
+ const i = argv.indexOf(name);
31
+ return i !== -1 ? argv[i + 1] : undefined;
32
+ }
33
+ const substr = opt('--substr');
34
+
35
+ const { ctx, dshOrigin } = await connect(argv);
36
+ function page() {
37
+ if (substr) {
38
+ const p = ctx.pages().find((p) => p.url().includes(substr));
39
+ if (!p) die('no tab whose URL contains ' + JSON.stringify(substr));
40
+ return p;
41
+ }
42
+ return pickByOrigin(ctx, dshOrigin);
43
+ }
44
+
45
+ async function readHostPos(p, sel) {
46
+ return p.evaluate((s) => {
47
+ const el = document.querySelector(s);
48
+ if (!el) return null;
49
+ const host = el.closest('[style*="left"]') ?? el;
50
+ return { x: host.style.left, y: host.style.top };
51
+ }, sel);
52
+ }
53
+
54
+ /**
55
+ * Execute one step; returns { ok, verdict }. Verdict shapes are byte-identical
56
+ * to the single-verb shapes (D17 — additive composition only).
57
+ */
58
+ async function execStep(p, step) {
59
+ const v = step.verb;
60
+ try {
61
+ if (v === 'click') {
62
+ const sel = step.selector;
63
+ if (!sel) return { ok: false, verdict: { error: 'click needs a selector' } };
64
+ const before = await p.evaluate((s) => !!document.querySelector(s), sel);
65
+ await p.locator(sel).first().click(); // locator.click raises interception loudly (no force)
66
+ return { ok: true, verdict: { verb: 'click', selector: sel, before, after: true, moved: before !== true } };
67
+ }
68
+ if (v === 'type') {
69
+ const sel = step.selector, text = step.text;
70
+ if (!sel || text === undefined) return { ok: false, verdict: { error: 'type needs selector + text' } };
71
+ const loc = p.locator(sel).first();
72
+ await loc.click();
73
+ await p.keyboard.type(text);
74
+ const after = await loc.inputValue().catch(() => undefined);
75
+ return { ok: after === text, verdict: { verb: 'type', selector: sel, before: '', after, moved: after === text } };
76
+ }
77
+ if (v === 'drag') {
78
+ const sel = step.selector, dx = Number(step.dx), dy = Number(step.dy);
79
+ if (!sel || !Number.isFinite(dx) || !Number.isFinite(dy)) {
80
+ return { ok: false, verdict: { error: 'drag needs selector + finite dx/dy' } };
81
+ }
82
+ const posBefore = await readHostPos(p, sel);
83
+ if (!posBefore) return { ok: false, verdict: { error: 'no element for ' + sel } };
84
+ const box = await p.locator(sel).first().boundingBox();
85
+ if (!box) return { ok: false, verdict: { error: 'boundingBox failed for ' + sel } };
86
+ const sx = box.x + box.width / 2, sy = box.y + Math.min(box.height / 2, 8);
87
+ await p.mouse.move(sx, sy);
88
+ await p.mouse.down();
89
+ for (let i = 1; i <= 10; i++) {
90
+ await p.mouse.move(sx + (dx * i) / 10, sy + (dy * i) / 10);
91
+ await new Promise((r) => setTimeout(r, 20));
92
+ }
93
+ await p.mouse.up();
94
+ await new Promise((r) => setTimeout(r, 300));
95
+ const posAfter = await readHostPos(p, sel);
96
+ const moved = JSON.stringify(posBefore) !== JSON.stringify(posAfter);
97
+ return { ok: moved, verdict: { verb: 'drag', selector: sel, before: posBefore, after: posAfter, moved } };
98
+ }
99
+ if (v === 'control') {
100
+ const expr = step.expr;
101
+ if (!expr) return { ok: false, verdict: { error: 'control needs a JS assertion expression' } };
102
+ const result = await p.evaluate('!!(' + expr + ')');
103
+ return { ok: result === true, verdict: { verb: 'control', expr, pass: result === true, result } };
104
+ }
105
+ if (v === 'persist') {
106
+ const key = step.key;
107
+ if (!key) return { ok: false, verdict: { error: 'persist needs a localStorage key' } };
108
+ const raw = await p.evaluate((k) => window.localStorage.getItem(k), key);
109
+ return { ok: true, verdict: { verb: 'persist', key, raw } };
110
+ }
111
+ return { ok: false, verdict: { error: 'unknown step verb ' + JSON.stringify(v) + ' — click | type | drag | control | persist' } };
112
+ } catch (e) {
113
+ return { ok: false, verdict: { error: String(e?.message ?? e) } };
114
+ }
115
+ }
116
+
117
+ if (verb === 'run') {
118
+ const file = argv[1];
119
+ if (!file) die('run needs a steps.json path');
120
+ let steps;
121
+ try {
122
+ steps = JSON.parse(readFileSync(file, 'utf8'));
123
+ } catch (e) {
124
+ die('run cannot read steps: ' + String(e?.message ?? e));
125
+ }
126
+ if (!Array.isArray(steps)) die('run steps must be a JSON array of {verb, …}');
127
+ const p = page();
128
+ const results = [];
129
+ let failedAt = null;
130
+ for (let i = 0; i < steps.length; i++) {
131
+ const step = steps[i];
132
+ const { ok, verdict } = await execStep(p, step);
133
+ results.push({ label: step.label ?? (step.verb + '#' + i), ok, ...verdict });
134
+ if (!ok) { failedAt = i; break; }
135
+ }
136
+ console.log(JSON.stringify({
137
+ verb: 'run', steps: steps.length, executed: results.length,
138
+ failedAt, ok: failedAt === null, results,
139
+ }));
140
+ process.exit(failedAt === null ? 0 : 1);
141
+ } else if (verb === 'click') {
142
+ const sel = argv[1];
143
+ if (!sel) die('click needs a selector');
144
+ const { verdict } = await execStep(page(), { verb: 'click', selector: sel });
145
+ if (verdict.error) die(verdict.error);
146
+ console.log(JSON.stringify(verdict));
147
+ } else if (verb === 'type') {
148
+ const sel = argv[1], text = argv[2];
149
+ if (!sel || text === undefined) die('type needs <selector> <text>');
150
+ const { verdict } = await execStep(page(), { verb: 'type', selector: sel, text });
151
+ console.log(JSON.stringify(verdict));
152
+ } else if (verb === 'drag') {
153
+ const sel = argv[1], dx = Number(argv[2]), dy = Number(argv[3]);
154
+ if (!sel || !Number.isFinite(dx) || !Number.isFinite(dy)) die('drag needs <selector> <dx> <dy>');
155
+ const { verdict } = await execStep(page(), { verb: 'drag', selector: sel, dx, dy });
156
+ console.log(JSON.stringify(verdict));
157
+ } else if (verb === 'control') {
158
+ const expr = argv[1];
159
+ if (!expr) die('control needs a JS assertion expression');
160
+ const { verdict } = await execStep(page(), { verb: 'control', expr });
161
+ console.log(JSON.stringify(verdict));
162
+ } else if (verb === 'persist') {
163
+ const key = argv[1];
164
+ if (!key) die('persist needs a localStorage key');
165
+ const { verdict } = await execStep(page(), { verb: 'persist', key });
166
+ console.log(JSON.stringify(verdict));
167
+ } else {
168
+ die('unknown verb ' + JSON.stringify(verb) + ' — click | type | drag | control | persist | run');
169
+ }
170
+ process.exit(0);
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * glass-dsh.mjs — origin-aware driver verbs for the glass-dsh skill (D13).
4
+ * Run from the DSI repo root (playwright-core resolves there). One JSON verdict
5
+ * on stdout; loud die() on refusal (D7/D11).
6
+ *
7
+ * Verbs:
8
+ * pages list CDP pages with origin classification vs the resolved DSH origin
9
+ * eval --b64 <b64> [substr] evaluate base64 JS in the DSH tab (or the tab whose URL contains substr);
10
+ * D10 grammar law: with --b64, a leftover argument IS the substring — never positional
11
+ * shot <path> screenshot of the DSH tab
12
+ *
13
+ * Flags: --dsh-url <url> | --dsh-port <n> | --owner-session <id> (see _connect.mjs)
14
+ */
15
+ import { connect, pickByOrigin, originOf, die } from './_connect.mjs';
16
+
17
+ const argv = process.argv.slice(2);
18
+ const verb = argv[0];
19
+
20
+ function pick(ctx, dshOrigin, substr) {
21
+ if (substr) {
22
+ const page = ctx.pages().find((p) => p.url().includes(substr));
23
+ if (!page) die('no tab whose URL contains ' + JSON.stringify(substr) + ' — refusing positional guesses (D2/D10)');
24
+ return page;
25
+ }
26
+ return pickByOrigin(ctx, dshOrigin);
27
+ }
28
+
29
+ const { ctx, dshOrigin } = await connect(argv);
30
+
31
+ if (verb === 'pages') {
32
+ const pages = ctx.pages().map((p) => ({
33
+ url: p.url(),
34
+ origin: originOf(p.url()),
35
+ isDsh: originOf(p.url()) === dshOrigin,
36
+ }));
37
+ console.log(JSON.stringify({ dshOrigin, pages }));
38
+ } else if (verb === 'eval') {
39
+ const rest = argv.slice(1);
40
+ const b64 = rest.indexOf('--b64');
41
+ let expr, substr = undefined;
42
+ if (b64 !== -1) {
43
+ expr = Buffer.from(rest[b64 + 1] ?? '', 'base64').toString('utf8');
44
+ const leftover = rest[b64 + 2];
45
+ if (leftover) substr = leftover; // D10: leftover argument IS the substring
46
+ } else {
47
+ expr = rest[0];
48
+ if (!expr) die('eval needs inline JS or --b64 <b64> [substr]');
49
+ substr = rest[1];
50
+ }
51
+ const page = pick(ctx, dshOrigin, substr);
52
+ const result = await page.evaluate(expr);
53
+ console.log(JSON.stringify({ url: page.url(), result }));
54
+ } else if (verb === 'shot') {
55
+ const path = argv[1];
56
+ if (!path) die('shot needs a file path');
57
+ const page = pickByOrigin(ctx, dshOrigin);
58
+ await page.screenshot({ path, fullPage: false });
59
+ console.log(JSON.stringify({ ok: true, path, url: page.url() }));
60
+ } else {
61
+ die('unknown verb ' + JSON.stringify(verb) + ' — pages | eval --b64 <b64> [substr] | shot <path>');
62
+ }
63
+ process.exit(0);
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * probe-dsh.mjs — Gates 0–2 of the glass-dsh skill: console baseline + crash scan.
4
+ * Reloads the DSH tab, arms a console listener, and emits ONE JSON verdict.
5
+ * Evidence over presence: `slot entry crashed in '<slot>'` naming the plugin is
6
+ * an automatic FAIL (D14) even when everything else looks fine.
7
+ *
8
+ * Usage (from the DSI repo root):
9
+ * node skills/glass-dsh/scripts/probe-dsh.mjs [--expect '<selector>'] [--store '<localStorageKey>']
10
+ * [--plugin-id '<plugin id>'] [--dsh-url|--dsh-port|--owner-session]
11
+ *
12
+ * Emits: { dshOrigin, url, moduleLoader, expectPresent?, storeRaw?,
13
+ * consoleErrors, crashedSlots, pass }
14
+ * pass=false when a crashedSlot matches --plugin-id (or any crash, if no id given).
15
+ */
16
+ import { connect, pickByOrigin } from './_connect.mjs';
17
+
18
+ const argv = process.argv.slice(2);
19
+ function opt(name) {
20
+ const i = argv.indexOf(name);
21
+ return i !== -1 ? argv[i + 1] : undefined;
22
+ }
23
+ const expectSel = opt('--expect');
24
+ const storeKey = opt('--store');
25
+ const pluginId = opt('--plugin-id');
26
+
27
+ const { ctx, dshOrigin } = await connect(argv);
28
+ const page = pickByOrigin(ctx, dshOrigin);
29
+
30
+ const errors = [];
31
+ const onConsole = (m) => { if (m.type() === 'error') errors.push(m.text().slice(0, 500)); };
32
+ page.on('console', onConsole);
33
+
34
+ await page.reload({ waitUntil: 'networkidle' }).catch(() => {});
35
+ await new Promise((r) => setTimeout(r, 2500));
36
+
37
+ const probe = await page.evaluate(({ expectSel, storeKey }) => {
38
+ const el = expectSel ? document.querySelector(expectSel) : null;
39
+ return {
40
+ moduleLoader: typeof window.__ModuleLoader__ !== 'undefined' && typeof window.__ModuleLoader__.load === 'function',
41
+ expectPresent: expectSel ? !!el : undefined,
42
+ expectText: el && expectSel ? (el.textContent || '').slice(0, 80) : undefined,
43
+ storeRaw: storeKey ? window.localStorage.getItem(storeKey) : undefined,
44
+ };
45
+ }, { expectSel, storeKey });
46
+
47
+ page.off('console', onConsole);
48
+
49
+ const crashedSlots = errors
50
+ .filter((t) => t.includes("slot entry crashed in '"))
51
+ .map((t) => {
52
+ const slot = t.split("slot entry crashed in '")[1]?.split("'")[0] ?? '?';
53
+ return { slot, text: t };
54
+ });
55
+ const relevant = crashedSlots.filter((c) => !pluginId || c.text.includes(pluginId));
56
+ const pass = relevant.length === 0 && errors.length === 0;
57
+
58
+ console.log(JSON.stringify({ dshOrigin, url: page.url(), ...probe, consoleErrors: errors, crashedSlots, pass }));
59
+ process.exit(0);
package/README.md CHANGED
@@ -84,6 +84,12 @@ When it is up, your browser opens the page by itself (default `http://127.0.0.1:
84
84
  <img src="https://github.com/mitmplay/dsh-insight/blob/main/docs/images/dsi-insight-5.ws-explorer.png?raw=true"/>
85
85
  </details>
86
86
 
87
+ <details>
88
+ <summary><strong>Glass browser</strong></summary>
89
+ <p>The Glass browser is a real browser living inside DSI. While you work, agents build and change DSI itself — plugins, panels, settings — and instead of telling you "it's done", they can prove it: the work runs right there in the Glass panel, and you watch it live. If a plugin breaks, you see exactly what broke with your own eyes instead of trusting a green test report — the browser is the receipt.</p>
90
+ <img src="https://raw.githubusercontent.com/mitmplay/dsh-insight/refs/heads/main/docs/images/dsi-insight-3.glass-browser.png"/>
91
+ </details>
92
+
87
93
  ## Included skills for:
88
94
  - Coding agents: `/dsi-adr`, `/dsi-spec`, `/dsi-task`
89
95
  - Integration helper for [OpenViking](https://openviking.ai/): `/dsi-ov-setup`
@@ -164,4 +170,4 @@ DSI_PORT=5171 DSH_PORT=3081 GLASS_CHASSIS_URL=http://192.168.1.9:9525 pnpm dsi d
164
170
  [Apache-2.0](LICENSE) — Copyright © 2026 [Widi Harsojo](https://www.linkedin.com/in/wharsojo/)
165
171
  > OPEN TO WORK (SG_PR)
166
172
 
167
- **Your language.** The interface speaks English, Chinese, Indonesian & Spanish.
173
+ **Your language.** The interface speaks English, Chinese, Indonesian & Spanish.