@kuralle-agents/fs 0.13.0 → 0.14.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/README.md CHANGED
@@ -103,6 +103,13 @@ const agent = defineAgent({
103
103
 
104
104
  `defineSkill({ name, description, instructions, resources })` builds an inline skill without a filesystem.
105
105
 
106
+ Pass ordered roots to layer shared and project skills. Later roots win when two `SKILL.md` files
107
+ declare the same `name`, and the winning root owns both the body and its resources:
108
+
109
+ ```ts
110
+ skills: fsSkillStore(fs, ['/skills/shared', '/skills/project'])
111
+ ```
112
+
106
113
  ## Persistent workspaces (`SqlFileSystem`, platform-chosen)
107
114
 
108
115
  `InMemoryFs` is ephemeral. For a workspace that survives restarts — so agent files, skills, and the durable tool journal agree across process boundaries — use `SqlFileSystem`, a drop-in `FileSystem` over any SQL handle (+ optional blob store for large files). Pick the backend your platform gives you:
@@ -1,5 +1,4 @@
1
1
  import type { BufferEncoding, CpOptions, FileContent, FileSystem, FileSystemDirent, FsStat, InitialFiles, MkdirOptions, ReadFileOptions, RmOptions, WriteFileOptions } from '@kuralle-agents/core';
2
- export type { FileContent, FsStat, FileSystem };
3
2
  export interface FsData {
4
3
  [path: string]: import('@kuralle-agents/core').FsEntry;
5
4
  }
package/dist/index.d.ts CHANGED
@@ -4,9 +4,6 @@ export { CompositeFileSystem, type CompositeFileSystemConfig } from './composite
4
4
  export { normalizePath, validatePath, dirname, resolvePath, joinPath, resolveSymlinkTarget, createGlobMatcher, sortPaths, MAX_SYMLINK_DEPTH, DEFAULT_DIR_MODE, DEFAULT_FILE_MODE, SYMLINK_MODE, } from './path-utils.js';
5
5
  export { toBuffer, fromBuffer, getEncoding } from './encoding.js';
6
6
  export { createFsTool } from './tool.js';
7
- export { parseSkillFrontmatter, type ParsedSkill } from './skill-frontmatter.js';
8
- export { fsSkillStore } from './fs-skill-store.js';
9
- export { defineSkill } from './define-skill.js';
10
7
  export { parseOkfConcept, listOkfConcepts, okfBundleToFs, type OkfConcept, } from './okf.js';
11
8
  export { SqlFileSystem, type SqlFileSystemOptions } from './sql/sql-fs.js';
12
9
  export type { SqlBackend, BlobStore, SqlParam } from './sql/types.js';
package/dist/index.js CHANGED
@@ -3,9 +3,6 @@ export { CompositeFileSystem } from './composite-fs.js';
3
3
  export { normalizePath, validatePath, dirname, resolvePath, joinPath, resolveSymlinkTarget, createGlobMatcher, sortPaths, MAX_SYMLINK_DEPTH, DEFAULT_DIR_MODE, DEFAULT_FILE_MODE, SYMLINK_MODE, } from './path-utils.js';
4
4
  export { toBuffer, fromBuffer, getEncoding } from './encoding.js';
5
5
  export { createFsTool } from './tool.js';
6
- export { parseSkillFrontmatter } from './skill-frontmatter.js';
7
- export { fsSkillStore } from './fs-skill-store.js';
8
- export { defineSkill } from './define-skill.js';
9
6
  export { parseOkfConcept, listOkfConcepts, okfBundleToFs, } from './okf.js';
10
7
  export { SqlFileSystem } from './sql/sql-fs.js';
11
8
  export { sqlFileSystem, toSqlBackend, } from './sql/factory.js';
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/kuralle/kuralle-agents.git",
7
7
  "directory": "packages/fs"
8
8
  },
9
- "version": "0.13.0",
9
+ "version": "0.14.0",
10
10
  "description": "Portable filesystem primitive for Kuralle agents",
11
11
  "type": "module",
12
12
  "sideEffects": false,
@@ -31,7 +31,7 @@
31
31
  }
32
32
  },
