@duckmind/dm-darwin-x64 0.35.9 → 0.36.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.
package/dm CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "status": "ok",
3
- "prepared_at": "2026-06-06T16:01:10.933885+00:00",
3
+ "prepared_at": "2026-06-08T01:10:30.489829+00:00",
4
4
  "managed_entries": [
5
5
  {
6
6
  "id": "dm-context",
@@ -4,14 +4,21 @@ Browser-first Computer Use extension for DM.
4
4
 
5
5
  What it does:
6
6
 
7
- - launches or reuses a dedicated Chrome profile via the bundled `greedysearch-dm` CDP helpers
7
+ - launches or reuses a dedicated browser profile via the bundled `greedysearch-dm` CDP helpers
8
8
  - bootstraps a blank tab on fresh empty profiles so browser automation does not fail on the first run
9
9
  - exposes one tool, `browser_cua`, for list / navigate / snapshot / screenshot / click / type / evaluate / stop
10
10
  - stores browser screenshots under `~/.dm/agent/cua/screenshots/` by default
11
11
 
12
+ Browser selection:
13
+
14
+ - default: Chrome/Chromium auto-detected by `greedysearch-dm`
15
+ - opt-in CloakBrowser trial: set `DM_CUA_BROWSER=cloak` and one of `DM_CUA_BROWSER_PATH`, `DM_CUA_CLOAK_PATH`, `CLOAKBROWSER_BINARY_PATH`, or `CHROME_PATH` to the CloakBrowser Chromium executable
16
+ - DM does not bundle or auto-download the compiled CloakBrowser binary; install/pre-download it outside DM first because upstream permits use but not redistribution
17
+ - Current `browser_cua` actions still use CDP commands through `greedysearch-dm`; full CloakBrowser stealth parity would require a future Playwright-backed CUA lane.
18
+
12
19
  What it does **not** do yet:
13
20
 
14
21
  - it does not replace the macOS `computer-use` Codex plugin lane
15
22
  - it does not replace Android `adbridge`
16
23
 
17
- Those broader lanes are still tracked separately. This extension closes the most actionable CUA gap from the current DM snapshot: a first-class browser lane that works from a fresh dedicated Chrome profile instead of overloading `coding_task`.
24
+ Those broader lanes are still tracked separately. This extension closes the most actionable CUA gap from the current DM snapshot: a first-class browser lane that works from a fresh dedicated browser profile instead of overloading `coding_task`.
@@ -16,10 +16,12 @@ Usage:
16
16
  node browser-cua.mjs evaluate --expression <js> [--tab <prefix>] [--json]
17
17
  node browser-cua.mjs stop [--json]
18
18
 
19
- Notes:
20
- - This helper reuses DM's bundled greedysearch-dm CDP sidecar.
21
- - Fresh dedicated Chrome profiles auto-bootstrap a blank tab.
22
- - screenshot defaults to ~/.dm/agent/cua/screenshots when --output is omitted.`);
19
+ Notes:
20
+ - This helper reuses DM's bundled greedysearch-dm CDP sidecar.
21
+ - Fresh dedicated browser profiles auto-bootstrap a blank tab.
22
+ - Default browser is Chrome/Chromium. To try CloakBrowser, set DM_CUA_BROWSER=cloak
23
+ and point DM_CUA_BROWSER_PATH or CLOAKBROWSER_BINARY_PATH at its Chromium binary.
24
+ - screenshot defaults to ~/.dm/agent/cua/screenshots when --output is omitted.`);
23
25
  }
24
26
 
25
27
  function parseArgs(argv) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dm-cua",
3
3
  "version": "0.1.0",
4
- "description": "First-class browser CUA extension for DM using a dedicated Chrome CDP profile",
4
+ "description": "First-class browser CUA extension for DM using a dedicated Chrome/CDP-compatible profile",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "index.js",
@@ -24,6 +24,7 @@
24
24
  "browser",
25
25
  "cua",
26
26
  "chrome",
27
+ "cloakbrowser",
27
28
  "automation"
28
29
  ],
