@gajae-code/coding-agent 0.5.4 → 0.6.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.
- package/CHANGELOG.md +23 -0
- package/dist/types/cli/web-search-cli.d.ts +12 -0
- package/dist/types/commands/rlm.d.ts +10 -0
- package/dist/types/commands/web-search.d.ts +54 -0
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +61 -3
- package/dist/types/edit/notebook.d.ts +3 -0
- package/dist/types/eval/py/executor.d.ts +3 -0
- package/dist/types/eval/py/kernel.d.ts +3 -1
- package/dist/types/eval/py/runtime.d.ts +9 -1
- package/dist/types/exec/bash-executor.d.ts +4 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/custom-tools/wrapper.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +1 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +6 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +14 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +3 -3
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +18 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/goals/tools/goal-tool.d.ts +2 -0
- package/dist/types/main.d.ts +11 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -2
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +12 -0
- package/dist/types/modes/components/model-selector.d.ts +5 -2
- package/dist/types/modes/components/status-line.d.ts +4 -1
- package/dist/types/modes/controllers/input-controller.d.ts +3 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/print-mode.d.ts +6 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +21 -0
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +1 -0
- package/dist/types/rlm/artifacts.d.ts +9 -0
- package/dist/types/rlm/complete-research-tool.d.ts +35 -0
- package/dist/types/rlm/data-context.d.ts +6 -0
- package/dist/types/rlm/index.d.ts +35 -0
- package/dist/types/rlm/notebook.d.ts +12 -0
- package/dist/types/rlm/preset.d.ts +23 -0
- package/dist/types/rlm/python-tool.d.ts +16 -0
- package/dist/types/rlm/report.d.ts +14 -0
- package/dist/types/rlm/types.d.ts +37 -0
- package/dist/types/sdk.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -1
- package/dist/types/tools/browser/attach.d.ts +19 -3
- package/dist/types/tools/browser/registry.d.ts +15 -0
- package/dist/types/tools/browser/render.d.ts +3 -0
- package/dist/types/tools/browser.d.ts +18 -1
- package/dist/types/tools/computer/render.d.ts +17 -0
- package/dist/types/tools/computer.d.ts +465 -0
- package/dist/types/tools/index.d.ts +24 -1
- package/dist/types/tools/job.d.ts +13 -0
- package/dist/types/tools/tool-timeouts.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +32 -2
- package/dist/types/web/search/providers/base.d.ts +22 -0
- package/dist/types/web/search/providers/xai.d.ts +64 -0
- package/dist/types/web/search/types.d.ts +11 -3
- package/package.json +7 -7
- package/src/cli/web-search-cli.ts +123 -8
- package/src/cli.ts +2 -0
- package/src/commands/rlm.ts +19 -0
- package/src/commands/web-search.ts +66 -0
- package/src/config/keybindings.ts +11 -0
- package/src/config/model-profiles.ts +11 -3
- package/src/config/model-registry.ts +55 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +67 -1
- package/src/edit/notebook.ts +6 -2
- package/src/eval/py/executor.ts +8 -1
- package/src/eval/py/kernel.ts +9 -4
- package/src/eval/py/runtime.ts +153 -32
- package/src/exec/bash-executor.ts +10 -4
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/custom-tools/wrapper.ts +2 -0
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/extensions/wrapper.ts +1 -0
- package/src/gjc-runtime/launch-tmux.ts +129 -1
- package/src/gjc-runtime/session-state-sidecar.ts +61 -1
- package/src/gjc-runtime/tmux-common.ts +26 -2
- package/src/gjc-runtime/tmux-gc.ts +40 -27
- package/src/gjc-runtime/tmux-sessions.ts +13 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +340 -18
- package/src/goals/runtime.ts +4 -3
- package/src/goals/state.ts +1 -1
- package/src/goals/tools/goal-tool.ts +16 -3
- package/src/internal-urls/docs-index.generated.ts +13 -9
- package/src/main.ts +28 -3
- package/src/modes/components/custom-editor.ts +13 -4
- package/src/modes/components/custom-model-preset-wizard.ts +293 -0
- package/src/modes/components/hook-selector.ts +1 -1
- package/src/modes/components/model-selector.ts +72 -29
- package/src/modes/components/skill-message.ts +62 -8
- package/src/modes/components/status-line.ts +13 -1
- package/src/modes/controllers/input-controller.ts +60 -11
- package/src/modes/controllers/selector-controller.ts +39 -0
- package/src/modes/interactive-mode.ts +1 -1
- package/src/modes/print-mode.ts +14 -4
- package/src/modes/rpc/rpc-client.ts +250 -80
- package/src/modes/rpc/rpc-mode.ts +6 -12
- package/src/modes/rpc/rpc-socket-security.ts +103 -0
- package/src/modes/rpc/rpc-types.ts +10 -0
- package/src/modes/shared/agent-wire/command-dispatch.ts +7 -0
- package/src/modes/shared/agent-wire/command-validation.ts +1 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +9 -0
- package/src/modes/utils/hotkeys-markdown.ts +4 -2
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/prompts/goals/goal-continuation.md +1 -0
- package/src/prompts/goals/goal-mode-active.md +1 -0
- package/src/prompts/system/rlm-report-command.md +1 -0
- package/src/prompts/system/rlm-research.md +23 -0
- package/src/prompts/tools/bash.md +23 -2
- package/src/prompts/tools/browser.md +7 -3
- package/src/prompts/tools/computer.md +74 -0
- package/src/prompts/tools/goal.md +3 -0
- package/src/prompts/tools/job.md +9 -1
- package/src/prompts/tools/web-search.md +7 -0
- package/src/rlm/artifacts.ts +60 -0
- package/src/rlm/complete-research-tool.ts +163 -0
- package/src/rlm/data-context.ts +26 -0
- package/src/rlm/index.ts +339 -0
- package/src/rlm/notebook.ts +108 -0
- package/src/rlm/preset.ts +76 -0
- package/src/rlm/python-tool.ts +68 -0
- package/src/rlm/report.ts +70 -0
- package/src/rlm/types.ts +40 -0
- package/src/sdk.ts +12 -0
- package/src/session/agent-session.ts +48 -3
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/tools/bash-allowed-prefixes.ts +84 -1
- package/src/tools/bash.ts +80 -13
- package/src/tools/browser/attach.ts +103 -3
- package/src/tools/browser/registry.ts +176 -2
- package/src/tools/browser/render.ts +9 -1
- package/src/tools/browser.ts +33 -0
- package/src/tools/computer/render.ts +78 -0
- package/src/tools/computer.ts +640 -0
- package/src/tools/index.ts +41 -1
- package/src/tools/job.ts +88 -5
- package/src/tools/json-tree.ts +42 -29
- package/src/tools/renderers.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/web/search/index.ts +27 -2
- package/src/web/search/provider.ts +16 -1
- package/src/web/search/providers/base.ts +22 -0
- package/src/web/search/providers/xai.ts +511 -0
- package/src/web/search/render.ts +7 -0
- package/src/web/search/types.ts +11 -1
|
@@ -3,12 +3,28 @@ import { logger } from "@gajae-code/utils";
|
|
|
3
3
|
import type { Subprocess } from "bun";
|
|
4
4
|
import type { Browser, CDPSession } from "puppeteer-core";
|
|
5
5
|
import { ToolAbortError, ToolError } from "../tool-errors";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
findFreeCdpPort,
|
|
8
|
+
findReusableCdp,
|
|
9
|
+
findRunningChromeProfile,
|
|
10
|
+
gracefulKillTreeOnce,
|
|
11
|
+
killExistingByPath,
|
|
12
|
+
waitForCdp,
|
|
13
|
+
} from "./attach";
|
|
7
14
|
import { BROWSER_PROTOCOL_TIMEOUT_MS, launchHeadlessBrowser, loadPuppeteer, type UserAgentOverride } from "./launch";
|
|
8
15
|
|
|
9
16
|
export type BrowserKind =
|
|
10
17
|
| { kind: "headless"; headless: boolean }
|
|
11
18
|
| { kind: "spawned"; path: string }
|
|
19
|
+
| {
|
|
20
|
+
kind: "chrome-profile";
|
|
21
|
+
path: string;
|
|
22
|
+
userDataDir: string;
|
|
23
|
+
profileDirectory: string;
|
|
24
|
+
background: boolean;
|
|
25
|
+
noFocus: boolean;
|
|
26
|
+
cdpPort?: number;
|
|
27
|
+
}
|
|
12
28
|
| { kind: "connected"; cdpUrl: string };
|
|
13
29
|
|
|
14
30
|
export type BrowserKindTag = BrowserKind["kind"];
|
|
@@ -24,6 +40,8 @@ export interface BrowserHandle {
|
|
|
24
40
|
stealth: { browserSession: CDPSession | null; override: UserAgentOverride | null };
|
|
25
41
|
}
|
|
26
42
|
|
|
43
|
+
type SpawnedChromeProfileKind = Extract<BrowserKind, { kind: "chrome-profile" }>;
|
|
44
|
+
|
|
27
45
|
const browsers = new Map<string, BrowserHandle>();
|
|
28
46
|
|
|
29
47
|
/**
|
|
@@ -39,6 +57,8 @@ function browserKey(kind: BrowserKind): string {
|
|
|
39
57
|
return `headless:${kind.headless ? "1" : "0"}`;
|
|
40
58
|
case "spawned":
|
|
41
59
|
return `spawned:${kind.path}`;
|
|
60
|
+
case "chrome-profile":
|
|
61
|
+
return `chrome-profile:${kind.path}:${kind.userDataDir}:${kind.profileDirectory}:${kind.cdpPort ?? 0}`;
|
|
42
62
|
case "connected":
|
|
43
63
|
return `connected:${kind.cdpUrl}`;
|
|
44
64
|
}
|
|
@@ -94,7 +114,160 @@ async function openBrowserHandle(kind: BrowserKind, opts: AcquireBrowserOptions)
|
|
|
94
114
|
stealth: { browserSession: null, override: null },
|
|
95
115
|
};
|
|
96
116
|
}
|
|
117
|
+
if (kind.kind === "chrome-profile") {
|
|
118
|
+
return await openChromeProfileHandle(kind, opts);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return await openSpawnedBrowserHandle(kind, opts);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const CHROME_PROFILE_LOCK_FILES = ["SingletonLock", "SingletonSocket", "SingletonCookie"] as const;
|
|
125
|
+
|
|
126
|
+
async function hasChromeProfileLock(userDataDir: string): Promise<boolean> {
|
|
127
|
+
for (const lockFile of CHROME_PROFILE_LOCK_FILES) {
|
|
128
|
+
if (await Bun.file(path.join(userDataDir, lockFile)).exists()) return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const CHROME_PROFILE_MANAGED_FLAGS = new Set([
|
|
134
|
+
"--user-data-dir",
|
|
135
|
+
"--profile-directory",
|
|
136
|
+
"--remote-debugging-address",
|
|
137
|
+
"--remote-debugging-port",
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
function filterChromeProfileAppArgs(appArgs: readonly string[] | undefined): string[] {
|
|
141
|
+
if (!appArgs?.length) return [];
|
|
142
|
+
const filtered: string[] = [];
|
|
143
|
+
for (let i = 0; i < appArgs.length; i++) {
|
|
144
|
+
const arg = appArgs[i]!;
|
|
145
|
+
const flagName = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
|
|
146
|
+
if (CHROME_PROFILE_MANAGED_FLAGS.has(flagName)) {
|
|
147
|
+
if (!arg.includes("=") && i < appArgs.length - 1) i++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
filtered.push(arg);
|
|
151
|
+
}
|
|
152
|
+
return filtered;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function buildChromeProfileLaunchArgs(
|
|
156
|
+
kind: SpawnedChromeProfileKind,
|
|
157
|
+
appArgs: readonly string[] | undefined,
|
|
158
|
+
port: number,
|
|
159
|
+
): string[] {
|
|
160
|
+
const args = [
|
|
161
|
+
...filterChromeProfileAppArgs(appArgs),
|
|
162
|
+
`--user-data-dir=${kind.userDataDir}`,
|
|
163
|
+
`--profile-directory=${kind.profileDirectory}`,
|
|
164
|
+
`--remote-debugging-port=${port}`,
|
|
165
|
+
"--remote-debugging-address=127.0.0.1",
|
|
166
|
+
];
|
|
167
|
+
if (kind.background || kind.noFocus) args.push("--no-startup-window");
|
|
168
|
+
return args;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function buildChromeProfileLaunchArgsForTest(
|
|
172
|
+
kind: SpawnedChromeProfileKind,
|
|
173
|
+
appArgs: readonly string[] | undefined,
|
|
174
|
+
port: number,
|
|
175
|
+
): string[] {
|
|
176
|
+
return buildChromeProfileLaunchArgs(kind, appArgs, port);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function openChromeProfileHandle(
|
|
180
|
+
kind: SpawnedChromeProfileKind,
|
|
181
|
+
opts: AcquireBrowserOptions,
|
|
182
|
+
): Promise<BrowserHandle> {
|
|
183
|
+
const exe = kind.path;
|
|
184
|
+
if (!path.isAbsolute(exe)) {
|
|
185
|
+
throw new ToolError(
|
|
186
|
+
`app.path must be absolute for app.browser="chrome" (got ${JSON.stringify(exe)}). Pass the Chrome binary path, not the .app bundle.`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const running = await findRunningChromeProfile(
|
|
191
|
+
exe,
|
|
192
|
+
{ userDataDir: kind.userDataDir, profileDirectory: kind.profileDirectory },
|
|
193
|
+
opts.signal,
|
|
194
|
+
);
|
|
195
|
+
let cdpUrl: string;
|
|
196
|
+
let pid: number | undefined;
|
|
197
|
+
let subprocess: Subprocess | undefined;
|
|
198
|
+
if (running?.cdpUrl) {
|
|
199
|
+
logger.debug("Reusing existing Chrome profile CDP endpoint", {
|
|
200
|
+
exe,
|
|
201
|
+
pid: running.pid,
|
|
202
|
+
cdpUrl: running.cdpUrl,
|
|
203
|
+
profileDirectory: kind.profileDirectory,
|
|
204
|
+
});
|
|
205
|
+
cdpUrl = running.cdpUrl;
|
|
206
|
+
pid = running.pid;
|
|
207
|
+
} else if (running) {
|
|
208
|
+
throw new ToolError(
|
|
209
|
+
running.unsafeCdpReason ??
|
|
210
|
+
`Chrome profile ${JSON.stringify(kind.profileDirectory)} under ${kind.userDataDir} is already running without an attachable localhost CDP endpoint. ` +
|
|
211
|
+
"GJC will not kill or relaunch an existing Chrome profile. Close that Chrome profile first, or restart Chrome yourself with --remote-debugging-address=127.0.0.1 and --remote-debugging-port=<port> then use app.cdp_url.",
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
if (await hasChromeProfileLock(kind.userDataDir)) {
|
|
215
|
+
throw new ToolError(
|
|
216
|
+
`Chrome user data directory ${kind.userDataDir} appears to be locked by an existing Chrome process without an attachable localhost CDP endpoint. ` +
|
|
217
|
+
"GJC will not kill or relaunch an existing Chrome profile. Close that Chrome profile first, or restart Chrome yourself with --remote-debugging-address=127.0.0.1 and --remote-debugging-port=<port> then use app.cdp_url.",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const port = kind.cdpPort ?? (await findFreeCdpPort());
|
|
221
|
+
const launchArgs = buildChromeProfileLaunchArgs(kind, opts.appArgs, port);
|
|
222
|
+
const child = Bun.spawn([exe, ...launchArgs], {
|
|
223
|
+
stdout: "ignore",
|
|
224
|
+
stderr: "ignore",
|
|
225
|
+
stdin: "ignore",
|
|
226
|
+
});
|
|
227
|
+
child.unref();
|
|
228
|
+
subprocess = child;
|
|
229
|
+
pid = child.pid;
|
|
230
|
+
cdpUrl = `http://127.0.0.1:${port}`;
|
|
231
|
+
try {
|
|
232
|
+
await waitForCdp(cdpUrl, 30_000, opts.signal);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
await gracefulKillTreeOnce(child.pid).catch(() => undefined);
|
|
235
|
+
if (err instanceof ToolAbortError) throw err;
|
|
236
|
+
if (err instanceof Error && err.name === "AbortError") throw err;
|
|
237
|
+
throw new ToolError(
|
|
238
|
+
`Failed to attach to Chrome profile ${JSON.stringify(kind.profileDirectory)} on ${cdpUrl}: ${(err as Error).message}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const puppeteer = await loadPuppeteer();
|
|
244
|
+
let browser: Browser;
|
|
245
|
+
try {
|
|
246
|
+
browser = await puppeteer.connect({
|
|
247
|
+
browserURL: cdpUrl,
|
|
248
|
+
defaultViewport: null,
|
|
249
|
+
protocolTimeout: BROWSER_PROTOCOL_TIMEOUT_MS,
|
|
250
|
+
});
|
|
251
|
+
} catch (err) {
|
|
252
|
+
if (subprocess) await gracefulKillTreeOnce(subprocess.pid);
|
|
253
|
+
throw new ToolError(`Connected to ${cdpUrl} but puppeteer.connect failed: ${(err as Error).message}`);
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
key: browserKey(kind),
|
|
257
|
+
kind,
|
|
258
|
+
browser,
|
|
259
|
+
cdpUrl,
|
|
260
|
+
pid,
|
|
261
|
+
subprocess,
|
|
262
|
+
refCount: 0,
|
|
263
|
+
stealth: { browserSession: null, override: null },
|
|
264
|
+
};
|
|
265
|
+
}
|
|
97
266
|
|
|
267
|
+
async function openSpawnedBrowserHandle(
|
|
268
|
+
kind: Extract<BrowserKind, { kind: "spawned" }>,
|
|
269
|
+
opts: AcquireBrowserOptions,
|
|
270
|
+
): Promise<BrowserHandle> {
|
|
98
271
|
const exe = kind.path;
|
|
99
272
|
if (!path.isAbsolute(exe)) {
|
|
100
273
|
throw new ToolError(
|
|
@@ -203,8 +376,9 @@ async function disposeBrowserHandle(handle: BrowserHandle, opts: { kill: boolean
|
|
|
203
376
|
try {
|
|
204
377
|
handle.browser.disconnect();
|
|
205
378
|
} catch (err) {
|
|
206
|
-
logger.debug(
|
|
379
|
+
logger.debug(`Failed to disconnect from ${handle.kind.kind} browser`, { error: (err as Error).message });
|
|
207
380
|
}
|
|
208
381
|
}
|
|
382
|
+
if (handle.kind.kind === "chrome-profile" && !handle.subprocess) return;
|
|
209
383
|
if (opts.kill && handle.pid !== undefined) await gracefulKillTreeOnce(handle.pid);
|
|
210
384
|
}
|
|
@@ -23,7 +23,14 @@ interface BrowserRenderArgs {
|
|
|
23
23
|
code?: string;
|
|
24
24
|
all?: boolean;
|
|
25
25
|
kill?: boolean;
|
|
26
|
-
app?: {
|
|
26
|
+
app?: {
|
|
27
|
+
path?: string;
|
|
28
|
+
cdp_url?: string;
|
|
29
|
+
browser?: string;
|
|
30
|
+
user_data_dir?: string;
|
|
31
|
+
profile_directory?: string;
|
|
32
|
+
target?: string;
|
|
33
|
+
};
|
|
27
34
|
viewport?: { width: number; height: number; scale?: number };
|
|
28
35
|
timeout?: number;
|
|
29
36
|
}
|
|
@@ -35,6 +42,7 @@ interface BrowserRenderContext {
|
|
|
35
42
|
|
|
36
43
|
function describeBrowser(args: BrowserRenderArgs, details: BrowserToolDetails | undefined): string | undefined {
|
|
37
44
|
if (args.app?.cdp_url) return `connected ${args.app.cdp_url}`;
|
|
45
|
+
if (args.app?.browser === "chrome") return `Chrome profile ${args.app.profile_directory ?? "<profile>"}`;
|
|
38
46
|
if (args.app?.path) return `spawned ${shortenPath(args.app.path)}`;
|
|
39
47
|
switch (details?.browser) {
|
|
40
48
|
case "headless":
|
package/src/tools/browser.ts
CHANGED
|
@@ -21,6 +21,12 @@ const DEFAULT_TAB_NAME = "main";
|
|
|
21
21
|
const appSchema = z.object({
|
|
22
22
|
path: z.string().describe("binary path to spawn").optional(),
|
|
23
23
|
cdp_url: z.string().describe("existing cdp endpoint").optional(),
|
|
24
|
+
browser: z.enum(["chrome"]).describe("existing browser profile mode").optional(),
|
|
25
|
+
user_data_dir: z.string().describe("Chrome user data directory containing profiles").optional(),
|
|
26
|
+
profile_directory: z.string().describe("Chrome profile directory name, e.g. Profile 10").optional(),
|
|
27
|
+
background: z.boolean().describe("prefer background/hidden Chrome profile launch when supported").optional(),
|
|
28
|
+
no_focus: z.boolean().describe("avoid focusing Chrome during profile launch when supported").optional(),
|
|
29
|
+
cdp_port: z.number().int().positive().describe("local CDP port for launched Chrome profile").optional(),
|
|
24
30
|
args: z.array(z.string()).describe("extra cli args").optional(),
|
|
25
31
|
target: z.string().describe("substring to pick a window").optional(),
|
|
26
32
|
});
|
|
@@ -104,11 +110,31 @@ export interface BrowserToolDetails {
|
|
|
104
110
|
meta?: OutputMeta;
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
export function resolveBrowserKindForTest(params: BrowserParams, session: ToolSession): BrowserKind {
|
|
114
|
+
return resolveBrowserKind(params, session);
|
|
115
|
+
}
|
|
116
|
+
|
|
107
117
|
function resolveBrowserKind(params: BrowserParams, session: ToolSession): BrowserKind {
|
|
108
118
|
const app = params.app;
|
|
109
119
|
if (app?.cdp_url) {
|
|
110
120
|
return { kind: "connected", cdpUrl: app.cdp_url.replace(/\/+$/, "") };
|
|
111
121
|
}
|
|
122
|
+
if (app?.browser === "chrome") {
|
|
123
|
+
if (!app.path) throw new ToolError('app.path is required when app.browser is "chrome".');
|
|
124
|
+
if (!app.user_data_dir) throw new ToolError('app.user_data_dir is required when app.browser is "chrome".');
|
|
125
|
+
if (!app.profile_directory)
|
|
126
|
+
throw new ToolError('app.profile_directory is required when app.browser is "chrome".');
|
|
127
|
+
const exe = resolveToCwd(app.path, session.cwd);
|
|
128
|
+
return {
|
|
129
|
+
kind: "chrome-profile",
|
|
130
|
+
path: exe,
|
|
131
|
+
userDataDir: resolveToCwd(app.user_data_dir, session.cwd),
|
|
132
|
+
profileDirectory: app.profile_directory,
|
|
133
|
+
background: app.background ?? false,
|
|
134
|
+
noFocus: app.no_focus ?? false,
|
|
135
|
+
cdpPort: app.cdp_port,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
112
138
|
if (app?.path) {
|
|
113
139
|
const exe = resolveToCwd(app.path, session.cwd);
|
|
114
140
|
return { kind: "spawned", path: exe };
|
|
@@ -358,6 +384,8 @@ function describeBrowser(handle: BrowserHandle): string {
|
|
|
358
384
|
return `headless browser (${handle.kind.headless ? "hidden" : "visible"})`;
|
|
359
385
|
case "spawned":
|
|
360
386
|
return `spawned ${handle.kind.path} (pid ${handle.pid ?? "?"})`;
|
|
387
|
+
case "chrome-profile":
|
|
388
|
+
return `Chrome profile ${handle.kind.profileDirectory} at ${handle.kind.userDataDir} (${handle.subprocess ? `pid ${handle.pid ?? "?"}` : "external CDP"})`;
|
|
361
389
|
case "connected":
|
|
362
390
|
return `connected ${handle.cdpUrl ?? handle.kind.cdpUrl}`;
|
|
363
391
|
}
|
|
@@ -369,6 +397,8 @@ function describeKind(kind: BrowserKind): string {
|
|
|
369
397
|
return `headless ${kind.headless ? "hidden" : "visible"}`;
|
|
370
398
|
case "spawned":
|
|
371
399
|
return `spawned:${kind.path}`;
|
|
400
|
+
case "chrome-profile":
|
|
401
|
+
return `chrome-profile:${kind.path}:${kind.userDataDir}:${kind.profileDirectory}`;
|
|
372
402
|
case "connected":
|
|
373
403
|
return `connected:${kind.cdpUrl}`;
|
|
374
404
|
}
|
|
@@ -378,6 +408,9 @@ function sameBrowserKind(a: BrowserKind, b: BrowserKind): boolean {
|
|
|
378
408
|
if (a.kind !== b.kind) return false;
|
|
379
409
|
if (a.kind === "headless" && b.kind === "headless") return a.headless === b.headless;
|
|
380
410
|
if (a.kind === "spawned" && b.kind === "spawned") return a.path === b.path;
|
|
411
|
+
if (a.kind === "chrome-profile" && b.kind === "chrome-profile") {
|
|
412
|
+
return a.path === b.path && a.userDataDir === b.userDataDir && a.profileDirectory === b.profileDirectory;
|
|
413
|
+
}
|
|
381
414
|
if (a.kind === "connected" && b.kind === "connected") return a.cdpUrl === b.cdpUrl;
|
|
382
415
|
return false;
|
|
383
416
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Component } from "@gajae-code/tui";
|
|
2
|
+
import { Text } from "@gajae-code/tui";
|
|
3
|
+
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
4
|
+
import type { Theme } from "../../modes/theme/theme";
|
|
5
|
+
import type { ComputerToolDetails } from "../computer";
|
|
6
|
+
import { formatBadge, formatErrorMessage } from "../render-utils";
|
|
7
|
+
|
|
8
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
9
|
+
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function summarizeArgs(args: unknown): string {
|
|
13
|
+
const input = asRecord(args);
|
|
14
|
+
if (input.action === "batch" && Array.isArray(input.actions)) {
|
|
15
|
+
return `batch ${input.actions.length} step${input.actions.length === 1 ? "" : "s"}`;
|
|
16
|
+
}
|
|
17
|
+
const action = typeof input.action === "string" ? input.action : "computer";
|
|
18
|
+
const parts = [action];
|
|
19
|
+
if (typeof input.x === "number" && typeof input.y === "number") parts.push(`@ ${input.x},${input.y}`);
|
|
20
|
+
if (typeof input.to_x === "number" && typeof input.to_y === "number") parts.push(`→ ${input.to_x},${input.to_y}`);
|
|
21
|
+
if (typeof input.scroll_x === "number" || typeof input.scroll_y === "number") {
|
|
22
|
+
parts.push(`scroll ${input.scroll_x ?? 0},${input.scroll_y ?? 0}`);
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(input.keys)) parts.push(`keys ${input.keys.join("+")}`);
|
|
25
|
+
if (typeof input.ms === "number") parts.push(`${input.ms}ms`);
|
|
26
|
+
return parts.join(" ");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function summarizeComputerDetails(
|
|
30
|
+
details: ComputerToolDetails | undefined,
|
|
31
|
+
isError: boolean,
|
|
32
|
+
theme: Theme,
|
|
33
|
+
): string {
|
|
34
|
+
if (!details) return isError ? "Computer action failed" : "Computer action completed";
|
|
35
|
+
if (details.action === "batch" && details.steps) {
|
|
36
|
+
const successCount = details.steps.filter(s => s.status === "success").length;
|
|
37
|
+
const parts: string[] = [`batch ${successCount}/${details.steps.length}`];
|
|
38
|
+
if (details.screenshot) parts.push(`screenshot ${details.screenshot.widthPx}x${details.screenshot.heightPx}`);
|
|
39
|
+
if (details.code) parts.push(theme.fg(isError ? "error" : "muted", details.code));
|
|
40
|
+
return parts.join(" ");
|
|
41
|
+
}
|
|
42
|
+
const parts: string[] = [details.action];
|
|
43
|
+
if (details.x !== undefined && details.y !== undefined) parts.push(`@ ${details.x},${details.y}`);
|
|
44
|
+
if (details.toX !== undefined && details.toY !== undefined) parts.push(`→ ${details.toX},${details.toY}`);
|
|
45
|
+
if (details.scrollX !== undefined || details.scrollY !== undefined)
|
|
46
|
+
parts.push(`scroll ${details.scrollX ?? 0},${details.scrollY ?? 0}`);
|
|
47
|
+
if (details.screenshot) {
|
|
48
|
+
const shot = details.screenshot;
|
|
49
|
+
parts.push(`screenshot ${shot.widthPx}x${shot.heightPx}`);
|
|
50
|
+
if (shot.pngBytes !== undefined) parts.push(`${shot.pngBytes} bytes`);
|
|
51
|
+
if (shot.captureId) parts.push(`capture ${shot.captureId}`);
|
|
52
|
+
}
|
|
53
|
+
if (details.supervisor) parts.push(`supervisor ${details.supervisor}`);
|
|
54
|
+
if (details.code) parts.push(theme.fg(isError ? "error" : "muted", details.code));
|
|
55
|
+
return parts.join(" ");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const computerToolRenderer = {
|
|
59
|
+
renderCall(args: unknown, _options: RenderResultOptions, theme: Theme): Component {
|
|
60
|
+
return new Text(`${formatBadge("computer", "accent", theme)} ${summarizeArgs(args)}`);
|
|
61
|
+
},
|
|
62
|
+
renderResult(
|
|
63
|
+
result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
|
|
64
|
+
_options: RenderResultOptions,
|
|
65
|
+
theme: Theme,
|
|
66
|
+
): Component {
|
|
67
|
+
if (result.isError) {
|
|
68
|
+
const details = result.details as ComputerToolDetails | undefined;
|
|
69
|
+
return new Text(
|
|
70
|
+
formatErrorMessage(details?.message ?? result.content.find(c => c.type === "text")?.text, theme),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return new Text(
|
|
74
|
+
`${formatBadge("computer", "success", theme)} ${summarizeComputerDetails(result.details as ComputerToolDetails | undefined, false, theme)}`,
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
mergeCallAndResult: true,
|
|
78
|
+
};
|