@agentrhq/webcmd 0.2.0 → 0.2.1

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 (42) hide show
  1. package/README.md +23 -11
  2. package/cli-manifest.json +2 -0
  3. package/clis/twitter/delete.js +14 -6
  4. package/clis/twitter/delete.test.js +74 -1
  5. package/clis/twitter/timeline.js +3 -1
  6. package/clis/twitter/timeline.test.js +4 -0
  7. package/dist/src/browser/bridge-readiness.test.js +1 -1
  8. package/dist/src/browser/bridge.d.ts +1 -0
  9. package/dist/src/browser/bridge.js +6 -4
  10. package/dist/src/browser/cdp.d.ts +1 -0
  11. package/dist/src/browser/daemon-client.d.ts +1 -0
  12. package/dist/src/browser/daemon-client.js +7 -2
  13. package/dist/src/browser/daemon-client.test.js +33 -30
  14. package/dist/src/browser/daemon-transport.js +3 -19
  15. package/dist/src/browser/page.d.ts +2 -1
  16. package/dist/src/browser/page.js +5 -1
  17. package/dist/src/browser/profile.d.ts +9 -0
  18. package/dist/src/browser/profile.js +18 -7
  19. package/dist/src/browser/profile.test.d.ts +1 -0
  20. package/dist/src/browser/profile.test.js +44 -0
  21. package/dist/src/browser/protocol.d.ts +1 -0
  22. package/dist/src/browser/runtime/local-cloak/actions.js +24 -10
  23. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +1 -0
  24. package/dist/src/browser/runtime/local-cloak/session-manager.js +3 -0
  25. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +87 -0
  26. package/dist/src/cli.js +22 -17
  27. package/dist/src/cli.test.js +2 -2
  28. package/dist/src/commands/daemon.test.js +13 -13
  29. package/dist/src/constants.d.ts +2 -3
  30. package/dist/src/constants.js +3 -10
  31. package/dist/src/daemon.js +1 -5
  32. package/dist/src/doctor.js +12 -0
  33. package/dist/src/doctor.test.js +30 -3
  34. package/dist/src/execution.js +5 -3
  35. package/dist/src/external.js +19 -1
  36. package/dist/src/external.test.js +37 -1
  37. package/dist/src/main.js +0 -5
  38. package/dist/src/node-network.test.js +3 -3
  39. package/dist/src/runtime-identity.test.js +0 -8
  40. package/dist/src/runtime.d.ts +2 -0
  41. package/dist/src/runtime.js +1 -0
  42. package/package.json +2 -2
@@ -0,0 +1,44 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { ENV_PREFIX } from '../brand.js';
6
+ import { profileRouteParams, resolveProfileSelection } from './profile.js';
7
+ describe('profile selection', () => {
8
+ let configDir;
9
+ beforeEach(() => {
10
+ configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-profile-test-'));
11
+ vi.stubEnv(`${ENV_PREFIX}_CONFIG_DIR`, configDir);
12
+ vi.stubEnv(`${ENV_PREFIX}_PROFILE`, '');
13
+ });
14
+ afterEach(() => {
15
+ vi.unstubAllEnvs();
16
+ fs.rmSync(configDir, { recursive: true, force: true });
17
+ });
18
+ function writeConfig(config) {
19
+ fs.writeFileSync(path.join(configDir, 'browser-profiles.json'), JSON.stringify(config));
20
+ }
21
+ it('tags an explicit profile argument as explicit and resolves aliases', () => {
22
+ writeConfig({ version: 1, aliases: { work: 'profile-work' } });
23
+ expect(resolveProfileSelection('work')).toEqual({ contextId: 'profile-work', source: 'explicit' });
24
+ });
25
+ it('tags WEBCMD_PROFILE as explicit', () => {
26
+ vi.stubEnv(`${ENV_PREFIX}_PROFILE`, 'profile-env');
27
+ expect(resolveProfileSelection()).toEqual({ contextId: 'profile-env', source: 'explicit' });
28
+ });
29
+ it('tags the persisted config default as preferred', () => {
30
+ writeConfig({ version: 1, aliases: {}, defaultContextId: 'profile-default' });
31
+ expect(resolveProfileSelection()).toEqual({ contextId: 'profile-default', source: 'preferred' });
32
+ });
33
+ it('explicit argument beats env, and env beats config default', () => {
34
+ vi.stubEnv(`${ENV_PREFIX}_PROFILE`, 'from-env');
35
+ writeConfig({ version: 1, aliases: {}, defaultContextId: 'from-config' });
36
+ expect(resolveProfileSelection('from-arg')).toEqual({ contextId: 'from-arg', source: 'explicit' });
37
+ expect(resolveProfileSelection()).toEqual({ contextId: 'from-env', source: 'explicit' });
38
+ });
39
+ it('maps explicit routes to contextId and preferred routes to preferredContextId', () => {
40
+ expect(profileRouteParams({ contextId: 'a', source: 'explicit' })).toEqual({ contextId: 'a' });
41
+ expect(profileRouteParams({ contextId: 'b', source: 'preferred' })).toEqual({ preferredContextId: 'b' });
42
+ expect(profileRouteParams(undefined)).toEqual({});
43
+ });
44
+ });
@@ -34,6 +34,7 @@ export interface BrowserRuntimeCommand {
34
34
  idleTimeout?: number;
35
35
  frameIndex?: number;
36
36
  contextId?: string;
37
+ preferredContextId?: string;
37
38
  profileId?: string;
38
39
  }