33
33
  "dependencies": {
34
- "@kuralle-agents/core": "0.13.0"
34
+ "@kuralle-agents/core": "0.14.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "just-bash": "^3.1.0",
@@ -46,6 +46,7 @@
46
46
  "@cloudflare/vitest-pool-workers": "^0.12.7",
47
47
  "@types/node": "^22.5.0",
48
48
  "bun-types": "^1.3.0",
49
+ "dotenv": "^17.4.2",
49
50
  "just-bash": "^3.1.0",
50
51
  "typescript": "^5.3.0",
51
52
  "vitest": "^3.2.4",
@@ -65,6 +66,7 @@
65
66
  "test": "bun test ./test && vitest run --config vitest.config.ts",
66
67
  "test:inmemoryfs": "bun test test/in-memory-fs.test.ts",
67
68
  "test:fs-tool": "bun test test/fs-tool.test.ts",
68
- "test:fs-workers": "vitest run --config vitest.config.ts"
69
+ "test:fs-workers": "vitest run --config vitest.config.ts",
70
+ "typecheck:examples": "tsc --noEmit -p tsconfig.examples.json"
69
71
  }
70
72
  }
@@ -1,7 +0,0 @@
1
- import type { SkillLike } from '@kuralle-agents/core';
2
- export declare function defineSkill(opts: {
3
- name: string;
4
- description: string;
5
- instructions: string;
6
- resources?: Record<string, string>;
7
- }): SkillLike;
@@ -1,8 +0,0 @@
1
- export function defineSkill(opts) {
2
- return {
3
- name: opts.name,
4
- description: opts.description,
5
- body: opts.instructions,
6
- resources: opts.resources,
7
- };
8
- }
@@ -1,4 +0,0 @@
1
- import type { FileSystem, SkillStoreLike } from '@kuralle-agents/core';
2
- export declare function fsSkillStore(fs: FileSystem, opts?: {
3
- root?: string;
4
- }): SkillStoreLike;
@@ -1,101 +0,0 @@
1
- import { parseSkillFrontmatter } from './skill-frontmatter.js';
2
- const DEFAULT_ROOT = '/skills';
3
- export function fsSkillStore(fs, opts) {
4
- const root = opts?.root ?? DEFAULT_ROOT;
5
- return {
6
- async list() {
7
- const metas = [];
8
- let entries;
9
- try {
10
- entries = await fs.readdir(root);
11
- }
12
- catch {
13
- return metas;
14
- }
15
- for (const entry of entries) {
16
- const entryPath = fs.resolvePath(root, entry);
17
- let stat;
18
- try {
19
- stat = await fs.stat(entryPath);
20
- }
21
- catch {
22
- continue;
23
- }
24
- if (stat.type !== 'directory')
25
- continue;
26
- const skillPath = fs.resolvePath(root, `${entry}/SKILL.md`);
27
- if (!(await fs.exists(skillPath)))
28
- continue;
29
- try {
30
- const content = await fs.readFile(skillPath);
31
- const parsed = parseSkillFrontmatter(content, { path: skillPath });
32
- metas.push({ name: parsed.name, description: parsed.description });
33
- }
34
- catch (err) {
35
- console.warn(`[skills] Skipping ${skillPath}: ${err instanceof Error ? err.message : String(err)}`);
36
- }
37
- }
38
- return metas.sort((a, b) => a.name.localeCompare(b.name));
39
- },
40
- async loadBody(name) {
41
- const folder = await findSkillFolder(fs, root, name);
42
- if (!folder) {
43
- throw new Error(`[skills] Skill "${name}" not found.`);
44
- }
45
- const skillPath = fs.resolvePath(root, `${folder}/SKILL.md`);
46
- const content = await fs.readFile(skillPath);
47
- const parsed = parseSkillFrontmatter(content, { path: skillPath });
48
- return parsed.body;
49
- },
50
- async loadResource(name, path) {
51
- const folder = await findSkillFolder(fs, root, name);
52
- if (!folder) {
53
- throw new Error(`[skills] Skill "${name}" not found.`);
54
- }
55
- const normalized = path.trim().replace(/^\.?\//, '');
56
- if (normalized.includes('..') || normalized.startsWith('/')) {
57
- throw new Error(`[skills] Invalid resource path "${path}".`);
58
- }
59
- const resourcePath = fs.resolvePath(root, `${folder}/${normalized}`);
60
- if (!(await fs.exists(resourcePath))) {
61
- const err = new Error(`ENOENT: [skills] Resource "${normalized}" not found for skill "${name}".`);
62
- throw err;
63
- }
64
- return fs.readFile(resourcePath);
65
- },
66
- };
67
- }
68
- async function findSkillFolder(fs, root, name) {
69
- let entries;
70
- try {
71
- entries = await fs.readdir(root);
72
- }
73
- catch {
74
- return null;
75
- }
76
- for (const entry of entries) {
77
- const entryPath = fs.resolvePath(root, entry);
78
- let stat;
79
- try {
80
- stat = await fs.stat(entryPath);
81
- }
82
- catch {
83
- continue;
84
- }
85
- if (stat.type !== 'directory')
86
- continue;
87
- const skillPath = fs.resolvePath(root, `${entry}/SKILL.md`);
88
- if (!(await fs.exists(skillPath)))
89
- continue;
90
- try {
91
- const content = await fs.readFile(skillPath);
92
- const parsed = parseSkillFrontmatter(content, { path: skillPath });
93
- if (parsed.name === name)
94
- return entry;
95
- }
96
- catch {
97
- continue;
98
- }
99
- }
100
- return null;
101
- }
@@ -1 +0,0 @@
1
- export type { FileSystem, FsStat, FileSystemDirent, FileSystemEntryType, BufferEncoding, FileContent, MkdirOptions, RmOptions, CpOptions, ReadFileOptions, WriteFileOptions, FileEntry, DirectoryEntry, SymlinkEntry, LazyFileEntry, FsEntry, FileInit, LazyFileProvider, InitialFiles, FsError, } from '@kuralle-agents/core';
package/dist/interface.js DELETED
@@ -1 +0,0 @@
1
- export {};
@@ -1,12 +0,0 @@
1
- export interface ParsedSkill {
2
- name: string;
3
- description: string;
4
- body: string;
5
- license?: string;
6
- compatibility?: string;
7
- allowedTools?: string[];
8
- metadata?: Record<string, string>;
9
- }
10
- export declare function parseSkillFrontmatter(content: string, ctx: {
11
- path: string;
12
- }): ParsedSkill;
@@ -1,168 +0,0 @@
1
- const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)([\s\S]*)$/;
2
- const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3
- export function parseSkillFrontmatter(content, ctx) {
4
- const stripped = content.replace(/^\uFEFF/, '');
5
- const match = stripped.match(FRONTMATTER_RE);
6
- if (!match) {
7
- throw new Error(`[skills] Skill ${ctx.path} is missing YAML frontmatter.`);
8
- }
9
- const raw = parseFlatYaml(match[1] ?? '', ctx.path);
10
- const name = requireField(raw, 'name', ctx.path);
11
- const description = requireField(raw, 'description', ctx.path);
12
- validateName(name, ctx.path);
13
- validateDescription(description, ctx.path);
14
- const compatibility = optionalString(raw.compatibility);
15
- if (compatibility !== undefined && codePointLength(compatibility) > 500) {
16
- throw new Error(`[skills] Skill ${ctx.path} field "compatibility" exceeds 500 characters.`);
17
- }
18
- let body = match[2] ?? '';
19
- if (body.startsWith('\n')) {
20
- body = body.slice(1);
21
- }
22
- const result = { name, description, body };
23
- const license = optionalString(raw.license);
24
- if (license !== undefined)
25
- result.license = license;
26
- if (compatibility !== undefined)
27
- result.compatibility = compatibility;
28
- const allowedTools = parseAllowedTools(raw['allowed-tools']);
29
- if (allowedTools !== undefined)
30
- result.allowedTools = allowedTools;
31
- const metadata = parseMetadata(raw.metadata);
32
- if (metadata !== undefined)
33
- result.metadata = metadata;
34
- return result;
35
- }
36
- function parseFlatYaml(yaml, path) {
37
- const result = {};
38
- const lines = yaml.split(/\r?\n/);
39
- let i = 0;
40
- while (i < lines.length) {
41
- const line = lines[i];
42
- if (line.trim() === '') {
43
- i += 1;
44
- continue;
45
- }
46
- const keyMatch = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
47
- if (!keyMatch) {
48
- throw new Error(`[skills] Skill ${path} has invalid YAML frontmatter near line: ${line}`);
49
- }
50
- const key = keyMatch[1];
51
- const rest = (keyMatch[2] ?? '').trim();
52
- if (rest === '') {
53
- i += 1;
54
- if (i >= lines.length)
55
- break;
56
- const next = lines[i];
57
- if (next.match(/^\s+-\s+/)) {
58
- const items = [];
59
- while (i < lines.length) {
60
- const listLine = lines[i];
61
- const itemMatch = listLine.match(/^\s+-\s+(.*)$/);
62
- if (!itemMatch)
63
- break;
64
- items.push(parseScalar(itemMatch[1] ?? ''));
65
- i += 1;
66
- }
67
- result[key] = items;
68
- continue;
69
- }
70
- if (key === 'metadata') {
71
- const meta = {};
72
- while (i < lines.length) {
73
- const metaLine = lines[i];
74
- const metaMatch = metaLine.match(/^\s{2}([A-Za-z0-9_-]+):\s*(.*)$/);
75
- if (!metaMatch)
76
- break;
77
- meta[metaMatch[1]] = parseScalar((metaMatch[2] ?? '').trim());
78
- i += 1;
79
- }
80
- result[key] = meta;
81
- continue;
82
- }
83
- result[key] = '';
84
- continue;
85
- }
86
- if (rest.startsWith('[') && rest.endsWith(']')) {
87
- const inner = rest.slice(1, -1).trim();
88
- result[key] =
89
- inner === ''
90
- ? []
91
- : inner.split(',').map((s) => parseScalar(s.trim()));
92
- i += 1;
93
- continue;
94
- }
95
- result[key] = parseScalar(rest);
96
- i += 1;
97
- }
98
- return result;
99
- }
100
- function parseScalar(value) {
101
- const trimmed = value.trim();
102
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
103
- (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
104
- return trimmed.slice(1, -1);
105
- }
106
- return trimmed;
107
- }
108
- function requireField(raw, field, path) {
109
- const value = raw[field];
110
- if (typeof value !== 'string' || value.trim().length === 0) {
111
- throw new Error(`[skills] Skill ${path} must define frontmatter ${field} as a non-empty string.`);
112
- }
113
- return value.trim();
114
- }
115
- function optionalString(value) {
116
- if (typeof value !== 'string')
117
- return undefined;
118
- const trimmed = value.trim();
119
- return trimmed.length > 0 ? trimmed : undefined;
120
- }
121
- function validateName(name, path) {
122
- if (name.length > 64) {
123
- throw new Error(`[skills] Skill ${path} field "name" must be at most 64 characters.`);
124
- }
125
- if (!NAME_PATTERN.test(name)) {
126
- throw new Error(`[skills] Skill ${path} field "name" "${name}" must match /^[a-z0-9]+(?:-[a-z0-9]+)*$/.`);
127
- }
128
- }
129
- function validateDescription(description, path) {
130
- if (codePointLength(description) > 1024) {
131
- throw new Error(`[skills] Skill ${path} field "description" exceeds 1024 characters.`);
132
- }
133
- }
134
- function codePointLength(value) {
135
- return [...value].length;
136
- }
137
- function parseAllowedTools(value) {
138
- if (value === undefined || value === null)
139
- return undefined;
140
- if (Array.isArray(value)) {
141
- const tools = value.map((v) => String(v).trim()).filter(Boolean);
142
- return tools.length > 0 ? tools : undefined;
143
- }
144
- if (typeof value === 'string') {
145
- const trimmed = value.trim();
146
- if (!trimmed)
147
- return undefined;
148
- const tools = trimmed.includes(',')
149
- ? trimmed.split(',').map((s) => s.trim()).filter(Boolean)
150
- : trimmed.split(/\s+/).filter(Boolean);
151
- return tools.length > 0 ? tools : undefined;
152
- }
153
- return undefined;
154
- }
155
- function parseMetadata(value) {
156
- if (value === undefined || value === null)
157
- return undefined;
158
- if (typeof value !== 'object' || Array.isArray(value))
159
- return undefined;
160
- const entries = Object.entries(value);
161
- if (entries.length === 0)
162
- return undefined;
163
- const meta = {};
164
- for (const [k, v] of entries) {
165
- meta[k] = String(v);
166
- }
167
- return meta;
168
- }