dsh-browser-playwright-codex 0.7.1 → 0.7.4

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.
@@ -33,6 +33,26 @@ export declare function getEnabled(): boolean;
33
33
  export declare function setEnabled(next: boolean): boolean;
34
34
  /** Subscribe to enable-state transitions. Returns an unsubscribe function. */
35
35
  export declare function subscribe(fn: (enabled: boolean) => void): () => void;
36
+ /**
37
+ * The window mode the user picked (true = headless), or undefined when they
38
+ * never picked one — the configured default then applies.
39
+ */
40
+ export declare function getHeadless(): boolean | undefined;
41
+ /** Persist the window-mode choice. Returns true when the value changed. */
42
+ export declare function setHeadless(next: boolean): boolean;
43
+ /** Publish the configured default (launch.headless) for display. */
44
+ export declare function publishConfiguredHeadless(value: boolean): void;
45
+ /** The configured default, when the provider has published it. */
46
+ export declare function getConfiguredHeadless(): boolean | undefined;
47
+ /**
48
+ * Effective window mode: the user's choice, else the configured default, else
49
+ * the shipped default (headful).
50
+ *
51
+ * @param configuredFallback - config default when the provider has not published one
52
+ */
53
+ export declare function getEffectiveHeadless(configuredFallback?: boolean): boolean;
54
+ /** Did the user pick the window mode themselves? (The card shows "你选的".) */
55
+ export declare function isHeadlessUserChosen(): boolean;
36
56
  /**
37
57
  * The page-reading mode the user picked, or undefined when they never picked
38
58
  * one — the configured default then applies.
@@ -12,6 +12,10 @@ function stateFile() {
12
12
  }
13
13
  let _enabled = true;
14
14
  let _engine = undefined;
15
+ /** User's window-mode choice: true = headless, undefined = follow the config. */
16
+ let _headless = undefined;
17
+ /** Config default pushed in by the provider (display only). */
18
+ let _configuredHeadless = undefined;
15
19
  /** Default pushed in by the provider that is actually loaded (display only). */
16
20
  let _configuredEngine = undefined;
17
21
  /** Effective tool gates, pushed in by the tool family when it is applied (display only). */
@@ -33,6 +37,9 @@ function load() {
33
37
  // unknown value means "follow the configured default" (undefined here).
34
38
  // Files written before this setting existed therefore stay valid.
35
39
  _engine = obj.engine === 'legacy' || obj.engine === 'aria' ? obj.engine : undefined;
40
+ // Same contract as engine: absent/unknown means "follow the configured
41
+ // default", so files written before this setting existed stay valid.
42
+ _headless = typeof obj.headless === 'boolean' ? obj.headless : undefined;
36
43
  }
37
44
  }
38
45
  catch {
@@ -60,6 +67,8 @@ function persist() {
60
67
  // Keep the historical shape until the user actually picks a page-reading
61
68
  // mode: an absent `engine` means "use the configured default".
62
69
  const payload = _engine === undefined ? { enabled: _enabled } : { enabled: _enabled, engine: _engine };
70
+ if (_headless !== undefined)
71
+ payload.headless = _headless;
63
72
  fs.writeFileSync(tmp, JSON.stringify(payload, null, 2));
64
73
  fs.renameSync(tmp, target);
65
74
  }
@@ -99,6 +108,50 @@ export function subscribe(fn) {
99
108
  _subscribers.delete(fn);
100
109
  };
101
110
  }
