@offworld/sdk 0.2.2 → 0.3.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.
package/dist/index.mjs CHANGED
@@ -1,3910 +1,5 @@
1
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
2
- import { basename, dirname, join, resolve } from "node:path";
3
- import { ConfigSchema, GitHubRepoMetadataSchema, GlobalMapSchema, ModelsDevDataSchema, NpmPackageResponseSchema, ProjectMapSchema, WorkOSTokenResponseSchema } from "@offworld/types";
4
- import { xdgConfig, xdgData, xdgState } from "xdg-basedir";
5
- import { homedir } from "node:os";
6
- import { createHash } from "node:crypto";
7
- import { GlobalMapSchema as GlobalMapSchema$1, ProjectMapSchema as ProjectMapSchema$1 } from "@offworld/types/schemas";
8
- import { execFileSync, execSync, spawn } from "node:child_process";
9
- import { z } from "zod";
10
- import { ConvexHttpClient } from "convex/browser";
11
- import { api } from "@offworld/backend-api/api";
1
+ import { A as getToken, B as NotGitRepoError, C as getAllAgentConfigs, D as clearAuthData, E as TokenExpiredError, F as saveAuthData, G as DEFAULT_IGNORE_PATTERNS, H as RepoSourceError, I as getMapEntry, K as VERSION, L as getProjectMapPath, M as isLoggedIn, N as loadAuthData, O as getAuthPath, P as refreshAccessToken, R as resolveRepoKey, S as getAgentConfig, T as NotLoggedInError, U as getReferenceFileNameForSource, V as PathNotFoundError, W as parseRepoInput, _ as parseDependencies, a as discoverRepos, b as agents, c as pruneRepos, d as matchDependenciesToReferences, f as matchDependenciesToReferencesWithRemoteCheck, g as detectManifestType, h as resolveFromNpm, i as validateProviderModel, j as getTokenOrNull, k as getAuthStatus, l as updateAllRepos, m as resolveDependencyRepo, n as listProviders, o as gcRepos, p as FALLBACK_MAPPINGS, r as listProvidersWithModels, s as getRepoStatus, t as getProvider, u as isReferenceInstalled, v as installGlobalSkill, w as AuthError, x as detectInstalledAgents, y as installReference, z as searchMap } from "./public-DbZeh2Mr.mjs";
2
+ import { a as getRepoPath, c as saveConfig, d as toReferenceName, f as Paths, i as getReferencePath, l as toMetaDirName, n as getMetaPath, o as getRepoRoot, p as expandTilde, r as getMetaRoot, s as loadConfig, t as getConfigPath, u as toReferenceFileName } from "./config-DW8J4gl5.mjs";
3
+ import { _ as upsertGlobalMapEntry, a as cloneRepo, c as getCommitSha, d as listRepos, f as removeRepo, g as removeGlobalMapEntry, h as readGlobalMap, i as RepoNotFoundError, l as isRepoCloned, m as updateRepo, n as GitError, o as getClonedRepoPath, p as unshallowRepo, r as RepoExistsError, s as getCommitDistance, t as CloneError, u as isShallowClone, v as writeGlobalMap, y as writeProjectMap } from "./clone-DyLvmbJZ.mjs";
12
4
 