29
30
  "peerDependencies": {
@@ -15,4 +15,5 @@ Rules:
15
15
  - Start with an inspect action before click/type on unfamiliar pages.
16
16
  - Prefer `navigate` for deterministic URLs, including search URLs.
17
17
  - Use `screenshot` before complex click loops or when spatial layout matters.
18
- - Fresh Chrome profiles auto-bootstrap a blank tab, so the first browser action should not fail just because the page list is empty.
18
+ - Fresh dedicated browser profiles auto-bootstrap a blank tab, so the first browser action should not fail just because the page list is empty.
19
+ - Default browser is the bundled Chrome/Chromium-compatible CDP lane. To evaluate CloakBrowser, install/pre-download CloakBrowser outside DM, then run with `DM_CUA_BROWSER=cloak` and `DM_CUA_BROWSER_PATH` or `CLOAKBROWSER_BINARY_PATH` pointing at its Chromium executable. DM must not bundle the compiled CloakBrowser binary because upstream permits use but not redistribution.
@@ -12,6 +12,10 @@ const DEFAULT_CUA_PORT_BASE = 9322;
12
12
  const DEFAULT_CUA_PORT_RANGE = 400;
13
13
  const DEFAULT_TIMEOUT_MS = 60000;
14
14
  const URLISH_PATTERN = /^(about:|https?:\/\/|file:|chrome-error:\/\/)/i;
15
+ const BROWSER_DISPLAY_NAMES = {
16
+ chrome: "Chrome",
17
+ cloak: "CloakBrowser",
18
+ };
15
19
 
16
20
  function parsePort(value) {
17
21
  const port = Number.parseInt(String(value ?? ""), 10);
@@ -22,18 +26,40 @@ function hashScope(value) {
22
26
  return createHash("sha256").update(String(value || "default")).digest("hex");
23
27
  }
24
28
 
29
+ function normalizeBrowserKind(value) {
30
+ const raw = String(value || "chrome").trim().toLowerCase();
31
+ if (raw === "cloakbrowser" || raw === "cloak-browser") return "cloak";
32
+ if (raw === "chromium" || raw === "google-chrome" || raw === "chrome") return "chrome";
33
+ return raw.replace(/[^a-z0-9_.-]/g, "-") || "chrome";
34
+ }
35
+
36
+ function resolveBrowserExecutablePath(env, browserKind) {
37
+ const explicit =
38
+ env.DM_CUA_BROWSER_PATH
39
+ || (browserKind === "cloak" ? env.DM_CUA_CLOAK_PATH || env.CLOAKBROWSER_BINARY_PATH : undefined)
40
+ || env.CHROME_PATH;
41
+ return explicit ? resolve(String(explicit)) : undefined;
42
+ }
43
+
25
44
  export function resolveBrowserRuntimeConfig(env = process.env, scope = process.cwd()) {
26
45
  const rawScope = env.DM_CUA_SCOPE || env.DM_CODING_AGENT_DIR || env.PI_CODING_AGENT_DIR || scope;
27
46
  const scopeHash = hashScope(resolve(String(rawScope || ".")));
28
47
  const instanceId = String(env.DM_CUA_INSTANCE_ID || scopeHash.slice(0, 12)).replace(/[^a-zA-Z0-9_.-]/g, "-");
48
+ const browserKind = normalizeBrowserKind(env.DM_CUA_BROWSER || env.DM_BROWSER_CUA_BROWSER || env.DM_CUA_BROWSER_KIND);
49
+ const browserDisplayName = env.DM_CUA_BROWSER_DISPLAY_NAME || BROWSER_DISPLAY_NAMES[browserKind] || browserKind;
50
+ const browserExecutablePath = resolveBrowserExecutablePath(env, browserKind);
29
51
  const port =
30
52
  parsePort(env.DM_CUA_PORT)
31
53
  ?? parsePort(env.GREEDY_SEARCH_PORT)
32
54
  ?? DEFAULT_CUA_PORT_BASE + (Number.parseInt(scopeHash.slice(0, 8), 16) % DEFAULT_CUA_PORT_RANGE);
33
- const profileDir = resolve(env.DM_CUA_PROFILE_DIR || join(tmpdir(), `dm-cua-chrome-profile-${instanceId}`));
55
+ const profileSuffix = browserKind === "chrome" ? "chrome" : browserKind;
56
+ const profileDir = resolve(env.DM_CUA_PROFILE_DIR || join(tmpdir(), `dm-cua-${profileSuffix}-profile-${instanceId}`));
34
57
  const socketBaseDir = platform() === "win32" ? tmpdir() : "/tmp";
35
58
  return {
36
59
  instanceId,
60
+ browserKind,
61
+ browserDisplayName,
62
+ browserExecutablePath,
37
63
  port,
38
64
  profileDir,
39
65
  pidFile: join(profileDir, "chrome.pid"),
@@ -48,6 +74,31 @@ const CHROME_PORT = BROWSER_CONFIG.port;
48
74
 
49
75
  export const BROWSER_PROFILE_DIR = BROWSER_CONFIG.profileDir;
50
76
 
77
+ function browserSummary() {
78
+ return {
79
+ kind: BROWSER_CONFIG.browserKind,
80
+ displayName: BROWSER_CONFIG.browserDisplayName,
81
+ executablePath: BROWSER_CONFIG.browserExecutablePath,
82
+ profileDir: BROWSER_CONFIG.profileDir,
83
+ port: BROWSER_CONFIG.port,
84
+ };
85
+ }
86
+
87
+ function ensureBrowserSelectionUsable() {
88
+ if (BROWSER_CONFIG.browserKind === "cloak" && !BROWSER_CONFIG.browserExecutablePath) {
89
+ throw new Error(
90
+ [
91
+ "CloakBrowser selected for browser CUA, but no executable path was provided.",
92
+ "Install/pre-download CloakBrowser yourself, then set DM_CUA_BROWSER_PATH, DM_CUA_CLOAK_PATH, CLOAKBROWSER_BINARY_PATH, or CHROME_PATH.",
93
+ "DM does not bundle the compiled CloakBrowser binary because its upstream license allows use but not redistribution.",
94
+ ].join(" "),
95
+ );
96
+ }
97
+ if (BROWSER_CONFIG.browserExecutablePath && !existsSync(BROWSER_CONFIG.browserExecutablePath)) {
98
+ throw new Error(`${BROWSER_CONFIG.browserDisplayName} executable not found: ${BROWSER_CONFIG.browserExecutablePath}`);
99
+ }
100
+ }
101
+
51
102
  export function resolveBrowserHelperPaths() {
52
103
  return {
53
104
  launchScript: fileURLToPath(new URL("../../greedysearch-dm/bin/launch.mjs", import.meta.url)),
@@ -63,9 +114,11 @@ function browserEnv() {
63
114
  "1";
64
115
  return {
65
116
  ...process.env,
117
+ ...(BROWSER_CONFIG.browserExecutablePath ? { CHROME_PATH: BROWSER_CONFIG.browserExecutablePath } : {}),
66
118
  CDP_PROFILE_DIR: BROWSER_CONFIG.profileDir.replace(/\\/g, "/"),
67
119
  CDP_PAGES_CACHE: BROWSER_CONFIG.pagesCache.replace(/\\/g, "/"),
68
120
  CDP_SOCKET_DIR: BROWSER_CONFIG.socketDir.replace(/\\/g, "/"),
121
+ GREEDY_SEARCH_BROWSER_LABEL: BROWSER_CONFIG.browserDisplayName,
69
122
  GREEDY_SEARCH_PORT: String(BROWSER_CONFIG.port),
70
123
  GREEDY_SEARCH_PROFILE_DIR: BROWSER_CONFIG.profileDir.replace(/\\/g, "/"),
71
124
  GREEDY_SEARCH_PID_FILE: BROWSER_CONFIG.pidFile.replace(/\\/g, "/"),
@@ -171,7 +224,7 @@ function requestJson(method, requestPath) {
171
224
  });
172
225
  response.on("end", () => {
173
226
  if ((response.statusCode ?? 500) >= 400) {
174
- rejectPromise(new Error(`Chrome CDP HTTP ${response.statusCode}: ${body.trim()}`));
227
+ rejectPromise(new Error(`Browser CDP HTTP ${response.statusCode}: ${body.trim()}`));
175
228
  return;
176
229
  }
177
230
  try {
@@ -216,6 +269,7 @@ async function ensureBrowserHelpersExist() {
216
269
  }
217
270
 
218
271
  export async function ensureBrowserReady() {
272
+ ensureBrowserSelectionUsable();
219
273
  const { launchScript } = await ensureBrowserHelpersExist();
220
274
  await runNodeScript(launchScript, [], { env: browserEnv(), timeoutMs: DEFAULT_TIMEOUT_MS });
221
275
  let pages = await listPages();
@@ -252,7 +306,7 @@ export async function runBrowserAction({
252
306
  switch (action) {
253
307
  case "list": {
254
308
  const pages = await ensureBrowserReady();
255
- return { status: "ok", action, pages };
309
+ return { status: "ok", action, browser: browserSummary(), pages };
256
310
  }
257
311
  case "navigate": {
258
312
  if (!url) throw new Error("url is required for action=navigate");
@@ -263,6 +317,7 @@ export async function runBrowserAction({
263
317
  return {
264
318
  status: "ok",
265
319
  action,
320
+ browser: browserSummary(),
266
321
  tab: context.tab,
267
322
  url: page?.url || url,
268
323
  title: page?.title || "",
@@ -276,6 +331,7 @@ export async function runBrowserAction({
276
331
  return {
277
332
  status: "ok",
278
333
  action,
334
+ browser: browserSummary(),
279
335
  tab: context.tab,
280
336
  title: context.page?.title || "",
281
337
  url: context.page?.url || "",
@@ -291,6 +347,7 @@ export async function runBrowserAction({
291
347
  return {
292
348
  status: "ok",
293
349
  action,
350
+ browser: browserSummary(),
294
351
  tab: context.tab,
295
352
  title: context.page?.title || "",
296
353
  url: context.page?.url || "",
@@ -307,6 +364,7 @@ export async function runBrowserAction({
307
364
  return {
308
365
  status: "ok",
309
366
  action,
367
+ browser: browserSummary(),
310
368
  tab: context.tab,
311
369
  selector,
312
370
  message: result.stdout.trim(),
@@ -321,6 +379,7 @@ export async function runBrowserAction({
321
379
  return {
322
380
  status: "ok",
323
381
  action,
382
+ browser: browserSummary(),
324
383
  tab: context.tab,
325
384
  x,
326
385
  y,
@@ -334,6 +393,7 @@ export async function runBrowserAction({
334
393
  return {
335
394
  status: "ok",
336
395
  action,
396
+ browser: browserSummary(),
337
397
  tab: context.tab,
338
398
  textLength: text.length,
339
399
  message: result.stdout.trim(),
@@ -346,6 +406,7 @@ export async function runBrowserAction({
346
406
  return {
347
407
  status: "ok",
348
408
  action,
409
+ browser: browserSummary(),
349
410
  tab: context.tab,
350
411
  value: result.stdout.trim(),
351
412
  };
@@ -361,6 +422,7 @@ export async function runBrowserAction({
361
422
  return {
362
423
  status: "ok",
363
424
  action,
425
+ browser: browserSummary(),
364
426
  message: (result.stdout || "Stopped browser CUA lane.").trim(),
365
427
  };
366
428
  }
@@ -63,4 +63,20 @@ test("resolveBrowserRuntimeConfig accepts explicit safe overrides", () => {
63
63
  assert.equal(config.port, 9456);
64
64
  assert.equal(config.instanceId, "session-one");
65
65
  assert.equal(config.profileDir, "/tmp/custom-dm-cua-profile");
66
+ assert.equal(config.browserKind, "chrome");
67
+ assert.equal(config.browserDisplayName, "Chrome");
68
+ });
69
+
70
+ test("resolveBrowserRuntimeConfig supports opt-in CloakBrowser executable path", () => {
71
+ const config = resolveBrowserRuntimeConfig(
72
+ {
73
+ DM_CUA_BROWSER: "cloakbrowser",
74
+ CLOAKBROWSER_BINARY_PATH: "/opt/cloak/Chromium.app/Contents/MacOS/Chromium",
75
+ },
76
+ "/tmp/dm-cua-project-c",
77
+ );
78
+ assert.equal(config.browserKind, "cloak");
79
+ assert.equal(config.browserDisplayName, "CloakBrowser");
80
+ assert.equal(config.browserExecutablePath, "/opt/cloak/Chromium.app/Contents/MacOS/Chromium");
81
+ assert.match(config.profileDir, /dm-cua-cloak-profile-[a-f0-9]{12}$/);
66
82
  });
@@ -87,7 +87,7 @@ describe("registerCommands", () => {
87
87
  );
88
88
  });
89
89
 
90
- it("returns sync-only autocomplete and hides account identifiers", () => {
90
+ it("returns restored autocomplete while hiding repair-only account identifiers", () => {
91
91
  const registerCommand = vi.fn();
92
92
  registerCommands(
93
93
  { registerCommand } as never,
@@ -102,21 +102,29 @@ describe("registerCommands", () => {
102
102
  };
103
103
 
104
104
  const subcommands = commandOptions.getArgumentCompletions("");
105
- expect(subcommands?.map((item) => item.value)).toEqual(["sync"]);
105
+ expect(subcommands?.map((item) => item.value)).toEqual([
106
+ "sync",
107
+ "use",
108
+ "show",
109
+ "rotation",
110
+ "verify",
111
+ "path",
112
+ "reset",
113
+ "help",
114
+ ]);
106
115
  expect(subcommands?.map((item) => item.value)).not.toContain("accounts");
107
- expect(subcommands?.map((item) => item.value)).not.toContain("show");
108
- expect(subcommands?.map((item) => item.value)).not.toContain("use");
109
116
  expect(subcommands?.map((item) => item.value)).not.toContain("refresh");
110
- expect(subcommands?.map((item) => item.value)).not.toContain("rotation");
111
117
  expect(subcommands?.map((item) => item.value)).not.toContain("reauth");
112
118
  expect(subcommands?.map((item) => item.value)).not.toContain("footer");
113
- expect(subcommands?.map((item) => item.value)).not.toContain("verify");
114
- expect(subcommands?.map((item) => item.value)).not.toContain("path");
115
- expect(subcommands?.map((item) => item.value)).not.toContain("reset");
116
- expect(subcommands?.map((item) => item.value)).not.toContain("help");
117
119
 
118
120
  const useAccounts = commandOptions.getArgumentCompletions("use a");
119
- expect(useAccounts).toBeNull();
121
+ expect(useAccounts).toEqual([
122
+ { value: "use alpha@example.com", label: "alpha@example.com" },
123
+ ]);
124
+
125
+ expect(commandOptions.getArgumentCompletions("reset m")).toEqual([
126
+ { value: "reset manual", label: "manual" },
127
+ ]);
120
128
 
121
129
  const refreshAccounts = commandOptions.getArgumentCompletions("refresh a");
122
130
  expect(refreshAccounts).toBeNull();
@@ -140,12 +148,12 @@ describe("registerCommands", () => {
140
148
  });
141
149
 
142
150
  expect(notify).toHaveBeenCalledWith(
143
- "/ultradex requires a subcommand in non-interactive mode. Use /ultradex sync <secret>.",
151
+ "/ultradex requires a subcommand in non-interactive mode. Use /ultradex help.",
144
152
  "warning",
145
153
  );
146
154
  });
147
155
 
148
- it("hides old subcommands even when typed directly", async () => {
156
+ it("hides repair-only subcommands even when typed directly", async () => {
149
157
  const registerCommand = vi.fn();
150
158
  registerCommands(
151
159
  { registerCommand } as never,
@@ -162,7 +170,57 @@ describe("registerCommands", () => {
162
170
  ui: { notify },
163
171
  });
164
172
 
165
- expect(notify).toHaveBeenCalledWith("Usage: /ultradex sync [secret]", "info");
173
+ expect(notify).toHaveBeenCalledWith(
174
+ expect.stringContaining(
175
+ "use: select, activate, or remove managed account",
176
+ ),
177
+ "info",
178
+ );
179
+ const helpText = String(notify.mock.calls.at(-1)?.[0] ?? "");
180
+ for (const label of [
181
+ "sync: download and decrypt DuckMind managed accounts",
182
+ "use: select, activate, or remove managed account",
183
+ "show: managed account and usage summary",
184
+ "rotation: current rotation behavior",
185
+ "verify: runtime health checks",
186
+ "path: storage and settings locations",
187
+ "reset: clear manual or quota state",
188
+ "help: command usage",
189
+ ]) {
190
+ expect(helpText).toContain(label);
191
+ }
192
+ });
193
+
194
+ it("runs restored state subcommands instead of falling back to sync-only help", async () => {
195
+ const registerCommand = vi.fn();
196
+ const accountManager = {
197
+ getAccounts: () => [],
198
+ hasManualAccount: vi.fn(() => true),
199
+ clearManualAccount: vi.fn(),
200
+ clearAllQuotaExhaustion: vi.fn(() => 0),
201
+ } as unknown as AccountManager;
202
+ const statusController = createStatusControllerMock();
203
+ registerCommands(
204
+ { registerCommand } as never,
205
+ accountManager,
206
+ statusController,
207
+ );
208
+
209
+ const commandOptions = registerCommand.mock.calls[0]?.[1] as {
210
+ handler: (args: string, ctx: unknown) => Promise<void>;
211
+ };
212
+ const notify = vi.fn();
213
+ await commandOptions.handler("reset manual", {
214
+ hasUI: false,
215
+ ui: { notify },
216
+ });
217
+
218
+ expect(accountManager.clearManualAccount).toHaveBeenCalledOnce();
219
+ expect(mocks.syncManagedAccountsFromDuckMind).not.toHaveBeenCalled();
220
+ expect(notify).toHaveBeenCalledWith(
221
+ expect.stringContaining("reset: target=manual"),
222
+ "info",
223
+ );
166
224
  });
167
225
 
168
226
  it("uses the explicit sync secret without remembering it after a successful sync", async () => {
@@ -33,8 +33,17 @@ import { formatResetAt, isUsageUntouched } from "./usage";
33
33
  const SETTINGS_FILE = getAgentSettingsPath();
34
34
  const NO_ACCOUNTS_MESSAGE =
35
35
  "No managed accounts found. Run /ultradex sync <secret> to import DuckMind managed accounts.";
36
- const HELP_TEXT =
37
- "Usage: /ultradex sync [secret]";
36
+ const HELP_TEXT = [
37
+ "Usage: /ultradex [sync [secret]|use [identifier]|show|rotation|verify|path|reset [manual|quota|all]|help]",
38
+ "sync: download and decrypt DuckMind managed accounts",
39
+ "use: select, activate, or remove managed account",
40
+ "show: managed account and usage summary",
41
+ "rotation: current rotation behavior",
42
+ "verify: runtime health checks",
43
+ "path: storage and settings locations",
44
+ "reset: clear manual or quota state",
45
+ "help: command usage",
46
+ ].join("\n");
38
47
  const SUBCOMMANDS = [
39
48
  "accounts",
40
49
  "use",
@@ -52,6 +61,13 @@ const SUBCOMMANDS = [
52
61
  const RESET_TARGETS = ["manual", "quota", "all"] as const;
53
62
  const VISIBLE_SUBCOMMANDS = [
54
63
  "sync",
64
+ "use",
65
+ "show",
66
+ "rotation",
67
+ "verify",
68
+ "path",
69
+ "reset",
70
+ "help",
55
71
  ] as const;
56
72
 
57
73
  type Subcommand = (typeof SUBCOMMANDS)[number];
@@ -156,6 +172,10 @@ function getSubcommandCompletions(prefix: string): AutocompleteItem[] | null {
156
172
  return matches.length > 0 ? toAutocompleteItems(matches) : null;
157
173
  }
158
174
 
175
+ function isVisibleSubcommand(value: Subcommand): boolean {
176
+ return VISIBLE_SUBCOMMANDS.some((subcommand) => subcommand === value);
177
+ }
178
+
159
179
  function getAccountCompletions(
160
180
  subcommand: "accounts" | "use" | "reauth",
161
181
  prefix: string,
@@ -208,6 +228,16 @@ function getCommandCompletions(
208
228
  return getSubcommandCompletions(trimmedStart.toLowerCase());
209
229
  }
210
230
 
231
+ const subcommand = trimmedStart.slice(0, firstSpaceIndex).toLowerCase();
232
+ const rest = trimmedStart.slice(firstSpaceIndex + 1).trimStart();
233
+
234
+ if (subcommand === "use") {
235
+ return getAccountCompletions("use", rest, accountManager);
236
+ }
237
+ if (subcommand === "reset") {
238
+ return getResetCompletions(rest);
239
+ }
240
+
211
241
  return null;
212
242
  }
213
243
 
@@ -1016,6 +1046,13 @@ async function openMainPanel(
1016
1046
  ): Promise<void> {
1017
1047
  const actions = [
1018
1048
  "sync: download and decrypt DuckMind managed accounts",
1049
+ "use: select, activate, or remove managed account",
1050
+ "show: managed account and usage summary",
1051
+ "rotation: current rotation behavior",
1052
+ "verify: runtime health checks",
1053
+ "path: storage and settings locations",
1054
+ "reset: clear manual or quota state",
1055
+ "help: command usage",
1019
1056
  ];
1020
1057
 
1021
1058
  const selected = await ctx.ui.select("Ultradex", actions);
@@ -1043,7 +1080,7 @@ export function registerCommands(
1043
1080
  ): void {
1044
1081
  pi.registerCommand("ultradex", {
1045
1082
  description:
1046
- "Sync DuckMind managed accounts",
1083
+ "Manage Ultradex sync, account selection, status, rotation, and health",
1047
1084
  getArgumentCompletions: (argumentPrefix: string) =>
1048
1085
  getCommandCompletions(argumentPrefix, accountManager),
1049
1086
  handler: async (
@@ -1054,7 +1091,7 @@ export function registerCommands(
1054
1091
  if (!parsed.subcommand) {
1055
1092
  if (!ctx.hasUI) {
1056
1093
  ctx.ui.notify(
1057
- "/ultradex requires a subcommand in non-interactive mode. Use /ultradex sync <secret>.",
1094
+ "/ultradex requires a subcommand in non-interactive mode. Use /ultradex help.",
1058
1095
  "warning",
1059
1096
  );
1060
1097
  return;
@@ -1063,17 +1100,17 @@ export function registerCommands(
1063
1100
  return;
1064
1101
  }
1065
1102
 
1066
- if (!isSubcommand(parsed.subcommand)) {
1067
- ctx.ui.notify(`Unknown subcommand: ${parsed.subcommand}`, "warning");
1068
- runHelpSubcommand(ctx);
1069
- return;
1070
- }
1071
- if (parsed.subcommand !== "sync") {
1072
- ctx.ui.notify(HELP_TEXT, "info");
1073
- return;
1074
- }
1103
+ if (!isSubcommand(parsed.subcommand)) {
1104
+ ctx.ui.notify(`Unknown subcommand: ${parsed.subcommand}`, "warning");
1105
+ runHelpSubcommand(ctx);
1106
+ return;
1107
+ }
1108
+ if (!isVisibleSubcommand(parsed.subcommand)) {
1109
+ runHelpSubcommand(ctx);
1110
+ return;
1111
+ }
1075
1112
 
1076
- await runSubcommand(
1113
+ await runSubcommand(
1077
1114
  parsed.subcommand,
1078
1115
  parsed.rest,
1079
1116
  pi,
@@ -3605,9 +3605,9 @@
3605
3605
  "license": "MIT"
3606
3606
  },
3607
3607
  "node_modules/cosmiconfig": {
3608
- "version": "9.0.1",
3609
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz",
3610
- "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==",
3608
+ "version": "9.0.2",
3609
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz",
3610
+ "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==",
3611
3611
  "dev": true,
3612
3612
  "license": "MIT",
3613
3613
  "dependencies": {
@@ -7448,9 +7448,9 @@
7448
7448
  }
7449
7449
  },
7450
7450
  "node_modules/semver": {
7451
- "version": "7.8.2",
7452
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
7453
- "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
7451
+ "version": "7.8.3",
7452
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.3.tgz",
7453
+ "integrity": "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==",
7454
7454
  "dev": true,
7455
7455
  "license": "ISC",
7456
7456
  "bin": {
@@ -42,6 +42,7 @@ const ACTIVE_PORT = join(PROFILE_DIR, "DevToolsActivePort");
42
42
  const PID_FILE = process.env.GREEDY_SEARCH_PID_FILE || join(tmpdir(), "greedysearch-chrome.pid");
43
43
  const MODE_FILE = process.env.GREEDY_SEARCH_MODE_FILE || join(tmpdir(), "greedysearch-chrome-mode");
44
44
  const ALLOW_PORT_CLEANUP = /^(1|true|yes|on)$/i.test(process.env.GREEDY_SEARCH_ALLOW_PORT_CLEANUP ?? "1");
45
+ const BROWSER_LABEL = process.env.GREEDY_SEARCH_BROWSER_LABEL || "Chrome";
45
46
 
46
47
  function findChrome() {
47
48
  const os = platform();
@@ -283,11 +284,11 @@ function cleanupGhostChrome() {
283
284
 
284
285
  if (!ALLOW_PORT_CLEANUP) {
285
286
  console.error(
286
- `Chrome/CDP port ${PORT} is already owned by untracked pid ${portPid}; refusing to kill it. Set GREEDY_SEARCH_PORT or DM_CUA_PORT to a free port.`,
287
+ `${BROWSER_LABEL}/CDP port ${PORT} is already owned by untracked pid ${portPid}; refusing to kill it. Set GREEDY_SEARCH_PORT or DM_CUA_PORT to a free port.`,
287
288
  );
288
289
  process.exit(1);
289
290
  }
290
- console.log(`Ghost Chrome on port ${PORT} (pid ${portPid}) — cleaning up...`);
291
+ console.log(`Ghost ${BROWSER_LABEL} on port ${PORT} (pid ${portPid}) — cleaning up...`);
291
292
  killProcess(portPid);
292
293
  try {
293
294
  unlinkSync(PID_FILE);
@@ -351,10 +352,10 @@ async function main() {
351
352
  if (pid) {
352
353
  const ok = killProcess(pid);
353
354
  console.log(
354
- ok ? `Stopped Chrome (pid ${pid}).` : `Failed to stop pid ${pid}.`,
355
+ ok ? `Stopped ${BROWSER_LABEL} (pid ${pid}).` : `Failed to stop pid ${pid}.`,
355
356
  );
356
357
  } else {
357
- console.log("GreedySearch Chrome is not running.");
358
+ console.log(`GreedySearch ${BROWSER_LABEL} is not running.`);
358
359
  }
359
360
  try {
360
361
  unlinkSync(PID_FILE);
@@ -386,7 +387,7 @@ async function main() {
386
387
  !process.argv.includes("--headless");
387
388
  if (isWantingVisible && isModeFileHeadless()) {
388
389
  console.log(
389
- `Headless Chrome running (pid ${existing}) but visible requested — killing...`,
390
+ `Headless ${BROWSER_LABEL} running (pid ${existing}) but visible requested — killing...`,
390
391
  );
391
392
  killProcess(existing);
392
393
  try {
@@ -399,7 +400,7 @@ async function main() {
399
400
  } else {
400
401
  const ready = await writePortFile(5000);
401
402
  if (ready) {
402
- console.log(`GreedySearch Chrome already running (pid ${existing}).`);
403
+ console.log(`GreedySearch ${BROWSER_LABEL} already running (pid ${existing}).`);
403
404
  return;
404
405
  }
405
406
  console.log(`Stale PID ${existing} — launching fresh.`);
@@ -411,13 +412,13 @@ async function main() {
411
412
 
412
413
  const CHROME_EXE = process.env.CHROME_PATH || findChrome();
413
414
  if (!CHROME_EXE) {
414
- console.error("Chrome not found. Set CHROME_PATH env var.");
415
+ console.error(`${BROWSER_LABEL} not found. Set CHROME_PATH env var.`);
415
416
  process.exit(1);
416
417
  }
417
418
 
418
419
  mkdirSync(PROFILE_DIR, { recursive: true });
419
420
 
420
- console.log(`Launching GreedySearch Chrome on port ${PORT}...`);
421
+ console.log(`Launching GreedySearch ${BROWSER_LABEL} on port ${PORT}...`);
421
422
  if (isHeadless()) {
422
423
  console.log("Headless mode — no window will be shown");
423
424
  } else if (!isVisible()) {
@@ -435,7 +436,7 @@ async function main() {
435
436
 
436
437
  const portFileReady = await writePortFile();
437
438
  if (!portFileReady) {
438
- console.error("Chrome did not become ready within 15s.");
439
+ console.error(`${BROWSER_LABEL} did not become ready within 15s.`);
439
440
  process.exit(1);
440
441
  }
441
442
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duckmind/dm-darwin-x64",
3
- "version": "0.35.9",
3
+ "version": "0.36.3",
4
4
  "description": "DuckMind (dm) binary payload for darwin x64",
5
5
  "license": "MIT",
6
6
  "os": [