@oxygen-agent/cli 1.263.0 → 1.272.83

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.
@@ -0,0 +1,106 @@
1
+ export declare const MIRROR_LOCK_STALE_MS: number;
2
+ export declare const MIRROR_SLUG_PATTERN: RegExp;
3
+ export declare const GENERATED_MIRROR_FILE_HEADER = "<!-- generated by oxygen knowledge sync \u2014 local edits are overwritten -->";
4
+ export type MirrorPageRecord = {
5
+ id: string;
6
+ revision: number;
7
+ /** sha256 hex of the exact bytes the server rendered and this client wrote. */
8
+ content_sha256: string;
9
+ };
10
+ export type MirrorState = {
11
+ api_host: string;
12
+ org_id: string;
13
+ /** Resumable sync/delta keyset cursor; null before the first completed batch. */
14
+ cursor: string | null;
15
+ /** ISO timestamp of the last fully completed sync; null = stale/never synced. */
16
+ last_sync_at: string | null;
17
+ pages: Record<string, MirrorPageRecord>;
18
+ };
19
+ export type MirrorIndexPage = {
20
+ slug: string;
21
+ title?: string | null;
22
+ type?: string | null;
23
+ oneLiner?: string | null;
24
+ tags?: string[];
25
+ revision?: number;
26
+ canonical?: boolean;
27
+ degree?: number;
28
+ };
29
+ export type MirrorIndexSnapshot = {
30
+ pages: MirrorIndexPage[];
31
+ counts?: {
32
+ total?: number;
33
+ byType?: Record<string, number>;
34
+ byStatus?: Record<string, number>;
35
+ };
36
+ };
37
+ export type MirrorLogEntry = {
38
+ seq?: string;
39
+ event?: string;
40
+ slug?: string | null;
41
+ summary?: string | null;
42
+ createdAt?: string | null;
43
+ actorKind?: string | null;
44
+ };
45
+ /** Same resolution as the credential/session stores: OXYGEN_CONFIG_DIR wins. */
46
+ export declare function resolveDefaultConfigDir(env?: NodeJS.ProcessEnv): string;
47
+ export declare function resolveMirrorDir(input: {
48
+ configDir: string;
49
+ apiHost: string;
50
+ orgId: string;
51
+ }): string;
52
+ export declare function mirrorSidecarDir(dir: string): string;
53
+ export declare function mirrorManifestPath(dir: string): string;
54
+ export declare function mirrorLockPath(dir: string): string;
55
+ export declare function mirrorConflictsDir(dir: string): string;
56
+ export declare function pageFilePath(dir: string, slug: string): string;
57
+ /** Cheap "does a mirror exist here" check for post-write staleness hooks. */
58
+ export declare function mirrorExists(dir: string): boolean;
59
+ export declare function assertSafeMirrorSlug(slug: string): void;
60
+ export declare function emptyMirrorState(input: {
61
+ apiHost: string;
62
+ orgId: string;
63
+ }): MirrorState;
64
+ /** Null on a missing or corrupt manifest — the next sync re-clones from scratch. */
65
+ export declare function readMirrorState(dir: string): MirrorState | null;
66
+ export declare function writeMirrorState(dir: string, state: MirrorState): void;
67
+ /** Clear last_sync_at so the next `knowledge sync --if-stale` runs for real. */
68
+ export declare function markMirrorStale(dir: string): void;
69
+ export declare function findMirrorSlugByPageId(state: MirrorState, pageId: string): string | null;
70
+ export declare function writePageFile(dir: string, slug: string, bytes: string): void;
71
+ export declare function deletePageFile(dir: string, slug: string): void;
72
+ /** sha256 hex of the local page file bytes, or null when the file is missing. */
73
+ export declare function localPageSha256(dir: string, slug: string): string | null;
74
+ /** A missing file is never dirty; anything whose bytes differ from expectedSha256 is. */
75
+ export declare function isFileDirty(dir: string, slug: string, expectedSha256: string): boolean;
76
+ /**
77
+ * Move the CURRENT local page file into .oxygen/conflicts/<slug>-<timestamp>.md
78
+ * so a locally edited copy survives the incoming server write. Returns the
79
+ * conflict path, or null when there was no local file to preserve.
80
+ */
81
+ export declare function quarantineDirtyFile(dir: string, slug: string): string | null;
82
+ export declare function listConflictFiles(dir: string): string[];
83
+ export declare function clearConflictFiles(dir: string): number;
84
+ /**
85
+ * Take the mirror's sync lock (a pid file created with O_EXCL). Returns false
86
+ * while another sync holds a fresh lock; a lock older than MIRROR_LOCK_STALE_MS
87
+ * (a crashed sync) is broken and re-taken.
88
+ */
89
+ export declare function acquireMirrorLock(dir: string, options?: {
90
+ staleMs?: number;
91
+ }): boolean;
92
+ export declare function releaseMirrorLock(dir: string): void;
93
+ export declare function writeGeneratedIndexFile(dir: string, index: MirrorIndexSnapshot): void;
94
+ export declare function writeGeneratedLogFile(dir: string, entries: MirrorLogEntry[]): void;
95
+ /** Delete the whole mirror (pages, generated files, sidecar). Safe on a missing dir. */
96
+ export declare function purgeMirror(dir: string): void;
97
+ /**
98
+ * Reset the mirror for `sync --full` WITHOUT destroying evidence of local edits: every
99
+ * manifest page whose local bytes drifted from its recorded sha is quarantined into
100
+ * `.oxygen/conflicts/` first, and the conflicts dir survives the reset. A plain purge
101
+ * (logout / access revoked / `knowledge purge`) deliberately preserves nothing — this
102
+ * variant exists only so `--full` keeps the never-silently-clobber promise.
103
+ */
104
+ export declare function resetMirrorForFullResync(dir: string): {
105
+ quarantined: number;
106
+ };
@@ -0,0 +1,375 @@
1
+ // Local knowledge-wiki mirror directory management for `oxygen knowledge sync` —
2
+ // pure filesystem state, no network. The mirror stores SERVER-rendered markdown
3
+ // bytes verbatim: the server renders + hashes every page (content_sha256) and the
4
+ // client never re-renders, so renderer drift can't mass-quarantine mirrors (see
5
+ // packages/shared/src/knowledge-markdown.ts). Layout:
6
+ //
7
+ // <configDir>/knowledge/<api-host>/<org-id>/
8
+ // <slug>.md synced wiki pages (server bytes, 0600)
9
+ // index.md, log.md generated summaries (marker header, always overwritten)
10
+ // .oxygen/ sidecar: manifest.json, lock, conflicts/<slug>-<ts>.md
11
+ //
12
+ // Locally edited page files are never silently clobbered: the sync loop moves
13
+ // dirty bytes into .oxygen/conflicts/ before rewriting. The generated index.md
14
+ // and log.md are the one exception — their marker header says so.
15
+ import { createHash } from "node:crypto";
16
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { basename, dirname, join } from "node:path";
19
+ import { OxygenError } from "@oxygen/shared";
20
+ const MIRROR_SIDECAR_DIRNAME = ".oxygen";
21
+ const MIRROR_MANIFEST_FILENAME = "manifest.json";
22
+ const MIRROR_LOCK_FILENAME = "lock";
23
+ const MIRROR_CONFLICTS_DIRNAME = "conflicts";
24
+ const MIRROR_SLUG_MAX_LENGTH = 200;
25
+ export const MIRROR_LOCK_STALE_MS = 10 * 60 * 1000;
26
+ // Server page slugs are lowercase alphanumerics/dashes; anything else (path
27
+ // separators, dots, uppercase) is rejected before it can touch the filesystem.
28
+ export const MIRROR_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
29
+ export const GENERATED_MIRROR_FILE_HEADER = "<!-- generated by oxygen knowledge sync — local edits are overwritten -->";
30
+ // ── Paths ───────────────────────────────────────────────────────────────────────
31
+ /** Same resolution as the credential/session stores: OXYGEN_CONFIG_DIR wins. */
32
+ export function resolveDefaultConfigDir(env = process.env) {
33
+ return env.OXYGEN_CONFIG_DIR?.trim()
34
+ || (process.platform === "win32"
35
+ ? join(env.APPDATA || join(homedir(), "AppData", "Roaming"), "oxygen")
36
+ : join(homedir(), ".config", "oxygen"));
37
+ }
38
+ export function resolveMirrorDir(input) {
39
+ return join(input.configDir, "knowledge", safePathSegment(input.apiHost, "API host"), safePathSegment(input.orgId, "organization id"));
40
+ }
41
+ export function mirrorSidecarDir(dir) {
42
+ return join(dir, MIRROR_SIDECAR_DIRNAME);
43
+ }
44
+ export function mirrorManifestPath(dir) {
45
+ return join(mirrorSidecarDir(dir), MIRROR_MANIFEST_FILENAME);
46
+ }
47
+ export function mirrorLockPath(dir) {
48
+ return join(mirrorSidecarDir(dir), MIRROR_LOCK_FILENAME);
49
+ }
50
+ export function mirrorConflictsDir(dir) {
51
+ return join(mirrorSidecarDir(dir), MIRROR_CONFLICTS_DIRNAME);
52
+ }
53
+ export function pageFilePath(dir, slug) {
54
+ assertSafeMirrorSlug(slug);
55
+ return join(dir, `${slug}.md`);
56
+ }
57
+ /** Cheap "does a mirror exist here" check for post-write staleness hooks. */
58
+ export function mirrorExists(dir) {
59
+ return existsSync(mirrorManifestPath(dir));
60
+ }
61
+ function safePathSegment(value, label) {
62
+ const cleaned = value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "_");
63
+ if (!cleaned || /^\.+$/.test(cleaned)) {
64
+ throw new OxygenError("invalid_mirror_path", `Cannot derive a mirror directory from this ${label}.`, {
65
+ details: { value },
66
+ exitCode: 1,
67
+ });
68
+ }
69
+ return cleaned;
70
+ }
71
+ export function assertSafeMirrorSlug(slug) {
72
+ if (slug.length > MIRROR_SLUG_MAX_LENGTH || !MIRROR_SLUG_PATTERN.test(slug)) {
73
+ throw new OxygenError("invalid_slug", "Mirror page slugs must be lowercase alphanumerics and dashes.", {
74
+ details: { slug },
75
+ exitCode: 1,
76
+ });
77
+ }
78
+ }
79
+ // Every destructive/recursive operation double-checks it is pointed at a
80
+ // <configDir>/knowledge/<host>/<org> directory so a mis-resolved path can never
81
+ // delete outside the mirror namespace.
82
+ function assertMirrorShapedDir(dir) {
83
+ if (!basename(dir) || basename(dirname(dirname(dir))) !== "knowledge") {
84
+ throw new OxygenError("invalid_mirror_path", "Refusing to operate outside a knowledge mirror directory.", {
85
+ details: { dir },
86
+ exitCode: 1,
87
+ });
88
+ }
89
+ }
90
+ function ensureMirrorScaffold(dir) {
91
+ assertMirrorShapedDir(dir);
92
+ const conflictsDir = mirrorConflictsDir(dir);
93
+ mkdirSync(conflictsDir, { recursive: true, mode: 0o700 });
94
+ // Recursive mkdir applies the mode to created leaves only on some platforms —
95
+ // pin every mirror-owned level (knowledge root down) to 0700 explicitly.
96
+ for (const level of [dirname(dirname(dir)), dirname(dir), dir, mirrorSidecarDir(dir), conflictsDir]) {
97
+ chmodSync(level, 0o700);
98
+ }
99
+ }
100
+ // Same tmp+rename pattern as the credential store: a crash mid-write can never
101
+ // leave a torn page or manifest behind.
102
+ function writeFileAtomic(path, contents) {
103
+ const tempPath = `${path}.${process.pid}.tmp`;
104
+ writeFileSync(tempPath, contents, { mode: 0o600 });
105
+ chmodSync(tempPath, 0o600);
106
+ renameSync(tempPath, path);
107
+ chmodSync(path, 0o600);
108
+ }
109
+ // ── Manifest state ──────────────────────────────────────────────────────────────
110
+ export function emptyMirrorState(input) {
111
+ return {
112
+ api_host: input.apiHost,
113
+ org_id: input.orgId,
114
+ cursor: null,
115
+ last_sync_at: null,
116
+ pages: {},
117
+ };
118
+ }
119
+ // Parse the manifest's `pages` map, dropping any entry whose slug or record shape
120
+ // is invalid so a partially corrupt manifest degrades to fewer pages, not null.
121
+ function parseMirrorPages(rawPages) {
122
+ const pages = {};
123
+ for (const [slug, value] of Object.entries(rawPages)) {
124
+ if (slug.length > MIRROR_SLUG_MAX_LENGTH || !MIRROR_SLUG_PATTERN.test(slug))
125
+ continue;
126
+ if (!value || typeof value !== "object" || Array.isArray(value))
127
+ continue;
128
+ const page = value;
129
+ if (typeof page.id !== "string" || typeof page.content_sha256 !== "string")
130
+ continue;
131
+ pages[slug] = {
132
+ id: page.id,
133
+ revision: typeof page.revision === "number" && Number.isFinite(page.revision) ? page.revision : 0,
134
+ content_sha256: page.content_sha256,
135
+ };
136
+ }
137
+ return pages;
138
+ }
139
+ /** Null on a missing or corrupt manifest — the next sync re-clones from scratch. */
140
+ export function readMirrorState(dir) {
141
+ const path = mirrorManifestPath(dir);
142
+ if (!existsSync(path))
143
+ return null;
144
+ let parsed;
145
+ try {
146
+ parsed = JSON.parse(readFileSync(path, "utf8"));
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
152
+ return null;
153
+ const record = parsed;
154
+ const rawPages = record.pages;
155
+ if (typeof record.api_host !== "string" || typeof record.org_id !== "string")
156
+ return null;
157
+ if (!rawPages || typeof rawPages !== "object" || Array.isArray(rawPages))
158
+ return null;
159
+ const pages = parseMirrorPages(rawPages);
160
+ return {
161
+ api_host: record.api_host,
162
+ org_id: record.org_id,
163
+ cursor: typeof record.cursor === "string" && record.cursor ? record.cursor : null,
164
+ last_sync_at: typeof record.last_sync_at === "string" && record.last_sync_at ? record.last_sync_at : null,
165
+ pages,
166
+ };
167
+ }
168
+ export function writeMirrorState(dir, state) {
169
+ ensureMirrorScaffold(dir);
170
+ writeFileAtomic(mirrorManifestPath(dir), `${JSON.stringify(state, null, 2)}\n`);
171
+ }
172
+ /** Clear last_sync_at so the next `knowledge sync --if-stale` runs for real. */
173
+ export function markMirrorStale(dir) {
174
+ const state = readMirrorState(dir);
175
+ if (!state || state.last_sync_at === null)
176
+ return;
177
+ writeMirrorState(dir, { ...state, last_sync_at: null });
178
+ }
179
+ export function findMirrorSlugByPageId(state, pageId) {
180
+ for (const [slug, record] of Object.entries(state.pages)) {
181
+ if (record.id === pageId)
182
+ return slug;
183
+ }
184
+ return null;
185
+ }
186
+ // ── Page files ──────────────────────────────────────────────────────────────────
187
+ export function writePageFile(dir, slug, bytes) {
188
+ const path = pageFilePath(dir, slug);
189
+ ensureMirrorScaffold(dir);
190
+ writeFileAtomic(path, bytes);
191
+ }
192
+ export function deletePageFile(dir, slug) {
193
+ rmSync(pageFilePath(dir, slug), { force: true });
194
+ }
195
+ /** sha256 hex of the local page file bytes, or null when the file is missing. */
196
+ export function localPageSha256(dir, slug) {
197
+ const path = pageFilePath(dir, slug);
198
+ if (!existsSync(path))
199
+ return null;
200
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
201
+ }
202
+ /** A missing file is never dirty; anything whose bytes differ from expectedSha256 is. */
203
+ export function isFileDirty(dir, slug, expectedSha256) {
204
+ const localSha = localPageSha256(dir, slug);
205
+ return localSha !== null && localSha !== expectedSha256;
206
+ }
207
+ /**
208
+ * Move the CURRENT local page file into .oxygen/conflicts/<slug>-<timestamp>.md
209
+ * so a locally edited copy survives the incoming server write. Returns the
210
+ * conflict path, or null when there was no local file to preserve.
211
+ */
212
+ export function quarantineDirtyFile(dir, slug) {
213
+ const source = pageFilePath(dir, slug);
214
+ if (!existsSync(source))
215
+ return null;
216
+ ensureMirrorScaffold(dir);
217
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
218
+ let destination = join(mirrorConflictsDir(dir), `${slug}-${stamp}.md`);
219
+ for (let suffix = 2; existsSync(destination); suffix += 1) {
220
+ destination = join(mirrorConflictsDir(dir), `${slug}-${stamp}-${suffix}.md`);
221
+ }
222
+ renameSync(source, destination);
223
+ chmodSync(destination, 0o600);
224
+ return destination;
225
+ }
226
+ export function listConflictFiles(dir) {
227
+ const conflictsDir = mirrorConflictsDir(dir);
228
+ if (!existsSync(conflictsDir))
229
+ return [];
230
+ return readdirSync(conflictsDir)
231
+ .filter((name) => name.endsWith(".md"))
232
+ .sort((a, b) => a.localeCompare(b));
233
+ }
234
+ export function clearConflictFiles(dir) {
235
+ const names = listConflictFiles(dir);
236
+ for (const name of names) {
237
+ rmSync(join(mirrorConflictsDir(dir), name), { force: true });
238
+ }
239
+ return names.length;
240
+ }
241
+ // ── Locking ─────────────────────────────────────────────────────────────────────
242
+ /**
243
+ * Take the mirror's sync lock (a pid file created with O_EXCL). Returns false
244
+ * while another sync holds a fresh lock; a lock older than MIRROR_LOCK_STALE_MS
245
+ * (a crashed sync) is broken and re-taken.
246
+ */
247
+ export function acquireMirrorLock(dir, options = {}) {
248
+ ensureMirrorScaffold(dir);
249
+ const lockPath = mirrorLockPath(dir);
250
+ const payload = `${JSON.stringify({ pid: process.pid, acquired_at: new Date().toISOString() })}\n`;
251
+ const staleMs = options.staleMs ?? MIRROR_LOCK_STALE_MS;
252
+ for (let attempt = 0; attempt < 2; attempt += 1) {
253
+ try {
254
+ writeFileSync(lockPath, payload, { mode: 0o600, flag: "wx" });
255
+ return true;
256
+ }
257
+ catch (error) {
258
+ if (error.code !== "EEXIST")
259
+ throw error;
260
+ let heldSinceMs;
261
+ try {
262
+ heldSinceMs = statSync(lockPath).mtimeMs;
263
+ }
264
+ catch {
265
+ continue; // the holder released between write and stat — retry the acquire
266
+ }
267
+ if (Date.now() - heldSinceMs < staleMs)
268
+ return false;
269
+ rmSync(lockPath, { force: true }); // stale holder — break the lock and retry
270
+ }
271
+ }
272
+ return false;
273
+ }
274
+ export function releaseMirrorLock(dir) {
275
+ rmSync(mirrorLockPath(dir), { force: true });
276
+ }
277
+ // ── Generated files (index.md / log.md) ─────────────────────────────────────────
278
+ // Always overwritten, never quarantined: they summarize server projections and the
279
+ // marker header warns that local edits are lost on the next sync.
280
+ export function writeGeneratedIndexFile(dir, index) {
281
+ const pages = index.pages.filter((page) => MIRROR_SLUG_PATTERN.test(page.slug));
282
+ const total = index.counts?.total ?? pages.length;
283
+ const lines = [
284
+ GENERATED_MIRROR_FILE_HEADER,
285
+ "",
286
+ "# Knowledge wiki index",
287
+ "",
288
+ `${total} page${total === 1 ? "" : "s"}. Regenerated by every \`oxygen knowledge sync\`.`,
289
+ "",
290
+ ];
291
+ for (const page of pages) {
292
+ const meta = [
293
+ page.type ? oneLine(page.type) : null,
294
+ page.revision !== undefined ? `rev ${page.revision}` : null,
295
+ page.canonical ? "canonical" : null,
296
+ ].filter(Boolean).join(", ");
297
+ const title = oneLine(page.title ?? "") || page.slug;
298
+ const summary = page.oneLiner ? ` — ${oneLine(page.oneLiner)}` : "";
299
+ lines.push(`- [${page.slug}](./${page.slug}.md) — ${title}${meta ? ` (${meta})` : ""}${summary}`);
300
+ }
301
+ writeGeneratedFile(dir, "index.md", lines);
302
+ }
303
+ export function writeGeneratedLogFile(dir, entries) {
304
+ const lines = [
305
+ GENERATED_MIRROR_FILE_HEADER,
306
+ "",
307
+ "# Knowledge log",
308
+ "",
309
+ `${entries.length} most recent entr${entries.length === 1 ? "y" : "ies"}, newest first. Regenerated by every \`oxygen knowledge sync\`.`,
310
+ "",
311
+ ];
312
+ for (const entry of entries) {
313
+ const parts = [
314
+ entry.createdAt ? oneLine(entry.createdAt) : null,
315
+ entry.event ? oneLine(entry.event) : null,
316
+ entry.slug ? `[[${oneLine(entry.slug)}]]` : null,
317
+ entry.summary ? oneLine(entry.summary) : null,
318
+ ].filter(Boolean);
319
+ lines.push(`- ${parts.join(" — ")}${entry.seq ? ` (seq ${oneLine(entry.seq)})` : ""}`);
320
+ }
321
+ writeGeneratedFile(dir, "log.md", lines);
322
+ }
323
+ function writeGeneratedFile(dir, name, lines) {
324
+ ensureMirrorScaffold(dir);
325
+ writeFileAtomic(join(dir, name), `${lines.join("\n")}\n`);
326
+ }
327
+ function oneLine(value) {
328
+ return value.replace(/\s+/g, " ").trim();
329
+ }
330
+ // ── Purge ───────────────────────────────────────────────────────────────────────
331
+ /** Delete the whole mirror (pages, generated files, sidecar). Safe on a missing dir. */
332
+ export function purgeMirror(dir) {
333
+ assertMirrorShapedDir(dir);
334
+ rmSync(dir, { recursive: true, force: true });
335
+ }
336
+ /**
337
+ * Reset the mirror for `sync --full` WITHOUT destroying evidence of local edits: every
338
+ * manifest page whose local bytes drifted from its recorded sha is quarantined into
339
+ * `.oxygen/conflicts/` first, and the conflicts dir survives the reset. A plain purge
340
+ * (logout / access revoked / `knowledge purge`) deliberately preserves nothing — this
341
+ * variant exists only so `--full` keeps the never-silently-clobber promise.
342
+ */
343
+ export function resetMirrorForFullResync(dir) {
344
+ assertMirrorShapedDir(dir);
345
+ let quarantined = 0;
346
+ const state = readMirrorState(dir);
347
+ for (const [slug, entry] of Object.entries(state?.pages ?? {})) {
348
+ if (!entry?.content_sha256)
349
+ continue;
350
+ if (!isFileDirty(dir, slug, entry.content_sha256))
351
+ continue;
352
+ if (quarantineDirtyFile(dir, slug))
353
+ quarantined += 1;
354
+ }
355
+ let entries = [];
356
+ try {
357
+ entries = readdirSync(dir);
358
+ }
359
+ catch {
360
+ return { quarantined }; // mirror never existed — nothing to reset
361
+ }
362
+ for (const entry of entries) {
363
+ const path = join(dir, entry);
364
+ if (entry === MIRROR_SIDECAR_DIRNAME) {
365
+ for (const side of readdirSync(path)) {
366
+ if (side === MIRROR_CONFLICTS_DIRNAME)
367
+ continue;
368
+ rmSync(join(path, side), { recursive: true, force: true });
369
+ }
370
+ continue;
371
+ }
372
+ rmSync(path, { recursive: true, force: true });
373
+ }
374
+ return { quarantined };
375
+ }
package/dist/skills.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execFileSync, spawnSync } from "node:child_process";
2
+ import { type StoredCredentials } from "./credentials.js";
2
3
  export type ExecFileSyncLike = typeof execFileSync;
