@wingman-ai/gateway 0.3.0 → 0.3.2

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 (124) hide show
  1. package/README.md +8 -0
  2. package/dist/agent/config/agentConfig.cjs +12 -0
  3. package/dist/agent/config/agentConfig.d.ts +22 -0
  4. package/dist/agent/config/agentConfig.js +10 -1
  5. package/dist/agent/config/agentLoader.cjs +9 -0
  6. package/dist/agent/config/agentLoader.js +9 -0
  7. package/dist/agent/config/toolRegistry.cjs +17 -0
  8. package/dist/agent/config/toolRegistry.d.ts +15 -0
  9. package/dist/agent/config/toolRegistry.js +17 -0
  10. package/dist/agent/tests/agentConfig.test.cjs +6 -1
  11. package/dist/agent/tests/agentConfig.test.js +6 -1
  12. package/dist/agent/tests/browserControlHelpers.test.cjs +35 -0
  13. package/dist/agent/tests/browserControlHelpers.test.d.ts +1 -0
  14. package/dist/agent/tests/browserControlHelpers.test.js +29 -0
  15. package/dist/agent/tests/browserControlTool.test.cjs +2117 -0
  16. package/dist/agent/tests/browserControlTool.test.d.ts +1 -0
  17. package/dist/agent/tests/browserControlTool.test.js +2111 -0
  18. package/dist/agent/tests/internet_search.test.cjs +22 -28
  19. package/dist/agent/tests/internet_search.test.js +22 -28
  20. package/dist/agent/tests/toolRegistry.test.cjs +6 -0
  21. package/dist/agent/tests/toolRegistry.test.js +6 -0
  22. package/dist/agent/tools/browser_control.cjs +1282 -0
  23. package/dist/agent/tools/browser_control.d.ts +478 -0
  24. package/dist/agent/tools/browser_control.js +1242 -0
  25. package/dist/agent/tools/internet_search.cjs +9 -5
  26. package/dist/agent/tools/internet_search.js +9 -5
  27. package/dist/cli/commands/agent.cjs +16 -2
  28. package/dist/cli/commands/agent.js +16 -2
  29. package/dist/cli/commands/browser.cjs +603 -0
  30. package/dist/cli/commands/browser.d.ts +13 -0
  31. package/dist/cli/commands/browser.js +566 -0
  32. package/dist/cli/commands/gateway.cjs +18 -7
  33. package/dist/cli/commands/gateway.d.ts +5 -1
  34. package/dist/cli/commands/gateway.js +18 -7
  35. package/dist/cli/commands/init.cjs +134 -45
  36. package/dist/cli/commands/init.js +134 -45
  37. package/dist/cli/commands/skill.cjs +3 -2
  38. package/dist/cli/commands/skill.js +3 -2
  39. package/dist/cli/config/loader.cjs +15 -0
  40. package/dist/cli/config/loader.js +15 -0
  41. package/dist/cli/config/schema.cjs +51 -2
  42. package/dist/cli/config/schema.d.ts +49 -0
  43. package/dist/cli/config/schema.js +44 -1
  44. package/dist/cli/core/workspace.cjs +89 -0
  45. package/dist/cli/core/workspace.d.ts +1 -0
  46. package/dist/cli/core/workspace.js +55 -0
  47. package/dist/cli/index.cjs +53 -5
  48. package/dist/cli/index.js +53 -5
  49. package/dist/cli/types/browser.cjs +18 -0
  50. package/dist/cli/types/browser.d.ts +9 -0
  51. package/dist/cli/types/browser.js +0 -0
  52. package/dist/gateway/browserRelayServer.cjs +338 -0
  53. package/dist/gateway/browserRelayServer.d.ts +38 -0
  54. package/dist/gateway/browserRelayServer.js +301 -0
  55. package/dist/gateway/http/agents.cjs +22 -0
  56. package/dist/gateway/http/agents.js +22 -0
  57. package/dist/gateway/http/fs.cjs +57 -0
  58. package/dist/gateway/http/fs.js +58 -1
  59. package/dist/gateway/server.cjs +43 -6
  60. package/dist/gateway/server.d.ts +4 -1
  61. package/dist/gateway/server.js +36 -5
  62. package/dist/gateway/transport/websocket.cjs +45 -10
  63. package/dist/gateway/transport/websocket.d.ts +1 -0
  64. package/dist/gateway/transport/websocket.js +41 -9
  65. package/dist/gateway/types.d.ts +4 -0
  66. package/dist/tests/agents-api.test.cjs +52 -0
  67. package/dist/tests/agents-api.test.js +53 -1
  68. package/dist/tests/browser-command.test.cjs +264 -0
  69. package/dist/tests/browser-command.test.d.ts +1 -0
  70. package/dist/tests/browser-command.test.js +258 -0
  71. package/dist/tests/browser-relay-server.test.cjs +20 -0
  72. package/dist/tests/browser-relay-server.test.d.ts +1 -0
  73. package/dist/tests/browser-relay-server.test.js +14 -0
  74. package/dist/tests/cli-config-loader.test.cjs +43 -0
  75. package/dist/tests/cli-config-loader.test.js +43 -0
  76. package/dist/tests/cli-init.test.cjs +25 -2
  77. package/dist/tests/cli-init.test.js +25 -2
  78. package/dist/tests/cli-workspace-root.test.cjs +114 -0
  79. package/dist/tests/cli-workspace-root.test.d.ts +1 -0
  80. package/dist/tests/cli-workspace-root.test.js +108 -0
  81. package/dist/tests/fs-api.test.cjs +138 -0
  82. package/dist/tests/fs-api.test.d.ts +1 -0
  83. package/dist/tests/fs-api.test.js +132 -0
  84. package/dist/tests/gateway-command-workspace.test.cjs +150 -0
  85. package/dist/tests/gateway-command-workspace.test.d.ts +1 -0
  86. package/dist/tests/gateway-command-workspace.test.js +144 -0
  87. package/dist/tests/gateway-request-execution-overrides.test.cjs +42 -0
  88. package/dist/tests/gateway-request-execution-overrides.test.d.ts +1 -0
  89. package/dist/tests/gateway-request-execution-overrides.test.js +36 -0
  90. package/dist/tests/gateway.test.cjs +31 -0
  91. package/dist/tests/gateway.test.js +31 -0
  92. package/dist/tests/websocket-transport.test.cjs +31 -0
  93. package/dist/tests/websocket-transport.test.d.ts +1 -0
  94. package/dist/tests/websocket-transport.test.js +25 -0
  95. package/dist/webui/assets/index-Cwkg4DKj.css +11 -0
  96. package/dist/webui/assets/{index-0nUBsUUq.js → index-DHbfLOUR.js} +109 -107
  97. package/dist/webui/index.html +2 -2
  98. package/extensions/wingman-browser-extension/README.md +27 -0
  99. package/extensions/wingman-browser-extension/background.js +416 -0
  100. package/extensions/wingman-browser-extension/manifest.json +19 -0
  101. package/extensions/wingman-browser-extension/options.html +156 -0
  102. package/extensions/wingman-browser-extension/options.js +106 -0
  103. package/package.json +8 -8
  104. package/{.wingman → templates}/agents/README.md +2 -1
  105. package/{.wingman → templates}/agents/coding/agent.md +0 -1
  106. package/{.wingman → templates}/agents/coding-v2/agent.md +0 -1
  107. package/{.wingman → templates}/agents/game-dev/agent.md +8 -1
  108. package/{.wingman → templates}/agents/game-dev/art-generation.md +1 -0
  109. package/{.wingman → templates}/agents/main/agent.md +5 -0
  110. package/{.wingman → templates}/agents/researcher/agent.md +9 -0
  111. package/{.wingman → templates}/agents/stock-trader/agent.md +1 -0
  112. package/dist/webui/assets/index-kk7OrD-G.css +0 -11
  113. /package/{.wingman → templates}/agents/coding-v2/implementor.md +0 -0
  114. /package/{.wingman → templates}/agents/game-dev/asset-refinement.md +0 -0
  115. /package/{.wingman → templates}/agents/game-dev/planning-idea.md +0 -0
  116. /package/{.wingman → templates}/agents/game-dev/ui-specialist.md +0 -0
  117. /package/{.wingman → templates}/agents/stock-trader/chain-curator.md +0 -0
  118. /package/{.wingman → templates}/agents/stock-trader/goal-translator.md +0 -0
  119. /package/{.wingman → templates}/agents/stock-trader/guardrails-veto.md +0 -0
  120. /package/{.wingman → templates}/agents/stock-trader/path-planner.md +0 -0
  121. /package/{.wingman → templates}/agents/stock-trader/regime-analyst.md +0 -0
  122. /package/{.wingman → templates}/agents/stock-trader/risk.md +0 -0
  123. /package/{.wingman → templates}/agents/stock-trader/selection.md +0 -0
  124. /package/{.wingman → templates}/agents/stock-trader/strategy-composer.md +0 -0