13
- //#region src/constants.ts
14
- /**
15
- * SDK Constants
16
- */
17
- /** SDK version - must match package.json */
18
- const VERSION = "0.2.2";
19
- /**
20
- * Default patterns to ignore when scanning repositories.
21
- * Includes directories, binary files, IDE configs, and build outputs.
22
- */
23
- const DEFAULT_IGNORE_PATTERNS = [
24
- ".git",
25
- ".git/**",
26
- ".svn",
27
- ".hg",
28
- "node_modules",
29
- "node_modules/**",
30
- "vendor",
31
- "vendor/**",
32
- ".pnpm",
33
- ".yarn",
34
- "dist",
35
- "dist/**",
36
- "build",
37
- "build/**",
38
- "out",
39
- "out/**",
40
- ".next",
41
- ".nuxt",
42
- ".output",
43
- "target",
44
- "__pycache__",
45
- "*.pyc",
46
- ".vscode",
47
- ".vscode/**",
48
- ".idea",
49
- ".idea/**",
50
- "*.swp",
51
- "*.swo",
52
- ".DS_Store",
53
- "*.jpg",
54
- "*.jpeg",
55
- "*.png",
56
- "*.gif",
57
- "*.ico",
58
- "*.webp",
59
- "*.svg",
60
- "*.bmp",
61
- "*.tiff",
62
- "*.mp4",
63
- "*.webm",
64
- "*.mov",
65
- "*.avi",
66
- "*.mkv",
67
- "*.mp3",
68
- "*.wav",
69
- "*.flac",
70
- "*.ogg",
71
- "*.pdf",
72
- "*.zip",
73
- "*.tar",
74
- "*.gz",
75
- "*.rar",
76
- "*.7z",
77
- "*.exe",
78
- "*.dll",
79
- "*.so",
80
- "*.dylib",
81
- "*.bin",
82
- "*.wasm",
83
- "*.woff",
84
- "*.woff2",
85
- "*.ttf",
86
- "*.eot",
87
- "*.otf",
88
- "package-lock.json",
89
- "yarn.lock",
90
- "pnpm-lock.yaml",
91
- "bun.lockb",
92
- "Cargo.lock",
93
- "Gemfile.lock",
94
- "poetry.lock",
95
- "composer.lock",
96
- "go.sum",
97
- "coverage",
98
- "coverage/**",
99
- ".nyc_output",
100
- ".coverage",
101
- "htmlcov",
102
- "*.log",
103
- "logs",
104
- "tmp",
105
- "temp",
106
- ".tmp",
107
- ".temp",
108
- ".cache",
109
- ".env",
110
- ".env.*",
111
- "*.pem",
112
- "*.key"
113
- ];
114
-
115
- //#endregion
116
- //#region src/paths.ts
117
- /**
118
- * XDG-based directory paths for offworld CLI
119
- * Uses xdg-basedir package for cross-platform compatibility (Linux/macOS)
120
- */
121
- const APP_NAME = "offworld";
122
- /**
123
- * Main namespace for all XDG-compliant paths
124
- */
125
- const Paths = {
126
- get config() {
127
- return join(xdgConfig ?? join(homedir(), ".config"), APP_NAME);
128
- },
129
- get data() {
130
- return join(xdgData ?? join(homedir(), ".local", "share"), APP_NAME);
131
- },
132
- get state() {
133
- return join(xdgState ?? join(homedir(), ".local", "state"), APP_NAME);
134
- },
135
- get configFile() {
136
- return join(this.config, "offworld.json");
137
- },
138
- get authFile() {
139
- return join(this.data, "auth.json");
140
- },
141
- get metaDir() {
142
- return join(this.data, "meta");
143
- },
144
- get defaultRepoRoot() {
145
- return join(homedir(), "ow");
146
- },
147
- get offworldSkillDir() {
148
- return join(this.data, "skill", "offworld");
149
- },
150
- get offworldReferencesDir() {
151
- return join(this.offworldSkillDir, "references");
152
- },
153
- get offworldAssetsDir() {
154
- return join(this.offworldSkillDir, "assets");
155
- },
156
- get offworldGlobalMapPath() {
157
- return join(this.offworldAssetsDir, "map.json");
158
- }
159
- };
160
- /**
161
- * Expands ~ to user's home directory (for backward compatibility)
162
- */
163
- function expandTilde(path) {
164
- if (path.startsWith("~/")) return join(homedir(), path.slice(2));
165
- return path;
166
- }
167
-
168
- //#endregion
169
- //#region src/config.ts
170
- /**
171
- * Config utilities for path management and configuration loading
172
- */
173
- /**
174
- * Returns the repository root directory.
175
- * Uses configured repoRoot or defaults to ~/ow
176
- */
177
- function getMetaRoot() {
178
- return Paths.data;
179
- }
180
- function getRepoRoot(config) {
181
- return expandTilde(config?.repoRoot ?? Paths.defaultRepoRoot);
182
- }
183
- /**
184
- * Returns the path for a specific repository.
185
- * Format: {repoRoot}/{provider}/{owner}/{repo}
186
- *
187
- * @param fullName - The repo identifier in "owner/repo" format
188
- * @param provider - Git provider (defaults to "github")
189
- * @param config - Optional config for custom repoRoot
190
- */
191
- function getRepoPath(fullName, provider = "github", config) {
192
- const root = getRepoRoot(config);
193
- const [owner, repo] = fullName.split("/");
194
- if (!owner || !repo) throw new Error(`Invalid fullName format: ${fullName}. Expected "owner/repo"`);
195
- return join(root, provider, owner, repo);
196
- }
197
- /**
198
- * Convert owner/repo format to meta directory name.
199
- * Collapses owner==repo (e.g., better-auth/better-auth -> better-auth)
200
- */
201
- function toMetaDirName(repoName) {
202
- if (repoName.includes("/")) {
203
- const parts = repoName.split("/");
204
- const owner = parts[0];
205
- const repo = parts[1];
206
- if (!owner || !repo) return repoName;
207
- if (owner === repo) return repo;
208
- return `${owner}-${repo}`;
209
- }
210
- return repoName;
211
- }
212
- /**
213
- * Convert owner/repo format to reference filename.
214
- * Collapses redundant owner/repo pairs by checking if repo name is contained in owner:
215
- * - honojs/hono -> hono.md (hono is in honojs)
216
- * - get-convex/convex-backend -> convex-backend.md (convex is in get-convex)
217
- * - tanstack/query -> tanstack-query.md (query is not in tanstack)
218
- */
219
- function toReferenceFileName(repoName) {
220
- if (repoName.includes("/")) {
221
- const parts = repoName.split("/");
222
- const owner = parts[0];
223
- const repo = parts[1];
224
- if (!owner || !repo) return `${repoName.toLowerCase()}.md`;
225
- const ownerLower = owner.toLowerCase();
226
- const repoLower = repo.toLowerCase();
227
- if (repoLower.split("-").find((part) => part.length >= 3 && ownerLower.includes(part)) || ownerLower === repoLower) return `${repoLower}.md`;
228
- return `${ownerLower}-${repoLower}.md`;
229
- }
230
- return `${repoName.toLowerCase()}.md`;
231
- }
232
- function toReferenceName(repoName) {
233
- return toReferenceFileName(repoName).replace(/\.md$/, "");
234
- }
235
- function getReferencePath(fullName) {
236
- return join(Paths.offworldReferencesDir, toReferenceFileName(fullName));
237
- }
238
- function getMetaPath(fullName) {
239
- return join(Paths.data, "meta", toMetaDirName(fullName));
240
- }
241
- /**
242
- * Returns the path to the configuration file
243
- * Uses XDG Base Directory specification
244
- */
245
- function getConfigPath() {
246
- return Paths.configFile;
247
- }
248
- /**
249
- * Loads configuration from ~/.config/offworld/offworld.json
250
- * Returns defaults if file doesn't exist
251
- */
252
- function loadConfig() {
253
- const configPath = getConfigPath();
254
- if (!existsSync(configPath)) return ConfigSchema.parse({});
255
- try {
256
- const content = readFileSync(configPath, "utf-8");
257
- const data = JSON.parse(content);
258
- return ConfigSchema.parse(data);
259
- } catch {
260
- return ConfigSchema.parse({});
261
- }
262
- }
263
- /**
264
- * Saves configuration to ~/.config/offworld/offworld.json
265
- * Creates directory if it doesn't exist
266
- * Merges with existing config
267
- */
268
- function saveConfig(updates) {
269
- const configPath = getConfigPath();
270
- const configDir = dirname(configPath);
271
- if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
272
- const merged = {
273
- ...loadConfig(),
274
- ...updates
275
- };
276
- const validated = ConfigSchema.parse(merged);
277
- writeFileSync(configPath, JSON.stringify(validated, null, 2), "utf-8");
278
- return validated;
279
- }
280
-
281
- //#endregion
282
- //#region src/repo-source.ts
283
- /**
284
- * Repository source parsing utilities
285
- */
286
- var RepoSourceError = class extends Error {
287
- constructor(message) {
288
- super(message);
289
- this.name = "RepoSourceError";
290
- }
291
- };
292
- var PathNotFoundError = class extends RepoSourceError {
293
- constructor(path) {
294
- super(`Path does not exist: ${path}`);
295
- this.name = "PathNotFoundError";
296
- }
297
- };
298
- var NotGitRepoError = class extends RepoSourceError {
299
- constructor(path) {
300
- super(`Directory is not a git repository: ${path}`);
301
- this.name = "NotGitRepoError";
302
- }
303
- };
304
- const PROVIDER_HOSTS = {
305
- "github.com": "github",
306
- "gitlab.com": "gitlab",
307
- "bitbucket.org": "bitbucket"
308
- };
309
- const HTTPS_URL_REGEX = /^https?:\/\/(github\.com|gitlab\.com|bitbucket\.org)\/([^/]+)\/([^/]+?)(?:\.git)?$/;
310
- const SSH_URL_REGEX = /^git@(github\.com|gitlab\.com|bitbucket\.org):([^/]+)\/([^/]+?)(?:\.git)?$/;
311
- const SHORT_FORMAT_REGEX = /^([^/:@]+)\/([^/:@]+)$/;
312
- /**
313
- * Generates a short hash of a path for local repo identification
314
- */
315
- function hashPath(path) {
316
- return createHash("sha256").update(path).digest("hex").slice(0, 12);
317
- }
318
- /**
319
- * Builds a clone URL for a remote repository
320
- */
321
- function buildCloneUrl(provider, owner, repo) {
322
- return `https://${{
323
- github: "github.com",
324
- gitlab: "gitlab.com",
325
- bitbucket: "bitbucket.org"
326
- }[provider]}/${owner}/${repo}.git`;
327
- }
328
- /**
329
- * Parses a remote repository from HTTPS URL format
330
- */
331
- function parseHttpsUrl(input) {
332
- const match = input.match(HTTPS_URL_REGEX);
333
- if (!match) return null;
334
- const [, host, owner, repo] = match;
335
- if (!host || !owner || !repo) return null;
336
- const provider = PROVIDER_HOSTS[host];
337
- if (!provider) return null;
338
- const ownerLower = owner.toLowerCase();
339
- const repoLower = repo.toLowerCase();
340
- return {
341
- type: "remote",
342
- provider,
343
- owner: ownerLower,
344
- repo: repoLower,
345
- fullName: `${ownerLower}/${repoLower}`,
346
- qualifiedName: `${host}:${ownerLower}/${repoLower}`,
347
- cloneUrl: buildCloneUrl(provider, ownerLower, repoLower)
348
- };
349
- }
350
- /**
351
- * Parses a remote repository from SSH URL format
352
- */
353
- function parseSshUrl(input) {
354
- const match = input.match(SSH_URL_REGEX);
355
- if (!match) return null;
356
- const [, host, owner, repo] = match;
357
- if (!host || !owner || !repo) return null;
358
- const provider = PROVIDER_HOSTS[host];
359
- if (!provider) return null;
360
- const ownerLower = owner.toLowerCase();
361
- const repoLower = repo.toLowerCase();
362
- return {
363
- type: "remote",
364
- provider,
365
- owner: ownerLower,
366
- repo: repoLower,
367
- fullName: `${ownerLower}/${repoLower}`,
368
- qualifiedName: `${host}:${ownerLower}/${repoLower}`,
369
- cloneUrl: buildCloneUrl(provider, ownerLower, repoLower)
370
- };
371
- }
372
- /**
373
- * Parses a remote repository from short format (owner/repo)
374
- * Defaults to GitHub as provider
375
- */
376
- function parseShortFormat(input) {
377
- const match = input.match(SHORT_FORMAT_REGEX);
378
- if (!match) return null;
379
- const [, owner, repo] = match;
380
- if (!owner || !repo) return null;
381
- const provider = "github";
382
- const host = "github.com";
383
- const ownerLower = owner.toLowerCase();
384
- const repoLower = repo.toLowerCase();
385
- return {
386
- type: "remote",
387
- provider,
388
- owner: ownerLower,
389
- repo: repoLower,
390
- fullName: `${ownerLower}/${repoLower}`,
391
- qualifiedName: `${host}:${ownerLower}/${repoLower}`,
392
- cloneUrl: buildCloneUrl(provider, ownerLower, repoLower)
393
- };
394
- }
395
- /**
396
- * Parses a local repository path
397
- * Validates that the path exists and contains a .git directory
398
- */
399
- function parseLocalPath(input) {
400
- const absolutePath = resolve(expandTilde(input));
401
- if (!existsSync(absolutePath)) throw new PathNotFoundError(absolutePath);
402
- if (!statSync(absolutePath).isDirectory()) throw new RepoSourceError(`Path is not a directory: ${absolutePath}`);
403
- if (!existsSync(resolve(absolutePath, ".git"))) throw new NotGitRepoError(absolutePath);
404
- return {
405
- type: "local",
406
- path: absolutePath,
407
- name: basename(absolutePath),
408
- qualifiedName: `local:${hashPath(absolutePath)}`
409
- };
410
- }
411
- /**
412
- * Determines if input looks like a local path
413
- */
414
- function isLocalPath(input) {
415
- return input.startsWith(".") || input.startsWith("/") || input.startsWith("~");
416
- }
417
- /**
418
- * Parses a repository input and returns a structured RepoSource
419
- *
420
- * Supported formats:
421
- * - owner/repo (short format, defaults to GitHub)
422
- * - https://github.com/owner/repo
423
- * - https://gitlab.com/owner/repo
424
- * - https://bitbucket.org/owner/repo
425
- * - git@github.com:owner/repo.git (SSH format)
426
- * - . (current directory as local repo)
427
- * - /absolute/path (local repo)
428
- *
429
- * @throws PathNotFoundError if local path doesn't exist
430
- * @throws NotGitRepoError if local path is not a git repository
431
- * @throws RepoSourceError for other parsing failures
432
- */
433
- function parseRepoInput(input) {
434
- const trimmed = input.trim();
435
- const httpsResult = parseHttpsUrl(trimmed);
436
- if (httpsResult) return httpsResult;
437
- const sshResult = parseSshUrl(trimmed);
438
- if (sshResult) return sshResult;
439
- if (isLocalPath(trimmed)) return parseLocalPath(trimmed);
440
- const shortResult = parseShortFormat(trimmed);
441
- if (shortResult) return shortResult;
442
- throw new RepoSourceError(`Unable to parse repository input: ${input}. Expected formats: owner/repo, https://github.com/owner/repo, git@github.com:owner/repo.git, or a local path`);
443
- }
444
- function getReferenceFileNameForSource(source) {
445
- if (source.type === "remote") return toReferenceFileName(source.fullName);
446
- return toReferenceFileName(source.name);
447
- }
448
-
449
- //#endregion
450
- //#region src/index-manager.ts
451
- /**
452
- * Map manager for global and project maps
453
- *
454
- * Manages:
455
- * - Global map: ~/.local/share/offworld/skill/offworld/assets/map.json
456
- * - Project map: ./.offworld/map.json
457
- */
458
- /**
459
- * Reads the global map from ~/.local/share/offworld/skill/offworld/assets/map.json
460
- * Returns empty map if file doesn't exist or is invalid
461
- */
462
- function readGlobalMap() {
463
- const mapPath = Paths.offworldGlobalMapPath;
464
- if (!existsSync(mapPath)) return { repos: {} };
465
- try {
466
- const content = readFileSync(mapPath, "utf-8");
467
- const data = JSON.parse(content);
468
- return GlobalMapSchema.parse(data);
469
- } catch {
470
- return { repos: {} };
471
- }
472
- }
473
- /**
474
- * Writes the global map to ~/.local/share/offworld/skill/offworld/assets/map.json
475
- * Creates directory if it doesn't exist
476
- */
477
- function writeGlobalMap(map) {
478
- const mapPath = Paths.offworldGlobalMapPath;
479
- const mapDir = dirname(mapPath);
480
- if (!existsSync(mapDir)) mkdirSync(mapDir, { recursive: true });
481
- const validated = GlobalMapSchema.parse(map);
482
- writeFileSync(mapPath, JSON.stringify(validated, null, 2), "utf-8");
483
- }
484
- /**
485
- * Adds or updates a repo entry in the global map
486
- *
487
- * @param qualifiedName - The qualified repo name (owner/repo)
488
- * @param entry - The map entry to add/update
489
- */
490
- function upsertGlobalMapEntry(qualifiedName, entry) {
491
- const map = readGlobalMap();
492
- map.repos[qualifiedName] = entry;
493
- writeGlobalMap(map);
494
- }
495
- /**
496
- * Removes a repo entry from the global map
497
- *
498
- * @param qualifiedName - The qualified repo name (owner/repo)
499
- * @returns true if repo was removed, false if not found
500
- */
501
- function removeGlobalMapEntry(qualifiedName) {
502
- const map = readGlobalMap();
503
- if (!(qualifiedName in map.repos)) return false;
504
- delete map.repos[qualifiedName];
505
- writeGlobalMap(map);
506
- return true;
507
- }
508
- /**
509
- * Writes a project map to ./.offworld/map.json
510
- *
511
- * @param projectRoot - Absolute path to project root
512
- * @param entries - Map of qualified repo names to project map entries
513
- */
514
- function writeProjectMap(projectRoot, entries) {
515
- const mapPath = join(projectRoot, ".offworld", "map.json");
516
- const mapDir = dirname(mapPath);
517
- if (!existsSync(mapDir)) mkdirSync(mapDir, { recursive: true });
518
- const projectMap = {
519
- version: 1,
520
- scope: "project",
521
- globalMapPath: Paths.offworldGlobalMapPath,
522
- repos: entries
523
- };
524
- const validated = ProjectMapSchema.parse(projectMap);
525
- writeFileSync(mapPath, JSON.stringify(validated, null, 2), "utf-8");
526
- }
527
-
528
- //#endregion
529
- //#region src/map.ts
530
- /**
531
- * Map query helpers for fast routing without reading full map.json
532
- */
533
- function readGlobalMapSafe() {
534
- const mapPath = Paths.offworldGlobalMapPath;
535
- if (!existsSync(mapPath)) return null;
536
- try {
537
- const content = readFileSync(mapPath, "utf-8");
538
- return GlobalMapSchema$1.parse(JSON.parse(content));
539
- } catch {
540
- return null;
541
- }
542
- }
543
- function readProjectMapSafe(cwd) {
544
- const mapPath = resolve(cwd, ".offworld/map.json");
545
- if (!existsSync(mapPath)) return null;
546
- try {
547
- const content = readFileSync(mapPath, "utf-8");
548
- return ProjectMapSchema$1.parse(JSON.parse(content));
549
- } catch {
550
- return null;
551
- }
552
- }
553
- /**
554
- * Normalize input to match against repo keys.
555
- * Accepts: github.com:owner/repo, owner/repo, repo
556
- */
557
- function normalizeInput(input) {
558
- const trimmed = input.trim().toLowerCase();
559
- if (trimmed.includes(":")) {
560
- const parts = trimmed.split(":", 2);
561
- const provider = parts[0];
562
- const fullName = parts[1] ?? "";
563
- return {
564
- provider,
565
- fullName,
566
- repoName: fullName.split("/").pop() ?? fullName
567
- };
568
- }
569
- if (trimmed.includes("/")) return {
570
- fullName: trimmed,
571
- repoName: trimmed.split("/").pop() ?? trimmed
572
- };
573
- return {
574
- fullName: trimmed,
575
- repoName: trimmed
576
- };
577
- }
578
- /**
579
- * Tokenize a string for search matching.
580
- * Lowercase, strip @, split on /_- and whitespace.
581
- */
582
- function tokenize(str) {
583
- return str.toLowerCase().replace(/@/g, "").split(/[/_\-\s]+/).filter(Boolean);
584
- }
585
- /**
586
- * Resolve an input string to a qualified repo key in a map.
587
- *
588
- * @param input - Accepts github.com:owner/repo, owner/repo, or repo name
589
- * @param map - A global or project map
590
- * @returns The matching qualified name or null
591
- */
592
- function resolveRepoKey(input, map) {
593
- const { provider, fullName, repoName } = normalizeInput(input);
594
- const keys = Object.keys(map.repos);
595
- if (provider) {
596
- const qualifiedKey = `${provider}:${fullName}`;
597
- if (keys.includes(qualifiedKey)) return qualifiedKey;
598
- }
599
- for (const key of keys) if ((key.includes(":") ? key.split(":")[1] : key)?.toLowerCase() === fullName) return key;
600
- for (const key of keys) if (key.split("/").pop()?.toLowerCase() === repoName) return key;
601
- return null;
602
- }
603
- /**
604
- * Get a map entry for a repo, preferring project map if available.
605
- *
606
- * @param input - Repo identifier (github.com:owner/repo, owner/repo, or repo)
607
- * @param options - Options for lookup
608
- * @returns Entry with scope and qualified name, or null if not found
609
- */
610
- function getMapEntry(input, options = {}) {
611
- const { preferProject = true, cwd = process.cwd() } = options;
612
- const projectMap = preferProject ? readProjectMapSafe(cwd) : null;
613
- const globalMap = readGlobalMapSafe();
614
- if (projectMap) {
615
- const key = resolveRepoKey(input, projectMap);
616
- if (key && projectMap.repos[key]) return {
617
- scope: "project",
618
- qualifiedName: key,
619
- entry: projectMap.repos[key]
620
- };
621
- }
622
- if (globalMap) {
623
- const key = resolveRepoKey(input, globalMap);
624
- if (key && globalMap.repos[key]) return {
625
- scope: "global",
626
- qualifiedName: key,
627
- entry: globalMap.repos[key]
628
- };
629
- }
630
- return null;
631
- }
632
- /**
633
- * Search the map for repos matching a term.
634
- *
635
- * Scoring:
636
- * - Exact fullName match: 100
637
- * - Keyword hit: 50 per keyword
638
- * - Partial contains in fullName: 25
639
- * - Partial contains in keywords: 10
640
- *
641
- * @param term - Search term
642
- * @param options - Search options
643
- * @returns Sorted list of matches
644
- */
645
- function searchMap(term, options = {}) {
646
- const { limit = 10 } = options;
647
- const globalMap = readGlobalMapSafe();
648
- if (!globalMap) return [];
649
- const termTokens = tokenize(term);
650
- const termLower = term.toLowerCase();
651
- const results = [];
652
- for (const qualifiedName of Object.keys(globalMap.repos)) {
653
- const entry = globalMap.repos[qualifiedName];
654
- if (!entry) continue;
655
- const fullName = qualifiedName.includes(":") ? qualifiedName.split(":")[1] ?? qualifiedName : qualifiedName;
656
- const fullNameLower = fullName.toLowerCase();
657
- const keywords = entry.keywords ?? [];
658
- const keywordsLower = keywords.map((k) => k.toLowerCase());
659
- let score = 0;
660
- if (fullNameLower === termLower) score += 100;
661
- for (const token of termTokens) if (keywordsLower.includes(token)) score += 50;
662
- if (fullNameLower.includes(termLower) && score < 100) score += 25;
663
- for (const kw of keywordsLower) if (kw.includes(termLower)) score += 10;
664
- const fullNameTokens = tokenize(fullName);
665
- for (const token of termTokens) if (fullNameTokens.includes(token)) score += 30;
666
- if (score > 0) results.push({
667
- qualifiedName,
668
- fullName,
669
- localPath: entry.localPath,
670
- primary: entry.primary,
671
- keywords,
672
- score
673
- });
674
- }
675
- results.sort((a, b) => {
676
- if (b.score !== a.score) return b.score - a.score;
677
- return a.fullName.localeCompare(b.fullName);
678
- });
679
- return results.slice(0, limit);
680
- }
681
- /**
682
- * Get the project map path if it exists in cwd.
683
- */
684
- function getProjectMapPath(cwd = process.cwd()) {
685
- const mapPath = resolve(cwd, ".offworld/map.json");
686
- return existsSync(mapPath) ? mapPath : null;
687
- }
688
-
689
- //#endregion
690
- //#region src/clone.ts
691
- /**
692
- * Git clone and repository management utilities
693
- */
694
- var CloneError = class extends Error {
695
- constructor(message) {
696
- super(message);
697
- this.name = "CloneError";
698
- }
699
- };
700
- var RepoExistsError = class extends CloneError {
701
- constructor(path) {
702
- super(`Repository already exists at: ${path}`);
703
- this.name = "RepoExistsError";
704
- }
705
- };
706
- var RepoNotFoundError = class extends CloneError {
707
- constructor(qualifiedName) {
708
- super(`Repository not found in index: ${qualifiedName}`);
709
- this.name = "RepoNotFoundError";
710
- }
711
- };
712
- var GitError = class extends CloneError {
713
- constructor(message, command, exitCode) {
714
- super(`Git command failed: ${message}`);
715
- this.command = command;
716
- this.exitCode = exitCode;
717
- this.name = "GitError";
718
- }
719
- };
720
- function execGit(args, cwd) {
721
- try {
722
- return execFileSync("git", args, {
723
- cwd,
724
- encoding: "utf-8",
725
- stdio: [
726
- "pipe",
727
- "pipe",
728
- "pipe"
729
- ]
730
- }).trim();
731
- } catch (error) {
732
- const err = error;
733
- throw new GitError((err.stderr ? typeof err.stderr === "string" ? err.stderr : err.stderr.toString() : err.message || "Unknown error").trim(), `git ${args.join(" ")}`, err.status ?? null);
734
- }
735
- }
736
- function execGitAsync(args, cwd) {
737
- return new Promise((resolve, reject) => {
738
- const proc = spawn("git", args, {
739
- cwd,
740
- stdio: [
741
- "ignore",
742
- "pipe",
743
- "pipe"
744
- ],
745
- env: {
746
- ...process.env,
747
- GIT_TERMINAL_PROMPT: "0"
748
- }
749
- });
750
- let stdout = "";
751
- let stderr = "";
752
- proc.stdout.on("data", (data) => {
753
- stdout += data.toString();
754
- });
755
- proc.stderr.on("data", (data) => {
756
- stderr += data.toString();
757
- });
758
- proc.on("close", (code) => {
759
- if (code === 0) resolve(stdout.trim());
760
- else reject(new GitError(stderr.trim() || "Unknown error", `git ${args.join(" ")}`, code));
761
- });
762
- proc.on("error", (err) => {
763
- reject(new GitError(err.message, `git ${args.join(" ")}`, null));
764
- });
765
- });
766
- }
767
- function getCommitSha(repoPath) {
768
- return execGit(["rev-parse", "HEAD"], repoPath);
769
- }
770
- function getCommitDistance(repoPath, olderSha, newerSha = "HEAD") {
771
- try {
772
- try {
773
- execGit([
774
- "cat-file",
775
- "-e",
776
- olderSha
777
- ], repoPath);
778
- } catch {
779
- return null;
780
- }
781
- const count = execGit([
782
- "rev-list",
783
- "--count",
784
- `${olderSha}..${newerSha}`
785
- ], repoPath);
786
- return Number.parseInt(count, 10);
787
- } catch {
788
- return null;
789
- }
790
- }
791
- const SPARSE_CHECKOUT_DIRS = [
792
- "src",
793
- "lib",
794
- "packages",
795
- "docs",
796
- "README.md",
797
- "package.json"
798
- ];
799
- /**
800
- * Clone a remote repository to the local repo root.
801
- *
802
- * @param source - Remote repo source from parseRepoInput()
803
- * @param options - Clone options (shallow, branch, config)
804
- * @returns The local path where the repo was cloned
805
- * @throws RepoExistsError if repo already exists (unless force is true)
806
- * @throws GitError if clone fails
807
- */
808
- async function cloneRepo(source, options = {}) {
809
- const config = options.config ?? loadConfig();
810
- const repoPath = getRepoPath(source.fullName, source.provider, config);
811
- if (existsSync(repoPath)) if (options.force) rmSync(repoPath, {
812
- recursive: true,
813
- force: true
814
- });
815
- else throw new RepoExistsError(repoPath);
816
- try {
817
- if (options.sparse) await cloneSparse(source.cloneUrl, repoPath, options);
818
- else await cloneStandard(source.cloneUrl, repoPath, options);
819
- } catch (err) {
820
- cleanupEmptyParentDirs(repoPath);
821
- throw err;
822
- }
823
- const referenceFileName = toReferenceFileName(source.fullName);
824
- const hasReference = existsSync(join(Paths.offworldReferencesDir, referenceFileName));
825
- upsertGlobalMapEntry(source.qualifiedName, {
826
- localPath: repoPath,
827
- references: hasReference ? [referenceFileName] : [],
828
- primary: hasReference ? referenceFileName : "",
829
- keywords: [],
830
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
831
- });
832
- return repoPath;
833
- }
834
- function cleanupEmptyParentDirs(repoPath) {
835
- const ownerDir = dirname(repoPath);
836
- if (existsSync(ownerDir) && readdirSync(ownerDir).length === 0) rmSync(ownerDir, {
837
- recursive: true,
838
- force: true
839
- });
840
- }
841
- async function cloneStandard(cloneUrl, repoPath, options) {
842
- const args = ["clone"];
843
- if (options.shallow) args.push("--depth", "1");
844
- if (options.branch) args.push("--branch", options.branch);
845
- args.push(cloneUrl, repoPath);
846
- await execGitAsync(args);
847
- }
848
- async function cloneSparse(cloneUrl, repoPath, options) {
849
- const args = [
850
- "clone",
851
- "--filter=blob:none",
852
- "--no-checkout",
853
- "--sparse"
854
- ];
855
- if (options.shallow) args.push("--depth", "1");
856
- if (options.branch) args.push("--branch", options.branch);
857
- args.push(cloneUrl, repoPath);
858
- await execGitAsync(args);
859
- await execGitAsync([
860
- "sparse-checkout",
861
- "set",
862
- ...SPARSE_CHECKOUT_DIRS
863
- ], repoPath);
864
- await execGitAsync(["checkout"], repoPath);
865
- }
866
- function isShallowClone(repoPath) {
867
- try {
868
- return execGit(["rev-parse", "--is-shallow-repository"], repoPath) === "true";
869
- } catch {
870
- return false;
871
- }
872
- }
873
- async function unshallowRepo(repoPath) {
874
- if (!isShallowClone(repoPath)) return false;
875
- await execGitAsync(["fetch", "--unshallow"], repoPath);
876
- return true;
877
- }
878
- /**
879
- * Update a cloned repository by running git fetch and pull.
880
- *
881
- * @param qualifiedName - The qualified name of the repo (e.g., "github.com:owner/repo")
882
- * @param options - Update options
883
- * @returns Update result with commit SHAs
884
- * @throws RepoNotFoundError if repo not in index
885
- * @throws GitError if fetch/pull fails
886
- */
887
- async function updateRepo(qualifiedName, options = {}) {
888
- const entry = readGlobalMap().repos[qualifiedName];
889
- if (!entry) throw new RepoNotFoundError(qualifiedName);
890
- const repoPath = entry.localPath;
891
- if (!existsSync(repoPath)) throw new RepoNotFoundError(qualifiedName);
892
- const previousSha = getCommitSha(repoPath);
893
- let unshallowed = false;
894
- if (options.unshallow) unshallowed = await unshallowRepo(repoPath);
895
- await execGitAsync(["fetch"], repoPath);
896
- await execGitAsync(["pull", "--ff-only"], repoPath);
897
- const currentSha = getCommitSha(repoPath);
898
- upsertGlobalMapEntry(qualifiedName, {
899
- ...entry,
900
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
901
- });
902
- return {
903
- updated: previousSha !== currentSha,
904
- previousSha,
905
- currentSha,
906
- unshallowed
907
- };
908
- }
909
- async function removeRepo(qualifiedName, options = {}) {
910
- const entry = readGlobalMap().repos[qualifiedName];
911
- if (!entry) return false;
912
- const { referenceOnly = false, repoOnly = false } = options;
913
- const removeRepoFiles = !referenceOnly;
914
- const removeReferenceFiles = !repoOnly;
915
- if (removeRepoFiles && existsSync(entry.localPath)) {
916
- rmSync(entry.localPath, {
917
- recursive: true,
918
- force: true
919
- });
920
- cleanupEmptyParentDirs(entry.localPath);
921
- }
922
- if (removeReferenceFiles) {
923
- for (const referenceFileName of entry.references) {
924
- const referencePath = join(Paths.offworldReferencesDir, referenceFileName);
925
- if (existsSync(referencePath)) rmSync(referencePath, { force: true });
926
- }
927
- if (entry.primary) {
928
- const metaDirName = entry.primary.replace(/\.md$/, "");
929
- const metaPath = join(Paths.metaDir, metaDirName);
930
- if (existsSync(metaPath)) rmSync(metaPath, {
931
- recursive: true,
932
- force: true
933
- });
934
- }
935
- }
936
- if (removeRepoFiles) removeGlobalMapEntry(qualifiedName);
937
- else if (removeReferenceFiles) upsertGlobalMapEntry(qualifiedName, {
938
- ...entry,
939
- references: [],
940
- primary: ""
941
- });
942
- return true;
943
- }
944
- function listRepos() {
945
- const map = readGlobalMap();
946
- return Object.keys(map.repos);
947
- }
948
- function isRepoCloned(qualifiedName) {
949
- const entry = readGlobalMap().repos[qualifiedName];
950
- if (!entry) return false;
951
- return existsSync(entry.localPath);
952
- }
953
- /**
954
- * Get the local path for a cloned repository.
955
- *
956
- * @param qualifiedName - The qualified name of the repo
957
- * @returns The local path or undefined if not cloned
958
- */
959
- function getClonedRepoPath(qualifiedName) {
960
- const entry = readGlobalMap().repos[qualifiedName];
961
- if (!entry) return void 0;
962
- if (!existsSync(entry.localPath)) return void 0;
963
- return entry.localPath;
964
- }
965
-
966
- //#endregion
967
- //#region src/ai/errors.ts
968
- /**
969
- * Base class for OpenCode reference errors
970
- */
971
- var OpenCodeReferenceError = class extends Error {
972
- _tag = "OpenCodeReferenceError";
973
- constructor(message, details) {
974
- super(message);
975
- this.details = details;
976
- this.name = "OpenCodeReferenceError";
977
- }
978
- };
979
- /**
980
- * Error when the @opencode-ai/sdk package is not installed or invalid
981
- */
982
- var OpenCodeSDKError = class extends OpenCodeReferenceError {
983
- _tag = "OpenCodeSDKError";
984
- constructor(message) {
985
- super(message ?? "Failed to import @opencode-ai/sdk. Install it with: bun add @opencode-ai/sdk");
986
- this.name = "OpenCodeSDKError";
987
- }
988
- };
989
- /**
990
- * Error when the requested provider is not found
991
- */
992
- var InvalidProviderError = class extends OpenCodeReferenceError {
993
- _tag = "InvalidProviderError";
994
- hint;
995
- constructor(providerID, availableProviders) {
996
- const hint = availableProviders.length > 0 ? `Available providers: ${availableProviders.join(", ")}` : "No providers available. Check your OpenCode configuration.";
997
- super(`Provider "${providerID}" not found. ${hint}`);
998
- this.providerID = providerID;
999
- this.availableProviders = availableProviders;
1000
- this.name = "InvalidProviderError";
1001
- this.hint = hint;
1002
- }
1003
- };
1004
- /**
1005
- * Error when the provider exists but is not connected/authenticated
1006
- */
1007
- var ProviderNotConnectedError = class extends OpenCodeReferenceError {
1008
- _tag = "ProviderNotConnectedError";
1009
- hint;
1010
- constructor(providerID, connectedProviders) {
1011
- const hint = connectedProviders.length > 0 ? `Connected providers: ${connectedProviders.join(", ")}. Run 'opencode auth ${providerID}' to connect.` : `No providers connected. Run 'opencode auth ${providerID}' to authenticate.`;
1012
- super(`Provider "${providerID}" is not connected. ${hint}`);
1013
- this.providerID = providerID;
1014
- this.connectedProviders = connectedProviders;
1015
- this.name = "ProviderNotConnectedError";
1016
- this.hint = hint;
1017
- }
1018
- };
1019
- /**
1020
- * Error when the requested model is not found for a provider
1021
- */
1022
- var InvalidModelError = class extends OpenCodeReferenceError {
1023
- _tag = "InvalidModelError";
1024
- hint;
1025
- constructor(modelID, providerID, availableModels) {
1026
- const hint = availableModels.length > 0 ? `Available models for ${providerID}: ${availableModels.slice(0, 10).join(", ")}${availableModels.length > 10 ? ` (and ${availableModels.length - 10} more)` : ""}` : `No models available for provider "${providerID}".`;
1027
- super(`Model "${modelID}" not found for provider "${providerID}". ${hint}`);
1028
- this.modelID = modelID;
1029
- this.providerID = providerID;
1030
- this.availableModels = availableModels;
1031
- this.name = "InvalidModelError";
1032
- this.hint = hint;
1033
- }
1034
- };
1035
- /**
1036
- * Error when the OpenCode server fails to start
1037
- */
1038
- var ServerStartError = class extends OpenCodeReferenceError {
1039
- _tag = "ServerStartError";
1040
- hint;
1041
- constructor(message, port, details) {
1042
- const hint = port ? `Failed to start server on port ${port}. Ensure no other process is using this port.` : "Failed to start OpenCode server. Check your OpenCode installation and configuration.";
1043
- super(`${message}. ${hint}`, details);
1044
- this.port = port;
1045
- this.name = "ServerStartError";
1046
- this.hint = hint;
1047
- }
1048
- };
1049
- /**
1050
- * Error when a session operation fails
1051
- */
1052
- var SessionError = class extends OpenCodeReferenceError {
1053
- _tag = "SessionError";
1054
- hint;
1055
- constructor(message, sessionId, sessionState, details) {
1056
- const hint = `Session operation failed${sessionId ? ` (session: ${sessionId})` : ""}.${sessionState ? ` State: ${sessionState}.` : ""} Try creating a new session.`;
1057
- super(`${message}. ${hint}`, details);
1058
- this.sessionId = sessionId;
1059
- this.sessionState = sessionState;
1060
- this.name = "SessionError";
1061
- this.hint = hint;
1062
- }
1063
- };
1064
- /**
1065
- * Error when a request times out
1066
- */
1067
- var TimeoutError = class extends OpenCodeReferenceError {
1068
- _tag = "TimeoutError";
1069
- hint;
1070
- constructor(timeoutMs, operation = "operation") {
1071
- const hint = `The ${operation} did not complete within ${timeoutMs}ms. Consider increasing the timeout or checking if the model is responding.`;
1072
- super(`Timeout: ${operation} did not complete within ${timeoutMs}ms. ${hint}`);
1073
- this.timeoutMs = timeoutMs;
1074
- this.operation = operation;
1075
- this.name = "TimeoutError";
1076
- this.hint = hint;
1077
- }
1078
- };
1079
-
1080
- //#endregion
1081
- //#region src/ai/stream/types.ts
1082
- /**
1083
- * Zod schemas for OpenCode stream events
1084
- * Replaces inline `as` casts with runtime-validated types
1085
- */
1086
- const PartBaseSchema = z.object({
1087
- id: z.string().optional(),
1088
- sessionID: z.string().optional(),
1089
- messageID: z.string().optional()
1090
- });
1091
- const TextPartSchema = PartBaseSchema.extend({
1092
- type: z.literal("text"),
1093
- text: z.string().optional()
1094
- });
1095
- const ToolStatePendingSchema = z.object({ status: z.literal("pending") });
1096
- const ToolStateRunningSchema = z.object({
1097
- status: z.literal("running"),
1098
- title: z.string().optional(),
1099
- input: z.unknown().optional(),
1100
- metadata: z.record(z.string(), z.unknown()).optional(),
1101
- time: z.object({ start: z.number() }).optional()
1102
- });
1103
- const ToolStateCompletedSchema = z.object({
1104
- status: z.literal("completed"),
1105
- title: z.string().optional(),
1106
- input: z.record(z.string(), z.unknown()).optional(),
1107
- output: z.string().optional(),
1108
- metadata: z.record(z.string(), z.unknown()).optional(),
1109
- time: z.object({
1110
- start: z.number(),
1111
- end: z.number()
1112
- }).optional()
1113
- });
1114
- const ToolStateErrorSchema = z.object({
1115
- status: z.literal("error"),
1116
- error: z.string().optional(),
1117
- input: z.record(z.string(), z.unknown()).optional(),
1118
- time: z.object({
1119
- start: z.number(),
1120
- end: z.number()
1121
- }).optional()
1122
- });
1123
- const ToolStateSchema = z.discriminatedUnion("status", [
1124
- ToolStatePendingSchema,
1125
- ToolStateRunningSchema,
1126
- ToolStateCompletedSchema,
1127
- ToolStateErrorSchema
1128
- ]);
1129
- const ToolPartSchema = PartBaseSchema.extend({
1130
- type: z.literal("tool"),
1131
- callID: z.string().optional(),
1132
- tool: z.string().optional(),
1133
- state: ToolStateSchema.optional()
1134
- });
1135
- const StepStartPartSchema = PartBaseSchema.extend({ type: z.literal("step-start") });
1136
- const StepFinishPartSchema = PartBaseSchema.extend({
1137
- type: z.literal("step-finish"),
1138
- reason: z.string().optional()
1139
- });
1140
- const ToolUsePartSchema = PartBaseSchema.extend({
1141
- type: z.literal("tool-use"),
1142
- toolUseId: z.string().optional(),
1143
- name: z.string().optional()
1144
- });
1145
- const ToolResultPartSchema = PartBaseSchema.extend({
1146
- type: z.literal("tool-result"),
1147
- toolUseId: z.string().optional()
1148
- });
1149
- const MessagePartSchema = z.discriminatedUnion("type", [
1150
- TextPartSchema,
1151
- ToolPartSchema,
1152
- StepStartPartSchema,
1153
- StepFinishPartSchema,
1154
- ToolUsePartSchema,
1155
- ToolResultPartSchema
1156
- ]);
1157
- /**
1158
- * Session error payload
1159
- */
1160
- const SessionErrorSchema = z.object({
1161
- name: z.string().optional(),
1162
- message: z.string().optional(),
1163
- code: z.string().optional()
1164
- });
1165
- const MessagePartUpdatedPropsSchema = z.object({ part: MessagePartSchema });
1166
- /**
1167
- * Properties for session.idle event
1168
- */
1169
- const SessionIdlePropsSchema = z.object({ sessionID: z.string() });
1170
- /**
1171
- * Properties for session.error event
1172
- */
1173
- const SessionErrorPropsSchema = z.object({
1174
- sessionID: z.string(),
1175
- error: SessionErrorSchema.optional()
1176
- });
1177
- /**
1178
- * Properties for session.updated event
1179
- */
1180
- const SessionUpdatedPropsSchema = z.object({
1181
- sessionID: z.string(),
1182
- status: z.string().optional()
1183
- });
1184
- /**
1185
- * message.part.updated event
1186
- */
1187
- const MessagePartUpdatedEventSchema = z.object({
1188
- type: z.literal("message.part.updated"),
1189
- properties: MessagePartUpdatedPropsSchema
1190
- });
1191
- /**
1192
- * session.idle event
1193
- */
1194
- const SessionIdleEventSchema = z.object({
1195
- type: z.literal("session.idle"),
1196
- properties: SessionIdlePropsSchema
1197
- });
1198
- /**
1199
- * session.error event
1200
- */
1201
- const SessionErrorEventSchema = z.object({
1202
- type: z.literal("session.error"),
1203
- properties: SessionErrorPropsSchema
1204
- });
1205
- /**
1206
- * session.updated event
1207
- */
1208
- const SessionUpdatedEventSchema = z.object({
1209
- type: z.literal("session.updated"),
1210
- properties: SessionUpdatedPropsSchema
1211
- });
1212
- /**
1213
- * Generic event for unknown types (passthrough)
1214
- */
1215
- const GenericEventSchema = z.object({
1216
- type: z.string(),
1217
- properties: z.record(z.string(), z.unknown())
1218
- });
1219
-
1220
- //#endregion
1221
- //#region src/ai/stream/accumulator.ts
1222
- /**
1223
- * Accumulates text from streaming message parts.
1224
- * Tracks multiple parts by ID and provides delta text between updates.
1225
- */
1226
- var TextAccumulator = class {
1227
- parts = /* @__PURE__ */ new Map();
1228
- _firstTextReceived = false;
1229
- /**
1230
- * Whether any text has been received
1231
- */
1232
- get hasReceivedText() {
1233
- return this._firstTextReceived;
1234
- }
1235
- /**
1236
- * Accumulate text from a message part and return the delta (new text only).
1237
- * Returns null if the part should be skipped (non-text, no ID, no text).
1238
- */
1239
- accumulatePart(part) {
1240
- if (part.type !== "text" || !part.text || !part.id) return null;
1241
- const partId = part.id;
1242
- const prevText = this.parts.get(partId) ?? "";
1243
- this.parts.set(partId, part.text);
1244
- if (!this._firstTextReceived) this._firstTextReceived = true;
1245
- if (part.text.length > prevText.length) return part.text.slice(prevText.length);
1246
- return null;
1247
- }
1248
- /**
1249
- * Get the full accumulated text from all parts
1250
- */
1251
- getFullText() {
1252
- return Array.from(this.parts.values()).join("");
1253
- }
1254
- /**
1255
- * Get the number of distinct parts accumulated
1256
- */
1257
- getPartCount() {
1258
- return this.parts.size;
1259
- }
1260
- /**
1261
- * Get info about each part for debugging
1262
- */
1263
- getPartInfo() {
1264
- return Array.from(this.parts.entries()).map(([id, text]) => ({
1265
- id,
1266
- length: text.length
1267
- }));
1268
- }
1269
- /**
1270
- * Clear accumulated text
1271
- */
1272
- clear() {
1273
- this.parts.clear();
1274
- this._firstTextReceived = false;
1275
- }
1276
- };
1277
-
1278
- //#endregion
1279
- //#region src/ai/stream/transformer.ts
1280
- /**
1281
- * Stream event transformer and parser
1282
- * Safely parses and validates stream events using Zod schemas
1283
- */
1284
- /**
1285
- * Parse a raw stream event into a typed result.
1286
- * Uses safe parsing - returns type: "unknown" for unrecognized or invalid events.
1287
- */
1288
- function parseStreamEvent(event) {
1289
- switch (event.type) {
1290
- case "message.part.updated": {
1291
- const propsResult = MessagePartUpdatedPropsSchema.safeParse(event.properties);
1292
- if (!propsResult.success) return {
1293
- type: "unknown",
1294
- rawType: event.type
1295
- };
1296
- const props = propsResult.data;
1297
- return {
1298
- type: "message.part.updated",
1299
- props,
1300
- textPart: props.part.type === "text" ? props.part : null,
1301
- toolPart: props.part.type === "tool" ? props.part : null
1302
- };
1303
- }
1304
- case "session.idle": {
1305
- const propsResult = SessionIdlePropsSchema.safeParse(event.properties);
1306
- if (!propsResult.success) return {
1307
- type: "unknown",
1308
- rawType: event.type
1309
- };
1310
- return {
1311
- type: "session.idle",
1312
- props: propsResult.data
1313
- };
1314
- }
1315
- case "session.error": {
1316
- const propsResult = SessionErrorPropsSchema.safeParse(event.properties);
1317
- if (!propsResult.success) return {
1318
- type: "unknown",
1319
- rawType: event.type
1320
- };
1321
- const props = propsResult.data;
1322
- let error = null;
1323
- if (props.error) {
1324
- const errorResult = SessionErrorSchema.safeParse(props.error);
1325
- if (errorResult.success) error = errorResult.data;
1326
- }
1327
- return {
1328
- type: "session.error",
1329
- props,
1330
- error
1331
- };
1332
- }
1333
- default: return {
1334
- type: "unknown",
1335
- rawType: event.type
1336
- };
1337
- }
1338
- }
1339
- function isEventForSession(event, sessionId) {
1340
- const props = event.properties;
1341
- if ("sessionID" in props && typeof props.sessionID === "string") return props.sessionID === sessionId;
1342
- if ("part" in props && typeof props.part === "object" && props.part !== null && "sessionID" in props.part && typeof props.part.sessionID === "string") return props.part.sessionID === sessionId;
1343
- return true;
1344
- }
1345
-
1346
- //#endregion
1347
- //#region src/ai/opencode.ts
1348
- const DEFAULT_AI_PROVIDER = "opencode";
1349
- const DEFAULT_AI_MODEL = "claude-opus-4-5";
1350
- let cachedCreateOpencode = null;
1351
- let cachedCreateOpencodeClient = null;
1352
- async function getOpenCodeSDK() {
1353
- if (cachedCreateOpencode && cachedCreateOpencodeClient) return {
1354
- createOpencode: cachedCreateOpencode,
1355
- createOpencodeClient: cachedCreateOpencodeClient
1356
- };
1357
- try {
1358
- const sdk = await import("@opencode-ai/sdk");
1359
- if (typeof sdk.createOpencode !== "function" || typeof sdk.createOpencodeClient !== "function") throw new OpenCodeSDKError("SDK missing required exports");
1360
- cachedCreateOpencode = sdk.createOpencode;
1361
- cachedCreateOpencodeClient = sdk.createOpencodeClient;
1362
- return {
1363
- createOpencode: cachedCreateOpencode,
1364
- createOpencodeClient: cachedCreateOpencodeClient
1365
- };
1366
- } catch (error) {
1367
- if (error instanceof OpenCodeSDKError) throw error;
1368
- throw new OpenCodeSDKError();
1369
- }
1370
- }
1371
- function formatToolMessage(tool, state) {
1372
- if (state.title) return state.title;
1373
- if (!tool) return null;
1374
- const input = state.input && typeof state.input === "object" && !Array.isArray(state.input) ? state.input : void 0;
1375
- if (!input) return `Running ${tool}...`;
1376
- switch (tool) {
1377
- case "read": {
1378
- const path = input.filePath ?? input.path;
1379
- if (typeof path === "string") return `Reading ${path.split("/").pop()}...`;
1380
- return "Reading file...";
1381
- }
1382
- case "glob": {
1383
- const pattern = input.pattern;
1384
- if (typeof pattern === "string") return `Globbing ${pattern}...`;
1385
- return "Searching files...";
1386
- }
1387
- case "grep": {
1388
- const pattern = input.pattern;
1389
- if (typeof pattern === "string") return `Searching for "${pattern.length > 30 ? `${pattern.slice(0, 30)}...` : pattern}"...`;
1390
- return "Searching content...";
1391
- }
1392
- case "list": {
1393
- const path = input.path;
1394
- if (typeof path === "string") return `Listing ${path}...`;
1395
- return "Listing directory...";
1396
- }
1397
- default: return `Running ${tool}...`;
1398
- }
1399
- }
1400
- async function streamPrompt(options) {
1401
- const { prompt, cwd, systemPrompt, provider: optProvider, model: optModel, timeoutMs, onDebug, onStream } = options;
1402
- const debug = onDebug ?? (() => {});
1403
- const stream = onStream ?? (() => {});
1404
- const startTime = Date.now();
1405
- debug("Loading OpenCode SDK...");
1406
- const { createOpencode, createOpencodeClient } = await getOpenCodeSDK();
1407
- const maxAttempts = 10;
1408
- let server = null;
1409
- let client = null;
1410
- let port = 0;
1411
- const config = {
1412
- plugin: [],
1413
- mcp: {},
1414
- instructions: [],
1415
- agent: {
1416
- build: { disable: true },
1417
- general: { disable: true },
1418
- plan: { disable: true },
1419
- explore: { disable: true },
1420
- analyze: {
1421
- prompt: [
1422
- "You are an expert at analyzing open source codebases and producing documentation.",
1423
- "",
1424
- "Your job is to read the codebase and produce structured output based on the user's request.",
1425
- "Use glob to discover files, grep to search for patterns, and read to examine file contents.",
1426
- "",
1427
- "Guidelines:",
1428
- "- Explore the codebase thoroughly before producing output",
1429
- "- Focus on understanding architecture, key abstractions, and usage patterns",
1430
- "- When asked for JSON output, respond with ONLY valid JSON - no markdown, no code blocks",
1431
- "- When asked for prose, write clear and concise documentation",
1432
- "- Always base your analysis on actual code you've read, never speculate"
1433
- ].join("\n"),
1434
- mode: "primary",
1435
- description: "Analyze open source codebases and produce summaries and reference files",
1436
- tools: {
1437
- read: true,
1438
- grep: true,
1439
- glob: true,
1440
- list: true,
1441
- write: false,
1442
- bash: false,
1443
- delete: false,
1444
- edit: false,
1445
- patch: false,
1446
- path: false,
1447
- todowrite: false,
1448
- todoread: false,
1449
- websearch: false,
1450
- webfetch: false,
1451
- codesearch: false,
1452
- skill: false,
1453
- task: false,
1454
- mcp: false,
1455
- question: false,
1456
- plan_enter: false,
1457
- plan_exit: false
1458
- },
1459
- permission: {
1460
- edit: "deny",
1461
- bash: "deny",
1462
- webfetch: "deny",
1463
- external_directory: "deny"
1464
- }
1465
- }
1466
- }
1467
- };
1468
- debug("Starting embedded OpenCode server...");
1469
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
1470
- port = Math.floor(Math.random() * 3e3) + 3e3;
1471
- try {
1472
- server = (await createOpencode({
1473
- port,
1474
- cwd,
1475
- config
1476
- })).server;
1477
- client = createOpencodeClient({
1478
- baseUrl: `http://localhost:${port}`,
1479
- directory: cwd
1480
- });
1481
- debug(`Server started on port ${port}`);
1482
- break;
1483
- } catch (err) {
1484
- if (err instanceof Error && err.message?.includes("port")) continue;
1485
- throw new ServerStartError("Failed to start OpenCode server", port, err);
1486
- }
1487
- }
1488
- if (!server || !client) throw new ServerStartError("Failed to start OpenCode server after all attempts");
1489
- const providerID = optProvider ?? DEFAULT_AI_PROVIDER;
1490
- const modelID = optModel ?? DEFAULT_AI_MODEL;
1491
- try {
1492
- debug("Creating session...");
1493
- const sessionResult = await client.session.create();
1494
- if (sessionResult.error) throw new SessionError("Failed to create session", void 0, void 0, sessionResult.error);
1495
- const sessionId = sessionResult.data.id;
1496
- debug(`Session created: ${sessionId}`);
1497
- debug("Validating provider and model...");
1498
- const providerResult = await client.provider.list();
1499
- if (providerResult.error) throw new OpenCodeReferenceError("Failed to fetch provider list", providerResult.error);
1500
- const { all: allProviders, connected: connectedProviders } = providerResult.data;
1501
- const allProviderIds = allProviders.map((p) => p.id);
1502
- const provider = allProviders.find((p) => p.id === providerID);
1503
- if (!provider) throw new InvalidProviderError(providerID, allProviderIds);
1504
- if (!connectedProviders.includes(providerID)) throw new ProviderNotConnectedError(providerID, connectedProviders);
1505
- const availableModelIds = Object.keys(provider.models);
1506
- if (!provider.models[modelID]) throw new InvalidModelError(modelID, providerID, availableModelIds);
1507
- debug(`Provider "${providerID}" and model "${modelID}" validated`);
1508
- debug("Subscribing to events...");
1509
- const { stream: eventStream } = await client.event.subscribe();
1510
- const fullPrompt = systemPrompt ? `${systemPrompt}\n\nAnalyzing codebase at: ${cwd}\n\n${prompt}` : `Analyzing codebase at: ${cwd}\n\n${prompt}`;
1511
- debug("Sending prompt...");
1512
- const promptPromise = client.session.prompt({
1513
- path: { id: sessionId },
1514
- body: {
1515
- agent: "analyze",
1516
- parts: [{
1517
- type: "text",
1518
- text: fullPrompt
1519
- }],
1520
- model: {
1521
- providerID,
1522
- modelID
1523
- }
1524
- }
1525
- });
1526
- const textAccumulator = new TextAccumulator();
1527
- debug("Waiting for response...");
1528
- let timeoutId = null;
1529
- const processEvents = async () => {
1530
- for await (const event of eventStream) {
1531
- if (!isEventForSession(event, sessionId)) continue;
1532
- const parsed = parseStreamEvent(event);
1533
- switch (parsed.type) {
1534
- case "message.part.updated":
1535
- if (parsed.toolPart?.state) {
1536
- const { state, tool } = parsed.toolPart;
1537
- if (state.status === "running") {
1538
- const message = formatToolMessage(tool, state);
1539
- if (message) debug(message);
1540
- }
1541
- }
1542
- if (parsed.textPart) {
1543
- const delta = textAccumulator.accumulatePart(parsed.textPart);
1544
- if (!textAccumulator.hasReceivedText) debug("Writing reference...");
1545
- if (delta) stream(delta);
1546
- }
1547
- break;
1548
- case "session.idle":
1549
- if (parsed.props.sessionID === sessionId) {
1550
- debug("Response complete");
1551
- return textAccumulator.getFullText();
1552
- }
1553
- break;
1554
- case "session.error":
1555
- if (parsed.props.sessionID === sessionId) {
1556
- const errorName = parsed.error?.name ?? "Unknown session error";
1557
- debug(`Session error: ${JSON.stringify(parsed.props.error)}`);
1558
- throw new SessionError(errorName, sessionId, "error", parsed.props.error);
1559
- }
1560
- break;
1561
- case "unknown": break;
1562
- }
1563
- }
1564
- return textAccumulator.getFullText();
1565
- };
1566
- let responseText;
1567
- if (timeoutMs && timeoutMs > 0) {
1568
- const timeoutPromise = new Promise((_, reject) => {
1569
- timeoutId = setTimeout(() => {
1570
- reject(new TimeoutError(timeoutMs, "session response"));
1571
- }, timeoutMs);
1572
- });
1573
- responseText = await Promise.race([processEvents(), timeoutPromise]);
1574
- if (timeoutId) clearTimeout(timeoutId);
1575
- } else responseText = await processEvents();
1576
- await promptPromise;
1577
- if (!responseText) throw new OpenCodeReferenceError("No response received from OpenCode");
1578
- debug(`Response received (${responseText.length} chars)`);
1579
- const durationMs = Date.now() - startTime;
1580
- debug(`Complete in ${durationMs}ms`);
1581
- return {
1582
- text: responseText,
1583
- sessionId,
1584
- durationMs
1585
- };
1586
- } finally {
1587
- debug("Closing server...");
1588
- server.close();
1589
- }
1590
- }
1591
-
1592
- //#endregion
1593
- //#region src/sync.ts
1594
- /**
1595
- * Sync utilities for CLI-Convex communication
1596
- * Uses ConvexHttpClient for direct type-safe API calls
1597
- */
1598
- const PRODUCTION_CONVEX_URL = "https://trustworthy-coyote-128.convex.cloud";
1599
- function getConvexUrl() {
1600
- return process.env.CONVEX_URL ?? PRODUCTION_CONVEX_URL;
1601
- }
1602
- const GITHUB_API_BASE = "https://api.github.com";
1603
- var SyncError = class extends Error {
1604
- constructor(message) {
1605
- super(message);
1606
- this.name = "SyncError";
1607
- }
1608
- };
1609
- var NetworkError = class extends SyncError {
1610
- constructor(message, statusCode) {
1611
- super(message);
1612
- this.statusCode = statusCode;
1613
- this.name = "NetworkError";
1614
- }
1615
- };
1616
- var AuthenticationError = class extends SyncError {
1617
- constructor(message = "Authentication required. Please run 'ow auth login' first.") {
1618
- super(message);
1619
- this.name = "AuthenticationError";
1620
- }
1621
- };
1622
- var RateLimitError = class extends SyncError {
1623
- constructor(message = "Rate limit exceeded. You can push up to 3 times per repo per day.") {
1624
- super(message);
1625
- this.name = "RateLimitError";
1626
- }
1627
- };
1628
- var ConflictError = class extends SyncError {
1629
- constructor(message = "A newer reference already exists on the server.", remoteCommitSha) {
1630
- super(message);
1631
- this.remoteCommitSha = remoteCommitSha;
1632
- this.name = "ConflictError";
1633
- }
1634
- };
1635
- var CommitExistsError = class extends SyncError {
1636
- constructor(message = "A reference already exists for this commit SHA.") {
1637
- super(message);
1638
- this.name = "CommitExistsError";
1639
- }
1640
- };
1641
- var InvalidInputError = class extends SyncError {
1642
- constructor(message) {
1643
- super(message);
1644
- this.name = "InvalidInputError";
1645
- }
1646
- };
1647
- var InvalidReferenceError = class extends SyncError {
1648
- constructor(message) {
1649
- super(message);
1650
- this.name = "InvalidReferenceError";
1651
- }
1652
- };
1653
- var RepoNotFoundError$1 = class extends SyncError {
1654
- constructor(message = "Repository not found on GitHub.") {
1655
- super(message);
1656
- this.name = "RepoNotFoundError";
1657
- }
1658
- };
1659
- var LowStarsError = class extends SyncError {
1660
- constructor(message = "Repository has less than 5 stars.") {
1661
- super(message);
1662
- this.name = "LowStarsError";
1663
- }
1664
- };
1665
- var PrivateRepoError = class extends SyncError {
1666
- constructor(message = "Private repositories are not supported.") {
1667
- super(message);
1668
- this.name = "PrivateRepoError";
1669
- }
1670
- };
1671
- var CommitNotFoundError = class extends SyncError {
1672
- constructor(message = "Commit not found in repository.") {
1673
- super(message);
1674
- this.name = "CommitNotFoundError";
1675
- }
1676
- };
1677
- var GitHubError = class extends SyncError {
1678
- constructor(message = "GitHub API error.") {
1679
- super(message);
1680
- this.name = "GitHubError";
1681
- }
1682
- };
1683
- var PushNotAllowedError = class extends SyncError {
1684
- constructor(message, reason) {
1685
- super(message);
1686
- this.reason = reason;
1687
- this.name = "PushNotAllowedError";
1688
- }
1689
- };
1690
- function createClient(token) {
1691
- const client = new ConvexHttpClient(getConvexUrl());
1692
- if (token) client.setAuth(token);
1693
- return client;
1694
- }
1695
- /**
1696
- * Fetches reference from the remote server
1697
- * @param fullName - Repository full name (owner/repo)
1698
- * @returns Reference data or null if not found
1699
- */
1700
- async function pullReference(fullName) {
1701
- const client = createClient();
1702
- try {
1703
- let result = await client.query(api.references.pull, {
1704
- fullName,
1705
- referenceName: toReferenceName(fullName)
1706
- });
1707
- if (!result) result = await client.query(api.references.pull, { fullName });
1708
- if (!result) return null;
1709
- client.mutation(api.references.recordPull, {
1710
- fullName,
1711
- referenceName: result.referenceName
1712
- }).catch(() => {});
1713
- return {
1714
- fullName: result.fullName,
1715
- referenceName: result.referenceName,
1716
- referenceDescription: result.referenceDescription,
1717
- referenceContent: result.referenceContent,
1718
- commitSha: result.commitSha,
1719
- generatedAt: result.generatedAt
1720
- };
1721
- } catch (error) {
1722
- throw new NetworkError(`Failed to pull reference: ${error instanceof Error ? error.message : error}`);
1723
- }
1724
- }
1725
- /**
1726
- * Fetches a specific reference by name from the remote server
1727
- * @param fullName - Repository full name (owner/repo)
1728
- * @param referenceName - Specific reference name to pull
1729
- * @returns Reference data or null if not found
1730
- */
1731
- async function pullReferenceByName(fullName, referenceName) {
1732
- const client = createClient();
1733
- try {
1734
- const result = await client.query(api.references.pull, {
1735
- fullName,
1736
- referenceName
1737
- });
1738
- if (!result) return null;
1739
- client.mutation(api.references.recordPull, {
1740
- fullName,
1741
- referenceName
1742
- }).catch(() => {});
1743
- return {
1744
- fullName: result.fullName,
1745
- referenceName: result.referenceName,
1746
- referenceDescription: result.referenceDescription,
1747
- referenceContent: result.referenceContent,
1748
- commitSha: result.commitSha,
1749
- generatedAt: result.generatedAt
1750
- };
1751
- } catch (error) {
1752
- throw new NetworkError(`Failed to pull reference: ${error instanceof Error ? error.message : error}`);
1753
- }
1754
- }
1755
- /**
1756
- * Pushes reference to the remote server
1757
- * All validation happens server-side
1758
- * @param reference - Reference data to push
1759
- * @param token - Authentication token
1760
- * @returns Push result
1761
- */
1762
- async function pushReference(reference, token) {
1763
- const client = createClient(token);
1764
- try {
1765
- const result = await client.action(api.references.push, {
1766
- fullName: reference.fullName,
1767
- referenceName: reference.referenceName,
1768
- referenceDescription: reference.referenceDescription,
1769
- referenceContent: reference.referenceContent,
1770
- commitSha: reference.commitSha,
1771
- generatedAt: reference.generatedAt
1772
- });
1773
- if (!result.success) switch (result.error) {
1774
- case "auth_required": throw new AuthenticationError();
1775
- case "rate_limit": throw new RateLimitError("Rate limit exceeded. You can push up to 20 times per day.");
1776
- case "commit_already_exists": throw new CommitExistsError(result.message);
1777
- case "invalid_input": throw new InvalidInputError(result.message ?? "Invalid input");
1778
- case "invalid_reference": throw new InvalidReferenceError(result.message ?? "Invalid reference content");
1779
- case "repo_not_found": throw new RepoNotFoundError$1(result.message);
1780
- case "low_stars": throw new LowStarsError(result.message);
1781
- case "private_repo": throw new PrivateRepoError(result.message);
1782
- case "commit_not_found": throw new CommitNotFoundError(result.message);
1783
- case "github_error": throw new GitHubError(result.message);
1784
- default: throw new SyncError(result.message ?? "Unknown error");
1785
- }
1786
- return { success: true };
1787
- } catch (error) {
1788
- if (error instanceof SyncError) throw error;
1789
- throw new NetworkError(`Failed to push reference: ${error instanceof Error ? error.message : error}`);
1790
- }
1791
- }
1792
- /**
1793
- * Checks if reference exists on remote server (lightweight check)
1794
- * @param fullName - Repository full name (owner/repo)
1795
- * @returns Check result
1796
- */
1797
- async function checkRemote(fullName) {
1798
- const client = createClient();
1799
- try {
1800
- let result = await client.query(api.references.check, {
1801
- fullName,
1802
- referenceName: toReferenceName(fullName)
1803
- });
1804
- if (!result.exists) result = await client.query(api.references.check, { fullName });
1805
- if (!result.exists) return { exists: false };
1806
- return {
1807
- exists: true,
1808
- commitSha: result.commitSha,
1809
- generatedAt: result.generatedAt
1810
- };
1811
- } catch (error) {
1812
- throw new NetworkError(`Failed to check remote: ${error instanceof Error ? error.message : error}`);
1813
- }
1814
- }
1815
- /**
1816
- * Checks if a specific reference exists on the remote server
1817
- * @param fullName - Repository full name (owner/repo)
1818
- * @param referenceName - Specific reference name to check
1819
- * @returns Check result
1820
- */
1821
- async function checkRemoteByName(fullName, referenceName) {
1822
- const client = createClient();
1823
- try {
1824
- const result = await client.query(api.references.check, {
1825
- fullName,
1826
- referenceName
1827
- });
1828
- if (!result.exists) return { exists: false };
1829
- return {
1830
- exists: true,
1831
- commitSha: result.commitSha,
1832
- generatedAt: result.generatedAt
1833
- };
1834
- } catch (error) {
1835
- throw new NetworkError(`Failed to check remote: ${error instanceof Error ? error.message : error}`);
1836
- }
1837
- }
1838
- /**
1839
- * Compares local vs remote commit SHA to check staleness
1840
- * @param fullName - Repository full name (owner/repo)
1841
- * @param localCommitSha - Local commit SHA
1842
- * @returns Staleness result
1843
- */
1844
- async function checkStaleness(fullName, localCommitSha) {
1845
- const remote = await checkRemote(fullName);
1846
- if (!remote.exists || !remote.commitSha) return {
1847
- isStale: false,
1848
- localCommitSha,
1849
- remoteCommitSha: void 0
1850
- };
1851
- return {
1852
- isStale: localCommitSha !== remote.commitSha,
1853
- localCommitSha,
1854
- remoteCommitSha: remote.commitSha
1855
- };
1856
- }
1857
- /**
1858
- * Fetches GitHub repository metadata
1859
- * @param owner - Repository owner
1860
- * @param repo - Repository name
1861
- * @returns Repository metadata or null on error
1862
- */
1863
- async function fetchGitHubMetadata(owner, repo) {
1864
- try {
1865
- const response = await fetch(`${GITHUB_API_BASE}/repos/${owner}/${repo}`, { headers: {
1866
- Accept: "application/vnd.github.v3+json",
1867
- "User-Agent": "offworld-cli"
1868
- } });
1869
- if (!response.ok) return null;
1870
- const json = await response.json();
1871
- const result = GitHubRepoMetadataSchema.safeParse(json);
1872
- if (!result.success) return null;
1873
- const data = result.data;
1874
- return {
1875
- stars: data.stargazers_count ?? 0,
1876
- description: data.description ?? void 0,
1877
- language: data.language ?? void 0,
1878
- defaultBranch: data.default_branch ?? "main"
1879
- };
1880
- } catch {
1881
- return null;
1882
- }
1883
- }
1884
- /**
1885
- * Fetches GitHub repository stars
1886
- * @param owner - Repository owner
1887
- * @param repo - Repository name
1888
- * @returns Number of stars, or 0 on error
1889
- */
1890
- async function fetchRepoStars(owner, repo) {
1891
- return (await fetchGitHubMetadata(owner, repo))?.stars ?? 0;
1892
- }
1893
- /**
1894
- * Checks if a repository can be pushed to offworld.sh (client-side quick checks)
1895
- * Note: Star count and other validations happen server-side
1896
- *
1897
- * @param source - Repository source
1898
- * @returns Can push result
1899
- */
1900
- function canPushToWeb(source) {
1901
- if (source.type === "local") return {
1902
- allowed: false,
1903
- reason: "Local repositories cannot be pushed to offworld.sh. Only remote repositories with a public URL are supported."
1904
- };
1905
- if (source.provider !== "github") return {
1906
- allowed: false,
1907
- reason: `${source.provider} repositories are not yet supported. GitHub support only for now - GitLab and Bitbucket coming soon!`
1908
- };
1909
- return { allowed: true };
1910
- }
1911
- /**
1912
- * Validates that a source can be pushed and throws appropriate error if not
1913
- * Note: This only does quick client-side checks. Full validation happens server-side.
1914
- * @param source - Repository source
1915
- * @throws PushNotAllowedError if push is not allowed
1916
- */
1917
- function validatePushAllowed(source) {
1918
- const result = canPushToWeb(source);
1919
- if (!result.allowed) {
1920
- const reason = source.type === "local" ? "local" : "not-github";
1921
- throw new PushNotAllowedError(result.reason, reason);
1922
- }
1923
- }
1924
-
1925
- //#endregion
1926
- //#region src/auth.ts
1927
- /**
1928
- * Authentication utilities for offworld CLI
1929
- */
1930
- const AuthDataSchema = z.object({
1931
- token: z.string(),
1932
- expiresAt: z.string().optional(),
1933
- workosId: z.string().optional(),
1934
- refreshToken: z.string().optional(),
1935
- email: z.string().optional()
1936
- });
1937
- var AuthError = class extends Error {
1938
- constructor(message) {
1939
- super(message);
1940
- this.name = "AuthError";
1941
- }
1942
- };
1943
- var NotLoggedInError = class extends AuthError {
1944
- constructor(message = "Not logged in. Please run 'ow auth login' first.") {
1945
- super(message);
1946
- this.name = "NotLoggedInError";
1947
- }
1948
- };
1949
- var TokenExpiredError = class extends AuthError {
1950
- constructor(message = "Session expired. Please run 'ow auth login' again.") {
1951
- super(message);
1952
- this.name = "TokenExpiredError";
1953
- }
1954
- };
1955
- function extractJwtExpiration(token) {
1956
- try {
1957
- const parts = token.split(".");
1958
- if (parts.length !== 3) return void 0;
1959
- const payload = parts[1];
1960
- if (!payload) return void 0;
1961
- const decoded = JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
1962
- if (typeof decoded.exp !== "number") return void 0;
1963
- return (/* @__PURE__ */ new Date(decoded.exp * 1e3)).toISOString();
1964
- } catch {
1965
- return;
1966
- }
1967
- }
1968
- function getAuthPath() {
1969
- return Paths.authFile;
1970
- }
1971
- function saveAuthData(data) {
1972
- const authPath = getAuthPath();
1973
- const authDir = dirname(authPath);
1974
- if (!existsSync(authDir)) mkdirSync(authDir, { recursive: true });
1975
- writeFileSync(authPath, JSON.stringify(data, null, 2), "utf-8");
1976
- chmodSync(authPath, 384);
1977
- }
1978
- /**
1979
- * Loads authentication data from ~/.local/share/offworld/auth.json
1980
- * Returns null if file doesn't exist or is invalid
1981
- */
1982
- function loadAuthData() {
1983
- const authPath = getAuthPath();
1984
- if (!existsSync(authPath)) return null;
1985
- try {
1986
- const content = readFileSync(authPath, "utf-8");
1987
- const json = JSON.parse(content);
1988
- const parsed = AuthDataSchema.safeParse(json);
1989
- if (!parsed.success) return null;
1990
- return parsed.data;
1991
- } catch {
1992
- return null;
1993
- }
1994
- }
1995
- /**
1996
- * Clears stored authentication data
1997
- * @returns true if auth file was deleted, false if it didn't exist
1998
- */
1999
- function clearAuthData() {
2000
- const authPath = getAuthPath();
2001
- if (!existsSync(authPath)) return false;
2002
- try {
2003
- unlinkSync(authPath);
2004
- return true;
2005
- } catch {
2006
- return false;
2007
- }
2008
- }
2009
- async function getToken() {
2010
- const data = loadAuthData();
2011
- if (!data) throw new NotLoggedInError();
2012
- let expiresAtStr = data.expiresAt;
2013
- if (!expiresAtStr) {
2014
- expiresAtStr = extractJwtExpiration(data.token);
2015
- if (expiresAtStr) {
2016
- data.expiresAt = expiresAtStr;
2017
- saveAuthData(data);
2018
- }
2019
- }
2020
- if (expiresAtStr) {
2021
- const expiresAt = new Date(expiresAtStr);
2022
- const now = /* @__PURE__ */ new Date();
2023
- const oneMinute = 60 * 1e3;
2024
- if (expiresAt <= now) {
2025
- if (data.refreshToken) try {
2026
- return (await refreshAccessToken()).token;
2027
- } catch {
2028
- throw new TokenExpiredError();
2029
- }
2030
- throw new TokenExpiredError();
2031
- }
2032
- if (expiresAt.getTime() - now.getTime() < oneMinute) {
2033
- if (data.refreshToken) try {
2034
- return (await refreshAccessToken()).token;
2035
- } catch {
2036
- return data.token;
2037
- }
2038
- }
2039
- }
2040
- return data.token;
2041
- }
2042
- /**
2043
- * Gets the current authentication token, or null if not logged in
2044
- * Does not throw errors
2045
- */
2046
- async function getTokenOrNull() {
2047
- try {
2048
- return await getToken();
2049
- } catch {
2050
- return null;
2051
- }
2052
- }
2053
- async function isLoggedIn() {
2054
- return await getTokenOrNull() !== null;
2055
- }
2056
- async function getAuthStatus() {
2057
- const data = loadAuthData();
2058
- if (!data) return { isLoggedIn: false };
2059
- let expiresAtStr = data.expiresAt;
2060
- if (!expiresAtStr) {
2061
- expiresAtStr = extractJwtExpiration(data.token);
2062
- if (expiresAtStr) {
2063
- data.expiresAt = expiresAtStr;
2064
- saveAuthData(data);
2065
- }
2066
- }
2067
- if (expiresAtStr) {
2068
- const expiresAt = new Date(expiresAtStr);
2069
- const now = /* @__PURE__ */ new Date();
2070
- const oneMinute = 60 * 1e3;
2071
- if (expiresAt <= now) {
2072
- if (data.refreshToken) try {
2073
- const refreshed = await refreshAccessToken();
2074
- return {
2075
- isLoggedIn: true,
2076
- email: refreshed.email,
2077
- workosId: refreshed.workosId,
2078
- expiresAt: refreshed.expiresAt
2079
- };
2080
- } catch {
2081
- return { isLoggedIn: false };
2082
- }
2083
- return { isLoggedIn: false };
2084
- }
2085
- if (expiresAt.getTime() - now.getTime() < oneMinute) {
2086
- if (data.refreshToken) try {
2087
- const refreshed = await refreshAccessToken();
2088
- return {
2089
- isLoggedIn: true,
2090
- email: refreshed.email,
2091
- workosId: refreshed.workosId,
2092
- expiresAt: refreshed.expiresAt
2093
- };
2094
- } catch {
2095
- return {
2096
- isLoggedIn: true,
2097
- email: data.email,
2098
- workosId: data.workosId,
2099
- expiresAt: expiresAtStr
2100
- };
2101
- }
2102
- }
2103
- }
2104
- return {
2105
- isLoggedIn: true,
2106
- email: data.email,
2107
- workosId: data.workosId,
2108
- expiresAt: expiresAtStr
2109
- };
2110
- }
2111
- const WORKOS_API = "https://api.workos.com";
2112
- const PRODUCTION_WORKOS_CLIENT_ID = "client_01KFAD76TNGN02AP96982HG35E";
2113
- function getWorkosClientId() {
2114
- return process.env.WORKOS_CLIENT_ID ?? PRODUCTION_WORKOS_CLIENT_ID;
2115
- }
2116
- async function refreshAccessToken() {
2117
- const data = loadAuthData();
2118
- if (!data?.refreshToken) throw new AuthError("No refresh token available. Please log in again.");
2119
- try {
2120
- const response = await fetch(`${WORKOS_API}/user_management/authenticate`, {
2121
- method: "POST",
2122
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
2123
- body: new URLSearchParams({
2124
- grant_type: "refresh_token",
2125
- refresh_token: data.refreshToken,
2126
- client_id: getWorkosClientId()
2127
- })
2128
- });
2129
- if (!response.ok) throw new AuthError(`Token refresh failed: ${await response.text()}`);
2130
- const json = await response.json();
2131
- const tokenData = WorkOSTokenResponseSchema.parse(json);
2132
- const newAuthData = {
2133
- token: tokenData.access_token,
2134
- email: tokenData.user.email,
2135
- workosId: tokenData.user.id,
2136
- refreshToken: tokenData.refresh_token,
2137
- expiresAt: tokenData.expires_at ? (/* @__PURE__ */ new Date(tokenData.expires_at * 1e3)).toISOString() : extractJwtExpiration(tokenData.access_token)
2138
- };
2139
- saveAuthData(newAuthData);
2140
- return newAuthData;
2141
- } catch (error) {
2142
- if (error instanceof AuthError) throw error;
2143
- throw new AuthError(`Failed to refresh token: ${error instanceof Error ? error.message : "Unknown error"}`);
2144
- }
2145
- }
2146
-
2147
- //#endregion
2148
- //#region src/agents.ts
2149
- /**
2150
- * Agent Registry & Auto-Detection
2151
- *
2152
- * Centralized registry of supported AI coding agents with their
2153
- * skill directory locations and detection functions.
2154
- */
2155
- const agents = {
2156
- opencode: {
2157
- name: "opencode",
2158
- displayName: "OpenCode",
2159
- skillsDir: ".opencode/skills",
2160
- globalSkillsDir: "~/.config/opencode/skills",
2161
- detectInstalled: () => existsSync(expandTilde("~/.config/opencode"))
2162
- },
2163
- "claude-code": {
2164
- name: "claude-code",
2165
- displayName: "Claude Code",
2166
- skillsDir: ".claude/skills",
2167
- globalSkillsDir: "~/.claude/skills",
2168
- detectInstalled: () => existsSync(expandTilde("~/.claude"))
2169
- },
2170
- codex: {
2171
- name: "codex",
2172
- displayName: "Codex (OpenAI)",
2173
- skillsDir: ".codex/skills",
2174
- globalSkillsDir: "~/.codex/skills",
2175
- detectInstalled: () => existsSync(expandTilde("~/.codex"))
2176
- },
2177
- amp: {
2178
- name: "amp",
2179
- displayName: "Amp",
2180
- skillsDir: ".agents/skills",
2181
- globalSkillsDir: "~/.config/agents/skills",
2182
- detectInstalled: () => existsSync(expandTilde("~/.config/amp"))
2183
- },
2184
- antigravity: {
2185
- name: "antigravity",
2186
- displayName: "Antigravity",
2187
- skillsDir: ".agent/skills",
2188
- globalSkillsDir: "~/.gemini/antigravity/skills",
2189
- detectInstalled: () => existsSync(expandTilde("~/.gemini/antigravity"))
2190
- },
2191
- cursor: {
2192
- name: "cursor",
2193
- displayName: "Cursor",
2194
- skillsDir: ".cursor/skills",
2195
- globalSkillsDir: "~/.cursor/skills",
2196
- detectInstalled: () => existsSync(expandTilde("~/.cursor"))
2197
- }
2198
- };
2199
- /**
2200
- * Detect which agents are installed on the system.
2201
- * Checks for the existence of each agent's config directory.
2202
- *
2203
- * @returns Array of installed agent identifiers
2204
- */
2205
- function detectInstalledAgents() {
2206
- const installed = [];
2207
- for (const config of Object.values(agents)) if (config.detectInstalled()) installed.push(config.name);
2208
- return installed;
2209
- }
2210
- /**
2211
- * Get the configuration for a specific agent.
2212
- *
2213
- * @param type - Agent identifier
2214
- * @returns AgentConfig for the specified agent
2215
- */
2216
- function getAgentConfig(type) {
2217
- return agents[type];
2218
- }
2219
- /**
2220
- * Get all agent configurations as an array.
2221
- *
2222
- * @returns Array of all agent configurations
2223
- */
2224
- function getAllAgentConfigs() {
2225
- return Object.values(agents);
2226
- }
2227
-
2228
- //#endregion
2229
- //#region src/generate.ts
2230
- /**
2231
- * This module provides a streamlined approach to generating reference files
2232
- * by delegating all codebase exploration to the AI agent via OpenCode.
2233
- */
2234
- const PackageJsonKeywordsSchema = z.object({
2235
- name: z.string().optional(),
2236
- keywords: z.array(z.string()).optional()
2237
- });
2238
- function normalizeKeyword(value) {
2239
- const trimmed = value.trim();
2240
- if (!trimmed) return [];
2241
- const normalized = trimmed.toLowerCase();
2242
- const tokens = /* @__PURE__ */ new Set();
2243
- const addToken = (token) => {
2244
- const cleaned = token.trim().toLowerCase();
2245
- if (cleaned.length < 2) return;
2246
- tokens.add(cleaned);
2247
- };
2248
- addToken(normalized);
2249
- addToken(normalized.replaceAll("/", "-"));
2250
- addToken(normalized.replaceAll("/", ""));
2251
- for (const token of normalized.split(/[\s/_-]+/)) addToken(token);
2252
- if (normalized.startsWith("@")) addToken(normalized.slice(1));
2253
- return Array.from(tokens);
2254
- }
2255
- function deriveKeywords(fullName, localPath, referenceContent) {
2256
- const keywords = /* @__PURE__ */ new Set();
2257
- const addKeywords = (value) => {
2258
- for (const token of normalizeKeyword(value)) keywords.add(token);
2259
- };
2260
- addKeywords(fullName);
2261
- const headingMatch = referenceContent.match(/^#\s+(.+)$/m);
2262
- if (headingMatch?.[1]) addKeywords(headingMatch[1]);
2263
- const packageJsonPath = join(localPath, "package.json");
2264
- if (existsSync(packageJsonPath)) try {
2265
- const content = readFileSync(packageJsonPath, "utf-8");
2266
- const json = JSON.parse(content);
2267
- const parsed = PackageJsonKeywordsSchema.safeParse(json);
2268
- if (parsed.success) {
2269
- if (parsed.data.name) addKeywords(parsed.data.name);
2270
- if (parsed.data.keywords) for (const keyword of parsed.data.keywords) addKeywords(keyword);
2271
- }
2272
- } catch {}
2273
- return Array.from(keywords);
2274
- }
2275
- function createReferenceGenerationPrompt(referenceName) {
2276
- return `You are an expert at analyzing open source libraries and producing reference documentation for AI coding agents.
2277
-
2278
- ## PRIMARY GOAL
2279
-
2280
- Generate a reference markdown file that helps developers USE this library effectively. This is NOT a contribution guide - it's a usage reference for developers consuming this library in their own projects.
2281
-
2282
- ## CRITICAL RULES
2283
-
2284
- 1. **USER PERSPECTIVE ONLY**: Write for developers who will npm/pip/cargo install this library and use it in THEIR code.
2285
- - DO NOT include: how to contribute, internal test commands, repo-specific policies
2286
- - DO NOT include: "never mock in tests" or similar internal dev guidelines
2287
- - DO NOT include: commands like "npx hereby", "just ready", "bun test" that run the library's own tests
2288
- - DO include: how to install, import, configure, and use the public API
2289
-
2290
- 2. **NO FRONTMATTER**: Output pure markdown with NO YAML frontmatter. Start directly with the library name heading.
2291
-
2292
- 3. **QUICK REFERENCES**: Include a "Quick References" section with paths to key entry points in the repo:
2293
- - Paths must be relative from repo root (e.g., \`src/index.ts\`, \`docs/api.md\`)
2294
- - Include: main entry point, type definitions, README, key docs
2295
- - DO NOT include absolute paths or user-specific paths
2296
- - Keep to 3-5 most important files that help users understand the library
2297
-
2298
- 4. **PUBLIC API FOCUS**: Document what users import and call, not internal implementation details.
2299
- - Entry points: what to import from the package
2300
- - Configuration: how to set up/initialize
2301
- - Core methods/functions: the main API surface
2302
- - Types: key TypeScript interfaces users need
2303
-
2304
- 5. **MONOREPO AWARENESS**: Many libraries are monorepos with multiple packages:
2305
- - Check for \`packages/\`, \`apps/\`, \`crates/\`, or \`libs/\` directories
2306
- - Check root package.json for \`workspaces\` field
2307
- - If monorepo: document the package structure and key packages users would install
2308
- - Use full paths from repo root (e.g., \`packages/core/src/index.ts\`)
2309
- - Identify which packages are publishable vs internal
2310
-
2311
- ## EXPLORATION STEPS
2312
-
2313
- Use Read, Grep, Glob tools to explore:
2314
- 1. Root package.json / Cargo.toml - check for workspaces/monorepo config
2315
- 2. Check for \`packages/\`, \`apps/\`, \`crates/\` directories
2316
- 3. README.md - official usage documentation
2317
- 4. For monorepos: explore each publishable package's entry point
2318
- 5. docs/ or website/ - find documentation
2319
- 6. examples/ - real usage patterns
2320
- 7. TypeScript definitions (.d.ts) - public API surface
2321
-
2322
- ## OUTPUT FORMAT
2323
-
2324
- IMPORTANT: Reference name is "${referenceName}" (for internal tracking only - do NOT include in output).
2325
-
2326
- \`\`\`markdown
2327
- # {Library Name}
2328
-
2329
- {2-3 sentence overview of what this library does and its key value proposition}
2330
-
2331
- ## Quick References
2332
-
2333
- | File | Purpose |
2334
- |------|---------|
2335
- | \`packages/{pkg}/src/index.ts\` | Main entry point (monorepo example) |
2336
- | \`src/index.ts\` | Main entry point (single-package example) |
2337
- | \`README.md\` | Documentation |
2338
-
2339
- (For monorepos, include paths to key publishable packages)
2340
-
2341
- ## Packages (for monorepos only)
2342
-
2343
- | Package | npm name | Description |
2344
- |---------|----------|-------------|
2345
- | \`packages/core\` | \`@scope/core\` | Core functionality |
2346
- | \`packages/react\` | \`@scope/react\` | React bindings |
2347
-
2348
- (OMIT this section for single-package repos)
2349
-
2350
- ## When to Use
2351
-
2352
- - {Practical scenario where a developer would reach for this library}
2353
- - {Another real-world use case}
2354
- - {Problem this library solves}
2355
-
2356
- ## Installation
2357
-
2358
- \`\`\`bash
2359
- # Single package
2360
- npm install {package-name}
2361
-
2362
- # Monorepo (show key packages)
2363
- npm install @scope/core @scope/react
2364
- \`\`\`
2365
-
2366
- ## Best Practices
2367
-
2368
- 1. {Actionable best practice for USERS of this library}
2369
- 2. {Common mistake to avoid when using this library}
2370
- 3. {Performance or correctness tip}
2371
-
2372
- ## Common Patterns
2373
-
2374
- **{Pattern Name}:**
2375
- \`\`\`{language}
2376
- {Minimal working code example}
2377
- \`\`\`
2378
-
2379
- **{Another Pattern}:**
2380
- \`\`\`{language}
2381
- {Another code example}
2382
- \`\`\`
2383
-
2384
- ## API Quick Reference
2385
-
2386
- | Export | Type | Description |
2387
- |--------|------|-------------|
2388
- | \`{main export}\` | {type} | {what it does} |
2389
- | \`{another export}\` | {type} | {what it does} |
2390
-
2391
- {Add more sections as appropriate for the library: Configuration, Types, CLI Commands (if user-facing), etc.}
2392
- \`\`\`
2393
-
2394
- ## QUALITY CHECKLIST
2395
-
2396
- Before outputting, verify:
2397
- - [ ] NO YAML frontmatter - start directly with # heading
2398
- - [ ] Every code example is something a USER would write, not a contributor
2399
- - [ ] No internal test commands or contribution workflows
2400
- - [ ] Quick References paths are relative from repo root (no absolute/user paths)
2401
- - [ ] Best practices are for using the library, not developing it
2402
- - [ ] If monorepo: Packages section lists publishable packages with npm names
2403
- - [ ] If monorepo: paths include package directory (e.g., \`packages/core/src/index.ts\`)
2404
-
2405
- Now explore the codebase and generate the reference content.
2406
-
2407
- ## OUTPUT INSTRUCTIONS
2408
-
2409
- After exploring, output your complete reference wrapped in XML tags like this:
2410
-
2411
- \`\`\`
2412
- <reference_output>
2413
- (your complete markdown reference here)
2414
- </reference_output>
2415
- \`\`\`
2416
-
2417
- REQUIREMENTS:
2418
- - Start with a level-1 heading with the actual library name (e.g., "# TanStack Query")
2419
- - Include sections: Quick References (table), When to Use (bullets), Installation, Best Practices, Common Patterns (with code), API Quick Reference (table)
2420
- - Minimum 2000 characters of actual content - short or placeholder content will be rejected
2421
- - Fill in real information from your exploration - do not use placeholder text like "{Library Name}"
2422
- - No YAML frontmatter - start directly with the markdown heading
2423
- - Output ONLY the reference inside the tags, no other text
2424
-
2425
- Begin exploring now.`;
2426
- }
2427
- /**
2428
- * Extract the actual reference markdown content from AI response.
2429
- * The response may include echoed prompt/system context before the actual reference.
2430
- * Handles multiple edge cases:
2431
- * - Model echoes the prompt template (skip template content)
2432
- * - Model forgets to close the tag (extract to end of response)
2433
- * - Multiple tag pairs (find the one with real content)
2434
- */
2435
- function extractReferenceContent(rawResponse, onDebug) {
2436
- const openTag = "<reference_output>";
2437
- const closeTag = "</reference_output>";
2438
- onDebug?.(`[extract] Raw response length: ${rawResponse.length} chars`);
2439
- const openIndices = [];
2440
- const closeIndices = [];
2441
- let pos = 0;
2442
- while ((pos = rawResponse.indexOf(openTag, pos)) !== -1) {
2443
- openIndices.push(pos);
2444
- pos += 18;
2445
- }
2446
- pos = 0;
2447
- while ((pos = rawResponse.indexOf(closeTag, pos)) !== -1) {
2448
- closeIndices.push(pos);
2449
- pos += 19;
2450
- }
2451
- onDebug?.(`[extract] Found ${openIndices.length} open tag(s), ${closeIndices.length} close tag(s)`);
2452
- const cleanContent = (raw) => {
2453
- let content = raw.trim();
2454
- if (content.startsWith("```")) {
2455
- content = content.replace(/^```(?:markdown)?\s*\n?/, "");
2456
- content = content.replace(/\n?```\s*$/, "");
2457
- }
2458
- return content.trim();
2459
- };
2460
- const isTemplateContent = (content) => {
2461
- return content.includes("{Library Name}") || content.includes("(your complete markdown reference here)");
2462
- };
2463
- for (let i = openIndices.length - 1; i >= 0; i--) {
2464
- const openIdx = openIndices[i];
2465
- if (openIdx === void 0) continue;
2466
- const closeIdx = closeIndices.find((c) => c > openIdx);
2467
- if (closeIdx !== void 0) {
2468
- const content = cleanContent(rawResponse.slice(openIdx + 18, closeIdx));
2469
- onDebug?.(`[extract] Pair ${i}: open=${openIdx}, close=${closeIdx}, len=${content.length}`);
2470
- onDebug?.(`[extract] Preview: "${content.slice(0, 200)}${content.length > 200 ? "..." : ""}"`);
2471
- if (isTemplateContent(content)) {
2472
- onDebug?.(`[extract] Skipping pair ${i} - template placeholder content`);
2473
- continue;
2474
- }
2475
- if (content.length >= 500) {
2476
- onDebug?.(`[extract] Using pair ${i} - valid content`);
2477
- validateReferenceContent(content);
2478
- return content;
2479
- }
2480
- onDebug?.(`[extract] Pair ${i} too short (${content.length} chars)`);
2481
- }
2482
- }
2483
- const lastOpenIdx = openIndices[openIndices.length - 1];
2484
- if (lastOpenIdx !== void 0) {
2485
- if (!closeIndices.some((c) => c > lastOpenIdx)) {
2486
- onDebug?.(`[extract] Last open tag at ${lastOpenIdx} is unclosed - extracting to end`);
2487
- const content = cleanContent(rawResponse.slice(lastOpenIdx + 18));
2488
- onDebug?.(`[extract] Unclosed content: ${content.length} chars`);
2489
- onDebug?.(`[extract] Preview: "${content.slice(0, 200)}${content.length > 200 ? "..." : ""}"`);
2490
- if (!isTemplateContent(content) && content.length >= 500) {
2491
- onDebug?.(`[extract] Using unclosed content - valid`);
2492
- validateReferenceContent(content);
2493
- return content;
2494
- }
2495
- }
2496
- }
2497
- onDebug?.(`[extract] No valid content found`);
2498
- onDebug?.(`[extract] Response tail: "${rawResponse.slice(-300)}"`);
2499
- throw new Error("Failed to extract reference content: no valid <reference_output> tags found. The AI may have failed to follow the output format or produced placeholder content.");
2500
- }
2501
- /**
2502
- * Validate extracted reference content has minimum required structure.
2503
- * Throws if content is invalid.
2504
- */
2505
- function validateReferenceContent(content) {
2506
- const foundPlaceholders = [
2507
- "{Library Name}",
2508
- "{Full overview paragraph}",
2509
- "{Table with 3-5 key files}",
2510
- "{3+ bullet points}",
2511
- "{Install commands}",
2512
- "{3+ numbered items}",
2513
- "{2+ code examples",
2514
- "{Table of key exports}",
2515
- "{Additional sections"
2516
- ].filter((p) => content.includes(p));
2517
- if (foundPlaceholders.length > 0) throw new Error(`Invalid reference content: contains template placeholders (${foundPlaceholders.slice(0, 3).join(", ")}). The AI echoed the format instead of generating actual content.`);
2518
- if (content.length < 500) throw new Error(`Invalid reference content: too short (${content.length} chars, minimum 500). The AI may have produced placeholder or incomplete content.`);
2519
- if (!content.startsWith("#")) throw new Error("Invalid reference content: must start with markdown heading. Content must begin with '# Library Name' (no YAML frontmatter).");
2520
- }
2521
- /**
2522
- * Generate a reference markdown file for a repository using AI.
2523
- *
2524
- * Opens an OpenCode session and instructs the AI agent to explore the codebase
2525
- * using Read, Grep, and Glob tools, then produce a comprehensive reference.
2526
- *
2527
- * @param repoPath - Path to the repository to analyze
2528
- * @param repoName - Qualified name of the repo (e.g., "tanstack/query" or "my-local-repo")
2529
- * @param options - Generation options (provider, model, callbacks)
2530
- * @returns The generated reference content and commit SHA
2531
- */
2532
- async function generateReferenceWithAI(repoPath, repoName, options = {}) {
2533
- const { provider, model, onDebug, onStream } = options;
2534
- const [configProvider, configModel] = loadConfig().defaultModel?.split("/") ?? [];
2535
- const aiProvider = provider ?? configProvider;
2536
- const aiModel = model ?? configModel;
2537
- onDebug?.(`Starting AI reference generation for ${repoName}`);
2538
- onDebug?.(`Repo path: ${repoPath}`);
2539
- onDebug?.(`Provider: ${aiProvider ?? "default"}, Model: ${aiModel ?? "default"}`);
2540
- const commitSha = getCommitSha(repoPath);
2541
- onDebug?.(`Commit SHA: ${commitSha}`);
2542
- const referenceName = toReferenceName(repoName);
2543
- onDebug?.(`Reference name: ${referenceName}`);
2544
- const result = await streamPrompt({
2545
- prompt: createReferenceGenerationPrompt(referenceName),
2546
- cwd: repoPath,
2547
- provider: aiProvider,
2548
- model: aiModel,
2549
- onDebug,
2550
- onStream
2551
- });
2552
- onDebug?.(`Generation complete (${result.durationMs}ms, ${result.text.length} chars)`);
2553
- const referenceContent = extractReferenceContent(result.text, onDebug);
2554
- onDebug?.(`Extracted reference content (${referenceContent.length} chars)`);
2555
- return {
2556
- referenceContent,
2557
- commitSha
2558
- };
2559
- }
2560
- /**
2561
- * Ensure a symlink exists, removing any existing file/directory at the path
2562
- */
2563
- function ensureSymlink(target, linkPath) {
2564
- try {
2565
- const stat = lstatSync(linkPath);
2566
- if (stat.isSymbolicLink()) unlinkSync(linkPath);
2567
- else if (stat.isDirectory()) rmSync(linkPath, { recursive: true });
2568
- else unlinkSync(linkPath);
2569
- } catch {}
2570
- mkdirSync(join(linkPath, ".."), { recursive: true });
2571
- symlinkSync(target, linkPath, "dir");
2572
- }
2573
- /**
2574
- * Static template for the global SKILL.md file.
2575
- * This is the single routing skill that all agents see.
2576
- */
2577
- const GLOBAL_SKILL_TEMPLATE = `---
2578
- name: offworld
2579
- description: Routes queries to Offworld reference files. Find and read per-repo references for dependency knowledge.
2580
- allowed-tools: Bash(ow:*) Read
2581
- ---
2582
-
2583
- # Offworld Reference Router
2584
-
2585
- Use \`ow\` to locate and read Offworld reference files for dependencies.
2586
-
2587
- ## What This Does
2588
-
2589
- - Finds references for libraries and repos
2590
- - Returns paths for reference files and local clones
2591
- - Helps you read the right context fast
2592
-
2593
- ## When to Use
2594
-
2595
- - You need docs or patterns for a dependency
2596
- - You want the verified reference instead of web search
2597
- - You are about to work inside a repo clone
2598
-
2599
- ## Prerequisites
2600
-
2601
- Check that the CLI is available:
2602
-
2603
- \`\`\`bash
2604
- ow --version
2605
- \`\`\`
2606
-
2607
- If \`ow\` is not available, install it:
2608
-
2609
- \`\`\`bash
2610
- curl -fsSL https://offworld.sh/install | bash
2611
- \`\`\`
2612
-
2613
- ## Setup
2614
-
2615
- Initialize Offworld once per machine:
2616
-
2617
- \`\`\`bash
2618
- ow init
2619
- \`\`\`
2620
-
2621
- For a specific project, build a project map:
2622
-
2623
- \`\`\`bash
2624
- ow project init
2625
- \`\`\`
2626
-
2627
- ## Usage
2628
-
2629
- **Find a reference:**
2630
- \`\`\`bash
2631
- ow map search <term> # search by name or keyword
2632
- ow map show <repo> # get info for specific repo
2633
- \`\`\`
2634
-
2635
- **Get paths for tools:**
2636
- \`\`\`bash
2637
- ow map show <repo> --ref # reference file path (use with Read)
2638
- ow map show <repo> --path # clone directory path
2639
- \`\`\`
2640
-
2641
- **Example workflow:**
2642
- \`\`\`bash
2643
- # 1. Find the repo
2644
- ow map search zod
2645
-
2646
- # 2. Get reference path
2647
- ow map show colinhacks/zod --ref
2648
- # Output: /Users/.../.local/share/offworld/skill/offworld/references/colinhacks-zod.md
2649
-
2650
- # 3. Read the reference with the path from step 2
2651
- \`\`\`
2652
-
2653
- ## If Reference Not Found
2654
-
2655
- \`\`\`bash
2656
- ow pull <owner/repo> # clone + generate reference
2657
- ow project init # scan project deps, install references
2658
- \`\`\`
2659
-
2660
- ## Notes
2661
-
2662
- - Project map (\`.offworld/map.json\`) takes precedence over global map when present
2663
- - Reference files are markdown with API docs, patterns, best practices
2664
- - Clone paths useful for exploring source code after reading reference
2665
-
2666
- ## Additional Resources
2667
-
2668
- - Docs: https://offworld.sh/cli
2669
- `;
2670
- /**
2671
- * Ensures the global SKILL.md exists and symlinks the offworld/ directory to all agent skill directories.
2672
- *
2673
- * Creates:
2674
- * - ~/.local/share/offworld/skill/offworld/SKILL.md (static routing template)
2675
- * - ~/.local/share/offworld/skill/offworld/assets/ (for map.json)
2676
- * - ~/.local/share/offworld/skill/offworld/references/ (for reference files)
2677
- * - Symlinks entire offworld/ directory to each agent's skill directory
2678
- */
2679
- function installGlobalSkill() {
2680
- const config = loadConfig();
2681
- mkdirSync(Paths.offworldSkillDir, { recursive: true });
2682
- mkdirSync(Paths.offworldAssetsDir, { recursive: true });
2683
- mkdirSync(Paths.offworldReferencesDir, { recursive: true });
2684
- const skillPath = join(Paths.offworldSkillDir, "SKILL.md");
2685
- if (!existsSync(skillPath)) writeFileSync(skillPath, GLOBAL_SKILL_TEMPLATE, "utf-8");
2686
- const configuredAgents = config.agents ?? [];
2687
- for (const agentName of configuredAgents) {
2688
- const agentConfig = agents[agentName];
2689
- if (agentConfig) {
2690
- const agentSkillDir = expandTilde(join(agentConfig.globalSkillsDir, "offworld"));
2691
- ensureSymlink(Paths.offworldSkillDir, agentSkillDir);
2692
- }
2693
- }
2694
- }
2695
- /**
2696
- * Install a reference file for a specific repository.
2697
- *
2698
- * Creates:
2699
- * - ~/.local/share/offworld/skill/offworld/references/{owner-repo}.md
2700
- * - ~/.local/share/offworld/meta/{owner-repo}/meta.json
2701
- * - Updates global map with reference info
2702
- *
2703
- * @param qualifiedName - Qualified key for map storage (e.g., "github.com:owner/repo" or "local:name")
2704
- * @param fullName - Full repo name for file naming (e.g., "owner/repo")
2705
- * @param localPath - Absolute path to the cloned repository
2706
- * @param referenceContent - The generated reference markdown content
2707
- * @param meta - Metadata about the generation (referenceUpdatedAt, commitSha, version)
2708
- * @param keywords - Optional array of keywords for search/routing
2709
- */
2710
- function installReference(qualifiedName, fullName, localPath, referenceContent, meta, keywords) {
2711
- installGlobalSkill();
2712
- const referenceFileName = toReferenceFileName(fullName);
2713
- const metaDirName = toMetaDirName(fullName);
2714
- const referencePath = join(Paths.offworldReferencesDir, referenceFileName);
2715
- mkdirSync(Paths.offworldReferencesDir, { recursive: true });
2716
- writeFileSync(referencePath, referenceContent, "utf-8");
2717
- const metaDir = join(Paths.metaDir, metaDirName);
2718
- mkdirSync(metaDir, { recursive: true });
2719
- const metaJson = JSON.stringify(meta, null, 2);
2720
- writeFileSync(join(metaDir, "meta.json"), metaJson, "utf-8");
2721
- const map = readGlobalMap();
2722
- const existingEntry = map.repos[qualifiedName];
2723
- const legacyProviderMap = {
2724
- "github.com": "github",
2725
- "gitlab.com": "gitlab",
2726
- "bitbucket.org": "bitbucket"
2727
- };
2728
- const [host] = qualifiedName.split(":");
2729
- const legacyProvider = host ? legacyProviderMap[host] : void 0;
2730
- const legacyQualifiedName = legacyProvider ? `${legacyProvider}:${fullName}` : void 0;
2731
- const legacyEntry = legacyQualifiedName ? map.repos[legacyQualifiedName] : void 0;
2732
- const references = [...existingEntry?.references ?? [], ...legacyEntry?.references ?? []];
2733
- if (!references.includes(referenceFileName)) references.push(referenceFileName);
2734
- const derivedKeywords = keywords ?? deriveKeywords(fullName, localPath, referenceContent);
2735
- const keywordsSet = new Set([
2736
- ...existingEntry?.keywords ?? [],
2737
- ...legacyEntry?.keywords ?? [],
2738
- ...derivedKeywords
2739
- ]);
2740
- map.repos[qualifiedName] = {
2741
- localPath,
2742
- references,
2743
- primary: referenceFileName,
2744
- keywords: Array.from(keywordsSet),
2745
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2746
- };
2747
- if (legacyQualifiedName && legacyQualifiedName in map.repos) delete map.repos[legacyQualifiedName];
2748
- writeGlobalMap(map);
2749
- }
2750
-
2751
- //#endregion
2752
- //#region src/manifest.ts
2753
- /**
2754
- * Dependency manifest parsing for multiple package ecosystems
2755
- */
2756
- const DEFAULT_IGNORED_DIRS = new Set([
2757
- ".git",
2758
- ".offworld",
2759
- ".turbo",
2760
- "build",
2761
- "dist",
2762
- "node_modules",
2763
- "out"
2764
- ]);
2765
- /**
2766
- * Detects the manifest type in a directory
2767
- */
2768
- function detectManifestType(dir) {
2769
- if (existsSync(join(dir, "package.json"))) return "npm";
2770
- if (existsSync(join(dir, "pyproject.toml"))) return "python";
2771
- if (existsSync(join(dir, "Cargo.toml"))) return "rust";
2772
- if (existsSync(join(dir, "go.mod"))) return "go";
2773
- if (existsSync(join(dir, "requirements.txt"))) return "python";
2774
- return "unknown";
2775
- }
2776
- /**
2777
- * Parses dependencies from manifest files
2778
- */
2779
- function parseDependencies(dir) {
2780
- switch (detectManifestType(dir)) {
2781
- case "npm": return parseNpmDependencies(dir);
2782
- case "python": return existsSync(join(dir, "pyproject.toml")) ? parsePyprojectToml(join(dir, "pyproject.toml")) : parseRequirementsTxt(join(dir, "requirements.txt"));
2783
- case "rust": return parseCargoToml(join(dir, "Cargo.toml"));
2784
- case "go": return parseGoMod(join(dir, "go.mod"));
2785
- default: return [];
2786
- }
2787
- }
2788
- function parseNpmDependencies(dir) {
2789
- return mergeDependencies(parsePackageJson(join(dir, "package.json")), parseWorkspaceDependencies(dir)).sort((a, b) => a.name.localeCompare(b.name));
2790
- }
2791
- function parseWorkspaceDependencies(dir) {
2792
- const workspacePatterns = getWorkspacePatterns(dir);
2793
- if (workspacePatterns.length === 0) return [];
2794
- const packageJsonPaths = resolveWorkspacePackageJsonPaths(dir, workspacePatterns);
2795
- const deps = [];
2796
- for (const path of packageJsonPaths) deps.push(...parsePackageJson(path));
2797
- return mergeDependencies([], deps);
2798
- }
2799
- function getWorkspacePatterns(dir) {
2800
- const patterns = /* @__PURE__ */ new Set();
2801
- const packageJsonPath = join(dir, "package.json");
2802
- if (existsSync(packageJsonPath)) {
2803
- const workspaces = readJson(packageJsonPath)?.workspaces;
2804
- if (Array.isArray(workspaces)) {
2805
- for (const pattern of workspaces) if (typeof pattern === "string") patterns.add(pattern);
2806
- } else if (workspaces && typeof workspaces === "object") {
2807
- const packagesField = workspaces.packages;
2808
- if (Array.isArray(packagesField)) {
2809
- for (const pattern of packagesField) if (typeof pattern === "string") patterns.add(pattern);
2810
- }
2811
- }
2812
- }
2813
- const pnpmWorkspacePath = existsSync(join(dir, "pnpm-workspace.yaml")) ? join(dir, "pnpm-workspace.yaml") : existsSync(join(dir, "pnpm-workspace.yml")) ? join(dir, "pnpm-workspace.yml") : null;
2814
- if (pnpmWorkspacePath) for (const pattern of parsePnpmWorkspacePackages(pnpmWorkspacePath)) patterns.add(pattern);
2815
- return Array.from(patterns);
2816
- }
2817
- function resolveWorkspacePackageJsonPaths(dir, patterns) {
2818
- const includePatterns = patterns.filter((pattern) => !pattern.startsWith("!"));
2819
- const excludePatterns = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
2820
- if (includePatterns.length === 0) return [];
2821
- const includeRegexes = includePatterns.map(patternToRegex);
2822
- const excludeRegexes = excludePatterns.map(patternToRegex);
2823
- const matches = [];
2824
- const directories = walkDirectories(dir);
2825
- for (const relativePath of directories) {
2826
- if (!includeRegexes.some((regex) => regex.test(relativePath))) continue;
2827
- if (excludeRegexes.some((regex) => regex.test(relativePath))) continue;
2828
- const packageJsonPath = join(dir, relativePath, "package.json");
2829
- if (existsSync(packageJsonPath)) matches.push(packageJsonPath);
2830
- }
2831
- return Array.from(new Set(matches));
2832
- }
2833
- function walkDirectories(root) {
2834
- const results = [];
2835
- const stack = [""];
2836
- while (stack.length > 0) {
2837
- const relativePath = stack.pop();
2838
- const currentPath = relativePath ? join(root, relativePath) : root;
2839
- let entries;
2840
- try {
2841
- entries = readdirSync(currentPath, { withFileTypes: true });
2842
- } catch {
2843
- continue;
2844
- }
2845
- for (const entry of entries) {
2846
- if (!entry.isDirectory()) continue;
2847
- if (DEFAULT_IGNORED_DIRS.has(entry.name)) continue;
2848
- const nextRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
2849
- results.push(nextRelative);
2850
- stack.push(nextRelative);
2851
- }
2852
- }
2853
- return results;
2854
- }
2855
- function patternToRegex(pattern) {
2856
- let normalized = pattern.trim().replace(/\\/g, "/");
2857
- if (normalized.startsWith("./")) normalized = normalized.slice(2);
2858
- if (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
2859
- const withGlob = normalized.replace(/[.+^${}()|[\]\\*]/g, "\\$&").replace(/\\\*\\\*/g, ".*").replace(/\\\*/g, "[^/]+");
2860
- return new RegExp(`^${withGlob}$`);
2861
- }
2862
- function parsePnpmWorkspacePackages(path) {
2863
- try {
2864
- const lines = readFileSync(path, "utf-8").split("\n");
2865
- const patterns = [];
2866
- let inPackages = false;
2867
- for (const line of lines) {
2868
- const trimmed = line.trim();
2869
- if (!trimmed || trimmed.startsWith("#")) continue;
2870
- if (/^packages\s*:/.test(trimmed)) {
2871
- inPackages = true;
2872
- continue;
2873
- }
2874
- if (!inPackages) continue;
2875
- const entryMatch = trimmed.match(/^-\s*(.+)$/);
2876
- if (entryMatch?.[1]) {
2877
- const value = entryMatch[1].trim().replace(/^['"]|['"]$/g, "");
2878
- if (value) patterns.push(value);
2879
- continue;
2880
- }
2881
- if (!line.startsWith(" ") && !line.startsWith(" ")) break;
2882
- }
2883
- return patterns;
2884
- } catch {
2885
- return [];
2886
- }
2887
- }
2888
- function readJson(path) {
2889
- try {
2890
- const content = readFileSync(path, "utf-8");
2891
- return JSON.parse(content);
2892
- } catch {
2893
- return null;
2894
- }
2895
- }
2896
- function mergeDependencies(base, incoming) {
2897
- const map = /* @__PURE__ */ new Map();
2898
- for (const dep of [...base, ...incoming]) {
2899
- const existing = map.get(dep.name);
2900
- if (!existing) {
2901
- map.set(dep.name, { ...dep });
2902
- continue;
2903
- }
2904
- const dev = existing.dev && dep.dev;
2905
- const version = existing.version ?? dep.version;
2906
- map.set(dep.name, {
2907
- name: dep.name,
2908
- version,
2909
- dev
2910
- });
2911
- }
2912
- return Array.from(map.values());
2913
- }
2914
- /**
2915
- * Parse package.json dependencies
2916
- */
2917
- function parsePackageJson(path) {
2918
- try {
2919
- const content = readFileSync(path, "utf-8");
2920
- const pkg = JSON.parse(content);
2921
- const deps = [];
2922
- if (pkg.dependencies && typeof pkg.dependencies === "object") for (const [name, version] of Object.entries(pkg.dependencies)) deps.push({
2923
- name,
2924
- version,
2925
- dev: false
2926
- });
2927
- if (pkg.devDependencies && typeof pkg.devDependencies === "object") for (const [name, version] of Object.entries(pkg.devDependencies)) deps.push({
2928
- name,
2929
- version,
2930
- dev: true
2931
- });
2932
- if (pkg.peerDependencies && typeof pkg.peerDependencies === "object") for (const [name, version] of Object.entries(pkg.peerDependencies)) deps.push({
2933
- name,
2934
- version,
2935
- dev: false
2936
- });
2937
- if (pkg.optionalDependencies && typeof pkg.optionalDependencies === "object") for (const [name, version] of Object.entries(pkg.optionalDependencies)) deps.push({
2938
- name,
2939
- version,
2940
- dev: false
2941
- });
2942
- return deps;
2943
- } catch {
2944
- return [];
2945
- }
2946
- }
2947
- /**
2948
- * Parse pyproject.toml dependencies
2949
- */
2950
- function parsePyprojectToml(path) {
2951
- try {
2952
- const content = readFileSync(path, "utf-8");
2953
- const deps = [];
2954
- const depsSection = content.match(/\[project\.dependencies\]([\s\S]*?)(?=\[|$)/);
2955
- if (!depsSection?.[1]) return [];
2956
- const lines = depsSection[1].split("\n");
2957
- for (const line of lines) {
2958
- const match = line.match(/["']([a-zA-Z0-9_-]+)(?:[>=<~!]+([^"']+))?["']/);
2959
- if (match?.[1]) deps.push({
2960
- name: match[1],
2961
- version: match[2]?.trim(),
2962
- dev: false
2963
- });
2964
- }
2965
- return deps;
2966
- } catch {
2967
- return [];
2968
- }
2969
- }
2970
- /**
2971
- * Parse Cargo.toml dependencies
2972
- */
2973
- function parseCargoToml(path) {
2974
- try {
2975
- const content = readFileSync(path, "utf-8");
2976
- const deps = [];
2977
- const depsSection = content.match(/\[dependencies\]([\s\S]*?)(?=\[|$)/);
2978
- if (!depsSection?.[1]) return [];
2979
- const lines = depsSection[1].split("\n");
2980
- for (const line of lines) {
2981
- const simpleMatch = line.match(/^([a-zA-Z0-9_-]+)\s*=\s*"([^"]+)"/);
2982
- const tableMatch = line.match(/^([a-zA-Z0-9_-]+)\s*=\s*{.*version\s*=\s*"([^"]+)"/);
2983
- if (simpleMatch?.[1] && simpleMatch[2]) deps.push({
2984
- name: simpleMatch[1],
2985
- version: simpleMatch[2],
2986
- dev: false
2987
- });
2988
- else if (tableMatch?.[1] && tableMatch[2]) deps.push({
2989
- name: tableMatch[1],
2990
- version: tableMatch[2],
2991
- dev: false
2992
- });
2993
- }
2994
- return deps;
2995
- } catch {
2996
- return [];
2997
- }
2998
- }
2999
- /**
3000
- * Parse go.mod dependencies
3001
- */
3002
- function parseGoMod(path) {
3003
- try {
3004
- const content = readFileSync(path, "utf-8");
3005
- const deps = [];
3006
- const requireSection = content.match(/require\s*\(([\s\S]*?)\)/);
3007
- if (!requireSection?.[1]) {
3008
- const singleRequire = content.match(/require\s+([^\s]+)\s+([^\s]+)/);
3009
- if (singleRequire?.[1] && singleRequire[2]) deps.push({
3010
- name: singleRequire[1],
3011
- version: singleRequire[2],
3012
- dev: false
3013
- });
3014
- return deps;
3015
- }
3016
- const lines = requireSection[1].split("\n");
3017
- for (const line of lines) {
3018
- const match = line.match(/^\s*([^\s]+)\s+([^\s]+)/);
3019
- if (match?.[1] && match[2]) deps.push({
3020
- name: match[1],
3021
- version: match[2],
3022
- dev: false
3023
- });
3024
- }
3025
- return deps;
3026
- } catch {
3027
- return [];
3028
- }
3029
- }
3030
- /**
3031
- * Parse requirements.txt dependencies
3032
- */
3033
- function parseRequirementsTxt(path) {
3034
- try {
3035
- const content = readFileSync(path, "utf-8");
3036
- const deps = [];
3037
- const lines = content.split("\n");
3038
- for (const line of lines) {
3039
- const trimmed = line.trim();
3040
- if (!trimmed || trimmed.startsWith("#")) continue;
3041
- const match = trimmed.match(/^([a-zA-Z0-9_-]+)(?:[>=<~!]+(.+))?/);
3042
- if (match?.[1]) deps.push({
3043
- name: match[1],
3044
- version: match[2]?.trim(),
3045
- dev: false
3046
- });
3047
- }
3048
- return deps;
3049
- } catch {
3050
- return [];
3051
- }
3052
- }
3053
-
3054
- //#endregion
3055
- //#region src/dep-mappings.ts
3056
- /**
3057
- * Dependency name to GitHub repo resolution:
3058
- * 1. Query npm registry for repository.url
3059
- * 2. Fall back to FALLBACK_MAPPINGS for packages missing repository field
3060
- * 3. Return unknown (caller handles)
3061
- */
3062
- /**
3063
- * Fallback mappings for packages where npm registry doesn't have repository.url.
3064
- * Only add packages here that genuinely don't have the field set.
3065
- */
3066
- const FALLBACK_MAPPINGS = {
3067
- "@convex-dev/react-query": "get-convex/convex-react-query",
3068
- "@opencode-ai/sdk": "anomalyco/opencode-sdk-js"
3069
- };
3070
- /**
3071
- * Parse GitHub repo from various git URL formats.
3072
- * Handles:
3073
- * - git+https://github.com/owner/repo.git
3074
- * - https://github.com/owner/repo
3075
- * - git://github.com/owner/repo.git
3076
- * - github:owner/repo
3077
- */
3078
- function parseGitHubUrl(url) {
3079
- for (const pattern of [/github\.com[/:]([\w-]+)\/([\w.-]+?)(?:\.git)?$/, /^github:([\w-]+)\/([\w.-]+)$/]) {
3080
- const match = url.match(pattern);
3081
- if (match) return `${match[1]}/${match[2]}`;
3082
- }
3083
- return null;
3084
- }
3085
- /**
3086
- * Fallback to npm registry to extract repository.url.
3087
- * Returns null if package not found, no repo field, or not a GitHub repo.
3088
- */
3089
- async function resolveFromNpm(packageName) {
3090
- try {
3091
- const res = await fetch(`https://registry.npmjs.org/${packageName}`);
3092
- if (!res.ok) return null;
3093
- const json = await res.json();
3094
- const result = NpmPackageResponseSchema.safeParse(json);
3095
- if (!result.success) return null;
3096
- const repoUrl = result.data.repository?.url;
3097
- if (!repoUrl) return null;
3098
- return parseGitHubUrl(repoUrl);
3099
- } catch {
3100
- return null;
3101
- }
3102
- }
3103
- /**
3104
- * Resolution order:
3105
- * 1. Query npm registry for repository.url
3106
- * 2. Check FALLBACK_MAPPINGS for packages missing repository field
3107
- * 3. Return unknown
3108
- */
3109
- async function resolveDependencyRepo(dep) {
3110
- const npmRepo = await resolveFromNpm(dep);
3111
- if (npmRepo) return {
3112
- dep,
3113
- repo: npmRepo,
3114
- source: "npm"
3115
- };
3116
- if (dep in FALLBACK_MAPPINGS) return {
3117
- dep,
3118
- repo: FALLBACK_MAPPINGS[dep] ?? null,
3119
- source: "fallback"
3120
- };
3121
- return {
3122
- dep,
3123
- repo: null,
3124
- source: "unknown"
3125
- };
3126
- }
3127
-
3128
- //#endregion
3129
- //#region src/reference-matcher.ts
3130
- /**
3131
- * Reference matching utilities for dependency resolution
3132
- *
3133
- * Maps dependencies to their reference status (installed, remote, generate, unknown)
3134
- */
3135
- /**
3136
- * Check if a reference is installed locally.
3137
- * A reference is considered installed if {owner-repo}.md exists in offworld/references/.
3138
- *
3139
- * @param repo - Repo name in owner/repo format
3140
- * @returns true if reference is installed locally
3141
- */
3142
- function isReferenceInstalled(repo) {
3143
- const referenceFileName = toReferenceFileName(repo);
3144
- return existsSync(join(Paths.offworldReferencesDir, referenceFileName));
3145
- }
3146
- /**
3147
- * Match dependencies to their reference availability status.
3148
- *
3149
- * Status logic:
3150
- * - installed: {owner-repo}.md exists in offworld/references/
3151
- * - remote: Reference exists on offworld.sh (quick pull)
3152
- * - generate: Has valid GitHub repo but needs AI generation (slow, uses tokens)
3153
- * - unknown: No GitHub repo found
3154
- *
3155
- * @param resolvedDeps - Array of resolved dependencies with repo info
3156
- * @returns Array of reference matches with status
3157
- */
3158
- function matchDependenciesToReferences(resolvedDeps) {
3159
- return resolvedDeps.map((dep) => {
3160
- if (!dep.repo) return {
3161
- dep: dep.dep,
3162
- repo: null,
3163
- status: "unknown",
3164
- source: dep.source
3165
- };
3166
- if (isReferenceInstalled(dep.repo)) return {
3167
- dep: dep.dep,
3168
- repo: dep.repo,
3169
- status: "installed",
3170
- source: dep.source
3171
- };
3172
- return {
3173
- dep: dep.dep,
3174
- repo: dep.repo,
3175
- status: "generate",
3176
- source: dep.source
3177
- };
3178
- });
3179
- }
3180
- /**
3181
- * Match dependencies to their reference availability status with remote check.
3182
- * This is async because it checks the remote server for each dependency.
3183
- *
3184
- * Status logic:
3185
- * - installed: {owner-repo}.md exists in offworld/references/
3186
- * - remote: Reference exists on offworld.sh (quick pull)
3187
- * - generate: Has valid GitHub repo but needs AI generation (slow, uses tokens)
3188
- * - unknown: No GitHub repo found
3189
- *
3190
- * @param resolvedDeps - Array of resolved dependencies with repo info
3191
- * @returns Promise of array of reference matches with status
3192
- */
3193
- async function matchDependenciesToReferencesWithRemoteCheck(resolvedDeps) {
3194
- return await Promise.all(resolvedDeps.map(async (dep) => {
3195
- if (!dep.repo) return {
3196
- dep: dep.dep,
3197
- repo: null,
3198
- status: "unknown",
3199
- source: dep.source
3200
- };
3201
- if (isReferenceInstalled(dep.repo)) return {
3202
- dep: dep.dep,
3203
- repo: dep.repo,
3204
- status: "installed",
3205
- source: dep.source
3206
- };
3207
- try {
3208
- if ((await checkRemote(dep.repo)).exists) return {
3209
- dep: dep.dep,
3210
- repo: dep.repo,
3211
- status: "remote",
3212
- source: dep.source
3213
- };
3214
- } catch {}
3215
- return {
3216
- dep: dep.dep,
3217
- repo: dep.repo,
3218
- status: "generate",
3219
- source: dep.source
3220
- };
3221
- }));
3222
- }
3223
-
3224
- //#endregion
3225
- //#region src/agents-md.ts
3226
- /**
3227
- * AGENTS.md manipulation utilities
3228
- *
3229
- * Manages updating project AGENTS.md and agent-specific files with reference information.
3230
- */
3231
- /**
3232
- * Generate markdown table for project references section.
3233
- *
3234
- * @param references - Array of installed references
3235
- * @returns Markdown string with table
3236
- */
3237
- function generateReferencesTable(references) {
3238
- const lines = [
3239
- "## Project References",
3240
- "",
3241
- "References installed for this project's dependencies:",
3242
- "",
3243
- "| Dependency | Reference | Path |",
3244
- "| --- | --- | --- |"
3245
- ];
3246
- for (const reference of references) lines.push(`| ${reference.dependency} | ${reference.reference} | ${reference.path} |`);
3247
- lines.push("");
3248
- lines.push("To update references: `ow pull <dependency>`");
3249
- lines.push("To regenerate all: `ow project init --all --generate`");
3250
- lines.push("");
3251
- return lines.join("\n");
3252
- }
3253
- /**
3254
- * Update or append Project References section in a markdown file.
3255
- * If the section exists, replaces its content. Otherwise, appends to end.
3256
- *
3257
- * @param filePath - Path to markdown file
3258
- * @param references - Array of installed references
3259
- */
3260
- function appendReferencesSection(filePath, references) {
3261
- const content = existsSync(filePath) ? readFileSync(filePath, "utf-8") : "";
3262
- const referencesMarkdown = generateReferencesTable(references);
3263
- const sectionRegex = /^## Project References\n(?:.*\n)*?(?=^## |$)/m;
3264
- const match = content.match(sectionRegex);
3265
- let updatedContent;
3266
- if (match) updatedContent = content.replace(sectionRegex, referencesMarkdown);
3267
- else updatedContent = content.trim() + "\n\n" + referencesMarkdown;
3268
- writeFileSync(filePath, updatedContent, "utf-8");
3269
- }
3270
- /**
3271
- * Update AGENTS.md and agent-specific files with project references.
3272
- * Creates files if they don't exist.
3273
- *
3274
- * @param projectRoot - Project root directory
3275
- * @param references - Array of installed references to document
3276
- */
3277
- function updateAgentFiles(projectRoot, references) {
3278
- const agentsMdPath = join(projectRoot, "AGENTS.md");
3279
- const claudeMdPath = join(projectRoot, "CLAUDE.md");
3280
- appendReferencesSection(agentsMdPath, references);
3281
- if (existsSync(claudeMdPath)) appendReferencesSection(claudeMdPath, references);
3282
- }
3283
-
3284
- //#endregion
3285
- //#region src/repo-manager.ts
3286
- function getDirSize(dirPath) {
3287
- if (!existsSync(dirPath)) return 0;
3288
- let size = 0;
3289
- try {
3290
- const entries = readdirSync(dirPath, { withFileTypes: true });
3291
- for (const entry of entries) {
3292
- const fullPath = join(dirPath, entry.name);
3293
- if (entry.isDirectory()) size += getDirSize(fullPath);
3294
- else if (entry.isFile()) try {
3295
- size += statSync(fullPath).size;
3296
- } catch {}
3297
- }
3298
- } catch {}
3299
- return size;
3300
- }
3301
- function getLastAccessTime(dirPath) {
3302
- if (!existsSync(dirPath)) return null;
3303
- let latestTime = null;
3304
- try {
3305
- latestTime = statSync(dirPath).mtime;
3306
- const fetchHead = join(dirPath, ".git", "FETCH_HEAD");
3307
- if (existsSync(fetchHead)) {
3308
- const fetchStat = statSync(fetchHead);
3309
- if (!latestTime || fetchStat.mtime > latestTime) latestTime = fetchStat.mtime;
3310
- }
3311
- } catch {}
3312
- return latestTime;
3313
- }
3314
- function matchesPattern(name, pattern) {
3315
- if (!pattern || pattern === "*") return true;
3316
- return new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$", "i").test(name);
3317
- }
3318
- const yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
3319
- async function getRepoStatus(options = {}) {
3320
- const { onProgress } = options;
3321
- const map = readGlobalMap();
3322
- const qualifiedNames = Object.keys(map.repos);
3323
- const total = qualifiedNames.length;
3324
- let withReference = 0;
3325
- let missing = 0;
3326
- let diskBytes = 0;
3327
- for (let i = 0; i < qualifiedNames.length; i++) {
3328
- const qualifiedName = qualifiedNames[i];
3329
- const entry = map.repos[qualifiedName];
3330
- onProgress?.(i + 1, total, qualifiedName);
3331
- await yieldToEventLoop();
3332
- if (!existsSync(entry.localPath)) {
3333
- missing++;
3334
- continue;
3335
- }
3336
- if (entry.references.length > 0) withReference++;
3337
- diskBytes += getDirSize(entry.localPath);
3338
- }
3339
- return {
3340
- total,
3341
- withReference,
3342
- missing,
3343
- diskBytes
3344
- };
3345
- }
3346
- async function updateAllRepos(options = {}) {
3347
- const { pattern, dryRun = false, unshallow = false, onProgress } = options;
3348
- const map = readGlobalMap();
3349
- const qualifiedNames = Object.keys(map.repos);
3350
- const updated = [];
3351
- const skipped = [];
3352
- const unshallowed = [];
3353
- const errors = [];
3354
- for (const qualifiedName of qualifiedNames) {
3355
- const entry = map.repos[qualifiedName];
3356
- if (pattern && !matchesPattern(qualifiedName, pattern)) continue;
3357
- if (!existsSync(entry.localPath)) {
3358
- skipped.push(qualifiedName);
3359
- onProgress?.(qualifiedName, "skipped", "missing on disk");
3360
- continue;
3361
- }
3362
- if (dryRun) {
3363
- updated.push(qualifiedName);
3364
- onProgress?.(qualifiedName, "updated", "would update");
3365
- continue;
3366
- }
3367
- onProgress?.(qualifiedName, "updating");
3368
- try {
3369
- const result = await updateRepo(qualifiedName, { unshallow });
3370
- if (result.unshallowed) {
3371
- unshallowed.push(qualifiedName);
3372
- onProgress?.(qualifiedName, "unshallowed", "converted to full clone");
3373
- }
3374
- if (result.updated) {
3375
- updated.push(qualifiedName);
3376
- onProgress?.(qualifiedName, "updated", `${result.previousSha.slice(0, 7)} → ${result.currentSha.slice(0, 7)}`);
3377
- } else if (!result.unshallowed) {
3378
- skipped.push(qualifiedName);
3379
- onProgress?.(qualifiedName, "skipped", "already up to date");
3380
- }
3381
- } catch (err) {
3382
- const message = err instanceof GitError ? err.message : String(err);
3383
- errors.push({
3384
- repo: qualifiedName,
3385
- error: message
3386
- });
3387
- onProgress?.(qualifiedName, "error", message);
3388
- }
3389
- }
3390
- return {
3391
- updated,
3392
- skipped,
3393
- unshallowed,
3394
- errors
3395
- };
3396
- }
3397
- async function pruneRepos(options = {}) {
3398
- const { dryRun = false, onProgress } = options;
3399
- const map = readGlobalMap();
3400
- const qualifiedNames = Object.keys(map.repos);
3401
- const removedFromIndex = [];
3402
- const orphanedDirs = [];
3403
- for (const qualifiedName of qualifiedNames) {
3404
- const entry = map.repos[qualifiedName];
3405
- await yieldToEventLoop();
3406
- if (!existsSync(entry.localPath)) {
3407
- onProgress?.(qualifiedName, "missing on disk");
3408
- removedFromIndex.push(qualifiedName);
3409
- if (!dryRun) removeGlobalMapEntry(qualifiedName);
3410
- }
3411
- }
3412
- const repoRoot = getRepoRoot(loadConfig());
3413
- if (existsSync(repoRoot)) {
3414
- const indexedPaths = new Set(Object.values(map.repos).map((r) => r.localPath));
3415
- try {
3416
- const providers = readdirSync(repoRoot, { withFileTypes: true });
3417
- for (const provider of providers) {
3418
- if (!provider.isDirectory()) continue;
3419
- const providerPath = join(repoRoot, provider.name);
3420
- const owners = readdirSync(providerPath, { withFileTypes: true });
3421
- for (const owner of owners) {
3422
- if (!owner.isDirectory()) continue;
3423
- const ownerPath = join(providerPath, owner.name);
3424
- const repoNames = readdirSync(ownerPath, { withFileTypes: true });
3425
- for (const repoName of repoNames) {
3426
- await yieldToEventLoop();
3427
- if (!repoName.isDirectory()) continue;
3428
- const repoPath = join(ownerPath, repoName.name);
3429
- if (!existsSync(join(repoPath, ".git"))) continue;
3430
- if (!indexedPaths.has(repoPath)) {
3431
- const fullName = `${owner.name}/${repoName.name}`;
3432
- onProgress?.(fullName, "not in map");
3433
- orphanedDirs.push(repoPath);
3434
- }
3435
- }
3436
- }
3437
- }
3438
- } catch {}
3439
- }
3440
- return {
3441
- removedFromIndex,
3442
- orphanedDirs
3443
- };
3444
- }
3445
- async function gcRepos(options = {}) {
3446
- const { olderThanDays, withoutReference = false, dryRun = false, onProgress } = options;
3447
- const map = readGlobalMap();
3448
- const qualifiedNames = Object.keys(map.repos);
3449
- const removed = [];
3450
- let freedBytes = 0;
3451
- const now = /* @__PURE__ */ new Date();
3452
- const cutoffDate = olderThanDays ? /* @__PURE__ */ new Date(now.getTime() - olderThanDays * 24 * 60 * 60 * 1e3) : null;
3453
- for (const qualifiedName of qualifiedNames) {
3454
- const entry = map.repos[qualifiedName];
3455
- await yieldToEventLoop();
3456
- if (!existsSync(entry.localPath)) continue;
3457
- let shouldRemove = false;
3458
- let reason = "";
3459
- if (cutoffDate) {
3460
- const lastAccess = getLastAccessTime(entry.localPath);
3461
- if (lastAccess && lastAccess < cutoffDate) {
3462
- shouldRemove = true;
3463
- reason = `not accessed in ${olderThanDays}+ days`;
3464
- }
3465
- }
3466
- if (withoutReference && entry.references.length === 0) {
3467
- shouldRemove = true;
3468
- reason = reason ? `${reason}, no reference` : "no reference";
3469
- }
3470
- if (!shouldRemove) continue;
3471
- const sizeBytes = getDirSize(entry.localPath);
3472
- onProgress?.(qualifiedName, reason, sizeBytes);
3473
- if (!dryRun) {
3474
- rmSync(entry.localPath, {
3475
- recursive: true,
3476
- force: true
3477
- });
3478
- for (const refFile of entry.references) {
3479
- const refPath = join(Paths.offworldReferencesDir, refFile);
3480
- if (existsSync(refPath)) rmSync(refPath, { force: true });
3481
- }
3482
- if (entry.primary) {
3483
- const metaDirName = entry.primary.replace(/\.md$/, "");
3484
- const metaPath = join(Paths.metaDir, metaDirName);
3485
- if (existsSync(metaPath)) rmSync(metaPath, {
3486
- recursive: true,
3487
- force: true
3488
- });
3489
- }
3490
- removeGlobalMapEntry(qualifiedName);
3491
- }
3492
- removed.push({
3493
- repo: qualifiedName,
3494
- reason,
3495
- sizeBytes
3496
- });
3497
- freedBytes += sizeBytes;
3498
- }
3499
- return {
3500
- removed,
3501
- freedBytes
3502
- };
3503
- }
3504
- async function discoverRepos(options = {}) {
3505
- const { dryRun = false, onProgress } = options;
3506
- const config = loadConfig();
3507
- const repoRoot = options.repoRoot ?? getRepoRoot(config);
3508
- const discovered = [];
3509
- let alreadyIndexed = 0;
3510
- if (!existsSync(repoRoot)) return {
3511
- discovered,
3512
- alreadyIndexed
3513
- };
3514
- const map = readGlobalMap();
3515
- const indexedPaths = new Set(Object.values(map.repos).map((r) => r.localPath));
3516
- try {
3517
- const providers = readdirSync(repoRoot, { withFileTypes: true });
3518
- for (const provider of providers) {
3519
- if (!provider.isDirectory()) continue;
3520
- const providerPath = join(repoRoot, provider.name);
3521
- const providerHost = {
3522
- github: "github.com",
3523
- gitlab: "gitlab.com",
3524
- bitbucket: "bitbucket.org"
3525
- }[provider.name] ?? provider.name;
3526
- const owners = readdirSync(providerPath, { withFileTypes: true });
3527
- for (const owner of owners) {
3528
- if (!owner.isDirectory()) continue;
3529
- const ownerPath = join(providerPath, owner.name);
3530
- const repoNames = readdirSync(ownerPath, { withFileTypes: true });
3531
- for (const repoName of repoNames) {
3532
- await yieldToEventLoop();
3533
- if (!repoName.isDirectory()) continue;
3534
- const repoPath = join(ownerPath, repoName.name);
3535
- if (!existsSync(join(repoPath, ".git"))) continue;
3536
- if (indexedPaths.has(repoPath)) {
3537
- alreadyIndexed++;
3538
- continue;
3539
- }
3540
- const fullName = `${owner.name}/${repoName.name}`;
3541
- const qualifiedName = `${providerHost}:${fullName}`;
3542
- onProgress?.(fullName, providerHost);
3543
- if (!dryRun) upsertGlobalMapEntry(qualifiedName, {
3544
- localPath: repoPath,
3545
- references: [],
3546
- primary: "",
3547
- keywords: [],
3548
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3549
- });
3550
- discovered.push({
3551
- fullName,
3552
- qualifiedName,
3553
- localPath: repoPath
3554
- });
3555
- }
3556
- }
3557
- }
3558
- } catch {}
3559
- return {
3560
- discovered,
3561
- alreadyIndexed
3562
- };
3563
- }
3564
-
3565
- //#endregion
3566
- //#region src/models.ts
3567
- const MODELS_DEV_URL = "https://models.dev/api.json";
3568
- let cachedData = null;
3569
- let cacheTime = 0;
3570
- const CACHE_TTL_MS = 300 * 1e3;
3571
- /**
3572
- * Fetch raw data from models.dev with caching
3573
- */
3574
- async function fetchModelsDevData() {
3575
- const now = Date.now();
3576
- if (cachedData && now - cacheTime < CACHE_TTL_MS) return cachedData;
3577
- const res = await fetch(MODELS_DEV_URL, { signal: AbortSignal.timeout(1e4) });
3578
- if (!res.ok) throw new Error(`Failed to fetch models.dev: ${res.status} ${res.statusText}`);
3579
- const json = await res.json();
3580
- const parsed = ModelsDevDataSchema.safeParse(json);
3581
- if (!parsed.success) throw new Error(`Invalid models.dev response: ${parsed.error.message}`);
3582
- cachedData = parsed.data;
3583
- cacheTime = now;
3584
- return cachedData;
3585
- }
3586
- /**
3587
- * List all available providers from models.dev
3588
- */
3589
- async function listProviders() {
3590
- const data = await fetchModelsDevData();
3591
- return Object.values(data).map((p) => ({
3592
- id: p.id,
3593
- name: p.name,
3594
- env: p.env ?? []
3595
- })).sort((a, b) => a.name.localeCompare(b.name));
3596
- }
3597
- /**
3598
- * Get a specific provider with all its models
3599
- */
3600
- async function getProvider(providerId) {
3601
- const provider = (await fetchModelsDevData())[providerId];
3602
- if (!provider) return null;
3603
- return {
3604
- id: provider.id,
3605
- name: provider.name,
3606
- env: provider.env ?? [],
3607
- models: Object.values(provider.models).filter((m) => m.status !== "deprecated").map((m) => ({
3608
- id: m.id,
3609
- name: m.name,
3610
- reasoning: m.reasoning ?? false,
3611
- experimental: m.experimental,
3612
- status: m.status
3613
- })).sort((a, b) => a.name.localeCompare(b.name))
3614
- };
3615
- }
3616
- /**
3617
- * Get all providers with their models
3618
- */
3619
- async function listProvidersWithModels() {
3620
- const data = await fetchModelsDevData();
3621
- return Object.values(data).map((p) => ({
3622
- id: p.id,
3623
- name: p.name,
3624
- env: p.env ?? [],
3625
- models: Object.values(p.models).filter((m) => m.status !== "deprecated").map((m) => ({
3626
- id: m.id,
3627
- name: m.name,
3628
- reasoning: m.reasoning ?? false,
3629
- experimental: m.experimental,
3630
- status: m.status
3631
- })).sort((a, b) => a.name.localeCompare(b.name))
3632
- })).sort((a, b) => a.name.localeCompare(b.name));
3633
- }
3634
- /**
3635
- * Validate that a provider/model combination exists
3636
- */
3637
- async function validateProviderModel(providerId, modelId) {
3638
- const provider = await getProvider(providerId);
3639
- if (!provider) {
3640
- const providers = await listProviders();
3641
- return {
3642
- valid: false,
3643
- error: `Provider "${providerId}" not found. Available: ${providers.slice(0, 10).map((p) => p.id).join(", ")}${providers.length > 10 ? "..." : ""}`
3644
- };
3645
- }
3646
- if (!provider.models.find((m) => m.id === modelId)) return {
3647
- valid: false,
3648
- error: `Model "${modelId}" not found for provider "${providerId}". Available: ${provider.models.slice(0, 10).map((m) => m.id).join(", ")}${provider.models.length > 10 ? "..." : ""}`
3649
- };
3650
- return { valid: true };
3651
- }
3652
-
3653
- //#endregion
3654
- //#region src/installation.ts
3655
- /**
3656
- * Installation utilities for upgrade/uninstall commands
3657
- */
3658
- const GITHUB_REPO = "oscabriel/offworld";
3659
- const NPM_PACKAGE = "offworld";
3660
- /**
3661
- * Detect how offworld was installed
3662
- */
3663
- function detectInstallMethod() {
3664
- const execPath = process.execPath;
3665
- if (execPath.includes(".local/bin")) return "curl";
3666
- const checks = [
3667
- {
3668
- name: "npm",
3669
- test: () => {
3670
- try {
3671
- return execSync("npm list -g --depth=0 2>/dev/null", { encoding: "utf-8" }).includes(NPM_PACKAGE);
3672
- } catch {
3673
- return false;
3674
- }
3675
- }
3676
- },
3677
- {
3678
- name: "pnpm",
3679
- test: () => {
3680
- try {
3681
- return execSync("pnpm list -g --depth=0 2>/dev/null", { encoding: "utf-8" }).includes(NPM_PACKAGE);
3682
- } catch {
3683
- return false;
3684
- }
3685
- }
3686
- },
3687
- {
3688
- name: "bun",
3689
- test: () => {
3690
- try {
3691
- return execSync("bun pm ls -g 2>/dev/null", { encoding: "utf-8" }).includes(NPM_PACKAGE);
3692
- } catch {
3693
- return false;
3694
- }
3695
- }
3696
- },
3697
- {
3698
- name: "brew",
3699
- test: () => {
3700
- try {
3701
- execSync("brew list --formula offworld 2>/dev/null", { encoding: "utf-8" });
3702
- return true;
3703
- } catch {
3704
- return false;
3705
- }
3706
- }
3707
- }
3708
- ];
3709
- if (execPath.includes("npm")) {
3710
- if (checks.find((c) => c.name === "npm")?.test()) return "npm";
3711
- }
3712
- if (execPath.includes("pnpm")) {
3713
- if (checks.find((c) => c.name === "pnpm")?.test()) return "pnpm";
3714
- }
3715
- if (execPath.includes("bun")) {
3716
- if (checks.find((c) => c.name === "bun")?.test()) return "bun";
3717
- }
3718
- if (execPath.includes("Cellar") || execPath.includes("homebrew")) {
3719
- if (checks.find((c) => c.name === "brew")?.test()) return "brew";
3720
- }
3721
- for (const check of checks) if (check.test()) return check.name;
3722
- return "unknown";
3723
- }
3724
- /**
3725
- * Get current installed version
3726
- */
3727
- function getCurrentVersion() {
3728
- return VERSION;
3729
- }
3730
- /**
3731
- * Fetch latest version from appropriate source
3732
- */
3733
- async function fetchLatestVersion(method) {
3734
- const installMethod = method ?? detectInstallMethod();
3735
- try {
3736
- if (installMethod === "npm" || installMethod === "pnpm" || installMethod === "bun") {
3737
- const response = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE}/latest`);
3738
- if (!response.ok) return null;
3739
- const json = await response.json();
3740
- const result = NpmPackageResponseSchema.safeParse(json);
3741
- if (!result.success) return null;
3742
- return result.data.version ?? null;
3743
- }
3744
- const response = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`, { headers: {
3745
- Accept: "application/vnd.github.v3+json",
3746
- "User-Agent": "offworld-cli"
3747
- } });
3748
- if (!response.ok) return null;
3749
- const json = await response.json();
3750
- return (typeof json === "object" && json !== null && "tag_name" in json ? String(json.tag_name) : null)?.replace(/^v/, "") ?? null;
3751
- } catch {
3752
- return null;
3753
- }
3754
- }
3755
- /**
3756
- * Execute upgrade for given method
3757
- */
3758
- function executeUpgrade(method, version) {
3759
- return new Promise((resolve, reject) => {
3760
- let cmd;
3761
- let args;
3762
- switch (method) {
3763
- case "curl":
3764
- cmd = "bash";
3765
- args = ["-c", `curl -fsSL https://offworld.sh/install | VERSION=${version} bash`];
3766
- break;
3767
- case "npm":
3768
- cmd = "npm";
3769
- args = [
3770
- "install",
3771
- "-g",
3772
- `${NPM_PACKAGE}@${version}`
3773
- ];
3774
- break;
3775
- case "pnpm":
3776
- cmd = "pnpm";
3777
- args = [
3778
- "install",
3779
- "-g",
3780
- `${NPM_PACKAGE}@${version}`
3781
- ];
3782
- break;
3783
- case "bun":
3784
- cmd = "bun";
3785
- args = [
3786
- "install",
3787
- "-g",
3788
- `${NPM_PACKAGE}@${version}`
3789
- ];
3790
- break;
3791
- case "brew":
3792
- cmd = "brew";
3793
- args = ["upgrade", "offworld"];
3794
- break;
3795
- default:
3796
- reject(/* @__PURE__ */ new Error(`Cannot upgrade: unknown installation method`));
3797
- return;
3798
- }
3799
- const proc = spawn(cmd, args, { stdio: "inherit" });
3800
- proc.on("close", (code) => {
3801
- if (code === 0) resolve();
3802
- else reject(/* @__PURE__ */ new Error(`Upgrade failed with exit code ${code}`));
3803
- });
3804
- proc.on("error", reject);
3805
- });
3806
- }
3807
- /**
3808
- * Execute uninstall for given method
3809
- */
3810
- function executeUninstall(method) {
3811
- return new Promise((resolve, reject) => {
3812
- let cmd;
3813
- let args;
3814
- switch (method) {
3815
- case "curl":
3816
- try {
3817
- const binPath = join(homedir(), ".local", "bin", "ow");
3818
- if (existsSync(binPath)) execSync(`rm -f "${binPath}"`, { stdio: "inherit" });
3819
- resolve();
3820
- } catch (err) {
3821
- reject(err);
3822
- }
3823
- return;
3824
- case "npm":
3825
- cmd = "npm";
3826
- args = [
3827
- "uninstall",
3828
- "-g",
3829
- NPM_PACKAGE
3830
- ];
3831
- break;
3832
- case "pnpm":
3833
- cmd = "pnpm";
3834
- args = [
3835
- "uninstall",
3836
- "-g",
3837
- NPM_PACKAGE
3838
- ];
3839
- break;
3840
- case "bun":
3841
- cmd = "bun";
3842
- args = [
3843
- "remove",
3844
- "-g",
3845
- NPM_PACKAGE
3846
- ];
3847
- break;
3848
- case "brew":
3849
- cmd = "brew";
3850
- args = ["uninstall", "offworld"];
3851
- break;
3852
- default:
3853
- reject(/* @__PURE__ */ new Error(`Cannot uninstall: unknown installation method`));
3854
- return;
3855
- }
3856
- const proc = spawn(cmd, args, { stdio: "inherit" });
3857
- proc.on("close", (code) => {
3858
- if (code === 0) resolve();
3859
- else reject(/* @__PURE__ */ new Error(`Uninstall failed with exit code ${code}`));
3860
- });
3861
- proc.on("error", reject);
3862
- });
3863
- }
3864
- /**
3865
- * Get shell config files to clean
3866
- */
3867
- function getShellConfigFiles() {
3868
- const home = homedir();
3869
- const configs = [];
3870
- for (const file of [
3871
- ".bashrc",
3872
- ".bash_profile",
3873
- ".profile",
3874
- ".zshrc",
3875
- ".zshenv",
3876
- ".config/fish/config.fish"
3877
- ]) {
3878
- const path = join(home, file);
3879
- if (existsSync(path)) configs.push(path);
3880
- }
3881
- return configs;
3882
- }
3883
- /**
3884
- * Clean PATH entries from shell config
3885
- */
3886
- function cleanShellConfig(filePath) {
3887
- try {
3888
- const lines = readFileSync(filePath, "utf-8").split("\n");
3889
- const filtered = [];
3890
- let modified = false;
3891
- for (const line of lines) {
3892
- const trimmed = line.trim();
3893
- if (trimmed.includes(".local/bin") && (trimmed.startsWith("export PATH=") || trimmed.startsWith("fish_add_path"))) {
3894
- if (trimmed.includes("# offworld") || trimmed === "export PATH=\"$HOME/.local/bin:$PATH\"") {
3895
- modified = true;
3896
- continue;
3897
- }
3898
- }
3899
- filtered.push(line);
3900
- }
3901
- if (modified) writeFileSync(filePath, filtered.join("\n"), "utf-8");
3902
- return modified;
3903
- } catch {
3904
- return false;
3905
- }
3906
- }
3907
-
3908
- //#endregion
3909
- export { AuthError, AuthenticationError, CloneError, CommitExistsError, CommitNotFoundError, ConflictError, DEFAULT_IGNORE_PATTERNS, FALLBACK_MAPPINGS, GitError, GitHubError, InvalidInputError, InvalidReferenceError, LowStarsError, NetworkError, NotGitRepoError, NotLoggedInError, OpenCodeSDKError, PathNotFoundError, Paths, PrivateRepoError, PushNotAllowedError, RateLimitError, RepoExistsError, RepoNotFoundError, RepoSourceError, SyncError, RepoNotFoundError$1 as SyncRepoNotFoundError, TokenExpiredError, VERSION, agents, appendReferencesSection, canPushToWeb, checkRemote, checkRemoteByName, checkStaleness, cleanShellConfig, clearAuthData, cloneRepo, detectInstallMethod, detectInstalledAgents, detectManifestType, discoverRepos, executeUninstall, executeUpgrade, expandTilde, fetchGitHubMetadata, fetchLatestVersion, fetchRepoStars, gcRepos, generateReferenceWithAI, getAgentConfig, getAllAgentConfigs, getAuthPath, getAuthStatus, getClonedRepoPath, getCommitDistance, getCommitSha, getConfigPath, getCurrentVersion, getMapEntry, getMetaPath, getMetaRoot, getProjectMapPath, getProvider, getReferenceFileNameForSource, getReferencePath, getRepoPath, getRepoRoot, getRepoStatus, getShellConfigFiles, getToken, getTokenOrNull, installGlobalSkill, installReference, isLoggedIn, isReferenceInstalled, isRepoCloned, isShallowClone, listProviders, listProvidersWithModels, listRepos, loadAuthData, loadConfig, matchDependenciesToReferences, matchDependenciesToReferencesWithRemoteCheck, parseDependencies, parseRepoInput, pruneRepos, pullReference, pullReferenceByName, pushReference, readGlobalMap, refreshAccessToken, removeGlobalMapEntry, removeRepo, resolveDependencyRepo, resolveFromNpm, resolveRepoKey, saveAuthData, saveConfig, searchMap, streamPrompt, toMetaDirName, toReferenceFileName, toReferenceName, unshallowRepo, updateAgentFiles, updateAllRepos, updateRepo, upsertGlobalMapEntry, validateProviderModel, validatePushAllowed, writeGlobalMap, writeProjectMap };
3910
- //# sourceMappingURL=index.mjs.map
5
+ export { AuthError, CloneError, DEFAULT_IGNORE_PATTERNS, FALLBACK_MAPPINGS, GitError, NotGitRepoError, NotLoggedInError, PathNotFoundError, Paths, RepoExistsError, RepoNotFoundError, RepoSourceError, TokenExpiredError, VERSION, agents, clearAuthData, cloneRepo, detectInstalledAgents, detectManifestType, discoverRepos, expandTilde, gcRepos, getAgentConfig, getAllAgentConfigs, getAuthPath, getAuthStatus, getClonedRepoPath, getCommitDistance, getCommitSha, getConfigPath, getMapEntry, getMetaPath, getMetaRoot, getProjectMapPath, getProvider, getReferenceFileNameForSource, getReferencePath, getRepoPath, getRepoRoot, getRepoStatus, getToken, getTokenOrNull, installGlobalSkill, installReference, isLoggedIn, isReferenceInstalled, isRepoCloned, isShallowClone, listProviders, listProvidersWithModels, listRepos, loadAuthData, loadConfig, matchDependenciesToReferences, matchDependenciesToReferencesWithRemoteCheck, parseDependencies, parseRepoInput, pruneRepos, readGlobalMap, refreshAccessToken, removeGlobalMapEntry, removeRepo, resolveDependencyRepo, resolveFromNpm, resolveRepoKey, saveAuthData, saveConfig, searchMap, toMetaDirName, toReferenceFileName, toReferenceName, unshallowRepo, updateAllRepos, updateRepo, upsertGlobalMapEntry, validateProviderModel, writeGlobalMap, writeProjectMap };