@mounaji_npm/agentfs 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/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @mounaji_npm/agentfs
2
+
3
+ A per-agent, portable filesystem home for AI agents. Each agent gets a
4
+ `yoagent-<id>/` directory under a configurable root, with a fixed set of
5
+ **layers** plus room for custom nested folders, a `manifest.json`, and
6
+ dependency-free `.tar.gz` **export/import bundles** — the foundation for
7
+ portable Tier S agents.
8
+
9
+ Zero runtime dependencies (Node built-ins only).
10
+
11
+ ## Layers
12
+
13
+ `learning · skills · memory · preferences · rules · soul · user · assets`
14
+
15
+ ## Usage
16
+
17
+ ```js
18
+ import { createAgentFs, AgentFs } from '@mounaji_npm/agentfs';
19
+
20
+ // Create + ensure the layered home exists.
21
+ const fs = createAgentFs({ root: '/data/agents', agentId: 'abc123' });
22
+
23
+ fs.writeJson('memory/facts.json', { likes: ['dark mode'] });
24
+ fs.appendFile('learning/feedback_log.jsonl', JSON.stringify({ ok: true }) + '\n');
25
+ fs.writeFile('assets/logo.png', someBuffer);
26
+ fs.patchManifest({ custom: { tier: 'S' } });
27
+
28
+ fs.listFiles('memory'); // ['memory/facts.json']
29
+ fs.readJson('memory/facts.json');// { likes: ['dark mode'] }
30
+
31
+ // Portable export → import (round-trips every file, text or binary).
32
+ const bundle = fs.exportBundle(); // Buffer (.tar.gz)
33
+ const clone = AgentFs.fromBundle(bundle, { root: '/tmp', agentId: 'abc123' });
34
+ ```
35
+
36
+ ## API
37
+
38
+ ### `createAgentFs({ root?, agentId, layers? })` → `AgentFs`
39
+ Constructs and `ensure()`s the agent home. `root` defaults to
40
+ `~/.yoagent/agents`; the agent lives at `<root>/yoagent-<agentId>`.
41
+
42
+ ### `AgentFs`
43
+ - `ensure()` — create the agent dir, all layers, and manifest (idempotent).
44
+ - `path(...segments)` / `layerPath(layer)` — absolute paths (traversal-guarded).
45
+ - `writeFile / readFile / appendFile / writeJson / readJson / exists / mkdir / remove`
46
+ — file ops on **agent-relative** paths (`memory/facts.json`).
47
+ - `listFiles(relDir?)` — recursive, sorted, forward-slashed relative paths.
48
+ - `readManifest / writeManifest / patchManifest`.
49
+ - `exportBundle()` → `Buffer` — gzipped tar of the whole agent tree.
50
+ - `importBundle(bundle, { overwrite=true })` → `{ written, total }`.
51
+ - `AgentFs.fromBundle(bundle, { root, agentId?, overwrite? })` — rehydrate an
52
+ agent (infers `agentId` from the bundle manifest if omitted).
53
+
54
+ ### Archive helpers
55
+ `packTar · unpackTar · packTarGz · unpackTarGz` — a minimal, standard-compatible
56
+ tar (ustar) implementation for regular files. Produced archives open with
57
+ `tar -xzf`.
58
+
59
+ ## Notes
60
+
61
+ - Paths are guarded: any attempt to escape the agent directory (`../…`) throws.
62
+ - Bundles are standard `.tar.gz`; inspect with `tar -tzf agent.tar.gz`.
63
+ - The `learning/` layer is designed to hold
64
+ [`@mounaji_npm/learning-core`](../learning-core) state.
65
+
66
+ ## Test
67
+
68
+ ```bash
69
+ npm test # node --test
70
+ ```
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@mounaji_npm/agentfs",
3
+ "version": "0.2.0",
4
+ "description": "Per-agent filesystem — a portable, layered home directory for an AI agent (learning, skills, memory, preferences, rules, soul, user, assets) with a manifest and dependency-free tar.gz export/import bundles. Foundation for portable Tier S agents.",
5
+ "keywords": [
6
+ "agent",
7
+ "filesystem",
8
+ "agentfs",
9
+ "portable",
10
+ "export",
11
+ "bundle",
12
+ "learning",
13
+ "mounaji"
14
+ ],
15
+ "license": "MIT",
16
+ "type": "module",
17
+ "publishConfig": {
18
+ "access": "public",
19
+ "registry": "https://registry.npmjs.org/"
20
+ },
21
+ "main": "./src/index.js",
22
+ "module": "./src/index.js",
23
+ "exports": {
24
+ ".": {
25
+ "import": "./src/index.js",
26
+ "require": "./src/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "src"
31
+ ],
32
+ "scripts": {
33
+ "build": "echo 'agentfs is source-only, no build needed'",
34
+ "test": "node --test"
35
+ }
36
+ }
package/src/agentfs.js ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * AgentFs — a per-agent filesystem home.
3
+ *
4
+ * Each agent gets a directory `yoagent-<id>/` under a configurable root, with a
5
+ * fixed set of "layers" (subdirectories) plus room for custom nested folders,
6
+ * and a `manifest.json` describing it. The whole tree round-trips through a
7
+ * dependency-free `.tar.gz` bundle (`exportBundle`/`importBundle`), which is the
8
+ * foundation for portable Tier S agents.
9
+ *
10
+ * Layers (from the platform's YOAgent design):
11
+ * learning/ — neural weighting engine data (see @mounaji_npm/learning-core)
12
+ * skills/ — SKILL.md definitions
13
+ * memory/ — persistent memory (facts, decisions, ...)
14
+ * preferences/ — user/coding preferences
15
+ * rules/ — execution rules
16
+ * soul/ — identity/persona
17
+ * user/ — user-scoped data
18
+ * assets/ — binary assets
19
+ */
20
+ import { join, resolve, dirname, sep, relative, isAbsolute } from 'node:path';
21
+ import { homedir } from 'node:os';
22
+ import {
23
+ mkdirSync, existsSync, readFileSync, writeFileSync, appendFileSync,
24
+ rmSync, readdirSync, statSync,
25
+ } from 'node:fs';
26
+ import { packTarGz, unpackTarGz } from './tar.js';
27
+
28
+ /** Canonical layer names created for every agent. */
29
+ export const DEFAULT_LAYERS = Object.freeze([
30
+ 'learning', 'skills', 'memory', 'preferences', 'rules', 'soul', 'user', 'assets',
31
+ ]);
32
+
33
+ const MANIFEST_FILE = 'manifest.json';
34
+ const MANIFEST_VERSION = 1;
35
+
36
+ /** Expand a leading `~/` and resolve to an absolute path. */
37
+ function expandRoot(root) {
38
+ if (!root) return join(homedir(), '.yoagent', 'agents');
39
+ if (root.startsWith('~/')) return join(homedir(), root.slice(2));
40
+ return resolve(root);
41
+ }
42
+
43
+ /** Normalize an agent-relative path and guard against escaping the agent dir. */
44
+ function safeRelative(relPath) {
45
+ const normalized = relPath.replace(/\\/g, '/').replace(/^\/+/, '');
46
+ const joined = join('/agent-root', normalized);
47
+ const rel = relative('/agent-root', joined);
48
+ if (rel.startsWith('..') || isAbsolute(rel)) {
49
+ throw new Error(`agentfs: path escapes agent directory: "${relPath}"`);
50
+ }
51
+ return normalized;
52
+ }
53
+
54
+ export class AgentFs {
55
+ /**
56
+ * @param {{ root?: string, agentId: string, layers?: string[] }} opts
57
+ * root — parent directory holding all agents (default ~/.yoagent/agents)
58
+ * agentId — stable id; the agent lives at `<root>/yoagent-<agentId>`
59
+ * layers — override the default layer set (custom layers still allowed at runtime)
60
+ */
61
+ constructor({ root, agentId, layers } = {}) {
62
+ if (!agentId || typeof agentId !== 'string') {
63
+ throw new Error('agentfs: agentId is required');
64
+ }
65
+ this.agentId = agentId;
66
+ this.rootDir = expandRoot(root);
67
+ this.dir = join(this.rootDir, `yoagent-${agentId}`);
68
+ this.layers = layers && layers.length ? [...layers] : [...DEFAULT_LAYERS];
69
+ }
70
+
71
+ /** Absolute path to an agent-relative location. */
72
+ path(...segments) {
73
+ const rel = safeRelative(segments.join('/'));
74
+ return join(this.dir, rel);
75
+ }
76
+
77
+ /** Absolute path to a layer directory. */
78
+ layerPath(layer) {
79
+ if (!layer) throw new Error('agentfs: layer name required');
80
+ return this.path(safeRelative(layer));
81
+ }
82
+
83
+ /** Create the agent directory, all layers, and the manifest. Idempotent. */
84
+ ensure() {
85
+ mkdirSync(this.dir, { recursive: true });
86
+ for (const layer of this.layers) {
87
+ mkdirSync(this.path(layer), { recursive: true });
88
+ }
89
+ if (!existsSync(this.path(MANIFEST_FILE))) {
90
+ const now = new Date().toISOString();
91
+ this.writeManifest({
92
+ agentId: this.agentId,
93
+ version: MANIFEST_VERSION,
94
+ createdAt: now,
95
+ updatedAt: now,
96
+ layers: this.layers,
97
+ custom: {},
98
+ });
99
+ }
100
+ return this;
101
+ }
102
+
103
+ // ── Manifest ───────────────────────────────────────────────────────────────
104
+
105
+ readManifest() {
106
+ try {
107
+ return JSON.parse(readFileSync(this.path(MANIFEST_FILE), 'utf-8'));
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ writeManifest(manifest) {
114
+ mkdirSync(this.dir, { recursive: true });
115
+ writeFileSync(this.path(MANIFEST_FILE), JSON.stringify(manifest, null, 2));
116
+ return manifest;
117
+ }
118
+
119
+ /** Shallow-merge `patch` into the manifest and bump `updatedAt`. */
120
+ patchManifest(patch) {
121
+ const current = this.readManifest() || { agentId: this.agentId, version: MANIFEST_VERSION, custom: {} };
122
+ const next = { ...current, ...patch, updatedAt: new Date().toISOString() };
123
+ return this.writeManifest(next);
124
+ }
125
+
126
+ // ── File operations (agent-relative paths) ──────────────────────────────────
127
+
128
+ exists(relPath) {
129
+ return existsSync(this.path(relPath));
130
+ }
131
+
132
+ writeFile(relPath, content) {
133
+ const abs = this.path(relPath);
134
+ mkdirSync(dirname(abs), { recursive: true });
135
+ writeFileSync(abs, content);
136
+ return abs;
137
+ }
138
+
139
+ readFile(relPath, encoding = 'utf-8') {
140
+ return readFileSync(this.path(relPath), encoding);
141
+ }
142
+
143
+ appendFile(relPath, content) {
144
+ const abs = this.path(relPath);
145
+ mkdirSync(dirname(abs), { recursive: true });
146
+ appendFileSync(abs, content);
147
+ return abs;
148
+ }
149
+
150
+ writeJson(relPath, data) {
151
+ return this.writeFile(relPath, JSON.stringify(data, null, 2));
152
+ }
153
+
154
+ readJson(relPath, fallback = null) {
155
+ try {
156
+ return JSON.parse(this.readFile(relPath));
157
+ } catch {
158
+ return fallback;
159
+ }
160
+ }
161
+
162
+ /** Create a directory (agent-relative). Recursive, idempotent. */
163
+ mkdir(relDir) {
164
+ const abs = this.path(relDir);
165
+ mkdirSync(abs, { recursive: true });
166
+ return abs;
167
+ }
168
+
169
+ remove(relPath) {
170
+ rmSync(this.path(relPath), { recursive: true, force: true });
171
+ }
172
+
173
+ /**
174
+ * List regular files under the agent dir (or a sub-path), agent-relative,
175
+ * using forward slashes. Recurses into subdirectories.
176
+ */
177
+ listFiles(relDir = '.') {
178
+ const base = this.path(relDir);
179
+ const out = [];
180
+ const walk = (absDir) => {
181
+ let entries;
182
+ try {
183
+ entries = readdirSync(absDir, { withFileTypes: true });
184
+ } catch {
185
+ return;
186
+ }
187
+ for (const e of entries) {
188
+ const abs = join(absDir, e.name);
189
+ if (e.isDirectory()) walk(abs);
190
+ else if (e.isFile()) out.push(relative(this.dir, abs).split(sep).join('/'));
191
+ }
192
+ };
193
+ walk(base);
194
+ return out.sort();
195
+ }
196
+
197
+ // ── Export / Import ─────────────────────────────────────────────────────────
198
+
199
+ /**
200
+ * Bundle the entire agent directory into a gzipped tar Buffer.
201
+ * Round-trips with `importBundle` / `AgentFs.fromBundle`.
202
+ */
203
+ exportBundle() {
204
+ const files = this.listFiles('.');
205
+ const entries = files.map((name) => ({
206
+ name,
207
+ data: readFileSync(join(this.dir, name.split('/').join(sep))),
208
+ }));
209
+ return packTarGz(entries);
210
+ }
211
+
212
+ /**
213
+ * Write the contents of a bundle into this agent directory.
214
+ * @param {Buffer} bundle
215
+ * @param {{ overwrite?: boolean }} [opts] overwrite existing files (default true)
216
+ */
217
+ importBundle(bundle, { overwrite = true } = {}) {
218
+ const entries = unpackTarGz(bundle);
219
+ mkdirSync(this.dir, { recursive: true });
220
+ let written = 0;
221
+ for (const { name, data } of entries) {
222
+ const rel = safeRelative(name);
223
+ const abs = join(this.dir, rel);
224
+ if (!overwrite && existsSync(abs)) continue;
225
+ mkdirSync(dirname(abs), { recursive: true });
226
+ writeFileSync(abs, data);
227
+ written++;
228
+ }
229
+ return { written, total: entries.length };
230
+ }
231
+
232
+ /** Rehydrate an agent from a bundle at a target root/agentId. */
233
+ static fromBundle(bundle, { root, agentId, overwrite = true } = {}) {
234
+ let id = agentId;
235
+ if (!id) {
236
+ // Infer agentId from the bundle's manifest if not provided.
237
+ const entries = unpackTarGz(bundle);
238
+ const manifest = entries.find((e) => e.name === MANIFEST_FILE);
239
+ if (manifest) {
240
+ try { id = JSON.parse(manifest.data.toString('utf-8')).agentId; } catch { /* ignore */ }
241
+ }
242
+ }
243
+ if (!id) throw new Error('agentfs: agentId required (not found in bundle manifest)');
244
+ const fs = new AgentFs({ root, agentId: id });
245
+ fs.importBundle(bundle, { overwrite });
246
+ return fs;
247
+ }
248
+ }
249
+
250
+ /** Convenience factory: build and `ensure()` an agent filesystem. */
251
+ export function createAgentFs(opts) {
252
+ return new AgentFs(opts).ensure();
253
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * AsyncAgentFs — the AgentFs model (layers + manifest + portable bundles) over
3
+ * a pluggable async StorageAdapter (see storage.js).
4
+ *
5
+ * Same concepts as the sync, disk-only AgentFs: a per-agent home with fixed
6
+ * layers, a manifest.json, safe agent-relative paths, and `.tar.gz`
7
+ * export/import round-trips. The storage backend is injected, so the same
8
+ * agent home can live on disk, in memory, or in a database — the platform's
9
+ * Supabase adapter (mounaji-backendv3 `agent_fs_nodes`) implements the same
10
+ * contract server-side.
11
+ */
12
+ import { join, relative, isAbsolute } from 'node:path';
13
+ import { DEFAULT_LAYERS } from './agentfs.js';
14
+ import { packTarGz, unpackTarGz } from './tar.js';
15
+
16
+ const MANIFEST_FILE = 'manifest.json';
17
+ const MANIFEST_VERSION = 1;
18
+
19
+ /** Normalize an agent-relative path and guard against escaping the agent home. */
20
+ function safeRelative(relPath) {
21
+ const normalized = String(relPath).replace(/\\/g, '/').replace(/^\/+/, '');
22
+ const joined = join('/agent-root', normalized);
23
+ const rel = relative('/agent-root', joined);
24
+ if (rel.startsWith('..') || isAbsolute(rel)) {
25
+ throw new Error(`agentfs: path escapes agent directory: "${relPath}"`);
26
+ }
27
+ return normalized;
28
+ }
29
+
30
+ export class AsyncAgentFs {
31
+ /**
32
+ * @param {{ agentId: string, storage: object, layers?: string[] }} opts
33
+ * storage — a StorageAdapter (read/write/remove/list/exists)
34
+ */
35
+ constructor({ agentId, storage, layers } = {}) {
36
+ if (!agentId || typeof agentId !== 'string') {
37
+ throw new Error('agentfs: agentId is required');
38
+ }
39
+ if (!storage || typeof storage.read !== 'function') {
40
+ throw new Error('agentfs: a StorageAdapter is required (see storage.js)');
41
+ }
42
+ this.agentId = agentId;
43
+ this.storage = storage;
44
+ this.layers = layers && layers.length ? [...layers] : [...DEFAULT_LAYERS];
45
+ }
46
+
47
+ /** Write the manifest if missing. Idempotent. */
48
+ async ensure() {
49
+ if (!(await this.storage.exists(MANIFEST_FILE))) {
50
+ const now = new Date().toISOString();
51
+ await this.writeManifest({
52
+ agentId: this.agentId,
53
+ version: MANIFEST_VERSION,
54
+ createdAt: now,
55
+ updatedAt: now,
56
+ layers: this.layers,
57
+ custom: {},
58
+ });
59
+ }
60
+ return this;
61
+ }
62
+
63
+ // ── Manifest ───────────────────────────────────────────────────────────────
64
+
65
+ async readManifest() {
66
+ const raw = await this.storage.read(MANIFEST_FILE);
67
+ if (raw == null) return null;
68
+ try {
69
+ return JSON.parse(raw.toString('utf-8'));
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ async writeManifest(manifest) {
76
+ await this.storage.write(MANIFEST_FILE, JSON.stringify(manifest, null, 2));
77
+ return manifest;
78
+ }
79
+
80
+ async patchManifest(patch) {
81
+ const current = (await this.readManifest())
82
+ || { agentId: this.agentId, version: MANIFEST_VERSION, custom: {} };
83
+ const next = { ...current, ...patch, updatedAt: new Date().toISOString() };
84
+ return this.writeManifest(next);
85
+ }
86
+
87
+ // ── File operations (agent-relative paths) ──────────────────────────────────
88
+
89
+ async exists(relPath) {
90
+ return this.storage.exists(safeRelative(relPath));
91
+ }
92
+
93
+ async writeFile(relPath, content) {
94
+ await this.storage.write(safeRelative(relPath), content);
95
+ }
96
+
97
+ async readFile(relPath, encoding = 'utf-8') {
98
+ const raw = await this.storage.read(safeRelative(relPath));
99
+ if (raw == null) throw new Error(`agentfs: not found: "${relPath}"`);
100
+ return encoding ? raw.toString(encoding) : raw;
101
+ }
102
+
103
+ async writeJson(relPath, data) {
104
+ return this.writeFile(relPath, JSON.stringify(data, null, 2));
105
+ }
106
+
107
+ async readJson(relPath, fallback = null) {
108
+ try {
109
+ return JSON.parse(await this.readFile(relPath));
110
+ } catch {
111
+ return fallback;
112
+ }
113
+ }
114
+
115
+ async remove(relPath) {
116
+ await this.storage.remove(safeRelative(relPath));
117
+ }
118
+
119
+ async listFiles(relDir = '') {
120
+ return this.storage.list(relDir ? safeRelative(relDir) : '');
121
+ }
122
+
123
+ // ── Export / Import (same bundle format as the sync AgentFs) ───────────────
124
+
125
+ async exportBundle() {
126
+ const files = await this.listFiles('');
127
+ const entries = [];
128
+ for (const name of files) {
129
+ const data = await this.storage.read(name);
130
+ if (data != null) {
131
+ entries.push({ name, data: Buffer.isBuffer(data) ? data : Buffer.from(String(data)) });
132
+ }
133
+ }
134
+ return packTarGz(entries);
135
+ }
136
+
137
+ async importBundle(bundle, { overwrite = true } = {}) {
138
+ const entries = unpackTarGz(bundle);
139
+ let written = 0;
140
+ for (const { name, data } of entries) {
141
+ const rel = safeRelative(name);
142
+ if (!overwrite && (await this.storage.exists(rel))) continue;
143
+ await this.storage.write(rel, data);
144
+ written++;
145
+ }
146
+ return { written, total: entries.length };
147
+ }
148
+ }
149
+
150
+ /** Convenience factory: build and `ensure()` an async agent filesystem. */
151
+ export async function createAsyncAgentFs(opts) {
152
+ return new AsyncAgentFs(opts).ensure();
153
+ }
package/src/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @mounaji_npm/agentfs — per-agent portable filesystem.
3
+ *
4
+ * @example
5
+ * import { createAgentFs, AgentFs } from '@mounaji_npm/agentfs';
6
+ *
7
+ * const fs = createAgentFs({ root: '/data/agents', agentId: 'abc123' });
8
+ * fs.writeJson('memory/facts.json', { likes: ['dark mode'] });
9
+ * fs.appendFile('learning/feedback_log.jsonl', JSON.stringify({ ok: true }) + '\n');
10
+ *
11
+ * const bundle = fs.exportBundle(); // Buffer (.tar.gz)
12
+ * const clone = AgentFs.fromBundle(bundle, { root: '/tmp', agentId: 'abc123' });
13
+ */
14
+ export { AgentFs, createAgentFs, DEFAULT_LAYERS } from './agentfs.js';
15
+ export { AsyncAgentFs, createAsyncAgentFs } from './asyncAgentfs.js';
16
+ export { MemoryStorageAdapter, DiskStorageAdapter } from './storage.js';
17
+ export { packTar, unpackTar, packTarGz, unpackTarGz } from './tar.js';
package/src/storage.js ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * StorageAdapter — pluggable backends for AsyncAgentFs.
3
+ *
4
+ * The sync AgentFs class (agentfs.js) is disk-only by design. AsyncAgentFs
5
+ * (asyncAgentfs.js) talks to any backend through this minimal async contract,
6
+ * so the same layered agent home can live on local disk, in memory (tests),
7
+ * or in a database (e.g. the platform's Supabase `agent_fs_nodes` adapter).
8
+ *
9
+ * Contract (all paths are agent-relative, forward slashes, already sanitized
10
+ * by AsyncAgentFs — adapters never see `..`):
11
+ * read(path) → Promise<Buffer|string|null> null when missing
12
+ * write(path, data) → Promise<void> creates parents
13
+ * remove(path) → Promise<void> recursive, tolerant
14
+ * list(prefix) → Promise<string[]> file paths under prefix
15
+ * exists(path) → Promise<boolean>
16
+ */
17
+ import { join, dirname, sep, relative } from 'node:path';
18
+ import {
19
+ mkdirSync, existsSync, readFileSync, writeFileSync, rmSync,
20
+ readdirSync,
21
+ } from 'node:fs';
22
+
23
+ /** In-memory adapter — reference implementation and test double. */
24
+ export class MemoryStorageAdapter {
25
+ constructor() {
26
+ this.files = new Map(); // path → Buffer
27
+ }
28
+
29
+ async read(path) {
30
+ return this.files.has(path) ? this.files.get(path) : null;
31
+ }
32
+
33
+ async write(path, data) {
34
+ this.files.set(path, Buffer.isBuffer(data) ? data : Buffer.from(String(data)));
35
+ }
36
+
37
+ async remove(path) {
38
+ this.files.delete(path);
39
+ const prefix = `${path}/`;
40
+ for (const key of [...this.files.keys()]) {
41
+ if (key.startsWith(prefix)) this.files.delete(key);
42
+ }
43
+ }
44
+
45
+ async list(prefix = '') {
46
+ const normalized = prefix && !prefix.endsWith('/') ? `${prefix}/` : prefix;
47
+ return [...this.files.keys()]
48
+ .filter((k) => !normalized || k.startsWith(normalized))
49
+ .sort();
50
+ }
51
+
52
+ async exists(path) {
53
+ if (this.files.has(path)) return true;
54
+ const prefix = `${path}/`;
55
+ for (const key of this.files.keys()) {
56
+ if (key.startsWith(prefix)) return true;
57
+ }
58
+ return false;
59
+ }
60
+ }
61
+
62
+ /** Disk adapter — same layout as the sync AgentFs (a base directory). */
63
+ export class DiskStorageAdapter {
64
+ /** @param {{ baseDir: string }} opts absolute directory holding this agent */
65
+ constructor({ baseDir }) {
66
+ if (!baseDir) throw new Error('agentfs: DiskStorageAdapter requires baseDir');
67
+ this.baseDir = baseDir;
68
+ }
69
+
70
+ _abs(path) {
71
+ return join(this.baseDir, path.split('/').join(sep));
72
+ }
73
+
74
+ async read(path) {
75
+ try {
76
+ return readFileSync(this._abs(path));
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ async write(path, data) {
83
+ const abs = this._abs(path);
84
+ mkdirSync(dirname(abs), { recursive: true });
85
+ writeFileSync(abs, data);
86
+ }
87
+
88
+ async remove(path) {
89
+ rmSync(this._abs(path), { recursive: true, force: true });
90
+ }
91
+
92
+ async list(prefix = '') {
93
+ const out = [];
94
+ const walk = (absDir) => {
95
+ let entries;
96
+ try {
97
+ entries = readdirSync(absDir, { withFileTypes: true });
98
+ } catch {
99
+ return;
100
+ }
101
+ for (const e of entries) {
102
+ const abs = join(absDir, e.name);
103
+ if (e.isDirectory()) walk(abs);
104
+ else if (e.isFile()) out.push(relative(this.baseDir, abs).split(sep).join('/'));
105
+ }
106
+ };
107
+ walk(prefix ? this._abs(prefix) : this.baseDir);
108
+ return out.sort();
109
+ }
110
+
111
+ async exists(path) {
112
+ return existsSync(this._abs(path));
113
+ }
114
+ }
package/src/tar.js ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Minimal, dependency-free tar (ustar) + gzip writer/reader.
3
+ *
4
+ * We roll our own instead of pulling `tar`/`jszip` so `@mounaji_npm/agentfs`
5
+ * stays dependency-light like the rest of the monorepo. Scope is intentionally
6
+ * narrow: regular files only (no symlinks/devices), which is all an agent's
7
+ * layered home needs. Produces standard `.tar.gz` archives that `tar -xzf`
8
+ * (and any tar library) can read.
9
+ */
10
+ import { gzipSync, gunzipSync } from 'node:zlib';
11
+
12
+ const BLOCK = 512;
13
+
14
+ /** Write a string into `buf` at `offset`, at most `len` bytes, NUL-padded. */
15
+ function writeField(buf, str, offset, len) {
16
+ const bytes = Buffer.from(String(str), 'utf-8');
17
+ if (bytes.length > len) {
18
+ throw new Error(`tar field overflow (${bytes.length} > ${len}): "${str}"`);
19
+ }
20
+ bytes.copy(buf, offset);
21
+ }
22
+
23
+ /** Octal-encode `num` into a fixed-width field: (len-1) digits + NUL. */
24
+ function octalField(num, len) {
25
+ return num.toString(8).padStart(len - 1, '0') + '\0';
26
+ }
27
+
28
+ /** Read a NUL-terminated string from a header field. */
29
+ function readField(buf, offset, len) {
30
+ let end = offset;
31
+ const limit = offset + len;
32
+ while (end < limit && buf[end] !== 0) end++;
33
+ return buf.toString('utf-8', offset, end);
34
+ }
35
+
36
+ /**
37
+ * Split a path longer than 100 bytes into ustar {prefix, name}. Returns null
38
+ * if it cannot be represented (name > 100 or prefix > 155 with no clean split).
39
+ */
40
+ function splitName(path) {
41
+ const full = Buffer.from(path, 'utf-8');
42
+ if (full.length <= 100) return { name: path, prefix: '' };
43
+ // Find a '/' such that the tail (name) is <= 100 bytes and head (prefix) <= 155.
44
+ for (let i = path.length - 1; i > 0; i--) {
45
+ if (path[i] !== '/') continue;
46
+ const name = path.slice(i + 1);
47
+ const prefix = path.slice(0, i);
48
+ if (Buffer.byteLength(name) <= 100 && Buffer.byteLength(prefix) <= 155) {
49
+ return { name, prefix };
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+
55
+ /** Build a single 512-byte ustar header for a regular file. */
56
+ function makeHeader({ name, prefix, size, mtime }) {
57
+ const h = Buffer.alloc(BLOCK);
58
+ writeField(h, name, 0, 100);
59
+ writeField(h, '0000644\0', 100, 8); // mode: rw-r--r--
60
+ writeField(h, octalField(0, 8), 108, 8); // uid
61
+ writeField(h, octalField(0, 8), 116, 8); // gid
62
+ writeField(h, octalField(size, 12), 124, 12);
63
+ writeField(h, octalField(mtime, 12), 136, 12);
64
+ h.fill(0x20, 148, 156); // checksum placeholder: 8 spaces
65
+ writeField(h, '0', 156, 1); // typeflag: regular file
66
+ writeField(h, 'ustar\0', 257, 6);
67
+ writeField(h, '00', 263, 2);
68
+ if (prefix) writeField(h, prefix, 345, 155);
69
+
70
+ let sum = 0;
71
+ for (let i = 0; i < BLOCK; i++) sum += h[i];
72
+ // Checksum field: 6 octal digits, NUL, space.
73
+ writeField(h, sum.toString(8).padStart(6, '0') + '\0 ', 148, 8);
74
+ return h;
75
+ }
76
+
77
+ /**
78
+ * Pack an array of `{ name, data }` (data: Buffer|string) into a tar buffer.
79
+ */
80
+ export function packTar(entries) {
81
+ const mtime = Math.floor(Date.now() / 1000);
82
+ const chunks = [];
83
+ for (const entry of entries) {
84
+ const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data ?? '', 'utf-8');
85
+ const split = splitName(entry.name.replace(/\\/g, '/'));
86
+ if (!split) throw new Error(`tar: path too long to encode: "${entry.name}"`);
87
+ chunks.push(makeHeader({ name: split.name, prefix: split.prefix, size: data.length, mtime }));
88
+ chunks.push(data);
89
+ const pad = (BLOCK - (data.length % BLOCK)) % BLOCK;
90
+ if (pad) chunks.push(Buffer.alloc(pad));
91
+ }
92
+ // Two zero blocks terminate the archive.
93
+ chunks.push(Buffer.alloc(BLOCK * 2));
94
+ return Buffer.concat(chunks);
95
+ }
96
+
97
+ /** Parse a tar buffer into an array of `{ name, data }` (regular files only). */
98
+ export function unpackTar(buf) {
99
+ const files = [];
100
+ let off = 0;
101
+ while (off + BLOCK <= buf.length) {
102
+ const header = buf.subarray(off, off + BLOCK);
103
+ // A full zero block marks the end of the archive.
104
+ let allZero = true;
105
+ for (let i = 0; i < BLOCK; i++) {
106
+ if (header[i] !== 0) { allZero = false; break; }
107
+ }
108
+ if (allZero) break;
109
+
110
+ const name = readField(header, 0, 100);
111
+ const prefix = readField(header, 345, 155);
112
+ const size = parseInt(readField(header, 124, 12).trim() || '0', 8) || 0;
113
+ const type = String.fromCharCode(header[156] || 0x30);
114
+ off += BLOCK;
115
+
116
+ const data = Buffer.from(buf.subarray(off, off + size));
117
+ // typeflag '0' or NUL (0x00 → '\0') means regular file; skip others.
118
+ if (type === '0' || type === '\0') {
119
+ const full = prefix ? `${prefix}/${name}` : name;
120
+ files.push({ name: full, data });
121
+ }
122
+ off += Math.ceil(size / BLOCK) * BLOCK;
123
+ }
124
+ return files;
125
+ }
126
+
127
+ /** Pack + gzip: `{ name, data }[]` → gzipped tar Buffer. */
128
+ export function packTarGz(entries) {
129
+ return gzipSync(packTar(entries));
130
+ }
131
+
132
+ /** Gunzip + unpack: gzipped tar Buffer → `{ name, data }[]`. */
133
+ export function unpackTarGz(buf) {
134
+ return unpackTar(gunzipSync(buf));
135
+ }