@@ -0,0 +1,566 @@
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { randomBytes } from "node:crypto";
4
+ import { isAbsolute, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createLogger, getLogFilePath } from "../../logger.js";
7
+ import { OutputManager } from "../core/outputManager.js";
8
+ const DEFAULT_CONFIG_DIR = ".wingman";
9
+ const DEFAULT_PROFILES_DIR = ".wingman/browser-profiles";
10
+ const DEFAULT_EXTENSIONS_DIR = ".wingman/browser-extensions";
11
+ const DEFAULT_BUNDLED_EXTENSION_ID = "wingman";
12
+ const BUNDLED_EXTENSION_RELATIVE_PATH = "../../../extensions/wingman-browser-extension";
13
+ const PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
14
+ async function executeBrowserCommand(args, options = {}) {
15
+ const outputManager = new OutputManager(args.outputMode);
16
+ try {
17
+ switch(args.subcommand){
18
+ case "profile":
19
+ await executeBrowserProfileCommand(args, outputManager, options);
20
+ break;
21
+ case "extension":
22
+ await executeBrowserExtensionCommand(args, outputManager, options);
23
+ break;
24
+ case "":
25
+ case "help":
26
+ case "--help":
27
+ case "-h":
28
+ showBrowserHelp(outputManager);
29
+ break;
30
+ default:
31
+ throw new Error(`Unknown subcommand: ${args.subcommand}. Run 'wingman browser help' for usage.`);
32
+ }
33
+ } catch (error) {
34
+ const message = error instanceof Error ? error.message : String(error);
35
+ const logFile = getLogFilePath();
36
+ createLogger().error("Browser command failed", {
37
+ error: message
38
+ });
39
+ if ("interactive" === outputManager.getMode()) {
40
+ console.error(`\nError: ${message}`);
41
+ console.error(`Logs: ${logFile}`);
42
+ process.exit(1);
43
+ } else {
44
+ outputManager.emitAgentError(error);
45
+ process.exit(1);
46
+ }
47
+ }
48
+ }
49
+ async function executeBrowserProfileCommand(args, outputManager, options) {
50
+ const action = args.args[0] || "";
51
+ switch(action){
52
+ case "init":
53
+ await handleProfileInit(args, outputManager, options);
54
+ break;
55
+ case "open":
56
+ await handleProfileOpen(args, outputManager, options);
57
+ break;
58
+ case "":
59
+ case "help":
60
+ case "--help":
61
+ case "-h":
62
+ showBrowserProfileHelp(outputManager);
63
+ break;
64
+ default:
65
+ throw new Error(`Unknown browser profile subcommand: ${action}. Run 'wingman browser profile help' for usage.`);
66
+ }
67
+ }
68
+ async function executeBrowserExtensionCommand(args, outputManager, options) {
69
+ const action = args.args[0] || "";
70
+ switch(action){
71
+ case "install":
72
+ await handleExtensionInstall(args, outputManager, options);
73
+ break;
74
+ case "path":
75
+ await handleExtensionPath(args, outputManager, options);
76
+ break;
77
+ case "list":
78
+ await handleExtensionList(args, outputManager, options);
79
+ break;
80
+ case "pair":
81
+ await handleExtensionPair(args, outputManager, options);
82
+ break;
83
+ case "":
84
+ case "help":
85
+ case "--help":
86
+ case "-h":
87
+ showBrowserExtensionHelp(outputManager);
88
+ break;
89
+ default:
90
+ throw new Error(`Unknown browser extension subcommand: ${action}. Run 'wingman browser extension help' for usage.`);
91
+ }
92
+ }
93
+ async function handleProfileInit(args, outputManager, options) {
94
+ const profileArg = getPositionalArg(args.args, 1);
95
+ if (!profileArg) throw new Error("Profile ID required. Usage: wingman browser profile init <profile-id>");
96
+ const profileId = validateProfileId(profileArg);
97
+ const workspace = options.workspace || process.cwd();
98
+ const configDir = options.configDir || DEFAULT_CONFIG_DIR;
99
+ const configRoot = join(workspace, configDir);
100
+ const configPath = join(configRoot, "wingman.config.json");
101
+ const config = readConfigObject(configPath);
102
+ const browserConfig = readObject(config.browser);
103
+ const profiles = readStringRecord(browserConfig.profiles);
104
+ const force = getBooleanOption(args.options, "force");
105
+ const profilesDirOption = getStringOption(args.options, "profiles-dir") || getStringOption(args.options, "profilesDir");
106
+ if (profilesDirOption) browserConfig.profilesDir = profilesDirOption;
107
+ const baseProfilesDir = resolveProfilesDir(browserConfig.profilesDir);
108
+ const profilePathOption = getStringOption(args.options, "path");
109
+ const profilePath = profilePathOption ? profilePathOption : normalizePathForConfig(join(baseProfilesDir, profileId));
110
+ const existingProfilePath = profiles[profileId];
111
+ if (existingProfilePath && existingProfilePath !== profilePath && !force) throw new Error(`Browser profile "${profileId}" already exists at ${existingProfilePath}. Use --force to overwrite.`);
112
+ profiles[profileId] = profilePath;
113
+ browserConfig.profiles = profiles;
114
+ browserConfig.profilesDir = baseProfilesDir;
115
+ const requestedDefault = getBooleanOption(args.options, "default");
116
+ const shouldSetDefault = requestedDefault || "string" != typeof browserConfig.defaultProfile;
117
+ if (shouldSetDefault) browserConfig.defaultProfile = profileId;
118
+ config.browser = browserConfig;
119
+ const absoluteProfilePath = resolveProfilePath(workspace, profilePath);
120
+ mkdirSync(absoluteProfilePath, {
121
+ recursive: true
122
+ });
123
+ mkdirSync(configRoot, {
124
+ recursive: true
125
+ });
126
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
127
+ writeLine(outputManager, `Initialized browser profile "${profileId}".`);
128
+ writeLine(outputManager, `Profile directory: ${absoluteProfilePath}`);
129
+ if (shouldSetDefault) writeLine(outputManager, `Default browser profile: ${profileId}`);
130
+ writeLine(outputManager, `Saved config: ${configPath}`);
131
+ }
132
+ async function handleProfileOpen(args, outputManager, options) {
133
+ const workspace = options.workspace || process.cwd();
134
+ const configDir = options.configDir || DEFAULT_CONFIG_DIR;
135
+ const configRoot = join(workspace, configDir);
136
+ const configPath = join(configRoot, "wingman.config.json");
137
+ const config = readConfigObject(configPath);
138
+ const browserConfig = readObject(config.browser);
139
+ const requestedProfile = getPositionalArg(args.args, 1);
140
+ const defaultProfile = "string" == typeof browserConfig.defaultProfile ? browserConfig.defaultProfile.trim() : "";
141
+ const resolvedProfileId = validateProfileId(requestedProfile || defaultProfile || "");
142
+ const profiles = readStringRecord(browserConfig.profiles);
143
+ const profilePath = profiles[resolvedProfileId] || normalizePathForConfig(join(resolveProfilesDir(browserConfig.profilesDir), resolvedProfileId));
144
+ const absoluteProfilePath = resolveProfilePath(workspace, profilePath);
145
+ mkdirSync(absoluteProfilePath, {
146
+ recursive: true
147
+ });
148
+ const url = getStringOption(args.options, "url") || getStringOption(args.options, "target-url") || "https://example.com";
149
+ const headless = getBooleanOption(args.options, "headless");
150
+ const explicitExecutablePath = getStringOption(args.options, "executable-path") || getStringOption(args.options, "executablePath");
151
+ const resolveExecutable = options.resolveExecutablePath || resolveChromeExecutablePath;
152
+ const executablePath = resolveExecutable(explicitExecutablePath);
153
+ const defaultExtensionIds = readStringArray(browserConfig.defaultExtensions);
154
+ const extensionArgs = resolveExtensionArgs(workspace, browserConfig, defaultExtensionIds);
155
+ const chromeArgs = [
156
+ `--user-data-dir=${absoluteProfilePath}`,
157
+ "--no-default-browser-check",
158
+ "--no-first-run",
159
+ "--disable-background-networking",
160
+ "--disable-sync",
161
+ "--mute-audio",
162
+ ...extensionArgs
163
+ ];
164
+ if (headless) chromeArgs.push("--headless=new");
165
+ chromeArgs.push(url);
166
+ const spawnProcess = options.spawnProcess || spawn;
167
+ const chromeProcess = spawnProcess(executablePath, chromeArgs, {
168
+ detached: true,
169
+ stdio: "ignore"
170
+ });
171
+ chromeProcess.unref();
172
+ writeLine(outputManager, `Opened profile "${resolvedProfileId}" at ${url} using ${absoluteProfilePath}`);
173
+ if (defaultExtensionIds.length > 0) writeLine(outputManager, `Loaded default extension(s): ${defaultExtensionIds.join(", ")}`);
174
+ }
175
+ async function handleExtensionInstall(args, outputManager, options) {
176
+ const extensionArg = getPositionalArg(args.args, 1);
177
+ const extensionIdOption = getStringOption(args.options, "id");
178
+ const extensionPathOption = getStringOption(args.options, "path");
179
+ const sourcePathOption = getStringOption(args.options, "source") || getStringOption(args.options, "from");
180
+ const selectedId = extensionArg || extensionIdOption;
181
+ const shouldInstallBundled = !selectedId && !extensionPathOption && !sourcePathOption;
182
+ if (!selectedId && !shouldInstallBundled) throw new Error("Extension ID required when using --source/--path. Usage: wingman browser extension install [extension-id] [options]");
183
+ const extensionId = validateProfileId(selectedId || DEFAULT_BUNDLED_EXTENSION_ID);
184
+ const workspace = options.workspace || process.cwd();
185
+ const configDir = options.configDir || DEFAULT_CONFIG_DIR;
186
+ const configRoot = join(workspace, configDir);
187
+ const configPath = join(configRoot, "wingman.config.json");
188
+ const config = readConfigObject(configPath);
189
+ const browserConfig = readObject(config.browser);
190
+ const force = getBooleanOption(args.options, "force");
191
+ const extensions = readStringRecord(browserConfig.extensions);
192
+ const extensionsDirOption = getStringOption(args.options, "extensions-dir") || getStringOption(args.options, "extensionsDir");
193
+ if (extensionsDirOption) browserConfig.extensionsDir = extensionsDirOption;
194
+ const extensionsDir = resolveExtensionsDir(browserConfig.extensionsDir);
195
+ const extensionPath = extensionPathOption ? extensionPathOption : normalizePathForConfig(join(extensionsDir, extensionId));
196
+ const absoluteExtensionPath = resolveProfilePath(workspace, extensionPath);
197
+ const sourcePath = sourcePathOption || (shouldInstallBundled ? resolveBundledExtensionSourcePath() : void 0);
198
+ if (sourcePath) {
199
+ const absoluteSourcePath = resolveProfilePath(workspace, sourcePath);
200
+ if (!existsSync(absoluteSourcePath)) throw new Error(`Extension source does not exist: ${absoluteSourcePath}`);
201
+ if (!statSync(absoluteSourcePath).isDirectory()) throw new Error(`Extension source must be a directory: ${absoluteSourcePath}`);
202
+ if (existsSync(absoluteExtensionPath) && !force) throw new Error(`Extension target already exists at ${absoluteExtensionPath}. Use --force to overwrite.`);
203
+ cpSync(absoluteSourcePath, absoluteExtensionPath, {
204
+ recursive: true,
205
+ force
206
+ });
207
+ }
208
+ if (!existsSync(absoluteExtensionPath)) throw new Error(`Extension path does not exist: ${absoluteExtensionPath}. Provide --source or --path to an unpacked extension directory.`);
209
+ if (!statSync(absoluteExtensionPath).isDirectory()) throw new Error(`Extension path must be a directory: ${absoluteExtensionPath}.`);
210
+ const manifestPath = join(absoluteExtensionPath, "manifest.json");
211
+ if (!existsSync(manifestPath)) throw new Error(`manifest.json not found in extension directory: ${absoluteExtensionPath}.`);
212
+ const existingExtensionPath = extensions[extensionId];
213
+ if (existingExtensionPath && existingExtensionPath !== extensionPath && !force) throw new Error(`Extension "${extensionId}" already exists at ${existingExtensionPath}. Use --force to overwrite.`);
214
+ extensions[extensionId] = extensionPath;
215
+ browserConfig.extensions = extensions;
216
+ browserConfig.extensionsDir = extensionsDir;
217
+ const setAsDefault = getBooleanOption(args.options, "default");
218
+ if (setAsDefault) {
219
+ const defaults = new Set(readStringArray(browserConfig.defaultExtensions));
220
+ defaults.add(extensionId);
221
+ browserConfig.defaultExtensions = Array.from(defaults);
222
+ }
223
+ config.browser = browserConfig;
224
+ mkdirSync(configRoot, {
225
+ recursive: true
226
+ });
227
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
228
+ writeLine(outputManager, `Registered extension "${extensionId}".`);
229
+ writeLine(outputManager, `Extension directory: ${absoluteExtensionPath}`);
230
+ if (shouldInstallBundled) writeLine(outputManager, `Installed bundled Wingman extension as "${extensionId}".`);
231
+ if (setAsDefault) writeLine(outputManager, `Added "${extensionId}" to browser.defaultExtensions`);
232
+ writeLine(outputManager, `Saved config: ${configPath}`);
233
+ }
234
+ async function handleExtensionPath(args, outputManager, options) {
235
+ const extensionArg = getPositionalArg(args.args, 1);
236
+ if (!extensionArg) throw new Error("Extension ID required. Usage: wingman browser extension path <extension-id>");
237
+ const extensionId = validateProfileId(extensionArg);
238
+ const workspace = options.workspace || process.cwd();
239
+ const configDir = options.configDir || DEFAULT_CONFIG_DIR;
240
+ const configRoot = join(workspace, configDir);
241
+ const configPath = join(configRoot, "wingman.config.json");
242
+ const config = readConfigObject(configPath);
243
+ const browserConfig = readObject(config.browser);
244
+ const extensions = readStringRecord(browserConfig.extensions);
245
+ const configuredPath = extensions[extensionId];
246
+ if (!configuredPath) throw new Error(`Extension "${extensionId}" is not configured. Run wingman browser extension install ${extensionId} --path <dir> first.`);
247
+ const absoluteExtensionPath = resolveProfilePath(workspace, configuredPath);
248
+ writeLine(outputManager, absoluteExtensionPath);
249
+ }
250
+ async function handleExtensionList(args, outputManager, options) {
251
+ const workspace = options.workspace || process.cwd();
252
+ const configDir = options.configDir || DEFAULT_CONFIG_DIR;
253
+ const configRoot = join(workspace, configDir);
254
+ const configPath = join(configRoot, "wingman.config.json");
255
+ const config = readConfigObject(configPath);
256
+ const browserConfig = readObject(config.browser);
257
+ const extensions = readStringRecord(browserConfig.extensions);
258
+ const defaults = new Set(readStringArray(browserConfig.defaultExtensions));
259
+ const entries = Object.entries(extensions);
260
+ if (0 === entries.length) return void writeLine(outputManager, "No browser extensions configured.");
261
+ for (const [extensionId, extensionPath] of entries){
262
+ const marker = defaults.has(extensionId) ? " (default)" : "";
263
+ writeLine(outputManager, `${extensionId}${marker}: ${extensionPath}`);
264
+ }
265
+ }
266
+ async function handleExtensionPair(args, outputManager, options) {
267
+ const workspace = options.workspace || process.cwd();
268
+ const configDir = options.configDir || DEFAULT_CONFIG_DIR;
269
+ const configRoot = join(workspace, configDir);
270
+ const configPath = join(configRoot, "wingman.config.json");
271
+ const config = readConfigObject(configPath);
272
+ const browserConfig = readObject(config.browser);
273
+ const relayConfig = readObject(browserConfig.relay);
274
+ const relayHost = getStringOption(args.options, "host") || "127.0.0.1";
275
+ if (![
276
+ "127.0.0.1",
277
+ "localhost",
278
+ "::1"
279
+ ].includes(relayHost)) throw new Error(`Relay host must be loopback (127.0.0.1, localhost, ::1). Received "${relayHost}".`);
280
+ const relayPort = getNumberOption(args.options, "port") || ("number" == typeof relayConfig.port ? relayConfig.port : 18792);
281
+ if (!Number.isInteger(relayPort) || relayPort < 1 || relayPort > 65535) throw new Error("Relay port must be an integer between 1 and 65535.");
282
+ const configuredToken = getStringOption(args.options, "token");
283
+ const token = configuredToken || createRelayToken();
284
+ if (token.length < 16) throw new Error("Relay token must be at least 16 characters.");
285
+ relayConfig.enabled = true;
286
+ relayConfig.host = relayHost;
287
+ relayConfig.port = relayPort;
288
+ relayConfig.requireAuth = true;
289
+ relayConfig.authToken = token;
290
+ if ("number" != typeof relayConfig.maxMessageBytes) relayConfig.maxMessageBytes = 262144;
291
+ browserConfig.relay = relayConfig;
292
+ const extensionsDir = resolveExtensionsDir(browserConfig.extensionsDir);
293
+ const extensions = readStringRecord(browserConfig.extensions);
294
+ if (!extensions[DEFAULT_BUNDLED_EXTENSION_ID]) {
295
+ const extensionPath = normalizePathForConfig(join(extensionsDir, DEFAULT_BUNDLED_EXTENSION_ID));
296
+ const absoluteExtensionPath = resolveProfilePath(workspace, extensionPath);
297
+ const bundledSourcePath = resolveBundledExtensionSourcePath();
298
+ mkdirSync(absoluteExtensionPath, {
299
+ recursive: true
300
+ });
301
+ cpSync(bundledSourcePath, absoluteExtensionPath, {
302
+ recursive: true,
303
+ force: true
304
+ });
305
+ extensions[DEFAULT_BUNDLED_EXTENSION_ID] = extensionPath;
306
+ }
307
+ browserConfig.extensions = extensions;
308
+ browserConfig.extensionsDir = extensionsDir;
309
+ const defaults = new Set(readStringArray(browserConfig.defaultExtensions));
310
+ defaults.add(DEFAULT_BUNDLED_EXTENSION_ID);
311
+ browserConfig.defaultExtensions = Array.from(defaults);
312
+ config.browser = browserConfig;
313
+ mkdirSync(configRoot, {
314
+ recursive: true
315
+ });
316
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
317
+ writeLine(outputManager, "Configured secure browser relay pairing.");
318
+ writeLine(outputManager, `Relay host: ${relayHost}`);
319
+ writeLine(outputManager, `Relay port: ${relayPort}`);
320
+ writeLine(outputManager, `Relay token: ${token}`);
321
+ writeLine(outputManager, `Saved config: ${configPath}`);
322
+ writeLine(outputManager, "Next: open the extension options page and set the same relay token.");
323
+ }
324
+ function showBrowserHelp(outputManager) {
325
+ if ("interactive" === outputManager.getMode()) return void console.log(`
326
+ Wingman Browser Tools
327
+
328
+ Usage:
329
+ wingman browser profile init <profile-id> [options]
330
+ wingman browser profile open [profile-id] [options]
331
+ wingman browser extension install [extension-id] [options]
332
+ wingman browser extension pair [options]
333
+ wingman browser extension path <extension-id>
334
+ wingman browser extension list
335
+ wingman browser extension help
336
+ wingman browser profile help
337
+ wingman browser help
338
+
339
+ Examples:
340
+ wingman browser profile init trading
341
+ wingman browser profile open trading --url https://robinhood.com/login
342
+ wingman browser extension install --default
343
+ wingman browser extension pair
344
+ wingman browser extension install relay --source ./my-extension --default
345
+ wingman browser extension path relay
346
+ wingman browser profile init shopping --default
347
+ wingman browser profile init work --path .wingman/profiles/work
348
+
349
+ Options:
350
+ --workspace <dir> Workspace root (defaults to nearest ancestor with .wingman/)
351
+ --path <dir> Path for profile/extension (relative to workspace or absolute)
352
+ --profiles-dir <dir> Base profile directory used when --path is omitted
353
+ --extensions-dir <dir> Base extension directory used when --path is omitted
354
+ --source <dir> Source directory to copy from during extension install
355
+ --url <url> Target URL for profile open
356
+ --headless Open profile in headless mode
357
+ --default Set as browser.defaultProfile or add to browser.defaultExtensions
358
+ --force Overwrite existing profile/extension mapping
359
+ --host <host> Relay host for extension pairing (must be loopback)
360
+ --port <port> Relay port for extension pairing
361
+ --token <token> Explicit relay token for extension pairing
362
+ `);
363
+ outputManager.emitLog("info", "Browser help requested");
364
+ }
365
+ function showBrowserProfileHelp(outputManager) {
366
+ if ("interactive" === outputManager.getMode()) return void console.log(`
367
+ Wingman Browser Profile Manager
368
+
369
+ Usage:
370
+ wingman browser profile init <profile-id> [options]
371
+ wingman browser profile open [profile-id] [options]
372
+ wingman browser profile help
373
+
374
+ Examples:
375
+ wingman browser profile init trading
376
+ wingman browser profile init work --path .wingman/profiles/work --default
377
+ wingman browser profile open trading --url https://robinhood.com/login
378
+ `);
379
+ outputManager.emitLog("info", "Browser profile help requested");
380
+ }
381
+ function showBrowserExtensionHelp(outputManager) {
382
+ if ("interactive" === outputManager.getMode()) return void console.log(`
383
+ Wingman Browser Extension Manager
384
+
385
+ Usage:
386
+ wingman browser extension install [extension-id] [options]
387
+ wingman browser extension pair [options]
388
+ wingman browser extension path <extension-id>
389
+ wingman browser extension list
390
+ wingman browser extension help
391
+
392
+ Examples:
393
+ wingman browser extension install --default
394
+ wingman browser extension pair
395
+ wingman browser extension install relay --path .wingman/browser-extensions/relay
396
+ wingman browser extension install relay --source ./relay-extension --default
397
+ wingman browser extension path relay
398
+ wingman browser extension list
399
+ `);
400
+ outputManager.emitLog("info", "Browser extension help requested");
401
+ }
402
+ function readConfigObject(configPath) {
403
+ if (!existsSync(configPath)) return {};
404
+ const raw = readFileSync(configPath, "utf-8");
405
+ let parsed;
406
+ try {
407
+ parsed = JSON.parse(raw);
408
+ } catch {
409
+ throw new Error("Existing wingman.config.json is invalid JSON. Fix the file or run wingman init --force.");
410
+ }
411
+ if (!isObject(parsed)) throw new Error("Existing wingman.config.json must be a JSON object.");
412
+ return {
413
+ ...parsed
414
+ };
415
+ }
416
+ function resolveProfilesDir(rawProfilesDir) {
417
+ if ("string" == typeof rawProfilesDir && rawProfilesDir.trim()) return rawProfilesDir.trim();
418
+ return DEFAULT_PROFILES_DIR;
419
+ }
420
+ function resolveExtensionsDir(rawExtensionsDir) {
421
+ if ("string" == typeof rawExtensionsDir && rawExtensionsDir.trim()) return rawExtensionsDir.trim();
422
+ return DEFAULT_EXTENSIONS_DIR;
423
+ }
424
+ function validateProfileId(value) {
425
+ const profileId = value.trim();
426
+ if (!profileId) throw new Error("Profile ID cannot be empty.");
427
+ if (!PROFILE_ID_PATTERN.test(profileId)) throw new Error(`Invalid profile ID "${value}". Use letters, numbers, dot, underscore, or dash.`);
428
+ return profileId;
429
+ }
430
+ function isObject(value) {
431
+ return Boolean(value) && "object" == typeof value && !Array.isArray(value);
432
+ }
433
+ function readObject(value) {
434
+ if (!isObject(value)) return {};
435
+ return {
436
+ ...value
437
+ };
438
+ }
439
+ function readStringRecord(value) {
440
+ if (!isObject(value)) return {};
441
+ const result = {};
442
+ for (const [key, innerValue] of Object.entries(value))if (key.trim() && "string" == typeof innerValue) result[key] = innerValue;
443
+ return result;
444
+ }
445
+ function readStringArray(value) {
446
+ if (!Array.isArray(value)) return [];
447
+ return value.filter((item)=>"string" == typeof item).map((item)=>item.trim()).filter(Boolean);
448
+ }
449
+ function getStringOption(options, key) {
450
+ const value = options[key];
451
+ if ("string" == typeof value && value.trim()) return value.trim();
452
+ }
453
+ function getNumberOption(options, key) {
454
+ const value = options[key];
455
+ if ("number" == typeof value && Number.isFinite(value)) return value;
456
+ if ("string" == typeof value) {
457
+ const parsed = Number.parseInt(value.trim(), 10);
458
+ if (Number.isFinite(parsed)) return parsed;
459
+ }
460
+ }
461
+ function getBooleanOption(options, key) {
462
+ const value = options[key];
463
+ if ("boolean" == typeof value) return value;
464
+ if ("string" == typeof value) {
465
+ const normalized = value.trim().toLowerCase();
466
+ if ([
467
+ "true",
468
+ "1",
469
+ "yes",
470
+ "y",
471
+ "on"
472
+ ].includes(normalized)) return true;
473
+ [
474
+ "false",
475
+ "0",
476
+ "no",
477
+ "n",
478
+ "off"
479
+ ].includes(normalized);
480
+ }
481
+ return false;
482
+ }
483
+ function getPositionalArg(args, index) {
484
+ const value = args[index];
485
+ if ("string" != typeof value) return;
486
+ const trimmed = value.trim();
487
+ if (!trimmed || trimmed.startsWith("--")) return;
488
+ return trimmed;
489
+ }
490
+ function normalizePathForConfig(pathValue) {
491
+ if (isAbsolute(pathValue)) return pathValue;
492
+ return pathValue.split("\\").join("/");
493
+ }
494
+ function resolveProfilePath(workspace, profilePath) {
495
+ if (isAbsolute(profilePath)) return profilePath;
496
+ return resolve(workspace, profilePath);
497
+ }
498
+ function resolveBundledExtensionSourcePath() {
499
+ const bundledPath = resolve(fileURLToPath(new URL(BUNDLED_EXTENSION_RELATIVE_PATH, import.meta.url)));
500
+ const bundledManifest = join(bundledPath, "manifest.json");
501
+ if (existsSync(bundledManifest)) return bundledPath;
502
+ throw new Error("Bundled Wingman extension assets were not found. Reinstall Wingman or provide --source <dir>.");
503
+ }
504
+ function createRelayToken() {
505
+ return randomBytes(24).toString("base64url");
506
+ }
507
+ function resolveExtensionArgs(workspace, browserConfig, extensionIds) {
508
+ if (0 === extensionIds.length) return [
509
+ "--disable-extensions"
510
+ ];
511
+ const extensions = readStringRecord(browserConfig.extensions);
512
+ const dirs = extensionIds.map((extensionId)=>{
513
+ const configuredPath = extensions[extensionId];
514
+ if (!configuredPath) throw new Error(`Extension "${extensionId}" is not configured in browser.extensions.`);
515
+ const absolutePath = resolveProfilePath(workspace, configuredPath);
516
+ if (!existsSync(absolutePath)) throw new Error(`Configured extension path does not exist: ${absolutePath}.`);
517
+ return absolutePath;
518
+ });
519
+ const joined = dirs.join(",");
520
+ return [
521
+ `--disable-extensions-except=${joined}`,
522
+ `--load-extension=${joined}`
523
+ ];
524
+ }
525
+ function getChromeCandidates() {
526
+ const candidates = [
527
+ process.env.WINGMAN_CHROME_EXECUTABLE,
528
+ "google-chrome",
529
+ "chromium-browser",
530
+ "chromium"
531
+ ].filter((candidate)=>Boolean(candidate?.trim()));
532
+ if ("darwin" === process.platform) candidates.push("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium");
533
+ if ("linux" === process.platform) candidates.push("/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium-browser", "/usr/bin/chromium");
534
+ if ("win32" === process.platform) candidates.push("C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe");
535
+ return candidates;
536
+ }
537
+ function resolveBinaryFromPath(binaryName) {
538
+ const locator = "win32" === process.platform ? "where" : "which";
539
+ const result = spawnSync(locator, [
540
+ binaryName
541
+ ], {
542
+ encoding: "utf-8"
543
+ });
544
+ if (0 !== result.status || !result.stdout) return null;
545
+ const firstLine = result.stdout.split(/\r?\n/).map((line)=>line.trim()).find(Boolean);
546
+ return firstLine || null;
547
+ }
548
+ function resolveChromeExecutablePath(explicitPath) {
549
+ const candidatePool = explicitPath?.trim() ? [
550
+ explicitPath.trim()
551
+ ] : getChromeCandidates();
552
+ for (const candidate of candidatePool)if (candidate) {
553
+ if (isAbsolute(candidate) && existsSync(candidate)) return candidate;
554
+ if (!isAbsolute(candidate)) {
555
+ const fromPath = resolveBinaryFromPath(candidate);
556
+ if (fromPath) return fromPath;
557
+ }
558
+ }
559
+ if (explicitPath?.trim()) throw new Error(`Chrome executable not found at "${explicitPath}". Provide a valid executable path.`);
560
+ throw new Error("No Chrome/Chromium executable found. Install Chrome/Chromium or set WINGMAN_CHROME_EXECUTABLE.");
561
+ }
562
+ function writeLine(outputManager, message) {
563
+ if ("interactive" === outputManager.getMode()) console.log(message);
564
+ else outputManager.emitLog("info", message);
565
+ }
566
+ export { executeBrowserCommand };
@@ -39,11 +39,11 @@ function reportGatewayError(context, error) {
39
39
  console.error(`✗ ${context}: ${errorMsg}`);
40
40
  console.error(`Logs: ${logFile}`);
41
41
  }
