@kuralle-agents/fs 0.12.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:
@@ -188,15 +195,15 @@ fs and shell tools are **non-replayed** (`replay: false`): they always execute f
188
195
  ## Examples
189
196
 
190
197
  ```bash
191
- bun packages/kuralle-fs/examples/kb-agent.ts
192
- bun packages/kuralle-fs/examples/skills-on-fs.ts # deterministic, no key
193
- KURALLE_EXAMPLE_PROVIDER=openai bun packages/kuralle-fs/examples/composite-workspace.ts
194
- KURALLE_EXAMPLE_PROVIDER=openai bun packages/kuralle-fs/examples/workspace-skills-shell.ts # live: model uses bash + a fs skill
195
- KURALLE_EXAMPLE_PROVIDER=openai bun packages/kuralle-fs/examples/okf-knowledge-agent.ts # live: agent navigates an OKF bundle
196
- KURALLE_EXAMPLE_PROVIDER=openai bun packages/kuralle-fs/examples/okf-benchmark.ts # benchmark: progressive vs whole-dump
197
- KURALLE_EXAMPLE_PROVIDER=openai bun packages/kuralle-fs/examples/skill-latency-spike.ts # benchmark: with vs without skills
198
- bun packages/kuralle-fs/examples/persistent-workspace.ts # SqlFileSystem: file+skill survive a restart
199
- KURALLE_EXAMPLE_PROVIDER=openai bun packages/kuralle-fs/examples/persistent-workspace-live.ts # live: model writes, fresh runtime reads back
198
+ bun packages/fs/examples/kb-agent.ts
199
+ bun packages/fs/examples/skills-on-fs.ts # deterministic, no key
200
+ KURALLE_EXAMPLE_PROVIDER=openai bun packages/fs/examples/composite-workspace.ts
201
+ KURALLE_EXAMPLE_PROVIDER=openai bun packages/fs/examples/workspace-skills-shell.ts # live: model uses bash + a fs skill
202
+ KURALLE_EXAMPLE_PROVIDER=openai bun packages/fs/examples/okf-knowledge-agent.ts # live: agent navigates an OKF bundle
203
+ KURALLE_EXAMPLE_PROVIDER=openai bun packages/fs/examples/okf-benchmark.ts # benchmark: progressive vs whole-dump
204
+ KURALLE_EXAMPLE_PROVIDER=openai bun packages/fs/examples/skill-latency-spike.ts # benchmark: with vs without skills
205
+ bun packages/fs/examples/persistent-workspace.ts # SqlFileSystem: file+skill survive a restart
206
+ KURALLE_EXAMPLE_PROVIDER=openai bun packages/fs/examples/persistent-workspace-live.ts # live: model writes, fresh runtime reads back
200
207
  ```
201
208
 
202
209
  ## License
@@ -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';
@@ -2,7 +2,7 @@ import type { BlobStore, SqlBackend, SqlParam } from './types.js';
2
2
  import { SqlFileSystem } from './sql-fs.js';
3
3
  /** Minimal structural shape of a Cloudflare DO SqlStorage (no @cloudflare/workers-types dep). */
4
4
  export interface SqlStorageLike {
5
- exec(query: string, ...bindings: SqlParam[]): Iterable<Record<string, SqlParam>>;
5
+ exec(query: string, ...bindings: SqlParam[]): Iterable<Record<string, SqlParam | ArrayBuffer>>;
6
6
  databaseSize: number;
7
7
  }
8
8
  /** Minimal structural shape of a D1Database. */
package/package.json CHANGED
@@ -4,9 +4,9 @@
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/kuralle/kuralle-agents.git",
7
- "directory": "packages/kuralle-fs"
7
+ "directory": "packages/fs"
8
8
  },
9
- "version": "0.12.0",
9
+ "version": "0.14.0",
10
10
  "description": "Portable filesystem primitive for Kuralle agents",
11
11
  "type": "module",
12
12
  "sideEffects": false,
@@ -31,16 +31,23 @@
31
31
  }
32
32
  },
33
33
  "dependencies": {
34
- "just-bash": "^3.1.0",
35
- "@kuralle-agents/core": "0.12.0"
34
+ "@kuralle-agents/core": "0.14.0"
36
35
  },
37
36
  "peerDependencies": {
37
+ "just-bash": "^3.1.0",
38
38
  "zod": "^4.0.0"
39
39
  },
40
+ "peerDependenciesMeta": {
41
+ "just-bash": {
42
+ "optional": true
43
+ }
44
+ },
40
45
  "devDependencies": {
41
46
  "@cloudflare/vitest-pool-workers": "^0.12.7",
42
47
  "@types/node": "^22.5.0",
43
48
  "bun-types": "^1.3.0",
49
+ "dotenv": "^17.4.2",
50
+ "just-bash": "^3.1.0",
44
51
  "typescript": "^5.3.0",
45
52
  "vitest": "^3.2.4",
46
53
  "zod": "^4.0.0"
@@ -59,6 +66,7 @@
59
66
  "test": "bun test ./test && vitest run --config vitest.config.ts",
60
67
  "test:inmemoryfs": "bun test test/in-memory-fs.test.ts",
61
68
  "test:fs-tool": "bun test test/fs-tool.test.ts",
62
- "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"
63
71
  }
64
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
- }