111
+ /**
112
+ * The window mode the user picked (true = headless), or undefined when they
113
+ * never picked one — the configured default then applies.
114
+ */
115
+ export function getHeadless() {
116
+ load();
117
+ return _headless;
118
+ }
119
+ /** Persist the window-mode choice. Returns true when the value changed. */
120
+ export function setHeadless(next) {
121
+ load();
122
+ if (typeof next !== 'boolean')
123
+ return false;
124
+ if (_headless === next)
125
+ return false;
126
+ _headless = next;
127
+ persist();
128
+ return true;
129
+ }
130
+ /** Publish the configured default (launch.headless) for display. */
131
+ export function publishConfiguredHeadless(value) {
132
+ _configuredHeadless = value === true;
133
+ }
134
+ /** The configured default, when the provider has published it. */
135
+ export function getConfiguredHeadless() {
136
+ return _configuredHeadless;
137
+ }
138
+ /**
139
+ * Effective window mode: the user's choice, else the configured default, else
140
+ * the shipped default (headful).
141
+ *
142
+ * @param configuredFallback - config default when the provider has not published one
143
+ */
144
+ export function getEffectiveHeadless(configuredFallback = false) {
145
+ load();
146
+ if (_headless !== undefined)
147
+ return _headless;
148
+ return _configuredHeadless !== undefined ? _configuredHeadless : configuredFallback === true;
149
+ }
150
+ /** Did the user pick the window mode themselves? (The card shows "你选的".) */
151
+ export function isHeadlessUserChosen() {
152
+ load();
153
+ return _headless !== undefined;
154
+ }
102
155
  /**
103
156
  * The page-reading mode the user picked, or undefined when they never picked
104
157
  * one — the configured default then applies.
@@ -64,6 +64,7 @@ import * as runtimeState from "./runtime-state.js";
64
64
  // without a DSH host (a half-written patch layer would break every plugin load).
65
65
  import { homePatchPath, parseGatesBlock, writeGates } from "./gates-patch.js";
66
66
  import { readFileSync } from 'node:fs';
67
+ import { clean, isTargetId, resolvePaths, scan } from "./cache-clean.js";
67
68
  const { getEnabled, setEnabled, getEffectiveEngine, setSnapshotEngine, isEngineUserChosen } = runtimeState;
68
69
  export const name = 'browser-toggle';
69
70
  export const inject = ['webServer'];
@@ -114,7 +115,121 @@ async function readBody(req) {
114
115
  }
115
116
  return { ok: true, text: Buffer.concat(chunks).toString('utf8') };
116
117
  }
118
+ /** The plugin's own version, used to refuse other versions' installers. */
119
+ function installedVersion() {
120
+ try {
121
+ const raw = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
122
+ return typeof raw.version === 'string' ? raw.version : '0.0.0';
123
+ }
124
+ catch {
125
+ return '0.0.0';
126
+ }
127
+ }
128
+ /** Resolved disk locations; never taken from the request body. */
129
+ function diskPaths() {
130
+ return resolvePaths(process.env, installedVersion());
131
+ }
132
+ /**
133
+ * GET /disk/stat — sizes per bucket; POST /disk/clean — preview or execute.
134
+ * Both require the CSRF header, both answer with per-target detail so partial
135
+ * failures are visible instead of being folded into one number.
136
+ */
137
+ function registerDiskRoutes(ctx) {
138
+ ctx.effect(() => ctx.webServer.register({
139
+ kind: 'exact',
140
+ path: PREFIX + '/disk/stat',
141
+ handler: async (req, res) => {
142
+ if (req.method !== 'GET')
143
+ return sendJson(res, 405, { error: { code: 'METHOD', message: 'GET only' } });
144
+ if (req.headers[HEADER] !== 'browser-toggle')
145
+ return sendJson(res, 403, { error: { code: 'CSRF', message: 'missing plugin header' } });
146
+ try {
147
+ const report = await scan(diskPaths());
148
+ sendJson(res, 200, { ...report, browserActive: getEnabled() === true });
149
+ }
150
+ catch (error) {
151
+ sendJson(res, 500, { error: { code: 'DISK_SCAN', message: String(error && error.message ? error.message : error) } });
152
+ }
153
+ },
154
+ }));
155
+ ctx.effect(() => ctx.webServer.register({
156
+ kind: 'exact',
157
+ path: PREFIX + '/disk/clean',
158
+ handler: async (req, res) => {
159
+ if (req.method !== 'POST')
160
+ return sendJson(res, 405, { error: { code: 'METHOD', message: 'POST only' } });
161
+ if (req.headers[HEADER] !== 'browser-toggle')
162
+ return sendJson(res, 403, { error: { code: 'CSRF', message: 'missing plugin header' } });
163
+ const body = await readBody(req);
164
+ if (!body.ok)
165
+ return sendJson(res, 400, { error: { code: 'BODY_READ', message: body.error } });
166
+ let payload;
167
+ try {
168
+ payload = JSON.parse(body.text || '{}');
169
+ }
170
+ catch {
171
+ return sendJson(res, 400, { error: { code: 'INVALID_JSON', message: 'body must be JSON' } });
172
+ }
173
+ if (typeof payload.dryRun !== 'boolean')
174
+ return sendJson(res, 400, { error: { code: 'INVALID_BODY', message: '`dryRun` must be a boolean' } });
175
+ if (!Array.isArray(payload.targets) || payload.targets.length === 0)
176
+ return sendJson(res, 400, { error: { code: 'INVALID_BODY', message: '`targets` must be a non-empty array' } });
177
+ const targets = [];
178
+ for (const value of payload.targets) {
179
+ if (!isTargetId(value))
180
+ return sendJson(res, 400, { error: { code: 'UNKNOWN_TARGET', message: 'unknown target: ' + String(value) } });
181
+ targets.push(value);
182
+ }
183
+ try {
184
+ const report = await clean({ ...diskPaths(), targets, dryRun: payload.dryRun, browserActive: getEnabled() === true });
185
+ sendJson(res, 200, report);
186
+ }
187
+ catch (error) {
188
+ sendJson(res, 500, { error: { code: 'DISK_CLEAN', message: String(error && error.message ? error.message : error) } });
189
+ }
190
+ },
191
+ }));
192
+ }
193
+ /**
194
+ * POST /window { headless } — the user's window-mode switch.
195
+ *
196
+ * The change is persisted immediately and applied by the provider on its next
197
+ * acquire (it compares the mode it launched with against the desired one and
198
+ * relaunches, exporting login state first). Nothing is killed mid-operation.
199
+ */
200
+ function registerWindowRoute(ctx) {
201
+ ctx.effect(() => ctx.webServer.register({
202
+ kind: 'exact',
203
+ path: PREFIX + '/window',
204
+ handler: async (req, res) => {
205
+ if (req.method !== 'POST')
206
+ return sendJson(res, 405, { error: { code: 'METHOD', message: 'POST only' } });
207
+ if (req.headers[HEADER] !== 'browser-toggle')
208
+ return sendJson(res, 403, { error: { code: 'CSRF', message: 'missing plugin header' } });
209
+ const body = await readBody(req);
210
+ if (!body.ok)
211
+ return sendJson(res, 400, { error: { code: 'BODY_READ', message: body.error } });
212
+ let payload;
213
+ try {
214
+ payload = JSON.parse(body.text || '{}');
215
+ }
216
+ catch {
217
+ return sendJson(res, 400, { error: { code: 'INVALID_JSON', message: 'body must be JSON' } });
218
+ }
219
+ if (typeof payload.headless !== 'boolean')
220
+ return sendJson(res, 400, { error: { code: 'INVALID_BODY', message: '`headless` must be a boolean' } });
221
+ const changed = runtimeState.setHeadless(payload.headless);
222
+ sendJson(res, 200, {
223
+ headless: runtimeState.getEffectiveHeadless(),
224
+ changed,
225
+ appliedOn: 'next-operation',
226
+ });
227
+ },
228
+ }));
229
+ }
117
230
  export function apply(ctx) {
231
+ registerDiskRoutes(ctx);
232
+ registerWindowRoute(ctx);
118
233
  ctx.effect(() => ctx.webServer.register({
119
234
  kind: 'exact',
120
235
  path: `${PREFIX}/state`,
@@ -136,6 +251,9 @@ export function apply(ctx) {
136
251
  allowEvaluate: gates ? gates.allowEvaluate : null,
137
252
  allowCdp: gates ? gates.allowCdp : null,
138
253
  diagnostics: gates ? gates.diagnostics : null,
254
+ headless: runtimeState.getEffectiveHeadless(),
255
+ headlessUserChosen: runtimeState.isHeadlessUserChosen(),
256
+ headlessConfigured: runtimeState.getConfiguredHeadless(),
139
257
  // What the user's own layer says right now. Differs from the above
140
258
  // between a write and the live reload that applies it.
141
259
  patchGates: managedBlockGates() ?? null,
@@ -1,5 +1,6 @@
1
- import { CHALLENGE_NOTE, detectChallenge, detectSilentPage } from "./challenge.js";
1
+ import { challengeNote, detectChallenge, detectSilentPage } from "./challenge.js";
2
2
  import { renderSnapshot, renderSnapshotDiff } from "./snapshot-render.js";
3
+ import * as runtimeState from "./runtime-state.js";
3
4
  /**
4
5
  * Render one diagnostics payload as one line per entry. Diagnostics are read when
5
6
  * something already looks wrong, so the compact form matters more than the rich
@@ -134,7 +135,7 @@ export function snapshotValue(snapshot) {
134
135
  ']: ' +
135
136
  challenge.hint +
136
137
  '.\n' +
137
- CHALLENGE_NOTE +
138
+ challengeNote(runtimeState.getEffectiveHeadless(true)) +
138
139
  '\n\n' +
139
140
  tree) +
140
141
  diffBlock,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-browser-playwright-codex",
3
- "version": "0.7.1",
3
+ "version": "0.7.4",
4
4
  "description": "Browser automation for DeepSeek Harness, one plugin: fill in forms, scrape lists and work across tabs in real pages, using the logins you already have. Runs in a visible window that stays quiet when minimised; native dialogs are parked instead of confirmed, CAPTCHA pages are flagged with a note that says stop and hand the page back, password / one-time-code / card fields are refused by browser_fill, and the Settings card (enable/disable, page-reading mode, capability gates) now lives inside this package.",
5
5
  "license": "MIT",
6
6
  "type": "module",