42
- async function executeGatewayCommand(args) {
42
+ async function executeGatewayCommand(args, commandOptions = {}) {
43
43
  const { subcommand, options } = args;
44
44
  switch(subcommand){
45
45
  case "start":
46
- await handleStart(options);
46
+ await handleStart(options, commandOptions);
47
47
  break;
48
48
  case "stop":
49
49
  await handleStop();
@@ -55,7 +55,7 @@ async function executeGatewayCommand(args) {
55
55
  await handleStatus();
56
56
  break;
57
57
  case "run":
58
- await handleRun(options);
58
+ await handleRun(options, commandOptions);
59
59
  break;
60
60
  case "join":
61
61
  await handleJoin(args.args, options);
@@ -77,8 +77,8 @@ async function executeGatewayCommand(args) {
77
77
  process.exit(1);
78
78
  }
79
79
  }
80
- async function handleStart(options) {
81
- const configLoader = new loader_cjs_namespaceObject.WingmanConfigLoader();
80
+ async function handleStart(options, commandOptions) {
81
+ const configLoader = createConfigLoader(commandOptions);
82
82
  const wingmanConfig = configLoader.loadConfig();
83
83
  const gatewayDefaults = wingmanConfig.gateway;
84
84
  const envToken = (0, env_cjs_namespaceObject.getGatewayTokenFromEnv)();
@@ -93,6 +93,8 @@ async function handleStart(options) {
93
93
  const config = {
94
94
  port: options.port || gatewayDefaults.port || 18789,
95
95
  host: options.host || gatewayDefaults.host || "127.0.0.1",
96
+ workspace: commandOptions.workspace,
97
+ configDir: commandOptions.configDir,
96
98
  requireAuth: auth?.mode !== "none",
97
99
  authToken: auth?.token,
98
100
  auth,
@@ -169,13 +171,15 @@ async function handleStatus() {
169
171
  }
170
172
  console.log(` Log File: ${daemon.getLogFile()}`);
171
173
  }
172
- async function handleRun(options) {
174
+ async function handleRun(options, commandOptions) {
173
175
  let config;
174
176
  if (options.daemon && process.env.WINGMAN_GATEWAY_CONFIG) {
175
177
  const configStr = (0, external_fs_namespaceObject.readFileSync)(process.env.WINGMAN_GATEWAY_CONFIG, "utf-8");
176
178
  config = JSON.parse(configStr);
179
+ config.workspace = config.workspace || commandOptions.workspace || process.cwd();
180
+ config.configDir = config.configDir || commandOptions.configDir || ".wingman";
177
181
  } else {
178
- const configLoader = new loader_cjs_namespaceObject.WingmanConfigLoader();
182
+ const configLoader = createConfigLoader(commandOptions);
179
183
  const wingmanConfig = configLoader.loadConfig();
180
184
  const gatewayDefaults = wingmanConfig.gateway;
181
185
  const envToken = (0, env_cjs_namespaceObject.getGatewayTokenFromEnv)();
@@ -190,6 +194,8 @@ async function handleRun(options) {
190
194
  config = {
191
195
  port: options.port || gatewayDefaults.port || 18789,
192
196
  host: options.host || gatewayDefaults.host || "127.0.0.1",
197
+ workspace: commandOptions.workspace,
198
+ configDir: commandOptions.configDir,
193
199
  requireAuth: auth?.mode !== "none",
194
200
  authToken: auth?.token,
195
201
  auth,
@@ -223,6 +229,11 @@ async function handleRun(options) {
223
229
  process.exit(1);
224
230
  }
225
231
  }
232
+ function createConfigLoader(commandOptions) {
233
+ const workspace = commandOptions.workspace || process.cwd();
234
+ const configDir = commandOptions.configDir || ".wingman";
235
+ return new loader_cjs_namespaceObject.WingmanConfigLoader(configDir, workspace);
236
+ }
226
237
  async function handleJoin(args, options) {
227
238
  const url = args[0] || "ws://localhost:18789/ws";
228
239
  const name = options.name || `node-${Date.now()}`;
@@ -6,7 +6,11 @@ export interface GatewayCommandArgs {
6
6
  args: string[];
7
7
  options: Record<string, unknown>;
8
8
  }
9
+ export interface GatewayCommandOptions {
10
+ workspace?: string;
11
+ configDir?: string;
12
+ }
9
13
  /**
10
14
  * Execute gateway command
11
15
  */
12
- export declare function executeGatewayCommand(args: GatewayCommandArgs): Promise<void>;
16
+ export declare function executeGatewayCommand(args: GatewayCommandArgs, commandOptions?: GatewayCommandOptions): Promise<void>;