@visual-z/xeno 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +143 -33
- package/docs/install.md +18 -6
- package/docs/providers.md +147 -0
- package/package.json +8 -2
- package/src/apps/cli.ts +42 -14
- package/src/apps/commands.ts +32 -4
- package/src/apps/completions.ts +9 -6
- package/src/apps/export-cli.ts +24 -0
- package/src/apps/extension-ui.ts +330 -0
- package/src/apps/input.ts +16 -1
- package/src/apps/model-catalog.ts +42 -0
- package/src/apps/project-trust.ts +49 -0
- package/src/apps/render.ts +1 -0
- package/src/apps/resource-browser.ts +75 -0
- package/src/apps/resources.ts +25 -0
- package/src/apps/session-browser.ts +113 -0
- package/src/apps/session-picker.ts +55 -0
- package/src/apps/shell.ts +1177 -378
- package/src/apps/startup.ts +400 -268
- package/src/core/action-defaults.ts +306 -0
- package/src/core/attachment-input.ts +17 -0
- package/src/core/catalog-merge.ts +19 -0
- package/src/core/event-validation.ts +19 -2
- package/src/core/events.ts +45 -2
- package/src/core/export.ts +6 -2
- package/src/core/extension-flags.ts +14 -0
- package/src/core/images.ts +46 -0
- package/src/core/json-event.ts +2 -0
- package/src/core/keymap.ts +43 -119
- package/src/core/model-replay.ts +22 -0
- package/src/core/model-scope.ts +53 -0
- package/src/core/packages.ts +47 -0
- package/src/core/project.ts +21 -3
- package/src/core/prompts.ts +33 -5
- package/src/core/queue.ts +18 -0
- package/src/core/resource-selection.ts +36 -0
- package/src/{runtime → core}/resume.ts +15 -22
- package/src/core/session-archive.ts +4 -0
- package/src/core/session-browser.ts +82 -0
- package/src/core/session-entry.ts +56 -0
- package/src/core/session-position.ts +26 -0
- package/src/core/settings.ts +169 -154
- package/src/core/theme.ts +15 -6
- package/src/core/thinking.ts +26 -0
- package/src/core/tokens.ts +2 -0
- package/src/core/transcript.ts +15 -5
- package/src/runtime/auth.ts +116 -11
- package/src/runtime/catalog.ts +54 -151
- package/src/runtime/clipboard.ts +114 -0
- package/src/runtime/extension-loader.ts +37 -0
- package/src/runtime/extension-ui.ts +55 -0
- package/src/runtime/extensions.ts +273 -0
- package/src/runtime/images.ts +76 -0
- package/src/runtime/loop.ts +213 -83
- package/src/runtime/model-config.ts +232 -0
- package/src/runtime/model-data.LICENSE +27 -0
- package/src/runtime/model-data.ts +21 -0
- package/src/runtime/model-replay.ts +10 -0
- package/src/runtime/model-scope.ts +20 -0
- package/src/runtime/model-sources.ts +151 -0
- package/src/runtime/model.ts +74 -11
- package/src/runtime/models-fetch.ts +6 -104
- package/src/runtime/oauth.ts +4 -3
- package/src/runtime/openai-responses.ts +12 -4
- package/src/runtime/openai.ts +13 -4
- package/src/runtime/package-manager.ts +125 -0
- package/src/runtime/pi-provider.ts +235 -0
- package/src/runtime/progress.ts +26 -0
- package/src/runtime/provider-access.ts +21 -0
- package/src/runtime/provider-protocols.ts +29 -0
- package/src/runtime/provider-request.ts +73 -0
- package/src/runtime/provider.ts +26 -6
- package/src/runtime/rich-text.ts +36 -0
- package/src/runtime/tools.ts +41 -5
- package/src/store/command.ts +7 -1
- package/src/store/context-files.ts +1 -1
- package/src/store/log.ts +65 -0
- package/src/store/models.ts +28 -9
- package/src/store/observations.ts +4 -2
- package/src/store/packages.ts +98 -0
- package/src/store/recovery.ts +8 -0
- package/src/store/resource-index.ts +170 -0
- package/src/store/resources.ts +75 -142
- package/src/store/session-archive.ts +162 -0
- package/src/store/session-browser.ts +68 -0
- package/src/store/session-position.ts +27 -0
- package/src/store/sessions.ts +87 -66
- package/src/store/trust.ts +13 -0
- package/src/tui/chat-viewport.ts +47 -0
- package/src/tui/editor.ts +62 -15
- package/src/tui/host.ts +50 -9
- package/src/tui/input.ts +8 -2
- package/src/tui/select.ts +24 -6
- package/src/tui/settings.ts +4 -0
- package/src/tui/system-theme.ts +700 -0
- package/src/tui/transcript.ts +63 -11
- package/src/tui/tree.ts +161 -95
- package/src/vendor/pi-rich/LICENSE.upstream +21 -0
- package/src/vendor/pi-rich/UPSTREAM.json +17 -0
- package/src/vendor/pi-rich/clipboard-command.ts +44 -0
- package/src/vendor/pi-rich/exif-orientation.ts +182 -0
- package/src/vendor/pi-rich/html.ts +51 -0
- package/src/vendor/pi-rich/image-convert.ts +49 -0
- package/src/vendor/pi-rich/image-resize-core.ts +164 -0
- package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
- package/src/vendor/pi-rich/image-resize.ts +123 -0
- package/src/vendor/pi-rich/mime.ts +116 -0
- package/src/vendor/pi-rich/photon.ts +139 -0
- package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
- package/src/vendor/pi-tui/UPSTREAM.json +50 -0
- package/src/vendor/pi-tui/VENDOR.md +7 -5
- package/src/vendor/pi-tui/autocomplete.ts +35 -10
- package/src/vendor/pi-tui/colors.ts +367 -0
- package/src/vendor/pi-tui/components/box.ts +8 -9
- package/src/vendor/pi-tui/components/editor.ts +4 -2
- package/src/vendor/pi-tui/components/markdown.ts +8 -2
- package/src/vendor/pi-tui/index.ts +31 -1
- package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
- package/src/vendor/pi-tui/native/clipboard.h +208 -0
- package/src/vendor/pi-tui/native/darwin/README.md +22 -0
- package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
- package/src/vendor/pi-tui/native/linux/README.md +23 -0
- package/src/vendor/pi-tui/native/linux/build.sh +65 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
- package/src/vendor/pi-tui/native/napi.h +91 -0
- package/src/vendor/pi-tui/native/win32/README.md +31 -0
- package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
- package/src/vendor/pi-tui/native-platform.ts +2 -0
- package/src/vendor/pi-tui/oklab.ts +233 -0
- package/src/vendor/pi-tui/terminal-colors.ts +26 -8
- package/src/vendor/pi-tui/terminal-image.ts +43 -8
- package/src/vendor/pi-tui/terminal.ts +16 -9
- package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
- package/src/vendor/pi-tui/tui.ts +112 -75
- package/src/vendor/pi-tui/utils.ts +89 -49
- package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Package storage is local metadata; only runtime/package-manager may install sources. */
|
|
2
|
+
import { mkdir, stat } from "node:fs/promises";
|
|
3
|
+
import { join, resolve, relative, isAbsolute, sep } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { readJsonFile } from "./config.ts";
|
|
7
|
+
import { atomicWrite } from "./atomic.ts";
|
|
8
|
+
import { serialized } from "./serial.ts";
|
|
9
|
+
import { packageConfig, packageDeclaration, parsePackageSource, type PackageDeclaration, type PackageSource } from "../core/packages.ts";
|
|
10
|
+
import type { ResourceKind } from "../core/resource-selection.ts";
|
|
11
|
+
|
|
12
|
+
export type PackageScope = "user" | "project" | "cli";
|
|
13
|
+
export interface PackageRecord { source: string; identity: string; path: string; version?: string; revision?: string }
|
|
14
|
+
export interface PackageRoot {
|
|
15
|
+
source: PackageSource; declaration: Exclude<PackageDeclaration,string>; scope: PackageScope; base: string;
|
|
16
|
+
root?: string; delta?: Exclude<PackageDeclaration,string>; trusted: boolean;
|
|
17
|
+
}
|
|
18
|
+
export function packageSource(source: string, base: string): PackageSource {
|
|
19
|
+
return parsePackageSource(source, path => resolve(base, path.startsWith("~/") ? join(homedir(),path.slice(2)) : path === "~" ? homedir() : path));
|
|
20
|
+
}
|
|
21
|
+
export function packageKey(source: PackageSource): string {
|
|
22
|
+
// Config identity omits versions, but installed references must retain them:
|
|
23
|
+
// publishing a new pin cannot invalidate the old declaration before its atomic write.
|
|
24
|
+
const version=source.kind==="npm"?source.version:source.kind==="git"?source.ref:undefined;
|
|
25
|
+
return createHash("sha256").update(source.id+"\0"+(version??"")).digest("hex");
|
|
26
|
+
}
|
|
27
|
+
export function packageHome(base: string): string { return join(base,"packages"); }
|
|
28
|
+
export function managedPackagePath(base: string, path: string): string {
|
|
29
|
+
const home = packageHome(base), target = resolve(home,path), rel = relative(home,target);
|
|
30
|
+
if (!rel || rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) throw Error("Package path escapes its managed directory");
|
|
31
|
+
return target;
|
|
32
|
+
}
|
|
33
|
+
export async function packageRecords(base: string): Promise<Record<string, PackageRecord>> {
|
|
34
|
+
const read = await readJsonFile<Record<string,PackageRecord>>(join(packageHome(base),"installed.json"));
|
|
35
|
+
if (read.error) throw Error(read.error);
|
|
36
|
+
if (read.value && (typeof read.value !== "object" || Array.isArray(read.value))) throw Error("Invalid package installation index");
|
|
37
|
+
const records=read.value ?? {};
|
|
38
|
+
for(const record of Object.values(records)) {
|
|
39
|
+
if (!record || typeof record.source !== "string" || typeof record.identity !== "string" || typeof record.path !== "string") throw Error("Invalid package installation entry");
|
|
40
|
+
managedPackagePath(base,record.path);
|
|
41
|
+
}
|
|
42
|
+
return records;
|
|
43
|
+
}
|
|
44
|
+
export async function savePackageRecord(base: string, source: PackageSource, record: PackageRecord | undefined): Promise<void> {
|
|
45
|
+
const path=join(packageHome(base),"installed.json");
|
|
46
|
+
await serialized(path,async()=>{const records=await packageRecords(base); if(record) records[packageKey(source)]=record; else delete records[packageKey(source)]; await mkdir(packageHome(base),{recursive:true}); await atomicWrite(path,JSON.stringify(records,null,2)+"\n");});
|
|
47
|
+
}
|
|
48
|
+
export async function removePackageRecords(base:string,identity:string):Promise<string[]> {
|
|
49
|
+
const path=join(packageHome(base),"installed.json");
|
|
50
|
+
return serialized(path,async()=>{const records=await packageRecords(base),keys=Object.keys(records).filter(key=>records[key]!.identity===identity);for(const key of keys)delete records[key];await mkdir(packageHome(base),{recursive:true});await atomicWrite(path,JSON.stringify(records,null,2)+"\n");return keys;});
|
|
51
|
+
}
|
|
52
|
+
export async function installedPackage(source: PackageSource, base: string): Promise<string | undefined> {
|
|
53
|
+
if(source.kind==="local") return await stat(source.path).then(()=>source.path,()=>undefined);
|
|
54
|
+
const record=(await packageRecords(base))[packageKey(source)];
|
|
55
|
+
if (!record || record.identity!==source.id) return undefined;
|
|
56
|
+
const installed=packageSource(record.source,base);
|
|
57
|
+
if(source.kind==="npm"&&(installed.kind!=="npm"||installed.version!==source.version)||source.kind==="git"&&(installed.kind!=="git"||installed.ref!==source.ref))return undefined;
|
|
58
|
+
const path=managedPackagePath(base,record.path); return await stat(path).then(s=>s.isDirectory()?path:undefined,()=>undefined);
|
|
59
|
+
}
|
|
60
|
+
export async function configuredPackages(base: string): Promise<PackageDeclaration[]> {
|
|
61
|
+
const read=await readJsonFile<Record<string,unknown>>(join(base,"settings.json")); if(read.error)throw Error(read.error);
|
|
62
|
+
if(read.value!==null&&(typeof read.value!=="object"||Array.isArray(read.value)))throw Error("Settings must be an object");
|
|
63
|
+
const list=read.value?.["packages"] ?? []; if(!Array.isArray(list)||!list.every(packageDeclaration))throw Error("Invalid packages configuration"); return list;
|
|
64
|
+
}
|
|
65
|
+
export async function savePackageDeclaration(base: string, source: PackageSource, declaration: PackageDeclaration | undefined): Promise<void> {
|
|
66
|
+
const file=join(base,"settings.json");
|
|
67
|
+
await serialized(file,async()=>{
|
|
68
|
+
const read=await readJsonFile<Record<string,unknown>>(file); if(read.error)throw Error(read.error);
|
|
69
|
+
if(read.value!==null&&(typeof read.value!=="object"||Array.isArray(read.value)))throw Error("Settings must be an object");
|
|
70
|
+
const previous=read.value?.["packages"]??[]; if(!Array.isArray(previous)||!previous.every(packageDeclaration))throw Error("Invalid packages configuration");
|
|
71
|
+
const packages=previous.filter(entry=>packageSource(packageConfig(entry).source,base).id!==source.id);
|
|
72
|
+
if(declaration!==undefined)packages.push(declaration);
|
|
73
|
+
await mkdir(base,{recursive:true}); await atomicWrite(file,JSON.stringify({...read.value,packages},null,2)+"\n");
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export async function packageRoots(user: readonly PackageDeclaration[], project: readonly PackageDeclaration[], opts:{userDir:string;projectDir:string;trusted:boolean}, diagnostics:string[]):Promise<PackageRoot[]> {
|
|
77
|
+
const roots=new Map<string,PackageRoot>();
|
|
78
|
+
for(const scope of ["user","project"] as const) for(const entry of scope==="user"?user:project) {
|
|
79
|
+
try {
|
|
80
|
+
const declaration=packageConfig(entry),base=scope==="user"?opts.userDir:opts.projectDir,source=packageSource(declaration.source,base),existing=roots.get(source.id);
|
|
81
|
+
if(scope==="project"&&existing&&(!opts.trusted||declaration.autoload===false)) { if(opts.trusted)existing.delta=declaration; continue; }
|
|
82
|
+
const root=await installedPackage(source,base);
|
|
83
|
+
if(!root)diagnostics.push(`Package ${source.source} is not installed; run xeno install ${source.source}${scope==="project"?" --local":""}`);
|
|
84
|
+
roots.set(source.id,{source,declaration,scope,base,...(root?{root}:{}),trusted:scope==="user"||opts.trusted});
|
|
85
|
+
} catch(error){diagnostics.push(String(error));}
|
|
86
|
+
}
|
|
87
|
+
return [...roots.values()];
|
|
88
|
+
}
|
|
89
|
+
export async function packageManifest(root: string): Promise<Partial<Record<ResourceKind, readonly string[]>>> {
|
|
90
|
+
if(await stat(root).then(s=>s.isFile(),()=>false)) return {extensions:[root],skills:[],prompts:[],themes:[]};
|
|
91
|
+
const read=await readJsonFile<{xeno?:unknown;pi?:unknown}>(join(root,"package.json"));if(read.error)throw Error(read.error);
|
|
92
|
+
const value=read.value?.xeno??read.value?.pi;
|
|
93
|
+
if(value===undefined)return {extensions:["extensions"],skills:["skills"],prompts:["prompts"],themes:["themes"]};
|
|
94
|
+
if(!value||typeof value!=="object"||Array.isArray(value))throw Error("Invalid package resource manifest");
|
|
95
|
+
const result:Partial<Record<ResourceKind,readonly string[]>>={};
|
|
96
|
+
for(const kind of ["extensions","skills","prompts","themes"] as const){const paths=(value as Record<string,unknown>)[kind]??[];if(!Array.isArray(paths)||paths.some(path=>typeof path!=="string"))throw Error("Invalid package manifest " + kind);result[kind]=paths;}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
package/src/store/recovery.ts
CHANGED
|
@@ -34,6 +34,14 @@ export async function recoverCut(dir: string, cursor: Cursor): Promise<Cursor> {
|
|
|
34
34
|
next_node: Math.max(cursor.next_node, cut.next_node + 1, ...nodes.map((n) => n + 1)),
|
|
35
35
|
model: cut.next_model ?? cursor.model, updated_at: cut.at,
|
|
36
36
|
};
|
|
37
|
+
// Old cut records did not identify providers. Do not carry the source's
|
|
38
|
+
// provider onto a different legacy model and manufacture an identity.
|
|
39
|
+
if (cut.next_provider !== undefined) next.provider = cut.next_provider;
|
|
40
|
+
else if (next.model !== cursor.model) delete next.provider;
|
|
41
|
+
if (cut.next_thinking !== undefined) next.thinking = cut.next_thinking;
|
|
42
|
+
if (cut.next_context_ref !== undefined) next.context_ref = cut.next_context_ref;
|
|
43
|
+
delete next.at_event;
|
|
44
|
+
delete next.needs_model_checkpoint;
|
|
37
45
|
await writeCursor(dir, next);
|
|
38
46
|
return next;
|
|
39
47
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** One resource inventory for loading and configuration; paths carry their declaring scope. */
|
|
2
|
+
import { readdir, realpath, stat, mkdir } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
5
|
+
import { configDir, readJsonFile } from "./config.ts";
|
|
6
|
+
import { atomicWrite } from "./atomic.ts";
|
|
7
|
+
import { serialized } from "./serial.ts";
|
|
8
|
+
import { parseSettings, type Settings } from "../core/settings.ts";
|
|
9
|
+
import { parseResourcePattern, resourceOverride, updateResourcePatterns, type ResourceEntry, type ResourceKind, type ResourceOverride } from "../core/resource-selection.ts";
|
|
10
|
+
import { packageResourceEnabled, packageConfig } from "../core/packages.ts";
|
|
11
|
+
import { packageRoots, packageManifest, packageSource, configuredPackages, savePackageDeclaration, type PackageRoot } from "./packages.ts";
|
|
12
|
+
|
|
13
|
+
export interface ResourceOptions {
|
|
14
|
+
workspace: string; trusted: boolean; configDir?: string;
|
|
15
|
+
noSkills?: boolean; noPromptTemplates?: boolean;
|
|
16
|
+
noThemes?: boolean; themePaths?: readonly string[];
|
|
17
|
+
skillPaths?: readonly string[]; promptTemplatePaths?: readonly string[];
|
|
18
|
+
noExtensions?: boolean; extensionPaths?: readonly string[];
|
|
19
|
+
packagePaths?: readonly { source: string; path: string }[];
|
|
20
|
+
local?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface ResourceIndex {
|
|
23
|
+
entries: ResourceEntry[]; diagnostics: string[]; user: Settings; project: Settings;
|
|
24
|
+
userDir: string; projectDir: string; trusted: boolean;
|
|
25
|
+
}
|
|
26
|
+
export function resourcePath(path: string, base: string): string {
|
|
27
|
+
const expanded = path === "~" ? homedir() : path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
|
|
28
|
+
return resolve(base, expanded);
|
|
29
|
+
}
|
|
30
|
+
function matches(path: string, pattern: string, base: string): boolean {
|
|
31
|
+
const absolute = resourcePath(pattern, base);
|
|
32
|
+
return path === absolute || path.startsWith(absolute + sep) || new Bun.Glob(absolute.replace(/\\/g, "/")).match(path.replace(/\\/g, "/"));
|
|
33
|
+
}
|
|
34
|
+
export async function discoverResources(opts: ResourceOptions): Promise<ResourceIndex> {
|
|
35
|
+
const userDir = opts.configDir ?? configDir(), projectDir = join(opts.workspace, ".xeno");
|
|
36
|
+
const index: ResourceIndex = { entries: [], diagnostics: [], user: {}, project: {}, userDir, projectDir, trusted: opts.trusted };
|
|
37
|
+
for (const scope of ["user", "project"] as const) {
|
|
38
|
+
const file = join(scope === "user" ? userDir : projectDir, "settings.json");
|
|
39
|
+
const read = await readJsonFile<unknown>(file);
|
|
40
|
+
if (read.error) index.diagnostics.push(read.error);
|
|
41
|
+
const parsed = parseSettings(read.value);
|
|
42
|
+
index.diagnostics.push(...parsed.diagnostics.map(message => `${file}: ${message}`));
|
|
43
|
+
// Global-only policy cannot be granted by the project it would trust.
|
|
44
|
+
const { defaultProjectTrust: _ignored, ...project } = parsed.settings;
|
|
45
|
+
index[scope] = scope === "project" ? project : parsed.settings;
|
|
46
|
+
}
|
|
47
|
+
const found = new Map<string, ResourceEntry>();
|
|
48
|
+
const packages=await packageRoots(index.user.packages??[],index.project.packages??[],index,index.diagnostics);
|
|
49
|
+
const explicitPackageRoots=new Set<string>();
|
|
50
|
+
for(const source of opts.extensionPaths??[]) {
|
|
51
|
+
const path=resourcePath(source,opts.workspace);
|
|
52
|
+
const manifest=await readJsonFile<{pi?:unknown;xeno?:unknown}>(join(path,"package.json"));
|
|
53
|
+
const conventional=(await Promise.all(["extensions","skills","prompts","themes"].map(kind=>stat(join(path,kind)).then(s=>s.isDirectory(),()=>false)))).some(Boolean);
|
|
54
|
+
if(manifest.value?.pi||manifest.value?.xeno||conventional) {
|
|
55
|
+
explicitPackageRoots.add(path); packages.push({source:packageSource(source,opts.workspace),declaration:{source},scope:"cli",base:opts.workspace,root:path,trusted:true});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for(const entry of opts.packagePaths??[])packages.push({source:packageSource(entry.source,opts.workspace),declaration:{source:entry.source},scope:"cli",base:opts.workspace,root:entry.path,trusted:true});
|
|
59
|
+
for (const kind of ["skills", "prompts", "themes", "extensions"] as const) {
|
|
60
|
+
const disabled = kind === "skills" ? opts.noSkills : kind === "prompts" ? opts.noPromptTemplates : kind === "extensions" ? opts.noExtensions : opts.noThemes;
|
|
61
|
+
for (const scope of ["user", "project"] as const) {
|
|
62
|
+
const base = scope === "user" ? userDir : projectDir;
|
|
63
|
+
const sources = [join(base, kind), ...(index[scope][kind] ?? []).map(parseResourcePattern).filter(p => p.enabled).map(p => resourcePath(p.pattern, base))];
|
|
64
|
+
for (const source of [...new Set(sources)]) for (const path of await resourceFiles(source, kind, index.diagnostics, source !== join(base, kind))) {
|
|
65
|
+
// A project override of an inherited path does not transfer resource ownership.
|
|
66
|
+
if (found.has(kind + ":" + path)) continue;
|
|
67
|
+
const global = resourceOverride(index.user[kind] ?? [], pattern => matches(path, pattern, userDir));
|
|
68
|
+
const local = resourceOverride(index.project[kind] ?? [], pattern => matches(path, pattern, projectDir));
|
|
69
|
+
const inherited = !disabled && global !== "skip";
|
|
70
|
+
const trusted = scope === "user" || opts.trusted;
|
|
71
|
+
const enabled = trusted && !disabled && (opts.trusted && local !== "inherit" ? local === "load" : inherited);
|
|
72
|
+
found.set(kind + ":" + path, { kind, path, name: kind === "skills" ? basename(dirname(path)) : basename(path).replace(/\.[^.]+$/, ""),
|
|
73
|
+
scope, source, enabled, inherited, override: local, trusted });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const cli = kind === "skills" ? opts.skillPaths : kind === "prompts" ? opts.promptTemplatePaths : kind === "extensions" ? opts.extensionPaths : opts.themePaths;
|
|
77
|
+
for (const source of cli ?? []) for (const path of explicitPackageRoots.has(resourcePath(source,opts.workspace)) ? [] : await resourceFiles(resourcePath(source, opts.workspace), kind, index.diagnostics, true)) {
|
|
78
|
+
found.set(kind + ":" + path, { kind, path, name: kind === "skills" ? basename(dirname(path)) : basename(path).replace(/\.[^.]+$/, ""),
|
|
79
|
+
scope: "cli", source, enabled: true, inherited: true, override: "inherit", trusted: true });
|
|
80
|
+
}
|
|
81
|
+
for(const pkg of packages) await addPackageResources(found,index,pkg,kind,!!disabled);
|
|
82
|
+
}
|
|
83
|
+
index.entries = [...found.values()];
|
|
84
|
+
return index;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function addPackageResources(found:Map<string,ResourceEntry>,index:ResourceIndex,pkg:PackageRoot,kind:ResourceKind,disabled:boolean):Promise<void> {
|
|
88
|
+
if(!pkg.root)return;
|
|
89
|
+
try {
|
|
90
|
+
const manifest=await packageManifest(pkg.root),patterns=manifest[kind]??[],candidates=new Set<string>();
|
|
91
|
+
for(const pattern of patterns.map(parseResourcePattern).filter(p=>p.enabled)) {
|
|
92
|
+
for(const path of await resourceFiles(resourcePath(pattern.pattern,pkg.root),kind,index.diagnostics))candidates.add(path);
|
|
93
|
+
}
|
|
94
|
+
for(const path of candidates) {
|
|
95
|
+
const match=(pattern:string,exact=false)=>exact?path===resourcePath(pattern,pkg.root!):matches(path,pattern,pkg.root!);
|
|
96
|
+
// The package manifest is the outer boundary; settings cannot expose undeclared files.
|
|
97
|
+
if(!packageResourceEnabled(patterns,match))continue;
|
|
98
|
+
const inherited=packageResourceEnabled(pkg.declaration[kind],match)&&pkg.declaration.autoload!==false;
|
|
99
|
+
const delta=packageResourceEnabled(pkg.delta?.[kind],match,inherited);
|
|
100
|
+
const global=resourceOverride(index.user[kind]??[],pattern=>matches(path,pattern,index.userDir));
|
|
101
|
+
const flat=resourceOverride(index.project[kind]??[],pattern=>matches(path,pattern,index.projectDir));
|
|
102
|
+
const local=flat!=="inherit"?flat:resourceOverride(pkg.delta?.[kind]??[],match);
|
|
103
|
+
const base=inherited&&global!=="skip",enabled=pkg.scope==="cli"||pkg.trusted&&!disabled&&(index.trusted&&local!=="inherit"?local==="load":delta&&global!=="skip");
|
|
104
|
+
if(found.get(kind+":"+path)?.scope==="cli")continue;
|
|
105
|
+
found.set(kind+":"+path,{kind,path,name:kind==="skills"?basename(dirname(path)):basename(path).replace(/\.[^.]+$/,""),scope:pkg.scope,source:pkg.source.source,
|
|
106
|
+
enabled,inherited:base,override:local,trusted:pkg.trusted,package:{source:pkg.source.source,identity:pkg.source.id,root:pkg.root}});
|
|
107
|
+
}
|
|
108
|
+
}catch(error){index.diagnostics.push(`${pkg.source.source}: ${String(error)}`);}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Iterative realpath tracking allows linked resource folders without following cycles. */
|
|
112
|
+
async function resourceFiles(source: string, kind: ResourceKind, diagnostics: string[], required = false): Promise<string[]> {
|
|
113
|
+
const files: string[] = [], seen = new Set<string>(), stack = [source];
|
|
114
|
+
if (/[*?{[]/.test(source)) {
|
|
115
|
+
try { return [...new Bun.Glob(source).scanSync({ absolute: true, onlyFiles: true })].sort(); }
|
|
116
|
+
catch (error) { diagnostics.push(`${source}: ${String(error)}`); return []; }
|
|
117
|
+
}
|
|
118
|
+
while (stack.length) {
|
|
119
|
+
const path = stack.pop()!;
|
|
120
|
+
try {
|
|
121
|
+
const canonical = await realpath(path); if (seen.has(canonical)) continue; seen.add(canonical);
|
|
122
|
+
const info = await stat(path);
|
|
123
|
+
if (info.isDirectory()) {
|
|
124
|
+
if (kind === "extensions" && path !== source) {
|
|
125
|
+
const manifest = await readJsonFile<{ xeno?: { extensions?: string[] }; pi?: { extensions?: string[] } }>(join(path, "package.json"));
|
|
126
|
+
const declared = manifest.value?.xeno?.extensions ?? manifest.value?.pi?.extensions;
|
|
127
|
+
if (declared) { for (const entry of declared) stack.push(resolve(path, entry)); continue; }
|
|
128
|
+
for (const ext of ["ts", "js", "mts", "mjs"]) {
|
|
129
|
+
const entry = join(path, `index.${ext}`);
|
|
130
|
+
if (await stat(entry).then(s => s.isFile(), () => false)) { stack.push(entry); break; }
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const entries = (await readdir(path, { withFileTypes: true })).sort((a, b) => b.name.localeCompare(a.name));
|
|
135
|
+
for (const entry of entries) if (!entry.name.startsWith(".") && entry.name !== "node_modules" && (kind === "skills" || kind === "extensions" || !entry.isDirectory())) stack.push(join(path, entry.name));
|
|
136
|
+
} else if (info.isFile() && (kind === "skills" ? basename(path) === "SKILL.md" || path === source : kind === "extensions" ? /\.[cm]?[jt]s$/.test(path) : path.endsWith(kind === "themes" ? ".json" : ".md"))) files.push(resolve(path));
|
|
137
|
+
} catch (error) { if ((error as { code?: string }).code !== "ENOENT" || required && path === source) diagnostics.push(`${path}: ${String(error)}`); }
|
|
138
|
+
}
|
|
139
|
+
return files;
|
|
140
|
+
}
|
|
141
|
+
export async function setResourceOverride(index: ResourceIndex, entry: ResourceEntry, scope: "user" | "project", state: ResourceOverride): Promise<void> {
|
|
142
|
+
if (entry.scope === "cli") throw new Error("Explicit command-line resources are controlled by their flags");
|
|
143
|
+
if (scope === "user" && entry.scope !== "user") throw new Error("Project resources are configured in project scope");
|
|
144
|
+
const base = scope === "user" ? index.userDir : index.projectDir;
|
|
145
|
+
if(entry.package) {
|
|
146
|
+
const metadata=entry.package,previous=(await configuredPackages(base)).find(value=>packageSource(packageConfig(value).source,base).id===metadata.identity);
|
|
147
|
+
const source=metadata.identity.startsWith("local:")?metadata.root:metadata.source;
|
|
148
|
+
const config=previous?{...packageConfig(previous)}:{source,...(scope==="project"&&entry.scope==="user"?{autoload:false}:{})};
|
|
149
|
+
const old=config[entry.kind],patterns=old?.length===0&&state!=="inherit"?["!**/*",...(old??[])]:old??[];
|
|
150
|
+
const selected=updateResourcePatterns(patterns,entry.path,state,pattern=>resourcePath(pattern,metadata.root)===entry.path);
|
|
151
|
+
if(selected.length)config[entry.kind]=selected;else delete config[entry.kind];
|
|
152
|
+
await savePackageDeclaration(base,packageSource(source,base),config);return;
|
|
153
|
+
}
|
|
154
|
+
const file = join(base, "settings.json");
|
|
155
|
+
await serialized(file, async () => {
|
|
156
|
+
const { value, error } = await readJsonFile<Record<string, unknown>>(file);
|
|
157
|
+
if (error) throw new Error(error);
|
|
158
|
+
if (value !== null && (typeof value !== "object" || Array.isArray(value))) throw new Error("Settings must be an object");
|
|
159
|
+
const previous = value?.[entry.kind];
|
|
160
|
+
if (previous !== undefined && (!Array.isArray(previous) || previous.some(p => typeof p !== "string"))) throw new Error("Resource paths must be strings");
|
|
161
|
+
const patterns = updateResourcePatterns((previous ?? []) as string[], entry.path, state, pattern => resourcePath(pattern, base) === entry.path);
|
|
162
|
+
await mkdir(base, { recursive: true });
|
|
163
|
+
await atomicWrite(file, JSON.stringify({ ...value, [entry.kind]: patterns }, null, 2) + "\n");
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function themeResourcePath(index: ResourceIndex, name: string): string | undefined {
|
|
168
|
+
const candidates = index.entries.filter(entry => entry.kind === "themes" && (entry.name === name || entry.path === name));
|
|
169
|
+
return candidates.findLast(entry => entry.enabled)?.path ?? (candidates.length ? "dark" : undefined);
|
|
170
|
+
}
|
package/src/store/resources.ts
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* Theme and keymap are UI state and are not logged.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { readdir, readFile
|
|
13
|
-
import
|
|
12
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
13
|
+
import { watch } from "node:fs";
|
|
14
14
|
import { basename, dirname, join } from "node:path";
|
|
15
15
|
import { configDir, readJsonFile, writeJsonFile, settingsPath } from "./config.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { discoverResources, themeResourcePath, type ResourceIndex } from "./resource-index.ts";
|
|
17
17
|
import { parseSkill, type Skill } from "../core/skills.ts";
|
|
18
18
|
import { parsePromptTemplate, type PromptTemplate } from "../core/prompts.ts";
|
|
19
19
|
import {
|
|
@@ -28,9 +28,7 @@ import {
|
|
|
28
28
|
} from "../core/keymap.ts";
|
|
29
29
|
import {
|
|
30
30
|
defaultSettings,
|
|
31
|
-
explicitSettings,
|
|
32
31
|
mergeSettings,
|
|
33
|
-
parseSettings,
|
|
34
32
|
settingsToWrite,
|
|
35
33
|
type Settings,
|
|
36
34
|
} from "../core/settings.ts";
|
|
@@ -40,6 +38,9 @@ export interface LoadUiOptions {
|
|
|
40
38
|
readonly trusted: boolean;
|
|
41
39
|
/** `--theme <name|path>`, wins over settings. */
|
|
42
40
|
readonly theme?: string;
|
|
41
|
+
readonly noThemes?: boolean;
|
|
42
|
+
readonly themePaths?: readonly string[];
|
|
43
|
+
readonly tuiMode?: "regular" | "fullscreen";
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export interface UiResources {
|
|
@@ -62,52 +63,60 @@ export function userThemePath(name: string): string {
|
|
|
62
63
|
return join(configDir(), "themes", `${name}.json`);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
export async function loadUiResources(opts: LoadUiOptions): Promise<UiResources> {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
if (userRead.error) diagnostics.push(userRead.error);
|
|
70
|
-
const userParsed = parseSettings(userRead.value);
|
|
71
|
-
diagnostics.push(...userParsed.diagnostics);
|
|
72
|
-
const userOverrides = explicitSettings(userRead.value, userParsed.settings);
|
|
66
|
+
export async function loadUiResources(opts: LoadUiOptions, resources?: ResourceIndex): Promise<UiResources> {
|
|
67
|
+
const index = resources ?? await discoverResources(opts);
|
|
68
|
+
const diagnostics = [...index.diagnostics];
|
|
69
|
+
const userOverrides = index.user;
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
if (opts.trusted) {
|
|
76
|
-
const projectRead = await readJsonFile<unknown>(projectSettingsPath(opts.workspace));
|
|
77
|
-
if (projectRead.error) diagnostics.push(projectRead.error);
|
|
78
|
-
if (projectRead.value !== null) {
|
|
79
|
-
const parsed = parseSettings(projectRead.value);
|
|
80
|
-
diagnostics.push(...parsed.diagnostics);
|
|
81
|
-
projectSettings = parsed.settings;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const cli: Settings = opts.theme !== undefined && opts.theme.length > 0 ? { theme: opts.theme } : {};
|
|
71
|
+
const cli: Settings = { ...(opts.theme ? { theme: opts.theme } : {}), ...(opts.tuiMode ? { tuiMode: opts.tuiMode } : {}) };
|
|
86
72
|
const settings = mergeSettings({
|
|
87
73
|
builtin: defaultSettings(),
|
|
88
|
-
user:
|
|
89
|
-
project:
|
|
74
|
+
user: index.user,
|
|
75
|
+
project: opts.trusted ? index.project : {},
|
|
90
76
|
cli,
|
|
91
77
|
});
|
|
92
78
|
|
|
93
|
-
const theme = await resolveTheme(settings.theme ?? "dark", diagnostics);
|
|
79
|
+
const theme = await resolveTheme(selectedThemePath(index, settings.theme ?? "dark", opts.noThemes), diagnostics);
|
|
94
80
|
const keymap = await resolveKeymap(opts.workspace, opts.trusted, diagnostics);
|
|
95
81
|
|
|
96
82
|
return { settings, theme, keymap, diagnostics, userOverrides };
|
|
97
83
|
}
|
|
98
84
|
|
|
85
|
+
/** Loading and watching share enablement; a disabled resource must not reactivate on file changes. */
|
|
86
|
+
export function selectedThemePath(index: ResourceIndex, name: string, noThemes = false): string {
|
|
87
|
+
const source = themeResourcePath(index, name);
|
|
88
|
+
const disabled = !source && (noThemes || index.entries.some(entry => entry.kind === "themes" && (entry.name === name || entry.path === name) && !entry.enabled));
|
|
89
|
+
return disabled && !["dark", "light", "system", "auto"].includes(name) ? "dark" : source ?? name;
|
|
90
|
+
}
|
|
91
|
+
|
|
99
92
|
export async function writeUserSettings(overrides: Partial<Settings>): Promise<void> {
|
|
100
93
|
const { value, error } = await readJsonFile<unknown>(settingsPath());
|
|
101
94
|
const prev =
|
|
102
95
|
error === null && typeof value === "object" && value !== null && !Array.isArray(value)
|
|
103
96
|
? (value as { readonly [k: string]: unknown })
|
|
104
97
|
: {};
|
|
105
|
-
const next = { ...prev, ...settingsToWrite(overrides)
|
|
98
|
+
const next = { ...prev, ...settingsToWrite(overrides),
|
|
99
|
+
...(overrides.terminal ? { terminal: { ...(typeof prev["terminal"] === "object" ? prev["terminal"] : {}), ...overrides.terminal } } : {}),
|
|
100
|
+
...(overrides.modelThinkingLevels ? { modelThinkingLevels: { ...(typeof prev["modelThinkingLevels"] === "object" ? prev["modelThinkingLevels"] : {}), ...overrides.modelThinkingLevels } } : {}),
|
|
101
|
+
...Object.fromEntries((["images", "markdown"] as const).filter(key => overrides[key]).map(key => [key, { ...(typeof prev[key] === "object" ? prev[key] : {}), ...overrides[key] }])),
|
|
102
|
+
};
|
|
106
103
|
await writeJsonFile(settingsPath(), next, false);
|
|
107
104
|
}
|
|
108
105
|
|
|
109
|
-
|
|
106
|
+
/** Remove only the user's override; project defaults and unrelated keys retain their ownership. */
|
|
107
|
+
export async function removeModelThinkingOverride(identity: string): Promise<void> {
|
|
108
|
+
const { value, error } = await readJsonFile<Record<string, unknown>>(settingsPath());
|
|
109
|
+
if (error) throw new Error(error);
|
|
110
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
111
|
+
const levels = value["modelThinkingLevels"];
|
|
112
|
+
if (!levels || typeof levels !== "object" || Array.isArray(levels)) return;
|
|
113
|
+
const next = Object.fromEntries(Object.entries(levels).filter(([key]) => key !== identity));
|
|
114
|
+
await writeJsonFile(settingsPath(), { ...value, modelThinkingLevels: next }, false);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function resolveTheme(spec: string, diagnostics: string[]): Promise<Theme> {
|
|
110
118
|
if (spec === "dark" || spec === "light") return builtinTheme(spec);
|
|
119
|
+
if (spec === "system" || spec === "auto") return { ...builtinTheme("dark"), name: spec };
|
|
111
120
|
|
|
112
121
|
const path = looksLikePath(spec) ? spec : userThemePath(spec);
|
|
113
122
|
const { value, error } = await readJsonFile<unknown>(path);
|
|
@@ -124,8 +133,37 @@ async function resolveTheme(spec: string, diagnostics: string[]): Promise<Theme>
|
|
|
124
133
|
return parsed.theme;
|
|
125
134
|
}
|
|
126
135
|
|
|
136
|
+
/** Directory watching survives atomic editor saves; malformed edits keep the last valid palette. */
|
|
137
|
+
export function watchTheme(spec: string, apply: (theme: Theme) => void, report: (message: string) => void): () => void {
|
|
138
|
+
if (["dark", "light", "system", "auto"].includes(spec)) return () => {};
|
|
139
|
+
const file = looksLikePath(spec) ? spec : userThemePath(spec);
|
|
140
|
+
let timer: ReturnType<typeof setTimeout> | undefined, closed = false;
|
|
141
|
+
const changed = () => {
|
|
142
|
+
if (timer) clearTimeout(timer);
|
|
143
|
+
timer = setTimeout(() => { void (async () => {
|
|
144
|
+
const diagnostics: string[] = [];
|
|
145
|
+
const theme = await resolveTheme(spec, diagnostics);
|
|
146
|
+
if (closed) return;
|
|
147
|
+
if (diagnostics.length) report(diagnostics.join("\n")); else apply(theme);
|
|
148
|
+
})(); }, 80);
|
|
149
|
+
};
|
|
150
|
+
try {
|
|
151
|
+
const watcher = watch(dirname(file), { persistent: false }, (_event, name) => { if (!name || String(name) === basename(file)) changed(); });
|
|
152
|
+
watcher.on("error", error => { if (!closed) report(String(error)); });
|
|
153
|
+
return () => { closed = true; if (timer) clearTimeout(timer); watcher.close(); };
|
|
154
|
+
} catch (error) { report(String(error)); return () => { closed = true; }; }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function discoverThemes(index?: ResourceIndex): Promise<string[]> {
|
|
158
|
+
if (index) return [...new Set(["dark", "light", "system", "auto", ...index.entries.filter(entry => entry.kind === "themes" && entry.enabled).map(entry => entry.name)])];
|
|
159
|
+
let names: string[] = [];
|
|
160
|
+
try { names = (await readdir(join(configDir(), "themes"))).filter(name => name.endsWith(".json")).map(name => name.slice(0, -5)).sort(); }
|
|
161
|
+
catch (error) { if (!isNotFound(error)) throw error; }
|
|
162
|
+
return [...new Set(["dark", "light", "system", "auto", ...names])];
|
|
163
|
+
}
|
|
164
|
+
|
|
127
165
|
async function resolveKeymap(workspace: string, trusted: boolean, diagnostics: string[]): Promise<Keymap> {
|
|
128
|
-
let map = defaultKeymap();
|
|
166
|
+
let map = defaultKeymap(process.platform === "win32" ? "win32" : process.env["WSL_DISTRO_NAME"] || process.env["WSL_INTEROP"] ? "wsl" : "posix");
|
|
129
167
|
|
|
130
168
|
const userRead = await readJsonFile<unknown>(keybindingsPath());
|
|
131
169
|
if (userRead.error) diagnostics.push(userRead.error);
|
|
@@ -171,7 +209,7 @@ export interface LoadedTextResources {
|
|
|
171
209
|
* Discover skills and prompt templates. Project directories load only when
|
|
172
210
|
* trusted, matching `.xeno/settings.json`. A bad file is named and skipped.
|
|
173
211
|
*/
|
|
174
|
-
export async function loadTextResources(opts: LoadTextOptions): Promise<LoadedTextResources> {
|
|
212
|
+
export async function loadTextResources(opts: LoadTextOptions, resources?: ResourceIndex): Promise<LoadedTextResources> {
|
|
175
213
|
const diagnostics: string[] = [];
|
|
176
214
|
const skills = new Map<string, Skill>();
|
|
177
215
|
const prompts = new Map<string, PromptTemplate>();
|
|
@@ -195,24 +233,11 @@ export async function loadTextResources(opts: LoadTextOptions): Promise<LoadedTe
|
|
|
195
233
|
prompts.set(prompt.name, prompt);
|
|
196
234
|
};
|
|
197
235
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
for (const path of opts.skillPaths ?? []) {
|
|
205
|
-
await loadSkillsFromPath(path, addSkill, diagnostics);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (opts.noPromptTemplates !== true) {
|
|
209
|
-
await loadPromptsFromDir(join(opts.configDir, "prompts"), addPrompt, diagnostics);
|
|
210
|
-
if (opts.trusted) {
|
|
211
|
-
await loadPromptsFromDir(join(opts.workspace, ".xeno", "prompts"), addPrompt, diagnostics);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
for (const path of opts.promptTemplatePaths ?? []) {
|
|
215
|
-
await loadPromptsFromPath(path, addPrompt, diagnostics);
|
|
236
|
+
const index = resources ?? await discoverResources(opts);
|
|
237
|
+
diagnostics.push(...index.diagnostics);
|
|
238
|
+
for (const entry of index.entries) if (entry.enabled) {
|
|
239
|
+
if (entry.kind === "skills") await absorbSkill(entry.path, addSkill, diagnostics);
|
|
240
|
+
if (entry.kind === "prompts") await absorbPrompt(entry.path, addPrompt, diagnostics);
|
|
216
241
|
}
|
|
217
242
|
|
|
218
243
|
return {
|
|
@@ -222,85 +247,6 @@ export async function loadTextResources(opts: LoadTextOptions): Promise<LoadedTe
|
|
|
222
247
|
};
|
|
223
248
|
}
|
|
224
249
|
|
|
225
|
-
async function loadSkillsFromDir(
|
|
226
|
-
dir: string,
|
|
227
|
-
add: (skill: Skill) => void,
|
|
228
|
-
diagnostics: string[],
|
|
229
|
-
): Promise<void> {
|
|
230
|
-
const files: string[] = [];
|
|
231
|
-
await collectSkillFiles(dir, files, diagnostics);
|
|
232
|
-
for (const file of files) await absorbSkill(file, add, diagnostics);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async function loadSkillsFromPath(
|
|
236
|
-
path: string,
|
|
237
|
-
add: (skill: Skill) => void,
|
|
238
|
-
diagnostics: string[],
|
|
239
|
-
): Promise<void> {
|
|
240
|
-
const info = await statKind(path);
|
|
241
|
-
if (info === "missing") {
|
|
242
|
-
diagnostics.push(`skill path not found: ${path}`);
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
if (info === "file") {
|
|
246
|
-
await absorbSkill(path, add, diagnostics);
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
await loadSkillsFromDir(path, add, diagnostics);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async function loadPromptsFromDir(
|
|
253
|
-
dir: string,
|
|
254
|
-
add: (prompt: PromptTemplate) => void,
|
|
255
|
-
diagnostics: string[],
|
|
256
|
-
): Promise<void> {
|
|
257
|
-
let entries: Dirent[];
|
|
258
|
-
try {
|
|
259
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
260
|
-
} catch (err) {
|
|
261
|
-
if (isNotFound(err)) return;
|
|
262
|
-
diagnostics.push(`${dir}: ${errMessage(err)}`);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
for (const entry of entries) {
|
|
266
|
-
if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
|
|
267
|
-
await absorbPrompt(join(dir, entry.name), add, diagnostics);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async function loadPromptsFromPath(
|
|
272
|
-
path: string,
|
|
273
|
-
add: (prompt: PromptTemplate) => void,
|
|
274
|
-
diagnostics: string[],
|
|
275
|
-
): Promise<void> {
|
|
276
|
-
const info = await statKind(path);
|
|
277
|
-
if (info === "missing") {
|
|
278
|
-
diagnostics.push(`prompt template not found: ${path}`);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
if (info === "dir") {
|
|
282
|
-
await loadPromptsFromDir(path, add, diagnostics);
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
await absorbPrompt(path, add, diagnostics);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
async function collectSkillFiles(dir: string, out: string[], diagnostics: string[]): Promise<void> {
|
|
289
|
-
let entries: Dirent[];
|
|
290
|
-
try {
|
|
291
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
292
|
-
} catch (err) {
|
|
293
|
-
if (isNotFound(err)) return;
|
|
294
|
-
diagnostics.push(`${dir}: ${errMessage(err)}`);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
for (const entry of entries) {
|
|
298
|
-
const full = join(dir, entry.name);
|
|
299
|
-
if (entry.isDirectory()) await collectSkillFiles(full, out, diagnostics);
|
|
300
|
-
else if (entry.isFile() && entry.name === "SKILL.md") out.push(full);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
250
|
async function absorbSkill(
|
|
305
251
|
filePath: string,
|
|
306
252
|
add: (skill: Skill) => void,
|
|
@@ -343,19 +289,6 @@ async function readUtf8(path: string): Promise<string | null> {
|
|
|
343
289
|
}
|
|
344
290
|
}
|
|
345
291
|
|
|
346
|
-
async function statKind(path: string): Promise<"file" | "dir" | "missing"> {
|
|
347
|
-
try {
|
|
348
|
-
const info = await stat(path);
|
|
349
|
-
return info.isDirectory() ? "dir" : "file";
|
|
350
|
-
} catch {
|
|
351
|
-
return "missing";
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
292
|
function isNotFound(err: unknown): boolean {
|
|
356
293
|
return (err as { code?: string }).code === "ENOENT";
|
|
357
294
|
}
|
|
358
|
-
|
|
359
|
-
function errMessage(err: unknown): string {
|
|
360
|
-
return err instanceof Error ? err.message : String(err);
|
|
361
|
-
}
|