39
40
  export interface BrowserRuntimeResult {
@@ -10,8 +10,22 @@ class CloakActionError extends Error {
10
10
  this.errorHint = errorHint;
11
11
  }
12
12
  }
13
- function commandProfileId(command) {
14
- return command.profileId ?? command.contextId ?? 'default';
13
+ function commandProfileId(manager, command) {
14
+ const requested = command.profileId ?? command.contextId;
15
+ if (requested?.trim())
16
+ return requested.trim();
17
+ const preferred = command.preferredContextId?.trim();
18
+ if (!preferred)
19
+ return 'default';
20
+ const active = manager.activeProfileIds();
21
+ if (active.includes(preferred))
22
+ return preferred;
23
+ if (active.length === 1)
24
+ return active[0];
25
+ if (active.length > 1) {
26
+ throw new CloakActionError('profile_required', `Default Cloak profile "${preferred}" is not active and multiple profiles are running; choose one with --profile.`, undefined, 'Run webcmd profile list, then update the default with webcmd profile use <name> or pass --profile <name>.');
27
+ }
28
+ return preferred;
15
29
  }
16
30
  function invalidRequest(command, error) {
17
31
  return { id: command.id, ok: false, errorCode: 'invalid_request', error };
@@ -24,7 +38,7 @@ async function resolveLease(manager, command) {
24
38
  throw new CloakActionError('stale_page_identity', `Page not found: ${command.page} — stale page identity`);
25
39
  }
26
40
  return manager.getPage({
27
- profileId: commandProfileId(command),
41
+ profileId: commandProfileId(manager, command),
28
42
  session: command.session,
29
43
  surface: command.surface,
30
44
  siteSession: command.siteSession,
@@ -93,12 +107,12 @@ export async function dispatchCloakAction(manager, command) {
93
107
  }
94
108
  case 'close-window': {
95
109
  if (command.page) {
96
- const closed = await manager.closePage({ profileId: commandProfileId(command), pageId: command.page });
110
+ const closed = await manager.closePage({ profileId: commandProfileId(manager, command), pageId: command.page });
97
111
  return { id: command.id, ok: true, data: { closed: Boolean(closed), page: closed ?? command.page, session: command.session } };
98
112
  }
99
113
  else {
100
114
  await manager.release({
101
- profileId: commandProfileId(command),
115
+ profileId: commandProfileId(manager, command),
102
116
  session: command.session,
103
117
  surface: command.surface,
104
118
  });
@@ -108,12 +122,12 @@ export async function dispatchCloakAction(manager, command) {
108
122
  case 'tabs': {
109
123
  switch (command.op ?? 'list') {
110
124
  case 'list': {
111
- const tabs = await manager.listPages({ profileId: commandProfileId(command) });
125
+ const tabs = await manager.listPages({ profileId: commandProfileId(manager, command) });
112
126
  return { id: command.id, ok: true, data: tabs };
113
127
  }
114
128
  case 'new': {
115
129
  const lease = await manager.newPage({
116
- profileId: commandProfileId(command),
130
+ profileId: commandProfileId(manager, command),
117
131
  session: command.session,
118
132
  surface: command.surface,
119
133
  siteSession: command.siteSession,
@@ -123,13 +137,13 @@ export async function dispatchCloakAction(manager, command) {
123
137
  return { id: command.id, ok: true, data: { title: await lease.page.title(), url: lease.page.url() }, page: lease.pageId };
124
138
  }
125
139
  case 'select': {
126
- const lease = await manager.selectPage({ profileId: commandProfileId(command), pageId: command.page, index: command.index });
140
+ const lease = await manager.selectPage({ profileId: commandProfileId(manager, command), pageId: command.page, index: command.index });
127
141
  if (!lease)
128
142
  return { id: command.id, ok: false, errorCode: 'runtime_command_failed', error: 'Tab not found' };
129
143
  return { id: command.id, ok: true, data: { selected: true, url: lease.page.url() }, page: lease.pageId };
130
144
  }
131
145
  case 'close': {
132
- const closed = await manager.closePage({ profileId: commandProfileId(command), pageId: command.page, index: command.index });
146
+ const closed = await manager.closePage({ profileId: commandProfileId(manager, command), pageId: command.page, index: command.index });
133
147
  if (!closed)
134
148
  return { id: command.id, ok: false, errorCode: 'runtime_command_failed', error: 'Tab not found' };
135
149
  return { id: command.id, ok: true, data: { closed } };
@@ -197,7 +211,7 @@ export async function dispatchCloakAction(manager, command) {
197
211
  }
198
212
  {
199
213
  const lease = await manager.bindPage({
200
- profileId: commandProfileId(command),
214
+ profileId: commandProfileId(manager, command),
201
215
  session: command.session,
202
216
  surface: command.surface,
203
217
  siteSession: command.siteSession,
@@ -46,6 +46,7 @@ export declare class CloakSessionManager {
46
46
  pending: number;
47
47
  lastSeenAt: number;
48
48
  }[];
49
+ activeProfileIds(): string[];
49
50
  getPage(input: SessionKeyInput): Promise<CloakPageLease>;
50
51
  findPageById(pageId: string, opts?: Pick<SessionKeyInput, 'idleTimeout'>): CloakPageLease | null;
51
52
  pageIdFor(page: PlaywrightPage): string | undefined;
@@ -31,6 +31,9 @@ export class CloakSessionManager {
31
31
  lastSeenAt: runtime.lastSeenAt,
32
32
  }));
33
33
  }
34
+ activeProfileIds() {
35
+ return [...this.profiles.keys()];
36
+ }
34
37
  async getPage(input) {
35
38
  const profileId = normalizeProfileId(input.profileId);
36
39
  const session = requireSession(input.session);
@@ -1,5 +1,7 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import path from 'node:path';
2
3
  import { CloakSessionManager } from './session-manager.js';
4
+ import { dispatchCloakAction } from './actions.js';
3
5
  function fakeContext() {
4
6
  const page = {
5
7
  goto: vi.fn().mockResolvedValue(undefined),
@@ -20,6 +22,9 @@ function fakeContext() {
20
22
  page,
21
23
  };
22
24
  }
25
+ function expectedProfileDir(profileId) {
26
+ return path.join('/tmp/webcmd-test', 'cloak', 'profiles', profileId);
27
+ }
23
28
  describe('CloakSessionManager', () => {
24
29
  afterEach(() => {
25
30
  vi.useRealTimers();
@@ -89,4 +94,86 @@ describe('CloakSessionManager', () => {
89
94
  expect(lease.page.close).not.toHaveBeenCalled();
90
95
  expect(await manager.listPages({ profileId: 'default' })).toHaveLength(1);
91
96
  });
97
+ it('launches a preferred profile when no Cloak profile is active', async () => {
98
+ const launched = fakeContext();
99
+ const launchPersistentContext = vi.fn().mockResolvedValue(launched.context);
100
+ const manager = new CloakSessionManager({
101
+ baseDir: '/tmp/webcmd-test',
102
+ launchPersistentContext,
103
+ });
104
+ await dispatchCloakAction(manager, {
105
+ id: 'cmd-preferred',
106
+ action: 'navigate',
107
+ session: 'work',
108
+ surface: 'browser',
109
+ url: 'https://example.com/',
110
+ preferredContextId: 'profile-default',
111
+ });
112
+ expect(launchPersistentContext).toHaveBeenCalledTimes(1);
113
+ expect(launchPersistentContext.mock.calls[0][0].userDataDir).toBe(expectedProfileDir('profile-default'));
114
+ });
115
+ it('falls back to the only active profile when the preferred profile is stale', async () => {
116
+ const launched = fakeContext();
117
+ const launchPersistentContext = vi.fn().mockResolvedValue(launched.context);
118
+ const manager = new CloakSessionManager({
119
+ baseDir: '/tmp/webcmd-test',
120
+ launchPersistentContext,
121
+ });
122
+ await dispatchCloakAction(manager, {
123
+ id: 'cmd-active',
124
+ action: 'navigate',
125
+ session: 'work',
126
+ surface: 'browser',
127
+ url: 'https://example.com/',
128
+ contextId: 'active',
129
+ });
130
+ await dispatchCloakAction(manager, {
131
+ id: 'cmd-stale-default',
132
+ action: 'navigate',
133
+ session: 'work',
134
+ surface: 'browser',
135
+ url: 'https://example.com/next',
136
+ preferredContextId: 'stale-default',
137
+ });
138
+ expect(launchPersistentContext).toHaveBeenCalledTimes(1);
139
+ expect(launchPersistentContext.mock.calls[0][0].userDataDir).toBe(expectedProfileDir('active'));
140
+ });
141
+ it('asks for an explicit profile when a stale preferred profile meets multiple active profiles', async () => {
142
+ const launched = fakeContext();
143
+ const launchPersistentContext = vi.fn().mockResolvedValue(launched.context);
144
+ const manager = new CloakSessionManager({
145
+ baseDir: '/tmp/webcmd-test',
146
+ launchPersistentContext,
147
+ });
148
+ await dispatchCloakAction(manager, {
149
+ id: 'cmd-a',
150
+ action: 'navigate',
151
+ session: 'work-a',
152
+ surface: 'browser',
153
+ url: 'https://example.com/a',
154
+ contextId: 'profile-a',
155
+ });
156
+ await dispatchCloakAction(manager, {
157
+ id: 'cmd-b',
158
+ action: 'navigate',
159
+ session: 'work-b',
160
+ surface: 'browser',
161
+ url: 'https://example.com/b',
162
+ contextId: 'profile-b',
163
+ });
164
+ const result = await dispatchCloakAction(manager, {
165
+ id: 'cmd-stale',
166
+ action: 'navigate',
167
+ session: 'work',
168
+ surface: 'browser',
169
+ url: 'https://example.com/',
170
+ preferredContextId: 'stale-default',
171
+ });
172
+ expect(result).toMatchObject({
173
+ id: 'cmd-stale',
174
+ ok: false,
175
+ errorCode: 'profile_required',
176
+ });
177
+ expect(launchPersistentContext).toHaveBeenCalledTimes(2);
178
+ });
92
179
  });
package/dist/src/cli.js CHANGED
@@ -36,7 +36,7 @@ import { daemonRestart, daemonStatus, daemonStop } from './commands/daemon.js';
36
36
  import { log } from './logger.js';
37
37
  import { bindTab, BrowserCommandError, sendCommand } from './browser/daemon-client.js';
38
38
  import { fetchDaemonStatus } from './browser/daemon-transport.js';
39
- import { aliasForContextId, loadProfileConfig, renameProfile, resolveProfileContextId, setDefaultProfile } from './browser/profile.js';
39
+ import { aliasForContextId, loadProfileConfig, profileRouteParams, renameProfile, resolveProfileSelection, setDefaultProfile } from './browser/profile.js';
40
40
  import { formatDaemonVersion, isDaemonStale } from './browser/daemon-version.js';
41
41
  import { DEFAULT_BROWSER_CONNECT_TIMEOUT } from './browser/config.js';
42
42
  import { CLI_COMMAND } from './brand.js';
@@ -428,7 +428,7 @@ async function resolveStoredBrowserTarget(page, scope) {
428
428
  return resolveBrowserTargetInSession(page, defaultPage, { scope, source: 'saved' });
429
429
  }
430
430
  /** Create a browser page for browser commands. Uses a named browser session for continuity. */
431
- async function getBrowserPage(session, targetPage, contextId, opts = {}) {
431
+ async function getBrowserPage(session, targetPage, profileSelection, opts = {}) {
432
432
  const { BrowserBridge } = await import('./browser/index.js');
433
433
  const bridge = new BrowserBridge();
434
434
  // Internal GC timeout for browser sessions. Not the per-command runtime timeout.
@@ -438,11 +438,11 @@ async function getBrowserPage(session, targetPage, contextId, opts = {}) {
438
438
  timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT,
439
439
  session,
440
440
  surface: 'browser',
441
- ...(contextId && { contextId }),
441
+ ...profileRouteParams(profileSelection),
442
442
  ...(idleTimeout && idleTimeout > 0 && { idleTimeout }),
443
443
  windowMode: opts.windowMode ?? getBrowserWindowMode(undefined, 'foreground'),
444
444
  });
445
- const targetScope = getBrowserScope(session, contextId);
445
+ const targetScope = getBrowserScope(session, profileSelection?.contextId);
446
446
  const resolvedTargetPage = targetPage
447
447
  ? await resolveBrowserTargetInSession(page, targetPage, { scope: targetScope, source: 'explicit' })
448
448
  : await resolveStoredBrowserTarget(page, targetScope);
@@ -497,9 +497,9 @@ function getBrowserSession(command) {
497
497
  return raw.trim();
498
498
  throw new Error('<session> is a required positional argument: webcmd browser <session> <command>');
499
499
  }
500
- function getBrowserContextId(command) {
500
+ function getBrowserProfileSelection(command) {
501
501
  const raw = getCommandOption(command, 'profile');
502
- return resolveProfileContextId(typeof raw === 'string' && raw.trim() ? raw.trim() : undefined);
502
+ return resolveProfileSelection(typeof raw === 'string' && raw.trim() ? raw.trim() : undefined);
503
503
  }
504
504
  function getPageSession(page) {
505
505
  const session = page.session;
@@ -508,8 +508,11 @@ function getPageSession(page) {
508
508
  throw new Error('Browser page is missing a session');
509
509
  }
510
510
  function getPageScope(page) {
511
- const contextId = page.contextId;
512
- return getBrowserScope(getPageSession(page), typeof contextId === 'string' && contextId.trim() ? contextId.trim() : undefined);
511
+ const { contextId, preferredContextId } = page;
512
+ const selected = typeof contextId === 'string' && contextId.trim()
513
+ ? contextId.trim()
514
+ : (typeof preferredContextId === 'string' && preferredContextId.trim() ? preferredContextId.trim() : undefined);
515
+ return getBrowserScope(getPageSession(page), selected);
513
516
  }
514
517
  function snapshotMetricText(snapshot) {
515
518
  return typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2);
@@ -882,9 +885,9 @@ Examples:
882
885
  const command = args.at(-1) instanceof Command ? args.at(-1) : undefined;
883
886
  const targetPage = getBrowserTargetId(command);
884
887
  const session = getBrowserSession(command);
885
- const contextId = getBrowserContextId(command);
888
+ const profileSelection = getBrowserProfileSelection(command);
886
889
  const windowMode = getBrowserWindowMode(command, 'foreground');
887
- page = await getBrowserPage(session, targetPage, contextId, { windowMode });
890
+ page = await getBrowserPage(session, targetPage, profileSelection, { windowMode });
888
891
  await fn(page, ...args);
889
892
  }
890
893
  catch (err) {
@@ -933,12 +936,14 @@ Examples:
933
936
  ? optsOrCommand
934
937
  : {};
935
938
  const session = getBrowserSession(command);
936
- const contextId = getBrowserContextId(command);
939
+ const profileSelection = getBrowserProfileSelection(command);
940
+ const contextId = profileSelection?.contextId;
941
+ const routing = profileRouteParams(profileSelection);
937
942
  try {
938
943
  const { BrowserBridge } = await import('./browser/index.js');
939
944
  const bridge = new BrowserBridge();
940
- await bridge.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, session, surface: 'browser', ...(contextId && { contextId }) });
941
- await fn({ session, contextId }, opts);
945
+ await bridge.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, session, surface: 'browser', ...routing });
946
+ await fn({ session, contextId, routing }, opts);
942
947
  }
943
948
  catch (err) {
944
949
  if (err instanceof BrowserCommandError) {
@@ -956,7 +961,7 @@ Examples:
956
961
  .description('Bind an existing Cloak runtime tab to the browser session named by <session>')
957
962
  .option('--page <id>', 'Cloak tab page id from `webcmd browser <session> tab list`')
958
963
  .option('--index <n>', 'Cloak tab index from `webcmd browser <session> tab list`')
959
- .action(browserSessionCommandAction(async ({ session, contextId }, opts) => {
964
+ .action(browserSessionCommandAction(async ({ session, contextId, routing }, opts) => {
960
965
  const page = typeof opts.page === 'string' && opts.page.trim() ? opts.page.trim() : undefined;
961
966
  const rawIndex = typeof opts.index === 'string' && opts.index.trim() ? opts.index.trim() : undefined;
962
967
  if ((page && rawIndex) || (!page && !rawIndex)) {
@@ -966,14 +971,14 @@ Examples:
966
971
  throw new BrowserCommandError('--index must be a non-negative integer.', 'invalid_request');
967
972
  }
968
973
  const index = rawIndex === undefined ? undefined : Number.parseInt(rawIndex, 10);
969
- const data = await bindTab(session, { ...(contextId && { contextId }), ...(page && { page }), ...(index !== undefined && { index }) });
974
+ const data = await bindTab(session, { ...routing, ...(page && { page }), ...(index !== undefined && { index }) });
970
975
  saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
971
976
  console.log(JSON.stringify({ session, ...((data && typeof data === 'object') ? data : { data }) }, null, 2));
972
977
  }));
973
978
  browser.command('unbind')
974
979
  .description('Compatibility command; release the Cloak browser session named by <session>')
975
- .action(browserSessionCommandAction(async ({ session, contextId }) => {
976
- await sendCommand('close-window', { session, surface: 'browser', ...(contextId && { contextId }) });
980
+ .action(browserSessionCommandAction(async ({ session, contextId, routing }) => {
981
+ await sendCommand('close-window', { session, surface: 'browser', ...routing });
977
982
  saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
978
983
  console.log(JSON.stringify({ unbound: true, session }, null, 2));
979
984
  }));
@@ -1004,7 +1004,7 @@ describe('profile list', () => {
1004
1004
  runtimeVersion: '1.0.3',
1005
1005
  pending: 0,
1006
1006
  memoryMB: 20,
1007
- port: 19825,
1007
+ port: 9777,
1008
1008
  }),
1009
1009
  });
1010
1010
  const program = createProgram('', '');
@@ -1027,7 +1027,7 @@ describe('profile list', () => {
1027
1027
  profiles: [],
1028
1028
  pending: 0,
1029
1029
  memoryMB: 20,
1030
- port: 19825,
1030
+ port: 9777,
1031
1031
  }),
1032
1032
  });
1033
1033
  const program = createProgram('', '');
@@ -39,7 +39,7 @@ describe('daemonStatus', () => {
39
39
  runtimeVersion: '1.6.8',
40
40
  pending: 0,
41
41
  memoryMB: 64,
42
- port: 19825,
42
+ port: 9777,
43
43
  });
44
44
  await daemonStatus();
45
45
  expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('running'));
@@ -49,7 +49,7 @@ describe('daemonStatus', () => {
49
49
  expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('connected'));
50
50
  expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('v1.6.8'));
51
51
  expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('64 MB'));
52
- expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('19825'));
52
+ expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('9777'));
53
53
  });
54
54
  it('shows disconnected when runtime is not connected', async () => {
55
55
  fetchDaemonStatusMock.mockResolvedValue({
@@ -61,7 +61,7 @@ describe('daemonStatus', () => {
61
61
  runtimeName: 'fake',
62
62
  pending: 0,
63
63
  memoryMB: 32,
64
- port: 19825,
64
+ port: 9777,
65
65
  });
66
66
  await daemonStatus();
67
67
  expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('disconnected'));
@@ -77,7 +77,7 @@ describe('daemonStatus', () => {
77
77
  runtimeVersion: undefined,
78
78
  pending: 0,
79
79
  memoryMB: 32,
80
- port: 19825,
80
+ port: 9777,
81
81
  });
82
82
  await daemonStatus();
83
83
  expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('fake connected'));
@@ -105,7 +105,7 @@ describe('daemonStatus runtime label states (#1575)', () => {
105
105
  daemonVersion: PKG_VERSION,
106
106
  pending: 0,
107
107
  memoryMB: 32,
108
- port: 19825,
108
+ port: 9777,
109
109
  runtimeName: 'fake',
110
110
  ...extra,
111
111
  });
@@ -173,7 +173,7 @@ describe('daemonStop', () => {
173
173
  runtimeName: 'fake',
174
174
  pending: 0,
175
175
  memoryMB: 50,
176
- port: 19825,
176
+ port: 9777,
177
177
  });
178
178
  requestDaemonShutdownMock.mockResolvedValue(true);
179
179
  await daemonStop();
@@ -190,7 +190,7 @@ describe('daemonStop', () => {
190
190
  runtimeName: 'fake',
191
191
  pending: 0,
192
192
  memoryMB: 50,
193
- port: 19825,
193
+ port: 9777,
194
194
  });
195
195
  requestDaemonShutdownMock.mockResolvedValue(false);
196
196
  await daemonStop();
@@ -221,7 +221,7 @@ describe('daemonRestart', () => {
221
221
  profiles: [{ contextId: 'work', runtimeConnected: true, pending: 0 }],
222
222
  pending: 0,
223
223
  memoryMB: 50,
224
- port: 19825,
224
+ port: 9777,
225
225
  });
226
226
  restartDaemonMock.mockResolvedValue({
227
227
  previousStatus: { daemonVersion: '1.7.6' },
@@ -237,13 +237,13 @@ describe('daemonRestart', () => {
237
237
  profiles: [{ contextId: 'work', runtimeConnected: true, pending: 0 }],
238
238
  pending: 0,
239
239
  memoryMB: 51,
240
- port: 19825,
240
+ port: 9777,
241
241
  },
242
242
  });
243
243
  await daemonRestart();
244
244
  expect(restartDaemonMock).toHaveBeenCalledTimes(1);
245
245
  expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('will disconnect 1 browser profile'));
246
- expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon restarted on port 19825 (v${PKG_VERSION})`));
246
+ expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon restarted on port 9777 (v${PKG_VERSION})`));
247
247
  expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Runtime connected; 1 profile connected'));
248
248
  });
249
249
  it('starts a new daemon when none was running', async () => {
@@ -261,11 +261,11 @@ describe('daemonRestart', () => {
261
261
  runtimeName: 'fake',
262
262
  pending: 0,
263
263
  memoryMB: 51,
264
- port: 19825,
264
+ port: 9777,
265
265
  },
266
266
  });
267
267
  await daemonRestart();
268
- expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon started on port 19825 (v${PKG_VERSION})`));
268
+ expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon started on port 9777 (v${PKG_VERSION})`));
269
269
  expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Cloak runtime has not connected yet'));
270
270
  });
271
271
  it('reports failure when the daemon cannot stop', async () => {
@@ -278,7 +278,7 @@ describe('daemonRestart', () => {
278
278
  runtimeName: 'fake',
279
279
  pending: 0,
280
280
  memoryMB: 50,
281
- port: 19825,
281
+ port: 9777,
282
282
  });
283
283
  restartDaemonMock.mockResolvedValue({
284
284
  previousStatus: { daemonVersion: '1.7.6' },
@@ -2,9 +2,8 @@
2
2
  * Shared constants used across explore, synthesize, and pipeline modules.
3
3
  */
4
4
  import { DAEMON_HEADER_NAME } from './brand.js';
5
- /** Default daemon port for HTTP/WebSocket communication with browser extension */
6
- export declare const DEFAULT_DAEMON_PORT = 19825;
7
- export declare function unsupportedDaemonPortEnvMessage(value?: string): string;
5
+ /** Default daemon port for HTTP communication with the browser runtime. */
6
+ export declare const DEFAULT_DAEMON_PORT = 9777;
8
7
  export { DAEMON_HEADER_NAME };
9
8
  /** URL query params that are volatile/ephemeral and should be stripped from patterns */
10
9
  export declare const VOLATILE_PARAMS: Set<string>;
@@ -1,16 +1,9 @@
1
1
  /**
2
2
  * Shared constants used across explore, synthesize, and pipeline modules.
3
3
  */
4
- import { CLI_COMMAND, DAEMON_HEADER_NAME, ENV_PREFIX, PRODUCT_DISPLAY_NAME } from './brand.js';
5
- /** Default daemon port for HTTP/WebSocket communication with browser extension */
6
- export const DEFAULT_DAEMON_PORT = 19825;
7
- export function unsupportedDaemonPortEnvMessage(value) {
8
- const envName = `${ENV_PREFIX}_DAEMON_PORT`;
9
- const suffix = value ? ` (received ${value})` : '';
10
- return `${envName} is no longer supported${suffix}. ` +
11
- `The ${PRODUCT_DISPLAY_NAME} Chrome extension can only connect to localhost:${DEFAULT_DAEMON_PORT}. ` +
12
- `Unset ${envName} and rerun ${CLI_COMMAND}.`;
13
- }
4
+ import { DAEMON_HEADER_NAME } from './brand.js';
5
+ /** Default daemon port for HTTP communication with the browser runtime. */
6
+ export const DEFAULT_DAEMON_PORT = 9777;
14
7
  export { DAEMON_HEADER_NAME };
15
8
  /** URL query params that are volatile/ephemeral and should be stripped from patterns */
16
9
  export const VOLATILE_PARAMS = new Set([
@@ -1,13 +1,9 @@
1
- import { DEFAULT_DAEMON_PORT, unsupportedDaemonPortEnvMessage } from './constants.js';
1
+ import { DEFAULT_DAEMON_PORT } from './constants.js';
2
2
  import { EXIT_CODES } from './errors.js';
3
3
  import { log } from './logger.js';
4
4
  import { PKG_VERSION } from './version.js';
5
5
  import { createDaemonServer } from './daemon/server.js';
6
6
  import { LocalCloakRuntimeProvider } from './browser/runtime/local-cloak/provider.js';
7
- if (process.env.WEBCMD_DAEMON_PORT) {
8
- log.error(unsupportedDaemonPortEnvMessage(process.env.WEBCMD_DAEMON_PORT));
9
- process.exit(EXIT_CODES.USAGE_ERROR);
10
- }
11
7
  const provider = new LocalCloakRuntimeProvider();
12
8
  const daemon = createDaemonServer(provider, { port: DEFAULT_DAEMON_PORT, host: '127.0.0.1', version: PKG_VERSION });
13
9
  daemon.listen().then(() => {
@@ -93,6 +93,18 @@ export async function runBrowserDoctor(opts = {}) {
93
93
  if (!connectivity.ok) {
94
94
  issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
95
95
  }
96
+ const profileConfig = loadProfileConfig();
97
+ const staleDefault = profileConfig.defaultContextId;
98
+ if (staleDefault && profiles?.length && !profiles.some((p) => p.contextId === staleDefault)) {
99
+ const alias = aliasForContextId(profileConfig, staleDefault);
100
+ const label = alias ? `${alias} (${staleDefault})` : staleDefault;
101
+ const fallbackNote = profiles.length === 1
102
+ ? `Commands currently fall back to the only active profile: ${profiles[0].contextId}.`
103
+ : 'Multiple profiles are active, so commands will ask you to choose.';
104
+ issues.push(`Default Cloak profile is not active: ${label}.\n` +
105
+ ` ${fallbackNote}\n` +
106
+ ' Refresh it with: webcmd profile list, then webcmd profile use <name>.');
107
+ }
96
108
  if (adapterShadows.length > 0) {
97
109
  issues.push(formatAdapterShadowIssue(adapterShadows));
98
110
  }
@@ -49,7 +49,7 @@ describe('doctor report rendering', () => {
49
49
  runtimeVersion: '1.6.8',
50
50
  issues: [],
51
51
  }));
52
- expect(text).toContain('[OK] Daemon: running on port 19825');
52
+ expect(text).toContain('[OK] Daemon: running on port 9777');
53
53
  expect(text).toContain('(v1.7.9)');
54
54
  expect(text).toContain('[OK] Runtime: Cloak connected (v1.6.8)');
55
55
  expect(text).toContain('Everything looks good!');
@@ -66,7 +66,7 @@ describe('doctor report rendering', () => {
66
66
  runtimeVersion: '1.0.3',
67
67
  issues: ['Stale daemon detected: daemon v1.7.6 != CLI v1.7.9.\n Run: webcmd daemon restart'],
68
68
  }));
69
- expect(text).toContain('[WARN] Daemon: running on port 19825 (v1.7.6, stale; CLI v1.7.9)');
69
+ expect(text).toContain('[WARN] Daemon: running on port 9777 (v1.7.6, stale; CLI v1.7.9)');
70
70
  expect(text).toContain('Run: webcmd daemon restart');
71
71
  expect(text).not.toContain('Everything looks good!');
72
72
  });
@@ -86,7 +86,7 @@ describe('doctor report rendering', () => {
86
86
  runtimeConnected: false,
87
87
  issues: ['Daemon is running but the Cloak runtime is not connected.'],
88
88
  }));
89
- expect(text).toContain('[OK] Daemon: running on port 19825');
89
+ expect(text).toContain('[OK] Daemon: running on port 9777');
90
90
  expect(text).toContain('[MISSING] Runtime: Cloak not connected');
91
91
  });
92
92
  it('renders OK when the connected Cloak runtime version is unknown', () => {
@@ -157,6 +157,33 @@ describe('doctor report rendering', () => {
157
157
  expect.stringContaining('Daemon is not running'),
158
158
  ]));
159
159
  });
160
+ it('reports a stale default Cloak profile when it is not active', async () => {
161
+ const fs = await import('node:fs');
162
+ const os = await import('node:os');
163
+ const path = await import('node:path');
164
+ const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-doctor-profile-'));
165
+ fs.writeFileSync(path.join(configDir, 'browser-profiles.json'), JSON.stringify({ version: 1, aliases: { work: 'profile-default' }, defaultContextId: 'profile-default' }));
166
+ vi.stubEnv('WEBCMD_CONFIG_DIR', configDir);
167
+ try {
168
+ mockGetDaemonHealth.mockResolvedValueOnce({
169
+ state: 'ready',
170
+ status: {
171
+ runtimeConnected: true,
172
+ runtimeName: 'Cloak',
173
+ profiles: [{ contextId: 'active-profile', runtimeConnected: true, pending: 0 }],
174
+ },
175
+ });
176
+ const report = await runBrowserDoctor();
177
+ expect(report.issues).toEqual(expect.arrayContaining([
178
+ expect.stringContaining('Default Cloak profile is not active: work (profile-default)'),
179
+ ]));
180
+ expect(report.issues.join('\n')).toContain('fall back to the only active profile: active-profile');
181
+ }
182
+ finally {
183
+ vi.unstubAllEnvs();
184
+ fs.rmSync(configDir, { recursive: true, force: true });
185
+ }
186
+ });
160
187
  it('reports flapping when live check succeeds but final status shows runtime disconnected', async () => {
161
188
  mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-runtime', status: { runtimeConnected: false, runtimeName: 'Cloak' } });
162
189
  const report = await runBrowserDoctor();