@duckmind/dm-windows-x64 0.61.9 → 0.62.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "status": "ok",
3
- "prepared_at": "2026-08-30T17:14:08.444877+00:00",
3
+ "prepared_at": "2026-08-30T20:04:04.470932+00:00",
4
4
  "managed_entries": [
5
5
  {
6
6
  "id": "dm-context",
@@ -518,8 +518,8 @@
518
518
  "src/utils/system-cmds.mjs"
519
519
  ],
520
520
  "file_count": 62,
521
- "before_bytes": 612563,
522
- "after_bytes": 495187,
521
+ "before_bytes": 613697,
522
+ "after_bytes": 496359,
523
523
  "runtime_specifier_rewrites": 0,
524
524
  "runtime_filename_rewrites": 8,
525
525
  "declaration_files_removed": 0
@@ -764,8 +764,8 @@
764
764
  "src/browser-install.mjs"
765
765
  ],
766
766
  "file_count": 4,
767
- "before_bytes": 35004,
768
- "after_bytes": 35672,
767
+ "before_bytes": 38392,
768
+ "after_bytes": 38745,
769
769
  "runtime_specifier_rewrites": 0,
770
770
  "runtime_filename_rewrites": 0,
771
771
  "declaration_files_removed": 0
@@ -11,11 +11,16 @@ Usage:
11
11
  node browser-cua.mjs click_xy --x <num> --y <num> [--tab <prefix>] [--json]
12
12
  node browser-cua.mjs type --text <text> [--tab <prefix>] [--json]
13
13
  node browser-cua.mjs evaluate --expression <js> [--tab <prefix>] [--json]
14
+ node browser-cua.mjs handoff [--tab <prefix>] [--json]
14
15
  node browser-cua.mjs stop [--json]
15
16
 
16
17
  Notes:
17
18
  - This helper reuses DM's bundled greedysearch-dm CDP sidecar.
18
- - Fresh dedicated browser profiles auto-bootstrap a blank tab.
19
+ - Fresh dedicated browser profiles auto-bootstrap a blank tab.
20
+ - handoff opens the dedicated browser visibly for user-authorized login or
21
+ verification. DM does not solve challenges or enter credentials for you.
22
+ - Profiles persist by project under ~/.dm/agent/cua/profiles by default;
23
+ set DM_CUA_EPHEMERAL=1 for a disposable profile.
19
24
  - Default browser is Chrome/Chromium. If absent, DM attempts a non-interactive
20
25
  package-manager install; set DM_CUA_AUTO_INSTALL=0 to opt out.
21
26
  - To try CloakBrowser, set DM_CUA_BROWSER=cloak
@@ -15,9 +15,10 @@ const browserActionSchema = Type.Object({
15
15
  Type.Literal("click_xy"),
16
16
  Type.Literal("type"),
17
17
  Type.Literal("evaluate"),
18
+ Type.Literal("handoff"),
18
19
  Type.Literal("stop")
19
20
  ], {
20
- description: "Browser action. Start with list, snapshot, or screenshot before mutating actions on unfamiliar pages."
21
+ description: "Browser action. Start with list, snapshot, or screenshot before mutating actions on unfamiliar pages. Use handoff to show the retained DM browser window for user-authorized login or verification."
21
22
  }),
