unbrowse 3.1.0 → 3.2.0

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 (107) hide show
  1. package/dist/cli.js +455 -96
  2. package/dist/index.js +2 -6
  3. package/dist/mcp.js +695 -46
  4. package/dist/server.js +25811 -0
  5. package/package.json +1 -2
  6. package/vendor/kuri/darwin-arm64/kuri +0 -0
  7. package/vendor/kuri/darwin-x64/kuri +0 -0
  8. package/vendor/kuri/linux-arm64/kuri +0 -0
  9. package/vendor/kuri/linux-x64/kuri +0 -0
  10. package/vendor/kuri/manifest.json +7 -10
  11. package/runtime-src/agent-outcome.ts +0 -166
  12. package/runtime-src/analytics-session.ts +0 -55
  13. package/runtime-src/api/browse-index.ts +0 -317
  14. package/runtime-src/api/browse-session.ts +0 -572
  15. package/runtime-src/api/browse-submit-prereqs.ts +0 -48
  16. package/runtime-src/api/browse-submit.ts +0 -1184
  17. package/runtime-src/api/routes.ts +0 -1823
  18. package/runtime-src/auth/browser-cookies.ts +0 -423
  19. package/runtime-src/auth/index.ts +0 -535
  20. package/runtime-src/auth/runtime.ts +0 -116
  21. package/runtime-src/browser/index.ts +0 -659
  22. package/runtime-src/browser/types.ts +0 -41
  23. package/runtime-src/build-info.generated.ts +0 -6
  24. package/runtime-src/capture/index.ts +0 -1794
  25. package/runtime-src/capture/prefetch.ts +0 -95
  26. package/runtime-src/capture/rsc.ts +0 -45
  27. package/runtime-src/cli/shortcuts.ts +0 -273
  28. package/runtime-src/cli.ts +0 -1572
  29. package/runtime-src/client/graph-client.ts +0 -100
  30. package/runtime-src/client/index.ts +0 -1425
  31. package/runtime-src/debug-trace.ts +0 -18
  32. package/runtime-src/domain.ts +0 -38
  33. package/runtime-src/execution/index.ts +0 -3397
  34. package/runtime-src/execution/retry.ts +0 -46
  35. package/runtime-src/execution/robots.ts +0 -167
  36. package/runtime-src/execution/search-forms.ts +0 -188
  37. package/runtime-src/extraction/index.ts +0 -1507
  38. package/runtime-src/foundry/publish-bundle.ts +0 -392
  39. package/runtime-src/graph/agent-augment.ts +0 -315
  40. package/runtime-src/graph/index.ts +0 -1524
  41. package/runtime-src/graph/local-fixtures.ts +0 -393
  42. package/runtime-src/graph/local-harness.ts +0 -646
  43. package/runtime-src/graph/planner.ts +0 -411
  44. package/runtime-src/graph/session.ts +0 -294
  45. package/runtime-src/graph/trace-store.ts +0 -136
  46. package/runtime-src/index.ts +0 -24
  47. package/runtime-src/indexer/index.ts +0 -465
  48. package/runtime-src/intent-match.ts +0 -1515
  49. package/runtime-src/kuri/client.ts +0 -1839
  50. package/runtime-src/logger.ts +0 -30
  51. package/runtime-src/marketplace/index.ts +0 -103
  52. package/runtime-src/mcp.ts +0 -1747
  53. package/runtime-src/orchestrator/browser-agent.ts +0 -374
  54. package/runtime-src/orchestrator/dag-advisor.ts +0 -59
  55. package/runtime-src/orchestrator/dag-feedback.ts +0 -257
  56. package/runtime-src/orchestrator/first-pass-action.ts +0 -403
  57. package/runtime-src/orchestrator/index.ts +0 -4480
  58. package/runtime-src/orchestrator/passive-publish.ts +0 -187
  59. package/runtime-src/orchestrator/timing-economics.ts +0 -80
  60. package/runtime-src/payments/cascade.ts +0 -137
  61. package/runtime-src/payments/index.ts +0 -270
  62. package/runtime-src/payments/lobster-pay.ts +0 -182
  63. package/runtime-src/payments/wallet.ts +0 -98
  64. package/runtime-src/publish/review-context.ts +0 -93
  65. package/runtime-src/publish/sanitize.ts +0 -197
  66. package/runtime-src/publish/schema-review.ts +0 -192
  67. package/runtime-src/publish-admission.ts +0 -388
  68. package/runtime-src/ratelimit/index.ts +0 -23
  69. package/runtime-src/reverse-engineer/bundle-scanner.ts +0 -127
  70. package/runtime-src/reverse-engineer/description-prompt.ts +0 -213
  71. package/runtime-src/reverse-engineer/index.ts +0 -1551
  72. package/runtime-src/router.ts +0 -17
  73. package/runtime-src/routing-telemetry.ts +0 -395
  74. package/runtime-src/runtime/browser-access.ts +0 -11
  75. package/runtime-src/runtime/browser-auth.ts +0 -12
  76. package/runtime-src/runtime/browser-host.ts +0 -48
  77. package/runtime-src/runtime/lifecycle.ts +0 -17
  78. package/runtime-src/runtime/local-server.ts +0 -311
  79. package/runtime-src/runtime/paths.ts +0 -99
  80. package/runtime-src/runtime/setup.ts +0 -251
  81. package/runtime-src/runtime/supervisor.ts +0 -69
  82. package/runtime-src/runtime/update-hints.ts +0 -351
  83. package/runtime-src/server.ts +0 -100
  84. package/runtime-src/session-logs.ts +0 -142
  85. package/runtime-src/settings.ts +0 -221
  86. package/runtime-src/single-binary.ts +0 -143
  87. package/runtime-src/site-policy.ts +0 -54
  88. package/runtime-src/stale-cleanup-runner.ts +0 -144
  89. package/runtime-src/stale-cleanup.ts +0 -133
  90. package/runtime-src/telemetry-attribution.ts +0 -120
  91. package/runtime-src/telemetry.ts +0 -253
  92. package/runtime-src/template-params.ts +0 -141
  93. package/runtime-src/transform/drift.ts +0 -60
  94. package/runtime-src/transform/index.ts +0 -277
  95. package/runtime-src/types/index.ts +0 -1
  96. package/runtime-src/types/skill.ts +0 -912
  97. package/runtime-src/vault/index.ts +0 -196
  98. package/runtime-src/verification/auth-gate.ts +0 -8
  99. package/runtime-src/verification/candidates.ts +0 -27
  100. package/runtime-src/verification/index.ts +0 -120
  101. package/runtime-src/verification/matrix.ts +0 -30
  102. package/runtime-src/version.ts +0 -148
  103. package/runtime-src/workflow/artifact.ts +0 -161
  104. package/runtime-src/workflow/compile.ts +0 -808
  105. package/runtime-src/workflow/publish.ts +0 -225
  106. package/runtime-src/workflow/runtime.ts +0 -213
  107. package/vendor/kuri/win-x64/kuri.exe +0 -0
