unbrowse 3.1.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/cli.js +454 -96
  2. package/dist/index.js +2 -6
  3. package/dist/mcp.js +695 -46
  4. package/dist/server.js +25994 -0
  5. package/package.json +1 -2
  6. package/vendor/kuri/manifest.json +1 -1
  7. package/runtime-src/agent-outcome.ts +0 -166
  8. package/runtime-src/analytics-session.ts +0 -55
  9. package/runtime-src/api/browse-index.ts +0 -317
  10. package/runtime-src/api/browse-session.ts +0 -572
  11. package/runtime-src/api/browse-submit-prereqs.ts +0 -48
  12. package/runtime-src/api/browse-submit.ts +0 -1184
  13. package/runtime-src/api/routes.ts +0 -1823
  14. package/runtime-src/auth/browser-cookies.ts +0 -423
  15. package/runtime-src/auth/index.ts +0 -535
  16. package/runtime-src/auth/runtime.ts +0 -116
  17. package/runtime-src/browser/index.ts +0 -659
  18. package/runtime-src/browser/types.ts +0 -41
  19. package/runtime-src/build-info.generated.ts +0 -6
  20. package/runtime-src/capture/index.ts +0 -1794
  21. package/runtime-src/capture/prefetch.ts +0 -95
  22. package/runtime-src/capture/rsc.ts +0 -45
  23. package/runtime-src/cli/shortcuts.ts +0 -273
  24. package/runtime-src/cli.ts +0 -1572
  25. package/runtime-src/client/graph-client.ts +0 -100
  26. package/runtime-src/client/index.ts +0 -1425
  27. package/runtime-src/debug-trace.ts +0 -18
  28. package/runtime-src/domain.ts +0 -38
  29. package/runtime-src/execution/index.ts +0 -3397
  30. package/runtime-src/execution/retry.ts +0 -46
  31. package/runtime-src/execution/robots.ts +0 -167
  32. package/runtime-src/execution/search-forms.ts +0 -188
  33. package/runtime-src/extraction/index.ts +0 -1507
  34. package/runtime-src/foundry/publish-bundle.ts +0 -392
  35. package/runtime-src/graph/agent-augment.ts +0 -315
  36. package/runtime-src/graph/index.ts +0 -1524
  37. package/runtime-src/graph/local-fixtures.ts +0 -393
  38. package/runtime-src/graph/local-harness.ts +0 -646
  39. package/runtime-src/graph/planner.ts +0 -411
  40. package/runtime-src/graph/session.ts +0 -294
  41. package/runtime-src/graph/trace-store.ts +0 -136
  42. package/runtime-src/index.ts +0 -24
  43. package/runtime-src/indexer/index.ts +0 -465
  44. package/runtime-src/intent-match.ts +0 -1515
  45. package/runtime-src/kuri/client.ts +0 -1839
  46. package/runtime-src/logger.ts +0 -30
  47. package/runtime-src/marketplace/index.ts +0 -103
  48. package/runtime-src/mcp.ts +0 -1747
  49. package/runtime-src/orchestrator/browser-agent.ts +0 -374
  50. package/runtime-src/orchestrator/dag-advisor.ts +0 -59
  51. package/runtime-src/orchestrator/dag-feedback.ts +0 -257
  52. package/runtime-src/orchestrator/first-pass-action.ts +0 -403
  53. package/runtime-src/orchestrator/index.ts +0 -4480
  54. package/runtime-src/orchestrator/passive-publish.ts +0 -187
  55. package/runtime-src/orchestrator/timing-economics.ts +0 -80
  56. package/runtime-src/payments/cascade.ts +0 -137
  57. package/runtime-src/payments/index.ts +0 -270
  58. package/runtime-src/payments/lobster-pay.ts +0 -182
  59. package/runtime-src/payments/wallet.ts +0 -98
  60. package/runtime-src/publish/review-context.ts +0 -93
  61. package/runtime-src/publish/sanitize.ts +0 -197
  62. package/runtime-src/publish/schema-review.ts +0 -192
  63. package/runtime-src/publish-admission.ts +0 -388
  64. package/runtime-src/ratelimit/index.ts +0 -23
  65. package/runtime-src/reverse-engineer/bundle-scanner.ts +0 -127
  66. package/runtime-src/reverse-engineer/description-prompt.ts +0 -213
  67. package/runtime-src/reverse-engineer/index.ts +0 -1551
  68. package/runtime-src/router.ts +0 -17
  69. package/runtime-src/routing-telemetry.ts +0 -395
  70. package/runtime-src/runtime/browser-access.ts +0 -11
  71. package/runtime-src/runtime/browser-auth.ts +0 -12
  72. package/runtime-src/runtime/browser-host.ts +0 -48
  73. package/runtime-src/runtime/lifecycle.ts +0 -17
  74. package/runtime-src/runtime/local-server.ts +0 -311
  75. package/runtime-src/runtime/paths.ts +0 -99
  76. package/runtime-src/runtime/setup.ts +0 -251
  77. package/runtime-src/runtime/supervisor.ts +0 -69
  78. package/runtime-src/runtime/update-hints.ts +0 -351
  79. package/runtime-src/server.ts +0 -100
  80. package/runtime-src/session-logs.ts +0 -142
  81. package/runtime-src/settings.ts +0 -221
  82. package/runtime-src/single-binary.ts +0 -143
  83. package/runtime-src/site-policy.ts +0 -54
  84. package/runtime-src/stale-cleanup-runner.ts +0 -144
  85. package/runtime-src/stale-cleanup.ts +0 -133
  86. package/runtime-src/telemetry-attribution.ts +0 -120
  87. package/runtime-src/telemetry.ts +0 -253
  88. package/runtime-src/template-params.ts +0 -141
  89. package/runtime-src/transform/drift.ts +0 -60
  90. package/runtime-src/transform/index.ts +0 -277
  91. package/runtime-src/types/index.ts +0 -1
  92. package/runtime-src/types/skill.ts +0 -912
  93. package/runtime-src/vault/index.ts +0 -196
  94. package/runtime-src/verification/auth-gate.ts +0 -8
  95. package/runtime-src/verification/candidates.ts +0 -27
  96. package/runtime-src/verification/index.ts +0 -120
  97. package/runtime-src/verification/matrix.ts +0 -30
  98. package/runtime-src/version.ts +0 -148
  99. package/runtime-src/workflow/artifact.ts +0 -161
  100. package/runtime-src/workflow/compile.ts +0 -808
  101. package/runtime-src/workflow/publish.ts +0 -225
  102. package/runtime-src/workflow/runtime.ts +0 -213
@@ -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
- }