@agentproto/corpus-cli 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ export { N as NodeFsAdapter, a as NodeFsAdapterOptions, O as OsIdentityAdapter, b as OsIdentityAdapterOptions } from './os-identity.adapter-CLBmnRoD.js';
2
+ import '@agentproto/corpus';
3
+
4
+ /**
5
+ * Version constant — pinned at build time. Stay in sync with
6
+ * package.json manually until we wire a build-time inliner.
7
+ */
8
+ declare const VERSION: "0.1.0-alpha.0";
9
+
10
+ export { VERSION };
package/dist/index.mjs ADDED
@@ -0,0 +1,172 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { stat, readFile, mkdir, writeFile, rename, appendFile, readdir, rm } from 'fs/promises';
3
+ import path from 'path';
4
+ import { existsSync } from 'fs';
5
+ import os from 'os';
6
+
7
+ /**
8
+ * @agentproto/corpus-cli v0.1.0-alpha
9
+ * The `corpus` binary — local-topology host for AIP-10 corpora.
10
+ */
11
+
12
+ // src/version.ts
13
+ var VERSION = "0.1.0-alpha.0";
14
+ var NodeFsAdapter = class {
15
+ root;
16
+ constructor(opts) {
17
+ this.root = opts.root;
18
+ }
19
+ async exists(p) {
20
+ try {
21
+ await stat(this.resolve(p));
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+ async readFile(p) {
28
+ return await readFile(this.resolve(p), "utf8");
29
+ }
30
+ /**
31
+ * Atomic write: write to `${target}.tmp-{nonce}`, fsync, rename.
32
+ * Rename is atomic on the same filesystem. Creates parent dirs as
33
+ * needed (most callers expect mkdir -p semantics).
34
+ */
35
+ async writeFile(p, content) {
36
+ const target = this.resolve(p);
37
+ await mkdir(path.dirname(target), { recursive: true });
38
+ const tmp = `${target}.tmp-${randomBytes(8).toString("hex")}`;
39
+ await writeFile(tmp, content, "utf8");
40
+ await rename(tmp, target);
41
+ }
42
+ async appendFile(p, content) {
43
+ const target = this.resolve(p);
44
+ await mkdir(path.dirname(target), { recursive: true });
45
+ await appendFile(target, content, "utf8");
46
+ }
47
+ async readdir(p) {
48
+ const target = this.resolve(p);
49
+ const entries = await readdir(target);
50
+ return entries;
51
+ }
52
+ async walk(p) {
53
+ const out = [];
54
+ const start = this.resolve(p);
55
+ const rootForRelative = start;
56
+ const visit = async (dir) => {
57
+ let entries = [];
58
+ try {
59
+ entries = await readdir(dir);
60
+ } catch {
61
+ return;
62
+ }
63
+ for (const ent of entries) {
64
+ if (ent.startsWith(".")) continue;
65
+ const full = path.join(dir, ent);
66
+ const s = await stat(full);
67
+ if (s.isDirectory()) {
68
+ await visit(full);
69
+ } else if (s.isFile()) {
70
+ const rel = path.relative(rootForRelative, full).split(path.sep).join("/");
71
+ out.push(rel);
72
+ }
73
+ }
74
+ };
75
+ await visit(start);
76
+ return out;
77
+ }
78
+ async stat(p) {
79
+ try {
80
+ const s = await stat(this.resolve(p));
81
+ return {
82
+ kind: s.isDirectory() ? "directory" : "file",
83
+ bytes: s.isFile() ? s.size : void 0,
84
+ modifiedAt: s.mtime
85
+ };
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+ /**
91
+ * Advisory lock via a lockfile. Spins waiting for the lockfile to
92
+ * disappear, then creates it with exclusive flag. Released by
93
+ * deleting it. Single-process safe; multi-process semi-safe (the
94
+ * write is best-effort exclusive — adequate for the corpus's
95
+ * promote-entry transaction which is rare and short-lived).
96
+ */
97
+ async lock(p) {
98
+ const target = this.resolve(p);
99
+ const lockDir = path.join(path.dirname(target), ".corpus-locks");
100
+ await mkdir(lockDir, { recursive: true });
101
+ const lockFile = path.join(lockDir, path.basename(target) + ".lock");
102
+ const myToken = `${process.pid}-${randomBytes(4).toString("hex")}`;
103
+ while (true) {
104
+ if (!existsSync(lockFile)) {
105
+ try {
106
+ await writeFile(lockFile, myToken, { encoding: "utf8", flag: "wx" });
107
+ const written = await readFile(lockFile, "utf8").catch(() => "");
108
+ if (written === myToken) break;
109
+ } catch {
110
+ }
111
+ }
112
+ await new Promise((r) => setTimeout(r, 25));
113
+ }
114
+ return {
115
+ release: async () => {
116
+ try {
117
+ await rm(lockFile, { force: true });
118
+ } catch {
119
+ }
120
+ }
121
+ };
122
+ }
123
+ // ── Helpers ────────────────────────────────────────────────────
124
+ /**
125
+ * Resolve a workspace-relative path to an absolute host path.
126
+ * Refuses to climb out of the workspace root (defense against
127
+ * `../../etc/passwd`-style abuse) — the corpus kit's paths must
128
+ * stay inside the workspace.
129
+ */
130
+ resolve(p) {
131
+ const normalized = path.posix.normalize("/" + p.replace(/^\/+/, ""));
132
+ const rel = normalized.slice(1);
133
+ if (rel.startsWith("..")) {
134
+ throw new Error(
135
+ `NodeFsAdapter: path "${p}" escapes the workspace root "${this.root}"`
136
+ );
137
+ }
138
+ return path.join(this.root, rel);
139
+ }
140
+ };
141
+ var OsIdentityAdapter = class {
142
+ cached;
143
+ constructor(opts) {
144
+ const user = opts.userOverride ?? process.env.USER ?? process.env.USERNAME ?? safeGetUser() ?? "anonymous";
145
+ const wsName = path.basename(opts.workspaceRoot) || "default";
146
+ this.cached = Object.freeze({
147
+ principal: `ws://users/${slugify(user)}`,
148
+ identityTree: Object.freeze([
149
+ `ws://users/${slugify(user)}`,
150
+ `ws://workspaces/${slugify(wsName)}`
151
+ ]),
152
+ displayName: user
153
+ });
154
+ }
155
+ async resolve() {
156
+ return this.cached;
157
+ }
158
+ };
159
+ function safeGetUser() {
160
+ try {
161
+ return os.userInfo().username;
162
+ } catch {
163
+ return void 0;
164
+ }
165
+ }
166
+ function slugify(s) {
167
+ return s.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 64) || "anonymous";
168
+ }
169
+
170
+ export { NodeFsAdapter, OsIdentityAdapter, VERSION };
171
+ //# sourceMappingURL=index.mjs.map
172
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/version.ts","../src/ports/local-fs.adapter.ts","../src/ports/os-identity.adapter.ts"],"names":["path"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Version constant — pinned at build time. Stay in sync with\n * package.json manually until we wire a build-time inliner.\n */\nexport const VERSION = \"0.1.0-alpha.0\" as const\n","/**\n * NodeFsAdapter — `FsPort` implementation backed by node:fs.\n *\n * Atomic writes via temp + rename (the same posix pattern every\n * mature tool uses). Recursive `walk` skips hidden segments\n * (anything beginning with `.`). Advisory locks via lockfiles in\n * `.corpus-locks/` next to the target — single-process safe; for\n * multi-process the lockfile owner pid is recorded so callers can\n * see who holds the lock.\n */\n\nimport { randomBytes } from \"node:crypto\"\nimport {\n appendFile,\n mkdir,\n readFile,\n readdir,\n rename,\n rm,\n stat,\n writeFile,\n} from \"node:fs/promises\"\nimport path from \"node:path\"\nimport { existsSync } from \"node:fs\"\nimport type {\n FsPort,\n FsLockHandle,\n FsStat,\n} from \"@agentproto/corpus\"\n\nexport interface NodeFsAdapterOptions {\n /**\n * Absolute path to the workspace root. All FsPort paths are\n * resolved relative to this — the kit never sees host filesystem\n * paths.\n */\n readonly root: string\n}\n\nexport class NodeFsAdapter implements FsPort {\n private readonly root: string\n\n constructor(opts: NodeFsAdapterOptions) {\n this.root = opts.root\n }\n\n async exists(p: string): Promise<boolean> {\n try {\n await stat(this.resolve(p))\n return true\n } catch {\n return false\n }\n }\n\n async readFile(p: string): Promise<string> {\n return await readFile(this.resolve(p), \"utf8\")\n }\n\n /**\n * Atomic write: write to `${target}.tmp-{nonce}`, fsync, rename.\n * Rename is atomic on the same filesystem. Creates parent dirs as\n * needed (most callers expect mkdir -p semantics).\n */\n async writeFile(p: string, content: string): Promise<void> {\n const target = this.resolve(p)\n await mkdir(path.dirname(target), { recursive: true })\n const tmp = `${target}.tmp-${randomBytes(8).toString(\"hex\")}`\n await writeFile(tmp, content, \"utf8\")\n await rename(tmp, target)\n }\n\n async appendFile(p: string, content: string): Promise<void> {\n const target = this.resolve(p)\n await mkdir(path.dirname(target), { recursive: true })\n await appendFile(target, content, \"utf8\")\n }\n\n async readdir(p: string): Promise<readonly string[]> {\n const target = this.resolve(p)\n const entries = await readdir(target)\n return entries\n }\n\n async walk(p: string): Promise<readonly string[]> {\n const out: string[] = []\n const start = this.resolve(p)\n const rootForRelative = start\n const visit = async (dir: string): Promise<void> => {\n let entries: string[] = []\n try {\n entries = await readdir(dir)\n } catch {\n return\n }\n for (const ent of entries) {\n // Skip hidden segments — matches the FsPort contract.\n if (ent.startsWith(\".\")) continue\n const full = path.join(dir, ent)\n const s = await stat(full)\n if (s.isDirectory()) {\n await visit(full)\n } else if (s.isFile()) {\n const rel = path.relative(rootForRelative, full).split(path.sep).join(\"/\")\n out.push(rel)\n }\n }\n }\n await visit(start)\n return out\n }\n\n async stat(p: string): Promise<FsStat | null> {\n try {\n const s = await stat(this.resolve(p))\n return {\n kind: s.isDirectory() ? \"directory\" : \"file\",\n bytes: s.isFile() ? s.size : undefined,\n modifiedAt: s.mtime,\n }\n } catch {\n return null\n }\n }\n\n /**\n * Advisory lock via a lockfile. Spins waiting for the lockfile to\n * disappear, then creates it with exclusive flag. Released by\n * deleting it. Single-process safe; multi-process semi-safe (the\n * write is best-effort exclusive — adequate for the corpus's\n * promote-entry transaction which is rare and short-lived).\n */\n async lock(p: string): Promise<FsLockHandle> {\n const target = this.resolve(p)\n const lockDir = path.join(path.dirname(target), \".corpus-locks\")\n await mkdir(lockDir, { recursive: true })\n const lockFile = path.join(lockDir, path.basename(target) + \".lock\")\n const myToken = `${process.pid}-${randomBytes(4).toString(\"hex\")}`\n\n // Spin until we acquire. Cheap polling is fine for corpus\n // workloads — transactions are < 1s.\n while (true) {\n if (!existsSync(lockFile)) {\n try {\n await writeFile(lockFile, myToken, { encoding: \"utf8\", flag: \"wx\" })\n // Race: verify our token is the one that landed.\n const written = await readFile(lockFile, \"utf8\").catch(() => \"\")\n if (written === myToken) break\n } catch {\n // Lost the race — fall through and retry.\n }\n }\n await new Promise((r) => setTimeout(r, 25))\n }\n return {\n release: async () => {\n try {\n await rm(lockFile, { force: true })\n } catch {\n // Lockfile already gone — no-op.\n }\n },\n }\n }\n\n // ── Helpers ────────────────────────────────────────────────────\n\n /**\n * Resolve a workspace-relative path to an absolute host path.\n * Refuses to climb out of the workspace root (defense against\n * `../../etc/passwd`-style abuse) — the corpus kit's paths must\n * stay inside the workspace.\n */\n private resolve(p: string): string {\n const normalized = path.posix.normalize(\"/\" + p.replace(/^\\/+/, \"\"))\n // Drop the leading \"/\" so path.join joins correctly under root.\n const rel = normalized.slice(1)\n if (rel.startsWith(\"..\")) {\n throw new Error(\n `NodeFsAdapter: path \"${p}\" escapes the workspace root \"${this.root}\"`\n )\n }\n return path.join(this.root, rel)\n }\n}\n","/**\n * OsIdentityAdapter — `IdentityPort` resolved from OS user + cwd.\n *\n * Local-topology identity model:\n * - principal = `ws://users/{os-user}`\n * - identityTree = [\n * `ws://users/{os-user}`,\n * `ws://workspaces/{workspace-dir-name}`,\n * ]\n *\n * Roles + guilds + orgs don't exist locally — single-user, single-\n * workspace machine. Scope-policy in the corpus adapter matches\n * against this minimal tree, so AIP-10 `appliesTo` entries scoped\n * narrower than `ws://users/...` or `ws://workspaces/...` are\n * invisible to the local user. That's the intended local-topology\n * semantics: you only see what's scoped to you or your workspace.\n */\n\nimport os from \"node:os\"\nimport path from \"node:path\"\nimport type {\n CallerIdentity,\n IdentityPort,\n} from \"@agentproto/corpus\"\n\nexport interface OsIdentityAdapterOptions {\n readonly workspaceRoot: string\n /** Override the OS user (useful for tests). Falls back to os.userInfo(). */\n readonly userOverride?: string\n}\n\nexport class OsIdentityAdapter implements IdentityPort {\n private readonly cached: CallerIdentity\n\n constructor(opts: OsIdentityAdapterOptions) {\n // Env vars win over os.userInfo() — matches Unix convention (env is\n // explicit intent, OS detection is fallback). Also keeps tests\n // deterministic across CI environments where the OS user differs.\n const user =\n opts.userOverride ??\n process.env.USER ??\n process.env.USERNAME ??\n safeGetUser() ??\n \"anonymous\"\n const wsName = path.basename(opts.workspaceRoot) || \"default\"\n this.cached = Object.freeze({\n principal: `ws://users/${slugify(user)}`,\n identityTree: Object.freeze([\n `ws://users/${slugify(user)}`,\n `ws://workspaces/${slugify(wsName)}`,\n ]),\n displayName: user,\n })\n }\n\n async resolve(): Promise<CallerIdentity> {\n return this.cached\n }\n}\n\nfunction safeGetUser(): string | undefined {\n try {\n return os.userInfo().username\n } catch {\n return undefined\n }\n}\n\nfunction slugify(s: string): string {\n return s\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .replace(/-{2,}/g, \"-\")\n .slice(0, 64) || \"anonymous\"\n}\n"]}
@@ -0,0 +1,83 @@
1
+ import { FsPort, FsStat, FsLockHandle, IdentityPort, CallerIdentity } from '@agentproto/corpus';
2
+
3
+ /**
4
+ * NodeFsAdapter — `FsPort` implementation backed by node:fs.
5
+ *
6
+ * Atomic writes via temp + rename (the same posix pattern every
7
+ * mature tool uses). Recursive `walk` skips hidden segments
8
+ * (anything beginning with `.`). Advisory locks via lockfiles in
9
+ * `.corpus-locks/` next to the target — single-process safe; for
10
+ * multi-process the lockfile owner pid is recorded so callers can
11
+ * see who holds the lock.
12
+ */
13
+
14
+ interface NodeFsAdapterOptions {
15
+ /**
16
+ * Absolute path to the workspace root. All FsPort paths are
17
+ * resolved relative to this — the kit never sees host filesystem
18
+ * paths.
19
+ */
20
+ readonly root: string;
21
+ }
22
+ declare class NodeFsAdapter implements FsPort {
23
+ private readonly root;
24
+ constructor(opts: NodeFsAdapterOptions);
25
+ exists(p: string): Promise<boolean>;
26
+ readFile(p: string): Promise<string>;
27
+ /**
28
+ * Atomic write: write to `${target}.tmp-{nonce}`, fsync, rename.
29
+ * Rename is atomic on the same filesystem. Creates parent dirs as
30
+ * needed (most callers expect mkdir -p semantics).
31
+ */
32
+ writeFile(p: string, content: string): Promise<void>;
33
+ appendFile(p: string, content: string): Promise<void>;
34
+ readdir(p: string): Promise<readonly string[]>;
35
+ walk(p: string): Promise<readonly string[]>;
36
+ stat(p: string): Promise<FsStat | null>;
37
+ /**
38
+ * Advisory lock via a lockfile. Spins waiting for the lockfile to
39
+ * disappear, then creates it with exclusive flag. Released by
40
+ * deleting it. Single-process safe; multi-process semi-safe (the
41
+ * write is best-effort exclusive — adequate for the corpus's
42
+ * promote-entry transaction which is rare and short-lived).
43
+ */
44
+ lock(p: string): Promise<FsLockHandle>;
45
+ /**
46
+ * Resolve a workspace-relative path to an absolute host path.
47
+ * Refuses to climb out of the workspace root (defense against
48
+ * `../../etc/passwd`-style abuse) — the corpus kit's paths must
49
+ * stay inside the workspace.
50
+ */
51
+ private resolve;
52
+ }
53
+
54
+ /**
55
+ * OsIdentityAdapter — `IdentityPort` resolved from OS user + cwd.
56
+ *
57
+ * Local-topology identity model:
58
+ * - principal = `ws://users/{os-user}`
59
+ * - identityTree = [
60
+ * `ws://users/{os-user}`,
61
+ * `ws://workspaces/{workspace-dir-name}`,
62
+ * ]
63
+ *
64
+ * Roles + guilds + orgs don't exist locally — single-user, single-
65
+ * workspace machine. Scope-policy in the corpus adapter matches
66
+ * against this minimal tree, so AIP-10 `appliesTo` entries scoped
67
+ * narrower than `ws://users/...` or `ws://workspaces/...` are
68
+ * invisible to the local user. That's the intended local-topology
69
+ * semantics: you only see what's scoped to you or your workspace.
70
+ */
71
+
72
+ interface OsIdentityAdapterOptions {
73
+ readonly workspaceRoot: string;
74
+ /** Override the OS user (useful for tests). Falls back to os.userInfo(). */
75
+ readonly userOverride?: string;
76
+ }
77
+ declare class OsIdentityAdapter implements IdentityPort {
78
+ private readonly cached;
79
+ constructor(opts: OsIdentityAdapterOptions);
80
+ resolve(): Promise<CallerIdentity>;
81
+ }
82
+
83
+ export { NodeFsAdapter as N, OsIdentityAdapter as O, type NodeFsAdapterOptions as a, type OsIdentityAdapterOptions as b };