22
23
  tab: Type.Optional(Type.String({
23
24
  description: "Optional target tab prefix from browser_cua(list). If omitted, the first live page in the dedicated profile is used."
@@ -41,7 +42,7 @@ export default function registerDmCua(pi) {
41
42
  pi.registerTool({
42
43
  name: "browser_cua",
43
44
  label: "Browser CUA",
44
- description: "Drive a dedicated Chrome browser profile through DM's bundled browser CUA lane. " + "If Chrome/Chromium is absent, DM attempts a non-interactive OS package-manager install before launch. " + "Inspect first with list, snapshot, or screenshot. Fresh empty profiles auto-bootstrap a blank tab. " + "Use navigate for deterministic URLs, click with a standard CSS selector/click_xy/type for page interaction, evaluate for focused DOM reads or text matching, and stop to close the dedicated browser lane.",
45
+ description: "Drive a dedicated Chrome browser profile through DM's bundled browser CUA lane. " + "If Chrome/Chromium is absent, DM attempts a non-interactive OS package-manager install before launch. " + "Inspect first with list, snapshot, or screenshot. Fresh empty profiles auto-bootstrap a blank tab. " + "Use handoff when a site needs you to complete a login or verification in the visible browser; DM never solves challenges or enters credentials. " + "Use navigate for deterministic URLs, click with a standard CSS selector/click_xy/type for page interaction, evaluate for focused DOM reads or text matching, and stop to close the dedicated browser lane.",
45
46
  promptSnippet: "First-class browser CUA for Chrome with automatic browser bootstrap. Inspect before acting. Screenshot complex pages before click/scroll loops.",
46
47
  parameters: browserActionSchema,
47
48
  async execute(_toolCallId, params) {
@@ -44,7 +44,8 @@ export function resolveBrowserRuntimeConfig(env = process.env, scope = process.c
44
44
  const browserExecutablePath = resolveBrowserExecutablePath(env, browserKind);
45
45
  const port = parsePort(env.DM_CUA_PORT) ?? parsePort(env.GREEDY_SEARCH_PORT) ?? DEFAULT_CUA_PORT_BASE + Number.parseInt(scopeHash.slice(0, 8), 16) % DEFAULT_CUA_PORT_RANGE;
46
46
  const profileSuffix = browserKind === "chrome" ? "chrome" : browserKind;
47
- const profileDir = resolve(env.DM_CUA_PROFILE_DIR || join(tmpdir(), `dm-cua-${profileSuffix}-profile-${instanceId}`));
47
+ const defaultProfileDir = env.DM_CUA_EPHEMERAL === "1" ? join(tmpdir(), `dm-cua-${profileSuffix}-profile-${instanceId}`) : join(resolveAgentDir(env), "cua", "profiles", `${profileSuffix}-${scopeHash}`);
48
+ const profileDir = resolve(env.DM_CUA_PROFILE_DIR || defaultProfileDir);
48
49
  const socketBaseDir = platform() === "win32" ? tmpdir() : "/tmp";
49
50
  return {
50
51
  instanceId,
@@ -133,8 +134,8 @@ export function resolveBrowserHelperPaths() {
133
134
  }
134
135
  return candidates[0];
135
136
  }
136
- export function resolveBrowserProcessEnv(config = BROWSER_CONFIG, env = process.env) {
137
- const visible = env.GREEDY_SEARCH_VISIBLE ?? env.DM_CUA_VISIBLE ?? env.DM_BROWSER_CUA_VISIBLE;
137
+ export function resolveBrowserProcessEnv(config = BROWSER_CONFIG, env = process.env, { visible } = {}) {
138
+ const visibleMode = visible === true ? "1" : visible === false ? "0" : env.GREEDY_SEARCH_VISIBLE ?? env.DM_CUA_VISIBLE ?? env.DM_BROWSER_CUA_VISIBLE;
138
139
  return {
139
140
  ...env,
140
141
  ...config.browserExecutablePath ? { CHROME_PATH: config.browserExecutablePath } : {},
@@ -147,11 +148,11 @@ export function resolveBrowserProcessEnv(config = BROWSER_CONFIG, env = process.
147
148
  GREEDY_SEARCH_PID_FILE: config.pidFile.replace(/\\/g, "/"),
148
149
  GREEDY_SEARCH_MODE_FILE: config.modeFile.replace(/\\/g, "/"),
149
150
  GREEDY_SEARCH_ALLOW_PORT_CLEANUP: env.GREEDY_SEARCH_ALLOW_PORT_CLEANUP ?? "0",
150
- ...visible === undefined ? {} : { GREEDY_SEARCH_VISIBLE: visible }
151
+ ...visibleMode === undefined ? {} : { GREEDY_SEARCH_VISIBLE: visibleMode }
151
152
  };
152
153
  }
153
- function browserEnv() {
154
- return resolveBrowserProcessEnv(BROWSER_CONFIG, process.env);
154
+ function browserEnv(options) {
155
+ return resolveBrowserProcessEnv(BROWSER_CONFIG, process.env, options);
155
156
  }
156
157
  function looksLikeUrl(value) {
157
158
  return URLISH_PATTERN.test(value);
@@ -184,8 +185,8 @@ export function validateBrowserSelector(selector) {
184
185
  }
185
186
  return value;
186
187
  }
187
- function resolveAgentDir() {
188
- const explicit = process.env.DM_CODING_AGENT_DIR || process.env.PI_CODING_AGENT_DIR;
188
+ function resolveAgentDir(env = process.env) {
189
+ const explicit = env.DM_CODING_AGENT_DIR || env.PI_CODING_AGENT_DIR;
189
190
  return explicit ? resolve(explicit) : join(homedir(), ".dm", "agent");
190
191
  }
191
192
  function defaultScreenshotPath() {
@@ -269,7 +270,10 @@ async function createBlankTarget(url = "about:blank") {
269
270
  }
270
271
  async function runCdp(args, options = {}) {
271
272
  const { cdpScript } = resolveBrowserHelperPaths();
272
- return runNodeScript(cdpScript, args, { env: browserEnv(), timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS });
273
+ return runNodeScript(cdpScript, args, {
274
+ env: browserEnv({ visible: options.visible }),
275
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS
276
+ });
273
277
  }
274
278
  async function listPages() {
275
279
  const result = await runCdp(["list"]);
@@ -288,10 +292,13 @@ async function ensureBrowserHelpersExist() {
288
292
  }
289
293
  return { launchScript, cdpScript };
290
294
  }
291
- export async function ensureBrowserReady() {
295
+ export async function ensureBrowserReady({ visible = false } = {}) {
292
296
  const { launchScript } = await ensureBrowserHelpersExist();
293
297
  await ensureConfiguredBrowserSelectionUsable();
294
- await runNodeScript(launchScript, [], { env: browserEnv(), timeoutMs: DEFAULT_TIMEOUT_MS });
298
+ await runNodeScript(launchScript, [], {
299
+ env: browserEnv({ visible }),
300
+ timeoutMs: DEFAULT_TIMEOUT_MS
301
+ });
295
302
  let pages = await listPages();
296
303
  if (pages.length === 0) {
297
304
  await createBlankTarget("about:blank");
@@ -310,6 +317,17 @@ async function resolveTabContext(tabPrefix) {
310
317
  }
311
318
  return { tab: page.target, page, pages };
312
319
  }
320
+ export function classifyHumanInteraction(value) {
321
+ const text = String(value ?? "");
322
+ const match = text.match(/cloudflare|turnstile|recaptcha|hcaptcha|aws waf|geetest|verify (?:you are )?human|just a moment|checking your browser|login required|sign[ -]?in required|authentication required/i);
323
+ return match ? {
324
+ needed: true,
325
+ reason: match[0]
326
+ } : {
327
+ needed: false,
328
+ reason: null
329
+ };
330
+ }
313
331
  export async function runBrowserAction({
314
332
  action,
315
333
  tab,
@@ -347,6 +365,7 @@ export async function runBrowserAction({
347
365
  case "snapshot": {
348
366
  const context = await resolveTabContext(tab);
349
367
  const result = await runCdp(["snap", context.tab]);
368
+ const snapshot = result.stdout.trim();
350
369
  return {
351
370
  status: "ok",
352
371
  action,
@@ -354,7 +373,33 @@ export async function runBrowserAction({
354
373
  tab: context.tab,
355
374
  title: context.page?.title || "",
356
375
  url: context.page?.url || "",
357
- snapshot: result.stdout.trim()
376
+ snapshot,
377
+ humanInteraction: classifyHumanInteraction(`${context.page?.title || ""}
378
+ ${context.page?.url || ""}
379
+ ${snapshot}`)
380
+ };
381
+ }
382
+ case "handoff": {
383
+ try {
384
+ await runCdp(["stop"]);
385
+ } catch {}
386
+ const pages = await ensureBrowserReady({ visible: true });
387
+ const page = findPage(pages, tab);
388
+ if (!page) {
389
+ throw new Error(tab ? `No browser tab matching prefix "${tab}". Run browser_cua(list) first.` : "No browser tab available after browser bootstrap.");
390
+ }
391
+ const snapshot = await runCdp(["snap", page.target], { visible: true }).then((result) => result.stdout.trim());
392
+ return {
393
+ status: "ok",
394
+ action,
395
+ browser: browserSummary(),
396
+ tab: page.target,
397
+ title: page.title || "",
398
+ url: page.url || "",
399
+ humanInteraction: classifyHumanInteraction(`${page.title || ""}
400
+ ${page.url || ""}
401
+ ${snapshot}`),
402
+ message: "Opened the dedicated DM browser window for human interaction. Complete only the site interaction you authorize, then return to DM; this scoped profile is retained for future CUA sessions."
358
403
  };
359
404
  }
360
405
  case "screenshot": {
@@ -465,7 +510,18 @@ export function renderBrowserActionText(result) {
465
510
  return [`Navigated browser tab ${result.tab} to ${result.url || "(unknown URL)"}`, result.title ? `Title: ${result.title}` : ""].filter(Boolean).join(`
466
511
  `);
467
512
  case "snapshot":
468
- return [`Browser snapshot for ${result.tab}`, result.snapshot || "(empty snapshot)"].join(`
513
+ return [
514
+ `Browser snapshot for ${result.tab}`,
515
+ result.snapshot || "(empty snapshot)",
516
+ result.humanInteraction?.needed ? `Human interaction may be required: ${result.humanInteraction.reason}. Use browser_cua(handoff) to open the DM browser window.` : ""
517
+ ].filter(Boolean).join(`
518
+ `);
519
+ case "handoff":
520
+ return [
521
+ result.message,
522
+ result.humanInteraction?.needed ? `Detected: ${result.humanInteraction.reason}.` : "No known verification marker was detected; the visible browser remains available if the page still needs you.",
523
+ "DM does not solve challenges or enter credentials for you."
524
+ ].join(`
469
525
  `);
470
526
  case "screenshot":
471
527
  return [
@@ -647,6 +647,15 @@ async function runDaemon(targetId) {
647
647
  }
648
648
  });
649
649
  });
650
+ server.once("error", (error) => {
651
+ process.stderr.write(`Daemon: socket listener failed at ${sp}: ${error.message}
652
+ `);
653
+ try {
654
+ unlinkSync(sp);
655
+ } catch {}
656
+ cdp.close();
657
+ process.exit(1);
658
+ });
650
659
  try {
651
660
  unlinkSync(sp);
652
661
  } catch {}
@@ -669,21 +678,37 @@ async function getOrStartTabDaemon(targetId) {
669
678
  } catch {}
670
679
  const child = spawn(nodeRuntimeCommand(), [process.argv[1], "_daemon", targetId], {
671
680
  detached: true,
672
- stdio: "ignore"
681
+ stdio: ["ignore", "ignore", "pipe"]
682
+ });
683
+ let daemonStderr = "";
684
+ let daemonExit = "";
685
+ child.stderr?.on("data", (chunk) => {
686
+ daemonStderr = `${daemonStderr}${chunk}`.slice(-2048);
687
+ });
688
+ child.on("error", (error) => {
689
+ daemonExit = error.message;
690
+ });
691
+ child.on("exit", (code, signal) => {
692
+ daemonExit = signal ? `terminated by ${signal}` : `exited with code ${code}`;
673
693
  });
674
694
  child.unref();
695
+ child.stderr?.unref?.();
675
696
  const backoffSchedule = [50, 100, 200];
676
697
  const deadline = Date.now() + DAEMON_CONNECT_RETRIES * DAEMON_CONNECT_DELAY;
677
698
  let attempt = 0;
678
699
  while (Date.now() < deadline) {
679
700
  try {
680
- return await connectToSocket(sp);
701
+ const conn = await connectToSocket(sp);
702
+ child.stderr?.destroy();
703
+ return conn;
681
704
  } catch {}
682
705
  const delay = backoffSchedule[attempt] ?? DAEMON_CONNECT_DELAY;
683
706
  attempt++;
684
707
  await sleep(delay);
685
708
  }
686
- throw new Error("Daemon failed to start — did you click Allow in Chrome?");
709
+ child.stderr?.destroy();
710
+ const diagnostic = (daemonStderr || daemonExit || "no startup diagnostic was emitted").replace(/\s+/g, " ").trim();
711
+ throw new Error(`Tab daemon did not become ready for ${targetId.slice(0, MIN_TARGET_PREFIX_LEN)}. ${diagnostic}. ` + "Keep the dedicated DM browser open and retry; use browser_cua(handoff) if the site needs your interaction.");
687
712
  }
688
713
  function sendCommand(conn, req) {
689
714
  return new Promise((resolve, reject) => {
@@ -9,7 +9,7 @@ import {
9
9
  } from "node:fs";
10
10
  import { platform, tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
- import { httpGet, minimizeViaCDP } from "../src/search/minimize.mjs";
12
+ import { httpGet } from "../src/search/minimize.mjs";
13
13
  import {
14
14
  ACTIVE_PORT_FILE,
15
15
  CHROME_MODE_FILE,
@@ -275,8 +275,7 @@ async function main() {
275
275
  if (isHeadless()) {
276
276
  console.log("Ready (headless).");
277
277
  } else {
278
- await minimizeViaCDP(PORT);
279
- console.log("Ready.");
278
+ console.log("Ready (visible).");
280
279
  }
281
280
  }
282
281
  main();
@@ -1,5 +1,6 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
+ import { homedir, platform } from "node:os";
3
4
  import { join } from "node:path";
4
5
  import { tmpdir } from "node:os";
5
6
  export function parseGreedySearchPort(value, fallback = 9222) {
@@ -16,7 +17,9 @@ export const CHROME_MODE_FILE = process.env.GREEDY_SEARCH_MODE_FILE || `${GREEDY
16
17
  export const GREEDY_METADATA_FILE = process.env.GREEDY_SEARCH_METADATA_FILE || `${GREEDY_PROFILE_DIR}/browser-metadata.json`;
17
18
  export const GREEDY_LAUNCH_LOCK_FILE = process.env.GREEDY_SEARCH_LAUNCH_LOCK_FILE || `${GREEDY_PROFILE_DIR}/browser-launch.lock`;
18
19
  export const GREEDY_ACTIVITY_FILE = process.env.GREEDY_SEARCH_ACTIVITY_FILE || `${GREEDY_PROFILE_DIR}/browser-last-activity`;
19
- export const GREEDY_CDP_SOCKET_DIR = (process.env.CDP_SOCKET_DIR || `${GREEDY_PROFILE_DIR}/cdp-sockets`).replaceAll("\\", "/");
20
+ const GREEDY_SOCKET_SCOPE = createHash("sha256").update(GREEDY_PROFILE_DIR).digest("hex").slice(0, 16);
21
+ const GREEDY_SOCKET_BASE = platform() === "win32" ? GREEDY_TMP : "/tmp";
22
+ export const GREEDY_CDP_SOCKET_DIR = (process.env.CDP_SOCKET_DIR || `${GREEDY_SOCKET_BASE}/dm-gs-${GREEDY_SOCKET_SCOPE}`).replaceAll("\\", "/");
20
23
  export const VISIBLE_RECOVERY_LOG = `${GREEDY_PROFILE_DIR}/visible-recovery.jsonl`;
21
24
  const CONFIG_DIR = join(homedir(), ".dm");
22
25
  const CONFIG_FILE = join(CONFIG_DIR, "greedyconfig");
@@ -19,7 +19,7 @@ import { runSimpleResearchMode } from "./simple-research.mjs";
19
19
  import { createProgressTracker } from "./progress.mjs";
20
20
  const __dir = fileURLToPath(new URL(".", import.meta.url)).replace(/^\/([A-Z]:)/, "$1");
21
21
  const SEARCH_BIN = join(__dir, "..", "..", "bin", "search.mjs");
22
- const DEFAULT_RESEARCH_BUNDLE_ROOT = join(process.cwd(), ".pi", "greedysearch-research");
22
+ const DEFAULT_RESEARCH_BUNDLE_ROOT = join(process.cwd(), ".dm", "greedysearch-research");
23
23
  const MAX_PROMPT_CHARS = 28000;
24
24
  function slugifyResearchName(value) {
25
25
  const slug = String(value || "research").toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-|-$/g, "").slice(0, 60);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duckmind/dm-windows-x64",
3
- "version": "0.61.9",
3
+ "version": "0.62.3",
4
4
  "description": "DuckMind (dm) binary payload for windows x64",
5
5
  "license": "MIT",
6
6
  "os": [