3
4
  export type SpawnSyncLike = typeof spawnSync;
4
5
  export type SkillsInstallOptions = {
@@ -28,20 +29,48 @@ export type AutomaticSkillsInstallResult = {
28
29
  details?: unknown;
29
30
  };
30
31
  };
31
- export declare function listAgentSkills(options: SkillsListOptions): Promise<Record<string, unknown>>;
32
- export declare function doctorAgentSkills(options: SkillsDoctorOptions): Promise<Record<string, unknown>>;
32
+ export type SkillsAccessResponse = {
33
+ base_url: string;
34
+ index_url: string;
35
+ expires_at: string;
36
+ skills_version?: string;
37
+ };
38
+ export type SkillsInstallSource = {
39
+ authenticated: boolean;
40
+ /** Base URL handed to `npx skills add` (tokenized when authenticated). */
41
+ install_url: string;
42
+ /** Concrete index.json URL, fetched by list/doctor. */
43
+ index_url: string;
44
+ expires_at?: string;
45
+ fallback_reason?: string;
46
+ };
47
+ export type SkillsSourceRuntime = {
48
+ credentials?: StoredCredentials | null;
49
+ fetch?: typeof fetch;
50
+ };
51
+ /**
52
+ * Resolves where to install skills from. Logged-in CLIs mint a short-lived
53
+ * org-bound access URL (the skills surface is gated; anonymous serving is
54
+ * being retired). Any failure falls back to the plain well-known URL so
55
+ * installs never regress while older deployments are still in circulation.
56
+ */
57
+ export declare function resolveSkillsInstallSource(options: {
58
+ apiUrl: string;
59
+ } & SkillsSourceRuntime): Promise<SkillsInstallSource>;
60
+ export declare function listAgentSkills(options: SkillsListOptions, runtime?: SkillsSourceRuntime): Promise<Record<string, unknown>>;
61
+ export declare function doctorAgentSkills(options: SkillsDoctorOptions, runtime?: SkillsSourceRuntime): Promise<Record<string, unknown>>;
33
62
  export declare function installAgentSkills(// skipcq: JS-R1005
34
63
  options: SkillsInstallOptions, runtime?: {
35
64
  env?: NodeJS.ProcessEnv;
36
65
  platform?: NodeJS.Platform;
37
66
  execFileSync?: ExecFileSyncLike;
38
67
  spawnSync?: SpawnSyncLike;
39
- }): Record<string, unknown>;
68
+ } & SkillsSourceRuntime): Promise<Record<string, unknown>>;
40
69
  export declare function runAutomaticSkillsInstall(options?: {
41
70
  apiUrl?: string;
42
71
  env?: NodeJS.ProcessEnv;
43
72
  platform?: NodeJS.Platform;
44
73
  execFileSync?: ExecFileSyncLike;
45
74
  spawnSync?: SpawnSyncLike;
46
- }): AutomaticSkillsInstallResult;
75
+ } & SkillsSourceRuntime): Promise<AutomaticSkillsInstallResult>;
47
76
  export declare function skippedAutomaticSkillsInstall(reason: string, apiUrl?: string): AutomaticSkillsInstallResult;