@luckypiggy/zhihu-ai-cli 0.1.0-test.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,573 @@
1
+ import { chmodSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, renameSync, rmdirSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, join, parse, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { CLI_VERSION, } from "./contracts.js";
7
+ import { ConfigError, readConfigSnapshot, validateProfile, } from "./config.js";
8
+ export const CODEX_SKILL_NAME = "zhihu-aiops";
9
+ export const CODEX_SKILL_VERSION = CLI_VERSION;
10
+ export const CODEX_SKILL_SCHEMA = "1";
11
+ export const CODEX_SKILL_MARKER = ".zhihu-aiops-install.json";
12
+ /** The package asset allowlist. Keep this list explicit: no directory copy is allowed. */
13
+ export const CODEX_SKILL_FILES = [
14
+ "SKILL.md",
15
+ "agents/openai.yaml",
16
+ "references/commands.md",
17
+ "references/errors.md",
18
+ "references/safety.md",
19
+ ];
20
+ export class CodexSkillError extends Error {
21
+ code;
22
+ exitCode = 2;
23
+ constructor(code, message) {
24
+ super(message);
25
+ this.name = "CodexSkillError";
26
+ this.code = code;
27
+ }
28
+ }
29
+ const SOURCE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "assets", "codex-skills", CODEX_SKILL_NAME);
30
+ const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
31
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
32
+ function localError(code, message) {
33
+ throw new CodexSkillError(code, message);
34
+ }
35
+ function isNotFound(error) {
36
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
37
+ }
38
+ function isDirectoryEntry(path) {
39
+ try {
40
+ return lstatSync(path).isDirectory();
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ function sourcePath(file) {
47
+ return join(SOURCE_ROOT, ...file.split("/"));
48
+ }
49
+ function sha256(data) {
50
+ return createHash("sha256").update(data).digest("hex");
51
+ }
52
+ function sourceFiles() {
53
+ const expected = new Set(CODEX_SKILL_FILES);
54
+ const expectedDirectories = new Set(["agents", "references"]);
55
+ let rootStat;
56
+ try {
57
+ rootStat = lstatSync(SOURCE_ROOT);
58
+ }
59
+ catch {
60
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill assets are unavailable");
61
+ }
62
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
63
+ localError("CODEX_SKILL_SYMLINK", "Codex Skill assets are not a regular directory");
64
+ }
65
+ const walk = (directory, prefix) => {
66
+ let entries;
67
+ try {
68
+ entries = readdirSync(directory, { withFileTypes: true });
69
+ }
70
+ catch {
71
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill assets cannot be read");
72
+ }
73
+ for (const entry of entries) {
74
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
75
+ const full = join(directory, entry.name);
76
+ if (entry.isSymbolicLink()) {
77
+ localError("CODEX_SKILL_SYMLINK", "Codex Skill assets contain a symbolic link");
78
+ }
79
+ if (entry.isDirectory()) {
80
+ if (!expectedDirectories.has(rel)) {
81
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill assets contain an unsupported entry");
82
+ }
83
+ walk(full, rel);
84
+ continue;
85
+ }
86
+ if (!entry.isFile() || !expected.has(rel)) {
87
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill assets contain an unsupported entry");
88
+ }
89
+ }
90
+ };
91
+ walk(SOURCE_ROOT, "");
92
+ const output = new Map();
93
+ for (const file of CODEX_SKILL_FILES) {
94
+ const path = sourcePath(file);
95
+ let stat;
96
+ try {
97
+ stat = lstatSync(path);
98
+ }
99
+ catch {
100
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill assets are incomplete");
101
+ }
102
+ if (stat.isSymbolicLink())
103
+ localError("CODEX_SKILL_SYMLINK", "Codex Skill assets contain a symbolic link");
104
+ if (!stat.isFile())
105
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill assets are incomplete");
106
+ try {
107
+ output.set(file, readFileSync(path));
108
+ }
109
+ catch {
110
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill assets cannot be read");
111
+ }
112
+ }
113
+ return output;
114
+ }
115
+ function markerFor(files) {
116
+ const hashes = {};
117
+ for (const file of CODEX_SKILL_FILES)
118
+ hashes[file] = sha256(files.get(file));
119
+ return {
120
+ owner: "zhihu-ai-cli",
121
+ package: "@luckypiggy/zhihu-ai-cli",
122
+ skill: CODEX_SKILL_NAME,
123
+ schema: CODEX_SKILL_SCHEMA,
124
+ schema_version: CODEX_SKILL_SCHEMA,
125
+ version: CODEX_SKILL_VERSION,
126
+ files: hashes,
127
+ };
128
+ }
129
+ function homePath(environment) {
130
+ const value = environment.HOME?.trim();
131
+ return resolve(value || homedir());
132
+ }
133
+ function codexHomePath(environment, home) {
134
+ const value = environment.CODEX_HOME?.trim();
135
+ return resolve(value || join(home, ".codex"));
136
+ }
137
+ function rejectUnsafeTarget(pathValue, environment) {
138
+ if (typeof pathValue !== "string" || pathValue.length === 0 || pathValue.length > 4096 || CONTROL_PATTERN.test(pathValue)) {
139
+ localError("CODEX_SKILL_PATH_INVALID", "Codex Skill target path is invalid");
140
+ }
141
+ if (!isAbsolute(pathValue))
142
+ localError("CODEX_SKILL_PATH_INVALID", "Codex Skill target path must be absolute");
143
+ const target = resolve(pathValue);
144
+ if (basename(target) !== CODEX_SKILL_NAME)
145
+ localError("CODEX_SKILL_PATH_INVALID", "Codex Skill target must end with zhihu-aiops");
146
+ const home = homePath(environment);
147
+ const codexHome = codexHomePath(environment, home);
148
+ const forbidden = new Set([
149
+ "/",
150
+ home,
151
+ codexHome,
152
+ join(codexHome, "skills"),
153
+ join(home, ".codex", "skills"),
154
+ ].map((value) => resolve(value)));
155
+ if (forbidden.has(target))
156
+ localError("CODEX_SKILL_PATH_INVALID", "Codex Skill target is a protected directory");
157
+ return target;
158
+ }
159
+ function basename(pathValue) {
160
+ const parsed = parse(pathValue);
161
+ return parsed.base;
162
+ }
163
+ function assertNoSymlinkTarget(pathValue) {
164
+ try {
165
+ if (lstatSync(pathValue).isSymbolicLink()) {
166
+ localError("CODEX_SKILL_SYMLINK", "Codex Skill target is a symbolic link");
167
+ }
168
+ }
169
+ catch (error) {
170
+ if (error instanceof CodexSkillError)
171
+ throw error;
172
+ if (!isNotFound(error))
173
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill target cannot be inspected");
174
+ }
175
+ }
176
+ function profileSkillDirectory(options, environment) {
177
+ if (options.codexSkillDir !== undefined)
178
+ return options.codexSkillDir;
179
+ let snapshot;
180
+ try {
181
+ snapshot = readConfigSnapshot(environment, options.profile);
182
+ }
183
+ catch (error) {
184
+ if (error instanceof ConfigError)
185
+ throw error;
186
+ localError("CODEX_SKILL_IO_ERROR", "Profile configuration cannot be read");
187
+ }
188
+ let profile;
189
+ try {
190
+ profile = validateProfile(snapshot.profile);
191
+ }
192
+ catch (error) {
193
+ if (error instanceof ConfigError)
194
+ throw error;
195
+ localError("CODEX_SKILL_IO_ERROR", "Profile configuration cannot be read");
196
+ }
197
+ return profile.codex_skill_dir;
198
+ }
199
+ export function resolveCodexSkillTarget(options = {}) {
200
+ const environment = options.environment ?? process.env;
201
+ const home = homePath(environment);
202
+ const codexHome = codexHomePath(environment, home);
203
+ const profileDir = profileSkillDirectory(options, environment);
204
+ const candidate = options.codexSkillDir
205
+ ?? profileDir
206
+ ?? join(codexHome, "skills", CODEX_SKILL_NAME);
207
+ const target = rejectUnsafeTarget(candidate, environment);
208
+ assertNoSymlinkTarget(target);
209
+ return target;
210
+ }
211
+ function markerPath(target) {
212
+ return join(target, CODEX_SKILL_MARKER);
213
+ }
214
+ function validMarker(value) {
215
+ if (!value || typeof value !== "object" || Array.isArray(value))
216
+ return false;
217
+ const object = value;
218
+ const allowedKeys = new Set(["owner", "package", "skill", "schema", "schema_version", "version", "files"]);
219
+ if (Object.keys(object).some((key) => !allowedKeys.has(key)))
220
+ return false;
221
+ if (object.owner !== "zhihu-ai-cli"
222
+ || object.package !== "@luckypiggy/zhihu-ai-cli"
223
+ || object.skill !== CODEX_SKILL_NAME
224
+ || object.schema !== CODEX_SKILL_SCHEMA
225
+ || object.schema_version !== CODEX_SKILL_SCHEMA
226
+ || typeof object.version !== "string"
227
+ || object.version.length < 1
228
+ || !object.files
229
+ || typeof object.files !== "object"
230
+ || Array.isArray(object.files))
231
+ return false;
232
+ const files = object.files;
233
+ const keys = Object.keys(files).sort();
234
+ const expected = [...CODEX_SKILL_FILES].sort();
235
+ if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index]))
236
+ return false;
237
+ if (keys.some((key) => typeof files[key] !== "string" || !SHA256_PATTERN.test(files[key])))
238
+ return false;
239
+ return true;
240
+ }
241
+ function readMarker(target) {
242
+ const path = markerPath(target);
243
+ let stat;
244
+ try {
245
+ stat = lstatSync(path);
246
+ }
247
+ catch (error) {
248
+ if (isNotFound(error))
249
+ return null;
250
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill marker cannot be inspected");
251
+ }
252
+ if (stat.isSymbolicLink())
253
+ localError("CODEX_SKILL_SYMLINK", "Codex Skill marker is a symbolic link");
254
+ if (!stat.isFile())
255
+ localError("CODEX_SKILL_FOREIGN", "Codex Skill target is not managed by this CLI");
256
+ let value;
257
+ try {
258
+ value = JSON.parse(readFileSync(path, "utf8"));
259
+ }
260
+ catch {
261
+ localError("CODEX_SKILL_FOREIGN", "Codex Skill target is not managed by this CLI");
262
+ }
263
+ if (!validMarker(value))
264
+ localError("CODEX_SKILL_FOREIGN", "Codex Skill target is not managed by this CLI");
265
+ return value;
266
+ }
267
+ function regularManagedState(target, marker) {
268
+ let state = "complete";
269
+ for (const file of CODEX_SKILL_FILES) {
270
+ const path = join(target, ...file.split("/"));
271
+ let stat;
272
+ try {
273
+ stat = lstatSync(path);
274
+ }
275
+ catch (error) {
276
+ if (!isNotFound(error))
277
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill file cannot be inspected");
278
+ state = "modified";
279
+ continue;
280
+ }
281
+ if (stat.isSymbolicLink())
282
+ localError("CODEX_SKILL_SYMLINK", "Codex Skill managed file is a symbolic link");
283
+ if (!stat.isFile()) {
284
+ state = "modified";
285
+ continue;
286
+ }
287
+ try {
288
+ if (sha256(readFileSync(path)) !== marker.files[file])
289
+ state = "modified";
290
+ }
291
+ catch {
292
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill file cannot be read");
293
+ }
294
+ }
295
+ return state;
296
+ }
297
+ function ensureParent(target) {
298
+ const parent = dirname(target);
299
+ try {
300
+ mkdirSync(parent, { recursive: true, mode: 0o700 });
301
+ }
302
+ catch {
303
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill target directory cannot be created");
304
+ }
305
+ if (!isDirectoryEntry(parent))
306
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill target parent is not a directory");
307
+ }
308
+ function temporaryDirectory(parent, label) {
309
+ try {
310
+ return mkdtempSync(join(parent, `.${label}.`));
311
+ }
312
+ catch {
313
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill temporary directory cannot be created");
314
+ }
315
+ }
316
+ function writeSkillDirectory(destination, files, marker) {
317
+ try {
318
+ for (const file of CODEX_SKILL_FILES) {
319
+ const path = join(destination, ...file.split("/"));
320
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
321
+ writeFileSync(path, files.get(file), { mode: 0o644 });
322
+ chmodSync(path, 0o644);
323
+ }
324
+ const path = markerPath(destination);
325
+ writeFileSync(path, `${JSON.stringify(marker, null, 2)}\n`, { mode: 0o600 });
326
+ chmodSync(path, 0o600);
327
+ }
328
+ catch {
329
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill files cannot be written");
330
+ }
331
+ }
332
+ function copyUnknownEntries(source, destination, prefix = "") {
333
+ let entries;
334
+ try {
335
+ entries = readdirSync(source, { withFileTypes: true });
336
+ }
337
+ catch {
338
+ localError("CODEX_SKILL_IO_ERROR", "Existing Codex Skill files cannot be read");
339
+ }
340
+ const managed = new Set([...CODEX_SKILL_FILES, CODEX_SKILL_MARKER]);
341
+ for (const entry of entries) {
342
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
343
+ if (managed.has(rel))
344
+ continue;
345
+ const sourcePathValue = join(source, entry.name);
346
+ const destinationPath = join(destination, entry.name);
347
+ let stat;
348
+ try {
349
+ stat = lstatSync(sourcePathValue);
350
+ }
351
+ catch {
352
+ localError("CODEX_SKILL_IO_ERROR", "Existing Codex Skill files cannot be inspected");
353
+ }
354
+ if (stat.isSymbolicLink()) {
355
+ try {
356
+ symlinkSync(readlinkSync(sourcePathValue), destinationPath);
357
+ }
358
+ catch {
359
+ localError("CODEX_SKILL_IO_ERROR", "Existing Codex Skill files cannot be preserved");
360
+ }
361
+ continue;
362
+ }
363
+ if (stat.isDirectory()) {
364
+ try {
365
+ mkdirSync(destinationPath, { recursive: false, mode: 0o700 });
366
+ }
367
+ catch {
368
+ localError("CODEX_SKILL_IO_ERROR", "Existing Codex Skill files cannot be preserved");
369
+ }
370
+ copyUnknownEntries(sourcePathValue, destinationPath, rel);
371
+ continue;
372
+ }
373
+ if (!stat.isFile())
374
+ localError("CODEX_SKILL_IO_ERROR", "Existing Codex Skill files cannot be preserved");
375
+ try {
376
+ writeFileSync(destinationPath, readFileSync(sourcePathValue), { mode: 0o644 });
377
+ }
378
+ catch {
379
+ localError("CODEX_SKILL_IO_ERROR", "Existing Codex Skill files cannot be preserved");
380
+ }
381
+ }
382
+ }
383
+ function removePathIfPresent(path) {
384
+ try {
385
+ rmSync(path, { recursive: true, force: true });
386
+ }
387
+ catch {
388
+ // Cleanup is best effort; the caller reports the primary installation error.
389
+ }
390
+ }
391
+ function atomicReplace(target, temporary) {
392
+ const parent = dirname(target);
393
+ const backup = join(parent, `.${CODEX_SKILL_NAME}.backup-${process.pid}-${randomBytes(6).toString("hex")}`);
394
+ let oldMoved = false;
395
+ let newMoved = false;
396
+ try {
397
+ renameSync(target, backup);
398
+ oldMoved = true;
399
+ renameSync(temporary, target);
400
+ newMoved = true;
401
+ removePathIfPresent(backup);
402
+ }
403
+ catch {
404
+ if (newMoved)
405
+ removePathIfPresent(target);
406
+ if (oldMoved) {
407
+ try {
408
+ renameSync(backup, target);
409
+ }
410
+ catch {
411
+ // The original location is retained if the filesystem refuses recovery.
412
+ }
413
+ }
414
+ removePathIfPresent(temporary);
415
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill installation could not be completed");
416
+ }
417
+ }
418
+ function inspectTarget(target) {
419
+ let stat;
420
+ try {
421
+ stat = lstatSync(target);
422
+ }
423
+ catch (error) {
424
+ if (isNotFound(error))
425
+ return "missing";
426
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill target cannot be inspected");
427
+ }
428
+ if (stat.isSymbolicLink())
429
+ localError("CODEX_SKILL_SYMLINK", "Codex Skill target is a symbolic link");
430
+ if (!stat.isDirectory())
431
+ localError("CODEX_SKILL_FOREIGN", "Codex Skill target is not a directory");
432
+ return "directory";
433
+ }
434
+ export function installCodexSkill(options = {}) {
435
+ const target = resolveCodexSkillTarget(options);
436
+ const files = sourceFiles();
437
+ const marker = markerFor(files);
438
+ ensureParent(target);
439
+ const state = inspectTarget(target);
440
+ if (state === "missing") {
441
+ const temporary = temporaryDirectory(dirname(target), CODEX_SKILL_NAME);
442
+ try {
443
+ writeSkillDirectory(temporary, files, marker);
444
+ renameSync(temporary, target);
445
+ }
446
+ catch {
447
+ removePathIfPresent(temporary);
448
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill installation could not be completed");
449
+ }
450
+ return { action: "install", status: "installed", skill: CODEX_SKILL_NAME, version: CODEX_SKILL_VERSION, target };
451
+ }
452
+ const existingMarker = readMarker(target);
453
+ if (!existingMarker)
454
+ localError("CODEX_SKILL_FOREIGN", "Codex Skill target is not managed by this CLI");
455
+ const managedState = regularManagedState(target, existingMarker);
456
+ if (managedState === "modified" && !options.force) {
457
+ localError("CODEX_SKILL_MODIFIED", "Codex Skill files were modified; use --force to replace them");
458
+ }
459
+ const sourceMatches = existingMarker.version === CODEX_SKILL_VERSION
460
+ && CODEX_SKILL_FILES.every((file) => existingMarker.files[file] === marker.files[file]);
461
+ if (managedState === "complete" && sourceMatches) {
462
+ return { action: "install", status: "unchanged", skill: CODEX_SKILL_NAME, version: CODEX_SKILL_VERSION, target };
463
+ }
464
+ const temporary = temporaryDirectory(dirname(target), CODEX_SKILL_NAME);
465
+ try {
466
+ copyUnknownEntries(target, temporary);
467
+ writeSkillDirectory(temporary, files, marker);
468
+ }
469
+ catch (error) {
470
+ removePathIfPresent(temporary);
471
+ throw error;
472
+ }
473
+ atomicReplace(target, temporary);
474
+ return { action: "install", status: "updated", skill: CODEX_SKILL_NAME, version: CODEX_SKILL_VERSION, target };
475
+ }
476
+ function removeManagedFile(path) {
477
+ let stat;
478
+ try {
479
+ stat = lstatSync(path);
480
+ }
481
+ catch (error) {
482
+ if (error instanceof CodexSkillError)
483
+ throw error;
484
+ if (isNotFound(error))
485
+ return false;
486
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill file cannot be inspected");
487
+ }
488
+ if (stat.isSymbolicLink())
489
+ localError("CODEX_SKILL_SYMLINK", "Codex Skill managed file is a symbolic link");
490
+ if (!stat.isFile())
491
+ return false;
492
+ try {
493
+ unlinkSync(path);
494
+ }
495
+ catch {
496
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill file cannot be removed");
497
+ }
498
+ return true;
499
+ }
500
+ function removeEmptyManagedDirectories(target) {
501
+ for (const directory of ["agents", "references"]) {
502
+ const path = join(target, directory);
503
+ try {
504
+ if (lstatSync(path).isDirectory())
505
+ rmdirSync(path);
506
+ }
507
+ catch (error) {
508
+ if (error instanceof CodexSkillError)
509
+ throw error;
510
+ if (!isNotFound(error) && !(error && typeof error === "object" && "code" in error && error.code === "ENOTEMPTY")) {
511
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill directory cannot be removed");
512
+ }
513
+ }
514
+ }
515
+ }
516
+ export function uninstallCodexSkill(options = {}) {
517
+ const target = resolveCodexSkillTarget(options);
518
+ const state = inspectTarget(target);
519
+ if (state === "missing") {
520
+ return { action: "uninstall", status: "not_found", skill: CODEX_SKILL_NAME, version: CODEX_SKILL_VERSION, target };
521
+ }
522
+ const marker = readMarker(target);
523
+ if (!marker)
524
+ localError("CODEX_SKILL_FOREIGN", "Codex Skill target is not managed by this CLI");
525
+ const managedState = regularManagedState(target, marker);
526
+ if (managedState === "modified" && !options.force) {
527
+ localError("CODEX_SKILL_MODIFIED", "Codex Skill files were modified; use --force to remove them");
528
+ }
529
+ for (const file of CODEX_SKILL_FILES)
530
+ removeManagedFile(join(target, ...file.split("/")));
531
+ removeManagedFile(markerPath(target));
532
+ removeEmptyManagedDirectories(target);
533
+ let preserved = false;
534
+ try {
535
+ rmdirSync(target);
536
+ }
537
+ catch (error) {
538
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOTEMPTY")
539
+ preserved = true;
540
+ else if (!isNotFound(error))
541
+ localError("CODEX_SKILL_IO_ERROR", "Codex Skill directory cannot be removed");
542
+ }
543
+ return {
544
+ action: "uninstall",
545
+ status: preserved ? "preserved" : "removed",
546
+ skill: CODEX_SKILL_NAME,
547
+ version: marker.version,
548
+ target,
549
+ };
550
+ }
551
+ /** Read-only status used by `zhihu doctor`; it never changes the target. */
552
+ export function inspectCodexSkill(options = {}) {
553
+ try {
554
+ const target = resolveCodexSkillTarget(options);
555
+ if (inspectTarget(target) === "missing")
556
+ return { state: "not_found", target };
557
+ const marker = readMarker(target);
558
+ if (!marker)
559
+ return { state: "foreign", target };
560
+ return { state: regularManagedState(target, marker) === "complete" ? "managed" : "modified", target };
561
+ }
562
+ catch (error) {
563
+ if (error instanceof CodexSkillError) {
564
+ if (error.code === "CODEX_SKILL_SYMLINK")
565
+ return { state: "symlink" };
566
+ if (error.code === "CODEX_SKILL_FOREIGN")
567
+ return { state: "foreign" };
568
+ if (error.code === "CODEX_SKILL_MODIFIED")
569
+ return { state: "modified" };
570
+ }
571
+ return { state: "unavailable" };
572
+ }
573
+ }