@agentproto/manifest 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.
package/LICENSE ADDED
@@ -0,0 +1 @@
1
+ MIT License — Copyright (c) 2026 agentproto contributors
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @agentproto/manifest
2
+
3
+ Generic verbs for AIP doctypes. See [AIP-1](https://agentproto.sh/docs/aip-1).
4
+
5
+ ```ts
6
+ import { createVerbs } from "@agentproto/manifest"
7
+
8
+ const verbs = createVerbs({
9
+ name: "tool",
10
+ aip: 14,
11
+ schemaLiteral: "agentproto/tool/v1",
12
+ pathOf: (h) => `${h.id}/TOOL.md`,
13
+ define: defineTool,
14
+ parse: parseToolManifest,
15
+ })
16
+
17
+ await verbs.create({ id: "echo", ... }, { dir: "tools" })
18
+ const echo = await verbs.load("tools/echo/TOOL.md")
19
+ const all = await verbs.list("tools")
20
+ await verbs.update("tools/echo/TOOL.md", (p) => ({ ...p, version: "1.0.1" }))
21
+ const resolved = await verbs.resolve({ ref: "@agentik/runners/python" }, ctx)
22
+ await verbs.delete("tools/echo/TOOL.md")
23
+ ```
24
+
25
+ ## License
26
+ MIT
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @agentproto/manifest — generic verbs for AIP doctypes.
3
+ *
4
+ * Every per-AIP package (`@agentproto/tool`, `@agentproto/driver`, …)
5
+ * supplies a `DoctypeSpec` describing the per-package knobs (name,
6
+ * AIP number, schema literal, path convention, validator, parser).
7
+ * `createVerbs(spec)` returns the full lifecycle:
8
+ *
9
+ * create(params, opts) params → .md on disk
10
+ * load(path) .md on disk → handle
11
+ * list(dir, filter?) walk → handle[]
12
+ * update(path, mutator) load → patch → write back
13
+ * resolve(block, ctx?) { inline | ref | file } → handle
14
+ * delete(path) fs.unlink
15
+ *
16
+ * Adding a new verb is a single place; every package picks it up
17
+ * automatically — same model as `createDoctype` for the validation
18
+ * pipeline.
19
+ */
20
+ /**
21
+ * Per-package descriptor consumed by `createVerbs`.
22
+ *
23
+ * Generic over the params shape (input to `define`) and the handle
24
+ * shape (output of `define`). For most AIPs `THandle` extends
25
+ * `TParams` — handles are just frozen, defaulted definitions.
26
+ */
27
+ interface DoctypeSpec<TParams extends {
28
+ id?: string;
29
+ slug?: string;
30
+ name?: string;
31
+ }, THandle> {
32
+ /** Lower-case singular: "tool", "driver", "operator". Used in errors. */
33
+ name: string;
34
+ /** AIP number — surfaces in error messages for provenance. */
35
+ aip: number;
36
+ /** Frontmatter `schema:` literal (e.g. "agentproto/tool/v1"). */
37
+ schemaLiteral: string;
38
+ /**
39
+ * Filename of the doctype's manifest. By default `<DOCTYPE>.md`,
40
+ * derived from `name.toUpperCase()`. Override only when the
41
+ * doctype's filename diverges from convention.
42
+ */
43
+ filename?: string;
44
+ /**
45
+ * Path-of-handle convention. Given a validated handle, return the
46
+ * workspace-relative path the manifest lives at. Examples:
47
+ * tool: `(h) => `${h.id}/TOOL.md``
48
+ * policy: `(h) => `policies/${h.slug}/POLICY.md``
49
+ * operator: `(h) => `${h.id}/OPERATOR.md``
50
+ */
51
+ pathOf(handle: THandle): string;
52
+ /** TS authoring path: validates params + applies defaults. */
53
+ define(params: TParams): THandle;
54
+ /** MD authoring path: parses YAML+body. Throws on invalid frontmatter. */
55
+ parse(source: string): {
56
+ frontmatter: Record<string, unknown>;
57
+ body: string;
58
+ };
59
+ /**
60
+ * Optional projection: handle → manifest-shaped frontmatter object.
61
+ * Default: pass the params through `filterSerializable` (drops zod
62
+ * schemas + functions). Override when the handle and frontmatter
63
+ * shapes diverge non-trivially.
64
+ */
65
+ toFrontmatter?(params: TParams): Record<string, unknown>;
66
+ }
67
+ interface CreateOptions {
68
+ /** Workspace-relative or absolute base directory. */
69
+ dir: string;
70
+ /** Body markdown after the frontmatter. Defaults to a one-line stub. */
71
+ body?: string;
72
+ /** Render only — don't write. Returns the rendered string. */
73
+ dryRun?: boolean;
74
+ }
75
+ interface VerbResult<THandle> {
76
+ path: string;
77
+ handle: THandle;
78
+ rendered: string;
79
+ }
80
+ interface ListOptions {
81
+ /** Filter the discovered handles. Receives the loaded handle. */
82
+ filter?: <H>(handle: H) => boolean;
83
+ /** Skip subdirectories matching these names. Default: ["node_modules", ".git", "dist", ".next"]. */
84
+ skipDirs?: readonly string[];
85
+ }
86
+ type RefBlock<TParams> = {
87
+ inline: TParams;
88
+ } | {
89
+ ref: string;
90
+ } | {
91
+ file: string;
92
+ };
93
+ interface ResolveContext {
94
+ /** Base dir for `file:` references. Required when resolving file blocks. */
95
+ baseDir?: string;
96
+ /**
97
+ * Registry resolver for `ref:` strings (e.g. `@agentik/runners/python-3.12`
98
+ * → handle). Optional; throws when an unresolvable ref is encountered.
99
+ */
100
+ resolveRef?: (ref: string) => Promise<unknown> | unknown;
101
+ }
102
+ interface Verbs<TParams, THandle> {
103
+ /** TS-authored params → write `<dir>/<pathOf(handle)>` on disk. */
104
+ create(params: TParams, opts: CreateOptions): Promise<VerbResult<THandle>>;
105
+ /** Read a manifest from disk → handle. */
106
+ load(path: string): Promise<{
107
+ path: string;
108
+ handle: THandle;
109
+ body: string;
110
+ }>;
111
+ /** Walk `dir` for the doctype's `*.md` files; return loaded handles. */
112
+ list(dir: string, opts?: ListOptions): Promise<THandle[]>;
113
+ /** Load → mutate → write back. Mutator receives the loaded params. */
114
+ update(path: string, mutator: (params: TParams, ctx: {
115
+ handle: THandle;
116
+ body: string;
117
+ }) => TParams | Promise<TParams>, opts?: {
118
+ body?: string;
119
+ dryRun?: boolean;
120
+ }): Promise<VerbResult<THandle>>;
121
+ /**
122
+ * Resolve a `{ inline | ref | file }` block to a handle. Used by
123
+ * doctypes that compose other doctypes (AIP-17/30/36 pattern).
124
+ */
125
+ resolve(block: RefBlock<TParams>, ctx?: ResolveContext): Promise<THandle>;
126
+ /** Remove the manifest file. Does NOT recurse into the containing dir. */
127
+ delete(path: string): Promise<void>;
128
+ }
129
+ declare function createVerbs<TParams extends {
130
+ id?: string;
131
+ slug?: string;
132
+ name?: string;
133
+ }, THandle>(spec: DoctypeSpec<TParams, THandle>): Verbs<TParams, THandle>;
134
+
135
+ export { type CreateOptions, type DoctypeSpec, type ListOptions, type RefBlock, type ResolveContext, type VerbResult, type Verbs, createVerbs };
package/dist/index.mjs ADDED
@@ -0,0 +1,128 @@
1
+ import { filterSerializable } from '@agentproto/define-doctype';
2
+ import matter from 'gray-matter';
3
+ import { mkdir, writeFile, readFile, rm, readdir } from 'fs/promises';
4
+ import { join, dirname, isAbsolute } from 'path';
5
+
6
+ /**
7
+ * @agentproto/manifest v0.1.0-alpha
8
+ * Generic verbs for AIP doctypes (create, load, list, update, resolve, …).
9
+ */
10
+
11
+ var DEFAULT_SKIP_DIRS = [
12
+ "node_modules",
13
+ ".git",
14
+ "dist",
15
+ ".next",
16
+ ".turbo"
17
+ ];
18
+ function createVerbs(spec) {
19
+ const filename = spec.filename ?? `${spec.name.toUpperCase()}.md`;
20
+ const toFrontmatter = spec.toFrontmatter ?? ((params) => filterSerializable({
21
+ schema: spec.schemaLiteral,
22
+ ...params
23
+ }));
24
+ async function create(params, opts) {
25
+ const handle = spec.define(params);
26
+ const relativePath = spec.pathOf(handle);
27
+ const path = join(opts.dir, relativePath);
28
+ const frontmatter = toFrontmatter(params);
29
+ const rendered = matter.stringify(
30
+ opts.body ?? defaultBody(spec, frontmatter),
31
+ frontmatter
32
+ );
33
+ if (!opts.dryRun) {
34
+ await mkdir(dirname(path), { recursive: true });
35
+ await writeFile(path, rendered, "utf8");
36
+ }
37
+ return { path, handle, rendered };
38
+ }
39
+ async function load(path) {
40
+ const source = await readFile(path, "utf8");
41
+ const { frontmatter, body } = spec.parse(source);
42
+ const handle = spec.define(frontmatter);
43
+ return { path, handle, body };
44
+ }
45
+ async function list(dir, opts = {}) {
46
+ const skip = new Set(opts.skipDirs ?? DEFAULT_SKIP_DIRS);
47
+ const out = [];
48
+ async function walk(current) {
49
+ let entries;
50
+ try {
51
+ entries = await readdir(current, { withFileTypes: true });
52
+ } catch {
53
+ return;
54
+ }
55
+ for (const entry of entries) {
56
+ const entryName = String(entry.name);
57
+ if (entry.isDirectory()) {
58
+ if (skip.has(entryName)) continue;
59
+ await walk(join(current, entryName));
60
+ continue;
61
+ }
62
+ if (!entry.isFile()) continue;
63
+ if (entryName !== filename) continue;
64
+ try {
65
+ const { handle } = await load(join(current, entryName));
66
+ if (!opts.filter || opts.filter(handle)) out.push(handle);
67
+ } catch {
68
+ }
69
+ }
70
+ }
71
+ await walk(dir);
72
+ return out;
73
+ }
74
+ async function update(path, mutator, opts = {}) {
75
+ const { handle, body } = await load(path);
76
+ const source = await readFile(path, "utf8");
77
+ const { frontmatter } = spec.parse(source);
78
+ const params = frontmatter;
79
+ const mutated = await mutator(params, { handle, body });
80
+ const newHandle = spec.define(mutated);
81
+ const newFrontmatter = toFrontmatter(mutated);
82
+ const rendered = matter.stringify(
83
+ opts.body ?? body,
84
+ newFrontmatter
85
+ );
86
+ if (!opts.dryRun) {
87
+ await mkdir(dirname(path), { recursive: true });
88
+ await writeFile(path, rendered, "utf8");
89
+ }
90
+ return { path, handle: newHandle, rendered };
91
+ }
92
+ async function resolve(block, ctx = {}) {
93
+ if ("inline" in block) {
94
+ return spec.define(block.inline);
95
+ }
96
+ if ("file" in block) {
97
+ const baseDir = ctx.baseDir ?? ".";
98
+ const path = isAbsolute(block.file) ? block.file : join(baseDir, block.file);
99
+ const { handle } = await load(path);
100
+ return handle;
101
+ }
102
+ if ("ref" in block) {
103
+ if (!ctx.resolveRef) {
104
+ throw new Error(
105
+ `${spec.name}.resolve (AIP-${spec.aip}): block has \`ref: '${block.ref}'\` but no resolveRef provided in context \u2014 registries must inject their own resolver`
106
+ );
107
+ }
108
+ const resolved = await ctx.resolveRef(block.ref);
109
+ return spec.define(resolved);
110
+ }
111
+ throw new Error(
112
+ `${spec.name}.resolve (AIP-${spec.aip}): block must have one of inline | ref | file`
113
+ );
114
+ }
115
+ async function deleteFn(path) {
116
+ await rm(path, { force: true });
117
+ }
118
+ return { create, load, list, update, resolve, delete: deleteFn };
119
+ }
120
+ function defaultBody(spec, frontmatter) {
121
+ const id = typeof frontmatter.name === "string" ? frontmatter.name : typeof frontmatter.id === "string" ? frontmatter.id : typeof frontmatter.slug === "string" ? frontmatter.slug : spec.name;
122
+ return `# ${id}
123
+ `;
124
+ }
125
+
126
+ export { createVerbs };
127
+ //# sourceMappingURL=index.mjs.map
128
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @agentproto/manifest — generic verbs for AIP doctypes.\n *\n * Every per-AIP package (`@agentproto/tool`, `@agentproto/driver`, …)\n * supplies a `DoctypeSpec` describing the per-package knobs (name,\n * AIP number, schema literal, path convention, validator, parser).\n * `createVerbs(spec)` returns the full lifecycle:\n *\n * create(params, opts) params → .md on disk\n * load(path) .md on disk → handle\n * list(dir, filter?) walk → handle[]\n * update(path, mutator) load → patch → write back\n * resolve(block, ctx?) { inline | ref | file } → handle\n * delete(path) fs.unlink\n *\n * Adding a new verb is a single place; every package picks it up\n * automatically — same model as `createDoctype` for the validation\n * pipeline.\n */\n\nimport { filterSerializable } from \"@agentproto/define-doctype\"\nimport matter from \"gray-matter\"\nimport type { Dirent } from \"node:fs\"\nimport { mkdir, readFile, readdir, rm, writeFile } from \"node:fs/promises\"\nimport { dirname, isAbsolute, join } from \"node:path\"\n\n/**\n * Per-package descriptor consumed by `createVerbs`.\n *\n * Generic over the params shape (input to `define`) and the handle\n * shape (output of `define`). For most AIPs `THandle` extends\n * `TParams` — handles are just frozen, defaulted definitions.\n */\nexport interface DoctypeSpec<\n TParams extends { id?: string; slug?: string; name?: string },\n THandle,\n> {\n /** Lower-case singular: \"tool\", \"driver\", \"operator\". Used in errors. */\n name: string\n /** AIP number — surfaces in error messages for provenance. */\n aip: number\n /** Frontmatter `schema:` literal (e.g. \"agentproto/tool/v1\"). */\n schemaLiteral: string\n /**\n * Filename of the doctype's manifest. By default `<DOCTYPE>.md`,\n * derived from `name.toUpperCase()`. Override only when the\n * doctype's filename diverges from convention.\n */\n filename?: string\n /**\n * Path-of-handle convention. Given a validated handle, return the\n * workspace-relative path the manifest lives at. Examples:\n * tool: `(h) => `${h.id}/TOOL.md``\n * policy: `(h) => `policies/${h.slug}/POLICY.md``\n * operator: `(h) => `${h.id}/OPERATOR.md``\n */\n pathOf(handle: THandle): string\n /** TS authoring path: validates params + applies defaults. */\n define(params: TParams): THandle\n /** MD authoring path: parses YAML+body. Throws on invalid frontmatter. */\n parse(source: string): { frontmatter: Record<string, unknown>; body: string }\n /**\n * Optional projection: handle → manifest-shaped frontmatter object.\n * Default: pass the params through `filterSerializable` (drops zod\n * schemas + functions). Override when the handle and frontmatter\n * shapes diverge non-trivially.\n */\n toFrontmatter?(params: TParams): Record<string, unknown>\n}\n\nexport interface CreateOptions {\n /** Workspace-relative or absolute base directory. */\n dir: string\n /** Body markdown after the frontmatter. Defaults to a one-line stub. */\n body?: string\n /** Render only — don't write. Returns the rendered string. */\n dryRun?: boolean\n}\n\nexport interface VerbResult<THandle> {\n path: string\n handle: THandle\n rendered: string\n}\n\nexport interface ListOptions {\n /** Filter the discovered handles. Receives the loaded handle. */\n filter?: <H>(handle: H) => boolean\n /** Skip subdirectories matching these names. Default: [\"node_modules\", \".git\", \"dist\", \".next\"]. */\n skipDirs?: readonly string[]\n}\n\nexport type RefBlock<TParams> =\n | { inline: TParams }\n | { ref: string }\n | { file: string }\n\nexport interface ResolveContext {\n /** Base dir for `file:` references. Required when resolving file blocks. */\n baseDir?: string\n /**\n * Registry resolver for `ref:` strings (e.g. `@agentik/runners/python-3.12`\n * → handle). Optional; throws when an unresolvable ref is encountered.\n */\n resolveRef?: (ref: string) => Promise<unknown> | unknown\n}\n\nexport interface Verbs<TParams, THandle> {\n /** TS-authored params → write `<dir>/<pathOf(handle)>` on disk. */\n create(params: TParams, opts: CreateOptions): Promise<VerbResult<THandle>>\n /** Read a manifest from disk → handle. */\n load(path: string): Promise<{ path: string; handle: THandle; body: string }>\n /** Walk `dir` for the doctype's `*.md` files; return loaded handles. */\n list(dir: string, opts?: ListOptions): Promise<THandle[]>\n /** Load → mutate → write back. Mutator receives the loaded params. */\n update(\n path: string,\n mutator: (params: TParams, ctx: { handle: THandle; body: string }) => TParams | Promise<TParams>,\n opts?: { body?: string; dryRun?: boolean },\n ): Promise<VerbResult<THandle>>\n /**\n * Resolve a `{ inline | ref | file }` block to a handle. Used by\n * doctypes that compose other doctypes (AIP-17/30/36 pattern).\n */\n resolve(\n block: RefBlock<TParams>,\n ctx?: ResolveContext,\n ): Promise<THandle>\n /** Remove the manifest file. Does NOT recurse into the containing dir. */\n delete(path: string): Promise<void>\n}\n\nconst DEFAULT_SKIP_DIRS: readonly string[] = [\n \"node_modules\",\n \".git\",\n \"dist\",\n \".next\",\n \".turbo\",\n]\n\nexport function createVerbs<\n TParams extends { id?: string; slug?: string; name?: string },\n THandle,\n>(spec: DoctypeSpec<TParams, THandle>): Verbs<TParams, THandle> {\n const filename = spec.filename ?? `${spec.name.toUpperCase()}.md`\n const toFrontmatter =\n spec.toFrontmatter ??\n ((params: TParams) =>\n filterSerializable({\n schema: spec.schemaLiteral,\n ...params,\n }) as Record<string, unknown>)\n\n async function create(\n params: TParams,\n opts: CreateOptions,\n ): Promise<VerbResult<THandle>> {\n const handle = spec.define(params)\n const relativePath = spec.pathOf(handle)\n const path = join(opts.dir, relativePath)\n const frontmatter = toFrontmatter(params)\n const rendered = matter.stringify(\n opts.body ?? defaultBody(spec, frontmatter),\n frontmatter,\n )\n if (!opts.dryRun) {\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, rendered, \"utf8\")\n }\n return { path, handle, rendered }\n }\n\n async function load(\n path: string,\n ): Promise<{ path: string; handle: THandle; body: string }> {\n const source = await readFile(path, \"utf8\")\n const { frontmatter, body } = spec.parse(source)\n // The frontmatter is already zod-validated by `parse` (each per-\n // package parser runs the schema). Cast through `define` so the\n // cross-AIP invariants run too — same path the TS authoring uses.\n const handle = spec.define(frontmatter as unknown as TParams)\n return { path, handle, body }\n }\n\n async function list(\n dir: string,\n opts: ListOptions = {},\n ): Promise<THandle[]> {\n const skip = new Set(opts.skipDirs ?? DEFAULT_SKIP_DIRS)\n const out: THandle[] = []\n\n async function walk(current: string): Promise<void> {\n // The Dirent overload returns `Dirent<NonSharedBuffer>[]` in\n // Node 22+'s @types/node when called with non-string buffer\n // input. Coerce by typing the entries as `Dirent[]` (string\n // names by default) since we always pass a string path.\n let entries: Dirent[]\n try {\n entries = (await readdir(current, { withFileTypes: true })) as Dirent[]\n } catch {\n return\n }\n for (const entry of entries) {\n const entryName = String(entry.name)\n if (entry.isDirectory()) {\n if (skip.has(entryName)) continue\n await walk(join(current, entryName))\n continue\n }\n if (!entry.isFile()) continue\n if (entryName !== filename) continue\n try {\n const { handle } = await load(join(current, entryName))\n if (!opts.filter || opts.filter(handle)) out.push(handle)\n } catch {\n // Malformed manifests skip the list — `load` will throw the\n // useful diagnostic when a caller wants to see it.\n }\n }\n }\n\n await walk(dir)\n return out\n }\n\n async function update(\n path: string,\n mutator: (\n params: TParams,\n ctx: { handle: THandle; body: string },\n ) => TParams | Promise<TParams>,\n opts: { body?: string; dryRun?: boolean } = {},\n ): Promise<VerbResult<THandle>> {\n const { handle, body } = await load(path)\n // Reconstruct params from the frontmatter — the load path already\n // validated, so this is just a reverse projection.\n const source = await readFile(path, \"utf8\")\n const { frontmatter } = spec.parse(source)\n const params = frontmatter as unknown as TParams\n const mutated = await mutator(params, { handle, body })\n const newHandle = spec.define(mutated)\n const newFrontmatter = toFrontmatter(mutated)\n const rendered = matter.stringify(\n opts.body ?? body,\n newFrontmatter,\n )\n if (!opts.dryRun) {\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, rendered, \"utf8\")\n }\n return { path, handle: newHandle, rendered }\n }\n\n async function resolve(\n block: RefBlock<TParams>,\n ctx: ResolveContext = {},\n ): Promise<THandle> {\n if (\"inline\" in block) {\n return spec.define(block.inline)\n }\n if (\"file\" in block) {\n const baseDir = ctx.baseDir ?? \".\"\n const path = isAbsolute(block.file) ? block.file : join(baseDir, block.file)\n const { handle } = await load(path)\n return handle\n }\n if (\"ref\" in block) {\n if (!ctx.resolveRef) {\n throw new Error(\n `${spec.name}.resolve (AIP-${spec.aip}): block has \\`ref: '${block.ref}'\\` but no resolveRef provided in context — registries must inject their own resolver`,\n )\n }\n const resolved = await ctx.resolveRef(block.ref)\n return spec.define(resolved as TParams)\n }\n throw new Error(\n `${spec.name}.resolve (AIP-${spec.aip}): block must have one of inline | ref | file`,\n )\n }\n\n async function deleteFn(path: string): Promise<void> {\n await rm(path, { force: true })\n }\n\n return { create, load, list, update, resolve, delete: deleteFn }\n}\n\nfunction defaultBody<P, H>(\n spec: DoctypeSpec<P extends { id?: string; slug?: string; name?: string } ? P : never, H>,\n frontmatter: Record<string, unknown>,\n): string {\n const id =\n typeof frontmatter.name === \"string\"\n ? frontmatter.name\n : typeof frontmatter.id === \"string\"\n ? frontmatter.id\n : typeof frontmatter.slug === \"string\"\n ? frontmatter.slug\n : spec.name\n return `# ${id}\\n`\n}\n\n"]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@agentproto/manifest",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "agentproto/manifest — generic verbs for AIP doctypes (create, load, list, update, resolve, delete). Each per-AIP package supplies a DoctypeSpec describing its name / AIP number / schema literal / path convention / validator+parser; createVerbs(spec) returns the full lifecycle. Adding a new verb (hash, sign, migrate, …) is one place; every package picks it up automatically.",
5
+ "keywords": [
6
+ "agentproto",
7
+ "manifest",
8
+ "io",
9
+ "verbs",
10
+ "createX",
11
+ "loadX",
12
+ "listX",
13
+ "open-standard"
14
+ ],
15
+ "homepage": "https://agentproto.sh",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/agentproto/ts",
19
+ "directory": "packages/manifest"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/agentproto/ts/issues"
23
+ },
24
+ "license": "MIT",
25
+ "type": "module",
26
+ "main": "dist/index.mjs",
27
+ "module": "dist/index.mjs",
28
+ "types": "dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.mjs",
33
+ "default": "./dist/index.mjs"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "gray-matter": "^4.0.3",
47
+ "@agentproto/define-doctype": "0.1.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^25.6.2",
51
+ "tsup": "^8.5.1",
52
+ "typescript": "^5.9.3",
53
+ "vitest": "^3.2.4",
54
+ "@agentproto/tooling": "0.1.0-alpha.0"
55
+ },
56
+ "scripts": {
57
+ "dev": "tsup --watch",
58
+ "build": "tsup",
59
+ "clean": "rm -rf dist",
60
+ "check-types": "tsc --noEmit",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest"
63
+ }
64
+ }