@@ -1,311 +0,0 @@
1
- import { openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
- import path from "node:path";
3
- import { spawn } from "node:child_process";
4
- import { ensureDir, getPackageRoot, getServerAutostartLogFile, getServerPidFile, resolveSiblingEntrypoint, runtimeArgsForEntrypoint } from "./paths.js";
5
- import { LocalSupervisor } from "./supervisor.js";
6
- import { CODE_HASH, PACKAGE_VERSION } from "../version.js";
7
-
8
- type PidState = {
9
- pid: number;
10
- base_url: string;
11
- started_at: string;
12
- entrypoint: string;
13
- version?: string;
14
- code_hash?: string;
15
- restart_count?: number;
16
- };
17
-
18
- type ServerHealth = {
19
- status?: string;
20
- package_version?: string;
21
- code_hash?: string;
22
- };
23
-
24
- export function isServerVersionMismatch(
25
- runningVersion: string | undefined,
26
- installedVersion: string,
27
- runningCodeHash?: string,
28
- installedCodeHash?: string,
29
- ): boolean {
30
- return (!!runningVersion && runningVersion !== installedVersion)
31
- || (!!runningCodeHash && !!installedCodeHash && runningCodeHash !== installedCodeHash);
32
- }
33
-
34
- async function fetchServerHealth(baseUrl: string, timeoutMs = 2_000): Promise<ServerHealth | null> {
35
- try {
36
- const res = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(timeoutMs) });
37
- if (!res.ok) return null;
38
- return await res.json() as ServerHealth;
39
- } catch {
40
- return null;
41
- }
42
- }
43
-
44
- async function isServerHealthy(baseUrl: string, timeoutMs = 2_000): Promise<boolean> {
45
- return !!(await fetchServerHealth(baseUrl, timeoutMs));
46
- }
47
-
48
- async function waitForHealthy(baseUrl: string, timeoutMs: number): Promise<boolean> {
49
- const start = Date.now();
50
- while (Date.now() - start < timeoutMs) {
51
- if (await isServerHealthy(baseUrl)) return true;
52
- await new Promise((resolve) => setTimeout(resolve, 500));
53
- }
54
- return false;
55
- }
56
-
57
- function isPidAlive(pid: number): boolean {
58
- try {
59
- process.kill(pid, 0);
60
- return true;
61
- } catch {
62
- return false;
63
- }
64
- }
65
-
66
- function readPidState(pidFile: string): PidState | null {
67
- try {
68
- return JSON.parse(readFileSync(pidFile, "utf-8")) as PidState;
69
- } catch {
70
- return null;
71
- }
72
- }
73
-
74
- function clearStalePidFile(pidFile: string): void {
75
- try {
76
- unlinkSync(pidFile);
77
- } catch {
78
- // ignore
79
- }
80
- }
81
-
82
- function deriveListenEnv(baseUrl: string): Record<string, string> {
83
- const url = new URL(baseUrl);
84
- const host = !url.hostname || url.hostname === "localhost" ? "127.0.0.1" : url.hostname;
85
- const port = url.port || (url.protocol === "https:" ? "443" : "80");
86
- return { HOST: host, PORT: port, UNBROWSE_URL: baseUrl };
87
- }
88
-
89
- function isCompiledBinary(): boolean {
90
- return !!(process.versions.bun && !process.argv[1]?.match(/\.(ts|js|mjs)$/));
91
- }
92
-
93
- export function getServerSpawnSpec(metaUrl: string, entrypoint = resolveSiblingEntrypoint(metaUrl, "index")): {
94
- command: string;
95
- args: string[];
96
- cwd: string;
97
- recordedEntrypoint: string;
98
- } {
99
- if (isCompiledBinary()) {
100
- return {
101
- command: process.execPath,
102
- args: ["serve"],
103
- cwd: process.cwd(),
104
- recordedEntrypoint: `${process.execPath} serve`,
105
- };
106
- }
107
-
108
- return {
109
- command: process.execPath,
110
- args: runtimeArgsForEntrypoint(metaUrl, entrypoint),
111
- cwd: getPackageRoot(metaUrl),
112
- recordedEntrypoint: entrypoint,
113
- };
114
- }
115
-
116
- const MAX_RESTART_ATTEMPTS = 3;
117
- const RESTART_BACKOFF_MS = 2_000;
118
-
119
- /**
120
- * Spawn the local server as a detached child process.
121
- * Returns the PidState written to the pid file.
122
- */
123
- function spawnServer(
124
- baseUrl: string,
125
- metaUrl: string,
126
- pidFile: string,
127
- restartCount = 0,
128
- ): PidState {
129
- const entrypoint = resolveSiblingEntrypoint(metaUrl, "index");
130
- const spawnSpec = getServerSpawnSpec(metaUrl, entrypoint);
131
- const logFile = getServerAutostartLogFile();
132
- ensureDir(path.dirname(logFile));
133
- const logFd = openSync(logFile, "a");
134
- const child = spawn(spawnSpec.command, spawnSpec.args, {
135
- cwd: spawnSpec.cwd,
136
- detached: true,
137
- stdio: ["ignore", logFd, logFd],
138
- env: {
139
- ...process.env,
140
- ...deriveListenEnv(baseUrl),
141
- UNBROWSE_NON_INTERACTIVE: process.env.UNBROWSE_NON_INTERACTIVE || "1",
142
- UNBROWSE_TOS_ACCEPTED: process.env.UNBROWSE_TOS_ACCEPTED || "1",
143
- UNBROWSE_PID_FILE: pidFile,
144
- },
145
- });
146
- child.unref();
147
-
148
- const state: PidState = {
149
- pid: child.pid!,
150
- base_url: baseUrl,
151
- started_at: new Date().toISOString(),
152
- entrypoint: spawnSpec.recordedEntrypoint,
153
- version: PACKAGE_VERSION,
154
- code_hash: CODE_HASH,
155
- restart_count: restartCount,
156
- };
157
- writeFileSync(pidFile, JSON.stringify(state, null, 2));
158
- return state;
159
- }
160
-
161
- /** Shared supervisor instance used for health checking. */
162
- const supervisor = new LocalSupervisor();
163
-
164
- export { supervisor };
165
-
166
- export async function ensureLocalServer(baseUrl: string, noAutoStart: boolean, metaUrl: string): Promise<void> {
167
- const installedVersion = PACKAGE_VERSION;
168
- const initialHealth = await fetchServerHealth(baseUrl);
169
- if (initialHealth) {
170
- const runningVersion = initialHealth.package_version;
171
- const runningCodeHash = initialHealth.code_hash;
172
- if (isServerVersionMismatch(runningVersion, installedVersion, runningCodeHash, CODE_HASH)) {
173
- const versionInfo = checkServerVersion(baseUrl, metaUrl, {
174
- runningVersion,
175
- runningCodeHash,
176
- });
177
- if (versionInfo?.needs_restart) {
178
- stopServer(baseUrl);
179
- await new Promise((resolve) => setTimeout(resolve, 1_000));
180
- } else {
181
- throw new Error(
182
- `Server runtime mismatch on ${baseUrl}: running ${runningVersion ?? "unknown"} (${runningCodeHash ?? "unknown"}), installed ${installedVersion} (${CODE_HASH}). Run "unbrowse restart" or stop the stale server bound to that port.`,
183
- );
184
- }
185
- } else {
186
- // Server already healthy — ensure supervisor state reflects this
187
- if (!supervisor.isRunning()) await supervisor.start();
188
- return;
189
- }
190
- }
191
-
192
- const pidFile = getServerPidFile(baseUrl);
193
- const existing = readPidState(pidFile);
194
-
195
- if (existing?.pid && isPidAlive(existing.pid)) {
196
- // Process alive but not healthy — wait then try supervisor restart
197
- if (await waitForHealthy(baseUrl, 15_000)) {
198
- if (!supervisor.isRunning()) await supervisor.start();
199
- return;
200
- }
201
- // Still unhealthy after wait — kill stale process and restart
202
- try { process.kill(existing.pid, "SIGTERM"); } catch { /* ignore */ }
203
- await new Promise((r) => setTimeout(r, 1_000));
204
- clearStalePidFile(pidFile);
205
- if (supervisor.isRunning()) await supervisor.stop();
206
- } else if (existing) {
207
- clearStalePidFile(pidFile);
208
- }
209
-
210
- if (noAutoStart) {
211
- throw new Error("Server not running and auto-start disabled (--no-auto-start).");
212
- }
213
-
214
- // Spawn with supervisor retry
215
- for (let attempt = 0; attempt <= MAX_RESTART_ATTEMPTS; attempt++) {
216
- spawnServer(baseUrl, metaUrl, pidFile, attempt);
217
-
218
- if (await waitForHealthy(baseUrl, 30_000)) {
219
- await supervisor.start();
220
- return;
221
- }
222
-
223
- // Failed to start — clear and retry with backoff
224
- const state = readPidState(pidFile);
225
- if (state?.pid) {
226
- try { process.kill(state.pid, "SIGTERM"); } catch { /* ignore */ }
227
- }
228
- clearStalePidFile(pidFile);
229
-
230
- if (attempt < MAX_RESTART_ATTEMPTS) {
231
- const backoff = RESTART_BACKOFF_MS * (attempt + 1);
232
- await new Promise((r) => setTimeout(r, backoff));
233
- }
234
- }
235
-
236
- const logFile = getServerAutostartLogFile();
237
- throw new Error(`Server failed to start after ${MAX_RESTART_ATTEMPTS + 1} attempts. Check ${logFile}`);
238
- }
239
-
240
- /**
241
- * Check if the running server version matches the installed CLI version.
242
- * Returns null if no server is running, or runtime/version details.
243
- */
244
- export function checkServerVersion(
245
- baseUrl: string,
246
- metaUrl: string,
247
- ): {
248
- running: string;
249
- installed: string;
250
- running_code_hash?: string;
251
- installed_code_hash: string;
252
- needs_restart: boolean;
253
- } | null;
254
- export function checkServerVersion(
255
- baseUrl: string,
256
- metaUrl: string,
257
- healthOverride?: {
258
- runningVersion?: string;
259
- runningCodeHash?: string;
260
- },
261
- ): {
262
- running: string;
263
- installed: string;
264
- running_code_hash?: string;
265
- installed_code_hash: string;
266
- needs_restart: boolean;
267
- } | null {
268
- const pidFile = getServerPidFile(baseUrl);
269
- const state = readPidState(pidFile);
270
- if (!state) return null;
271
- const installed = PACKAGE_VERSION;
272
- const running = healthOverride?.runningVersion ?? state.version ?? "unknown";
273
- const runningCodeHash = healthOverride?.runningCodeHash ?? state.code_hash;
274
- return {
275
- running,
276
- installed,
277
- ...(runningCodeHash ? { running_code_hash: runningCodeHash } : {}),
278
- installed_code_hash: CODE_HASH,
279
- needs_restart: isServerVersionMismatch(running, installed, runningCodeHash, CODE_HASH),
280
- };
281
- }
282
-
283
- /**
284
- * Stop the running server gracefully.
285
- * Returns true if a server was stopped, false if none was running.
286
- */
287
- export function stopServer(baseUrl: string): boolean {
288
- const pidFile = getServerPidFile(baseUrl);
289
- const state = readPidState(pidFile);
290
- if (!state?.pid) return false;
291
- try {
292
- process.kill(state.pid, "SIGTERM");
293
- clearStalePidFile(pidFile);
294
- // Synchronously mark supervisor as stopped (fire-and-forget the async stop)
295
- if (supervisor.isRunning()) void supervisor.stop();
296
- return true;
297
- } catch {
298
- clearStalePidFile(pidFile);
299
- return false;
300
- }
301
- }
302
-
303
- /**
304
- * Restart the local server (stop + start).
305
- * Used after CLI upgrades to pick up new code.
306
- */
307
- export async function restartServer(baseUrl: string, metaUrl: string): Promise<void> {
308
- stopServer(baseUrl);
309
- await new Promise((r) => setTimeout(r, 1_000));
310
- await ensureLocalServer(baseUrl, false, metaUrl);
311
- }
@@ -1,99 +0,0 @@
1
- import { existsSync, mkdirSync, realpathSync } from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { createRequire } from "node:module";
5
- import { fileURLToPath, pathToFileURL } from "node:url";
6
-
7
- export function getModuleDir(metaUrl: string): string {
8
- return path.dirname(fileURLToPath(metaUrl));
9
- }
10
-
11
- export function getPackageRoot(metaUrl: string): string {
12
- if (process.env.UNBROWSE_PACKAGE_ROOT) return process.env.UNBROWSE_PACKAGE_ROOT;
13
- let dir = getModuleDir(metaUrl);
14
- const root = path.parse(dir).root;
15
- while (dir !== root) {
16
- if (existsSync(path.join(dir, "package.json"))) return dir;
17
- dir = path.dirname(dir);
18
- }
19
- return getModuleDir(metaUrl);
20
- }
21
-
22
- export function resolveSiblingEntrypoint(metaUrl: string, basename: string): string {
23
- const file = fileURLToPath(metaUrl);
24
- return path.join(path.dirname(file), `${basename}${path.extname(file) || ".js"}`);
25
- }
26
-
27
- export function isBundledVirtualEntrypoint(entrypoint: string): boolean {
28
- return entrypoint.startsWith("/$bunfs/");
29
- }
30
-
31
- export function runtimeArgsForEntrypoint(metaUrl: string, entrypoint: string): string[] {
32
- if (path.extname(entrypoint) !== ".ts") return [entrypoint];
33
- if (process.versions.bun) return [entrypoint];
34
-
35
- try {
36
- const req = createRequire(metaUrl);
37
- const tsxPkg = req.resolve("tsx/package.json");
38
- const tsxLoader = path.join(path.dirname(tsxPkg), "dist", "loader.mjs");
39
- if (existsSync(tsxLoader)) return ["--import", pathToFileURL(tsxLoader).href, entrypoint];
40
- } catch {
41
- // fall through to bare specifier
42
- }
43
-
44
- return ["--import", "tsx", entrypoint];
45
- }
46
-
47
- export function isMainModule(metaUrl: string): boolean {
48
- const entry = process.argv[1];
49
- if (!entry) return false;
50
-
51
- const modulePath = fileURLToPath(metaUrl);
52
- try {
53
- return realpathSync(entry) === realpathSync(modulePath);
54
- } catch {
55
- return path.resolve(entry) === path.resolve(modulePath);
56
- }
57
- }
58
-
59
- export function getUnbrowseHome(): string {
60
- return path.join(os.homedir(), ".unbrowse");
61
- }
62
-
63
- export function ensureDir(dir: string): string {
64
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
65
- return dir;
66
- }
67
-
68
- export function getLogsDir(): string {
69
- return ensureDir(path.join(getUnbrowseHome(), "logs"));
70
- }
71
-
72
- export function getRunDir(): string {
73
- return ensureDir(process.env.UNBROWSE_RUN_DIR || path.join(getUnbrowseHome(), "run"));
74
- }
75
-
76
- function sanitizeSegment(value: string): string {
77
- return value.replace(/[^a-zA-Z0-9.-]+/g, "_");
78
- }
79
-
80
- export function getServerPidFile(baseUrl: string): string {
81
- const url = new URL(baseUrl);
82
- const port = url.port || (url.protocol === "https:" ? "443" : "80");
83
- const host = sanitizeSegment(url.hostname || "127.0.0.1");
84
- return path.join(getRunDir(), `server-${host}-${port}.json`);
85
- }
86
-
87
- export function getServerAutostartLogFile(): string {
88
- return path.join(getLogsDir(), "server-autostart.log");
89
- }
90
-
91
- export function isManagedSkillInstall(metaUrl: string): boolean {
92
- const managedDir = process.env.UNBROWSE_SKILL_DIR || path.join(os.homedir(), ".agents", "skills", "unbrowse");
93
- const pkgRoot = getPackageRoot(metaUrl);
94
- try {
95
- return realpathSync(pkgRoot) === realpathSync(managedDir);
96
- } catch {
97
- return false;
98
- }
99
- }
@@ -1,251 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { ensureDir } from "./paths.js";
6
- import { findKuriBinary, getKuriSourceCandidates } from "../kuri/client.js";
7
- import { detectHostEnvironment, type HostEnvironment } from "./browser-host.js";
8
- import { log } from "../logger.js";
9
- import { checkWalletConfigured, type WalletCheckResult } from "../payments/wallet.js";
10
- import { configureUpdateHintHooks, saveInstallSource, type UpdateHookStatus } from "./update-hints.js";
11
-
12
- export type SetupScope = "auto" | "global" | "project" | "off";
13
-
14
- export type SetupReport = {
15
- os: {
16
- platform: NodeJS.Platform;
17
- release: string;
18
- arch: string;
19
- };
20
- host_environment: HostEnvironment;
21
- package_managers: {
22
- npm: boolean;
23
- npx: boolean;
24
- bun: boolean;
25
- brew: boolean;
26
- };
27
- browser_engine: {
28
- installed: boolean;
29
- action: "already-installed" | "installed" | "failed" | "skipped";
30
- message?: string;
31
- };
32
- opencode: {
33
- detected: boolean;
34
- action: "installed" | "updated" | "skipped" | "not-detected";
35
- scope: "global" | "project" | "off";
36
- };
37
- update_hints: UpdateHookStatus[];
38
- wallet: WalletCheckResult & {
39
- message: string;
40
- };
41
- };
42
-
43
- function hasBinary(name: string): boolean {
44
- const checker = process.platform === "win32" ? "where" : "which";
45
- try {
46
- execFileSync(checker, [name], { stdio: "ignore" });
47
- return true;
48
- } catch {
49
- return false;
50
- }
51
- }
52
-
53
- function detectPackageManagers(): SetupReport["package_managers"] {
54
- return {
55
- npm: hasBinary("npm"),
56
- npx: hasBinary("npx"),
57
- bun: hasBinary("bun"),
58
- brew: hasBinary("brew"),
59
- };
60
- }
61
-
62
- function resolveConfigHome(): string {
63
- if (process.platform === "win32") {
64
- return process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
65
- }
66
- return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
67
- }
68
-
69
- function getOpenCodeGlobalCommandsDir(): string {
70
- return path.join(resolveConfigHome(), "opencode", "commands");
71
- }
72
-
73
- function getOpenCodeProjectCommandsDir(cwd: string): string {
74
- return path.join(cwd, ".opencode", "commands");
75
- }
76
-
77
- function detectOpenCode(cwd: string): boolean {
78
- return hasBinary("opencode") ||
79
- existsSync(path.join(resolveConfigHome(), "opencode")) ||
80
- existsSync(path.join(cwd, ".opencode"));
81
- }
82
-
83
- function renderOpenCodeCommand(): string {
84
- return `---
85
- description: Use Unbrowse as the exclusive web-access tool for a task.
86
- ---
87
- Use Unbrowse as the only allowed tool for website access in this task.
88
-
89
- Rules:
90
- - Do not use Brave Search, built-in web search, browser MCPs, curl, or other network tools for website access unless the user explicitly authorizes fallback.
91
- - If Unbrowse is slow on a first-time site, wait for it. Do not switch tools just because capture or indexing is still running.
92
- - If Unbrowse returns partial results, refine with more Unbrowse commands (\`resolve\`, \`search\`, \`execute\`, \`login\`) before considering fallback.
93
- - If Unbrowse genuinely cannot complete the task, explain why and ask before using another tool.
94
-
95
- Suggested start:
96
- \`\`\`bash
97
- npx unbrowse resolve --intent "$ARGUMENTS" --url "<target-url>" --pretty
98
- \`\`\`
99
- `;
100
- }
101
-
102
- function writeOpenCodeCommand(scope: SetupScope, cwd: string): SetupReport["opencode"] {
103
- if (scope === "off") {
104
- return { detected: detectOpenCode(cwd), action: "skipped", scope: "off" };
105
- }
106
-
107
- const detected = detectOpenCode(cwd);
108
- if (scope === "auto" && !detected) {
109
- return { detected: false, action: "not-detected", scope: "off" };
110
- }
111
-
112
- const resolvedScope: "global" | "project" =
113
- scope === "project" ? "project" : scope === "global"
114
- ? "global"
115
- : existsSync(path.join(cwd, ".opencode"))
116
- ? "project"
117
- : "global";
118
-
119
- const commandsDir = resolvedScope === "project"
120
- ? getOpenCodeProjectCommandsDir(cwd)
121
- : getOpenCodeGlobalCommandsDir();
122
- const commandFile = path.join(ensureDir(commandsDir), "unbrowse.md");
123
- const content = renderOpenCodeCommand();
124
- const action = existsSync(commandFile) ? "updated" : "installed";
125
- mkdirSync(path.dirname(commandFile), { recursive: true });
126
- writeFileSync(commandFile, content);
127
-
128
- return {
129
- detected: detected || scope !== "auto",
130
- action,
131
- scope: resolvedScope,
132
- command_file: commandFile,
133
- };
134
- }
135
-
136
- export async function ensureBrowserEngineInstalled(): Promise<SetupReport["browser_engine"]> {
137
- const binary = findKuriBinary();
138
- if (existsSync(binary)) {
139
- return { installed: true, action: "already-installed" };
140
- }
141
-
142
- const sourceDir = getKuriSourceCandidates().find((candidate) => existsSync(path.join(candidate, "build.zig")));
143
- if (!sourceDir) {
144
- return {
145
- installed: false,
146
- action: "failed",
147
- message: `Kuri binary not found. Checked ${binary}`,
148
- };
149
- }
150
-
151
- if (!hasBinary("zig")) {
152
- return {
153
- installed: false,
154
- action: "failed",
155
- message: `Kuri source found at ${sourceDir}, but Zig is not installed`,
156
- };
157
- }
158
-
159
- try {
160
- execFileSync("zig", ["build", "-Doptimize=ReleaseFast"], {
161
- cwd: sourceDir,
162
- stdio: "inherit",
163
- timeout: 300_000,
164
- });
165
- const builtBinary = findKuriBinary();
166
- if (existsSync(builtBinary)) {
167
- return {
168
- installed: true,
169
- action: "installed",
170
- message: `Built Kuri from ${sourceDir}`,
171
- };
172
- }
173
- return {
174
- installed: false,
175
- action: "failed",
176
- message: `Kuri build completed but ${builtBinary} was not created`,
177
- };
178
- } catch (error) {
179
- const message = error instanceof Error ? error.message : String(error);
180
- return { installed: false, action: "failed", message };
181
- }
182
- }
183
-
184
- export async function runSetup(options?: {
185
- cwd?: string;
186
- opencode?: SetupScope;
187
- installBrowser?: boolean;
188
- }): Promise<SetupReport> {
189
- const cwd = options?.cwd || process.cwd();
190
- const installSource = saveInstallSource(import.meta.url);
191
-
192
- const hostEnv = detectHostEnvironment();
193
- log("setup", `detected host environment: ${hostEnv}`);
194
-
195
- const browser = options?.installBrowser === false
196
- ? { installed: false, action: "skipped" as const }
197
- : await ensureBrowserEngineInstalled();
198
- const walletCheck = checkWalletConfigured();
199
- const skipWalletSetup = process.env.UNBROWSE_SKIP_WALLET_SETUP === "1";
200
- const lobsterInstalled = hasBinary("lobstercash") ||
201
- existsSync(path.join(os.homedir(), ".agents", "skills", "lobstercash", "SKILL.md"));
202
-
203
- // Auto-setup Crossmint lobster.cash if the wallet tooling is already present.
204
- if (!skipWalletSetup && !walletCheck.configured && lobsterInstalled) {
205
- console.log("[unbrowse] Crossmint lobster.cash detected but wallet not configured — running wallet setup...");
206
- try {
207
- execFileSync("npx", ["@crossmint/lobster-cli", "setup"], {
208
- stdio: "inherit",
209
- timeout: 60_000,
210
- });
211
- // Re-check after setup
212
- const recheck = checkWalletConfigured();
213
- if (recheck.configured) {
214
- console.log(`[unbrowse] wallet configured (${recheck.provider})`);
215
- }
216
- } catch {
217
- console.warn("[unbrowse] Crossmint lobster.cash setup failed or was skipped — continuing without wallet");
218
- }
219
- }
220
-
221
- // Re-check wallet state after potential setup
222
- const finalWalletCheck = checkWalletConfigured();
223
- const wallet = {
224
- ...finalWalletCheck,
225
- lobster_installed: lobsterInstalled,
226
- message: finalWalletCheck.configured
227
- ? `Wallet configured (${finalWalletCheck.provider}). This address is the contributor truth: it is synced onto your agent profile, used for contributor payouts when your routes earn, and used for paid-route spending.`
228
- : lobsterInstalled
229
- ? "Crossmint lobster.cash is installed but not paired. Pair it now so this wallet address becomes your contributor payout target and your paid-route spending wallet. Run: npx @crossmint/lobster-cli setup"
230
- : "No wallet configured. Recommended for new installs: set up Crossmint lobster.cash so contributor payouts have a destination address and paid-route spending can clear automatically. Without it you stay in free indexing mode only.",
231
- install_hint: finalWalletCheck.configured
232
- ? undefined
233
- : lobsterInstalled
234
- ? "npx @crossmint/lobster-cli setup"
235
- : "npx @crossmint/lobster-cli setup",
236
- };
237
-
238
- return {
239
- os: {
240
- platform: process.platform,
241
- release: os.release(),
242
- arch: process.arch,
243
- },
244
- host_environment: hostEnv,
245
- package_managers: detectPackageManagers(),
246
- browser_engine: browser,
247
- opencode: writeOpenCodeCommand(options?.opencode ?? "auto", cwd),
248
- update_hints: configureUpdateHintHooks(import.meta.url, installSource),
249
- wallet,
250
- };
251
- }