@cerebro-labs/agentnava-sdk 0.26.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/LICENSE +202 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/agent.d.ts +285 -0
- package/agent.js +394 -0
- package/agent.js.map +1 -0
- package/attach.d.ts +49 -0
- package/attach.js +115 -0
- package/attach.js.map +1 -0
- package/connection-status.d.ts +28 -0
- package/connection-status.js +61 -0
- package/connection-status.js.map +1 -0
- package/connection.d.ts +89 -0
- package/connection.js +143 -0
- package/connection.js.map +1 -0
- package/conversation.d.ts +177 -0
- package/conversation.js +348 -0
- package/conversation.js.map +1 -0
- package/end-user.d.ts +159 -0
- package/end-user.js +209 -0
- package/end-user.js.map +1 -0
- package/errors.d.ts +43 -0
- package/errors.js +55 -0
- package/errors.js.map +1 -0
- package/http.d.ts +32 -0
- package/http.js +192 -0
- package/http.js.map +1 -0
- package/import-tools.d.ts +282 -0
- package/import-tools.js +686 -0
- package/import-tools.js.map +1 -0
- package/index.d.ts +16 -0
- package/index.js +38 -0
- package/index.js.map +1 -0
- package/knowledge-base.d.ts +165 -0
- package/knowledge-base.js +274 -0
- package/knowledge-base.js.map +1 -0
- package/load-agent-dir.d.ts +17 -0
- package/load-agent-dir.js +192 -0
- package/load-agent-dir.js.map +1 -0
- package/package.json +32 -0
- package/project.d.ts +94 -0
- package/project.js +155 -0
- package/project.js.map +1 -0
- package/secret.d.ts +45 -0
- package/secret.js +69 -0
- package/secret.js.map +1 -0
- package/templates-inline.d.ts +2 -0
- package/templates-inline.js +16 -0
- package/templates-inline.js.map +1 -0
- package/templates.d.ts +16 -0
- package/templates.js +32 -0
- package/templates.js.map +1 -0
- package/turn.d.ts +11 -0
- package/turn.js +244 -0
- package/turn.js.map +1 -0
- package/types.d.ts +485 -0
- package/types.js +4 -0
- package/types.js.map +1 -0
- package/version.d.ts +1 -0
- package/version.js +5 -0
- package/version.js.map +1 -0
- package/workflows.d.ts +36 -0
- package/workflows.js +101 -0
- package/workflows.js.map +1 -0
- package/workspace.d.ts +290 -0
- package/workspace.js +390 -0
- package/workspace.js.map +1 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A knowledge base: a named folder of files your agents read.
|
|
3
|
+
*
|
|
4
|
+
* A base lives on the WORKSPACE, not on one agent, and an agent configuration
|
|
5
|
+
* attaches it by key. Two agents attaching the same base is the point: correct a
|
|
6
|
+
* file once and both see the correction.
|
|
7
|
+
*
|
|
8
|
+
* Attached bases are mounted READ-ONLY at `/knowledge/<key>/` when a session
|
|
9
|
+
* starts, so the agent finds things the way a person would, with `ls`, `grep`
|
|
10
|
+
* and `cat`. There is no search endpoint and no embedding step, deliberately;
|
|
11
|
+
* see the NO EMBEDDINGS decision in CLAUDE.md.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const kb = await ws.knowledgeBases.create({ key: 'product-docs', name: 'Docs' });
|
|
15
|
+
* await kb.files.put('/pricing/tiers.md', bytes, 'text/markdown');
|
|
16
|
+
* await ws.agents.create({ name: 'Nava', knowledge: ['product-docs'] });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class KnowledgeBase {
|
|
20
|
+
/** Workspace-unique, and what an agent configuration stores. Not editable:
|
|
21
|
+
* renaming it would move every attachment silently. */
|
|
22
|
+
key;
|
|
23
|
+
#http;
|
|
24
|
+
snapshot;
|
|
25
|
+
constructor(http, key, snapshot) {
|
|
26
|
+
this.#http = http;
|
|
27
|
+
this.key = key;
|
|
28
|
+
if (snapshot)
|
|
29
|
+
this.snapshot = snapshot;
|
|
30
|
+
}
|
|
31
|
+
get base() {
|
|
32
|
+
return `/v1/knowledge/bases/${encodeURIComponent(this.key)}`;
|
|
33
|
+
}
|
|
34
|
+
/** The base's current record: name, description, file count, timestamps. */
|
|
35
|
+
async get() {
|
|
36
|
+
const res = await this.#http.get(this.base);
|
|
37
|
+
this.snapshot = res.base;
|
|
38
|
+
return res.base;
|
|
39
|
+
}
|
|
40
|
+
/** What was known at the time this handle was made, without a round trip.
|
|
41
|
+
* Undefined on a handle built from a bare key. */
|
|
42
|
+
get data() {
|
|
43
|
+
return this.snapshot;
|
|
44
|
+
}
|
|
45
|
+
/** Change the display name or description. The KEY is deliberately not
|
|
46
|
+
* changeable here: agent configurations point at it. */
|
|
47
|
+
async update(fields) {
|
|
48
|
+
const res = await this.#http.patch(this.base, fields);
|
|
49
|
+
this.snapshot = res.base;
|
|
50
|
+
return new KnowledgeBase(this.#http, this.key, res.base);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Delete the base and everything in it.
|
|
54
|
+
*
|
|
55
|
+
* REFUSES while any agent still attaches it, with an error naming them. That
|
|
56
|
+
* refusal is what makes attaching safe: an agent configuration that named a
|
|
57
|
+
* base at save time would otherwise find it missing the next morning. Call
|
|
58
|
+
* `attachments()` first if you want to know before you try.
|
|
59
|
+
*/
|
|
60
|
+
async delete() {
|
|
61
|
+
await this.#http.delete(this.base);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Which agents read this base.
|
|
65
|
+
*
|
|
66
|
+
* The question worth asking before deleting or reorganising one: who breaks?
|
|
67
|
+
* Read from each agent's current configuration, so it reflects what a session
|
|
68
|
+
* would actually mount rather than what somebody typed into a draft.
|
|
69
|
+
*/
|
|
70
|
+
async attachments() {
|
|
71
|
+
const res = await this.#http.get(`${this.base}/attachments`);
|
|
72
|
+
return res.agents ?? [];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The files in this base.
|
|
76
|
+
*
|
|
77
|
+
* Paths, not flat names, so `/pricing/tiers.md` is a file inside `pricing/`
|
|
78
|
+
* and the session mount reproduces that shape exactly.
|
|
79
|
+
*/
|
|
80
|
+
files = {
|
|
81
|
+
/**
|
|
82
|
+
* Upload, or REPLACE what is already at this path. A path holds one file;
|
|
83
|
+
* re-uploading it is a correction, not a second copy.
|
|
84
|
+
*/
|
|
85
|
+
put: async (path, bytes, contentType) => {
|
|
86
|
+
const res = await this.#http.request(`${this.base}/files`, {
|
|
87
|
+
method: 'PUT',
|
|
88
|
+
bytes,
|
|
89
|
+
query: { path },
|
|
90
|
+
...(contentType && { contentType }),
|
|
91
|
+
});
|
|
92
|
+
return res.file;
|
|
93
|
+
},
|
|
94
|
+
/** List files, optionally under one prefix. Ordered by path, so a listing
|
|
95
|
+
* reads like a directory rather than an upload log. */
|
|
96
|
+
list: async (prefix = '/') => {
|
|
97
|
+
const res = await this.#http.get(`${this.base}/files`, {
|
|
98
|
+
...(prefix && prefix !== '/' ? { prefix } : {}),
|
|
99
|
+
});
|
|
100
|
+
return res.files ?? [];
|
|
101
|
+
},
|
|
102
|
+
/**
|
|
103
|
+
* Read a file's bytes back.
|
|
104
|
+
*
|
|
105
|
+
* Exists because until now you could write a file and list it but never
|
|
106
|
+
* read it, so a script had no way to verify its own upload except by
|
|
107
|
+
* asking an agent.
|
|
108
|
+
*/
|
|
109
|
+
get: async (path) => {
|
|
110
|
+
// `send`, not `request`. `request` ALWAYS parses the body as JSON; the
|
|
111
|
+
// `raw` flag it used to be passed was declared in the options type and
|
|
112
|
+
// read by nothing, so this returned a parsed object (or threw "Expected
|
|
113
|
+
// JSON") and then called `.arrayBuffer()` on it. Every call failed.
|
|
114
|
+
const res = await this.#http.send(`${this.base}/files/content`, { query: { path } });
|
|
115
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* Read a file back as a string.
|
|
119
|
+
*
|
|
120
|
+
* `get` returns bytes, which is right for a PDF and awkward for text: you
|
|
121
|
+
* decode, change something, and encode again. This is the same file
|
|
122
|
+
* already decoded, for the case people actually hit, which is reading a
|
|
123
|
+
* document to check it or to correct it.
|
|
124
|
+
*/
|
|
125
|
+
text: async (path) => {
|
|
126
|
+
// Same fix as `get` above, and this one failed in a nastier way: for a
|
|
127
|
+
// JSON file `request` PARSED it and returned an object, so the failure
|
|
128
|
+
// was `.text is not a function`; for markdown it threw "Expected JSON
|
|
129
|
+
// from ...". Two different errors, one cause, neither mentioning it.
|
|
130
|
+
const res = await this.#http.send(`${this.base}/files/content`, { query: { path } });
|
|
131
|
+
return res.text();
|
|
132
|
+
},
|
|
133
|
+
remove: async (path) => {
|
|
134
|
+
await this.#http.delete(`${this.base}/files`, { path });
|
|
135
|
+
},
|
|
136
|
+
/**
|
|
137
|
+
* Prepare a file again.
|
|
138
|
+
*
|
|
139
|
+
* Preparation happens by itself when a file is uploaded, so this is for
|
|
140
|
+
* the times it did not work: a scan that came out blank, or a document
|
|
141
|
+
* that failed before we could read its format and can now.
|
|
142
|
+
*
|
|
143
|
+
* It also hands back a file you have edited by hand. Anything you write
|
|
144
|
+
* yourself is left alone by later runs, and calling this returns it to
|
|
145
|
+
* being generated.
|
|
146
|
+
*
|
|
147
|
+
* Returns immediately. Watch `status` on the file listing to see it
|
|
148
|
+
* finish.
|
|
149
|
+
*/
|
|
150
|
+
retry: async (path) => {
|
|
151
|
+
await this.#http.post(`${this.base}/files/retry`, { path });
|
|
152
|
+
},
|
|
153
|
+
/**
|
|
154
|
+
* Upload a whole directory, preserving its structure under `prefix`.
|
|
155
|
+
*
|
|
156
|
+
* Sugar over repeated `put`, but it is the operation people actually
|
|
157
|
+
* perform, and hand-rolling the walk is where mistakes about path prefixes
|
|
158
|
+
* get made. Node and Bun only: it reads the local filesystem.
|
|
159
|
+
*
|
|
160
|
+
* Sequential on purpose. A knowledge base is uploaded once in a while, not
|
|
161
|
+
* in a hot path, and a burst of parallel writes against one base is a good
|
|
162
|
+
* way to meet a rate limit for no gain.
|
|
163
|
+
*/
|
|
164
|
+
putDirectory: async (dir, options = {}) => {
|
|
165
|
+
const { readdir, readFile } = await import('node:fs/promises');
|
|
166
|
+
const { join, relative, posix, sep } = await import('node:path');
|
|
167
|
+
const prefix = (options.prefix ?? '/').replace(/\/+$/, '');
|
|
168
|
+
const matches = (rel) => {
|
|
169
|
+
if (!options.include || options.include.length === 0)
|
|
170
|
+
return true;
|
|
171
|
+
return options.include.some((pattern) => globMatches(pattern, rel));
|
|
172
|
+
};
|
|
173
|
+
const walk = async (current) => {
|
|
174
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
175
|
+
const out = [];
|
|
176
|
+
for (const e of entries) {
|
|
177
|
+
const full = join(current, e.name);
|
|
178
|
+
if (e.isDirectory())
|
|
179
|
+
out.push(...(await walk(full)));
|
|
180
|
+
else if (e.isFile())
|
|
181
|
+
out.push(full);
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
};
|
|
185
|
+
const uploaded = [];
|
|
186
|
+
for (const full of await walk(dir)) {
|
|
187
|
+
const rel = relative(dir, full).split(sep).join(posix.sep);
|
|
188
|
+
if (!matches(rel))
|
|
189
|
+
continue;
|
|
190
|
+
const bytes = await readFile(full);
|
|
191
|
+
uploaded.push(await this.files.put(`${prefix}/${rel}`, new Uint8Array(bytes), contentTypeFor(rel)));
|
|
192
|
+
}
|
|
193
|
+
return uploaded;
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* The agent that looks after this base.
|
|
198
|
+
*
|
|
199
|
+
* Every base has one. It reads the documents you upload, turns the ones an
|
|
200
|
+
* agent cannot open into text, and keeps a list of what is in here. That
|
|
201
|
+
* happens by itself; these methods are for asking it to do something
|
|
202
|
+
* specific.
|
|
203
|
+
*
|
|
204
|
+
* It lives in your own workspace, so its runs cost turns like any other
|
|
205
|
+
* agent's, and you can see what it did.
|
|
206
|
+
*/
|
|
207
|
+
librarian = {
|
|
208
|
+
/**
|
|
209
|
+
* Work through the whole base: read what has not been read, and rewrite
|
|
210
|
+
* the list of what is in here.
|
|
211
|
+
*
|
|
212
|
+
* The same routine every time, which is the difference between this and
|
|
213
|
+
* `ask`. Reach for it after uploading a lot at once, or when something
|
|
214
|
+
* looks out of date.
|
|
215
|
+
*
|
|
216
|
+
* RETURNS IMMEDIATELY. The work is queued and runs within about a minute,
|
|
217
|
+
* because reading a base can take several minutes and no HTTP call should
|
|
218
|
+
* be held open that long. Watch `status` on the file listing to see it
|
|
219
|
+
* finish, exactly as with `files.retry`.
|
|
220
|
+
*/
|
|
221
|
+
organize: async () => {
|
|
222
|
+
await this.#http.post(`${this.base}/librarian/organize`, {});
|
|
223
|
+
},
|
|
224
|
+
/**
|
|
225
|
+
* Ask the librarian to do something to this base.
|
|
226
|
+
*
|
|
227
|
+
* 'split the handbook by chapter'
|
|
228
|
+
* 'the text from that scan came out garbled, read it again'
|
|
229
|
+
* 'what is in here about pricing?'
|
|
230
|
+
*
|
|
231
|
+
* It can add and change files, which is its job. Your documents are not
|
|
232
|
+
* sacred to it, so ask it for what you want rather than working around it.
|
|
233
|
+
*/
|
|
234
|
+
ask: async (text) => {
|
|
235
|
+
return this.#http.post(`${this.base}/librarian/ask`, { text });
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Enough glob for `**\/*.md` and `docs/*.txt`, which is what `include` is for.
|
|
241
|
+
*
|
|
242
|
+
* Deliberately not a glob library. Pulling a dependency into a published SDK so
|
|
243
|
+
* that one convenience method can match filenames is a poor trade, and the
|
|
244
|
+
* patterns people pass here are simple. Anything more complex: list the files
|
|
245
|
+
* yourself and call `put`.
|
|
246
|
+
*/
|
|
247
|
+
function globMatches(pattern, path) {
|
|
248
|
+
const rx = pattern
|
|
249
|
+
.split('/')
|
|
250
|
+
.map((seg) => {
|
|
251
|
+
if (seg === '**')
|
|
252
|
+
return '.*';
|
|
253
|
+
return seg.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
|
|
254
|
+
})
|
|
255
|
+
.join('/')
|
|
256
|
+
// `**/` should also match zero directories, so `**\/*.md` finds `a.md`.
|
|
257
|
+
.replace(/\.\*\//g, '(?:.*/)?');
|
|
258
|
+
return new RegExp(`^${rx}$`).test(path);
|
|
259
|
+
}
|
|
260
|
+
/** A content type from the extension, so uploads are not all octet-stream. */
|
|
261
|
+
function contentTypeFor(path) {
|
|
262
|
+
const ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase();
|
|
263
|
+
const known = {
|
|
264
|
+
md: 'text/markdown',
|
|
265
|
+
markdown: 'text/markdown',
|
|
266
|
+
txt: 'text/plain',
|
|
267
|
+
csv: 'text/csv',
|
|
268
|
+
json: 'application/json',
|
|
269
|
+
html: 'text/html',
|
|
270
|
+
pdf: 'application/pdf',
|
|
271
|
+
};
|
|
272
|
+
return known[ext] ?? 'application/octet-stream';
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=knowledge-base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knowledge-base.js","sourceRoot":"","sources":["../src/knowledge-base.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Http } from './http.ts';\nimport type { KnowledgeBaseData, KnowledgeFileEntry } from './types.ts';\n\n/**\n * A knowledge base: a named folder of files your agents read.\n *\n * A base lives on the WORKSPACE, not on one agent, and an agent configuration\n * attaches it by key. Two agents attaching the same base is the point: correct a\n * file once and both see the correction.\n *\n * Attached bases are mounted READ-ONLY at `/knowledge/<key>/` when a session\n * starts, so the agent finds things the way a person would, with `ls`, `grep`\n * and `cat`. There is no search endpoint and no embedding step, deliberately;\n * see the NO EMBEDDINGS decision in CLAUDE.md.\n *\n * ```ts\n * const kb = await ws.knowledgeBases.create({ key: 'product-docs', name: 'Docs' });\n * await kb.files.put('/pricing/tiers.md', bytes, 'text/markdown');\n * await ws.agents.create({ name: 'Nava', knowledge: ['product-docs'] });\n * ```\n */\nexport class KnowledgeBase {\n\t/** Workspace-unique, and what an agent configuration stores. Not editable:\n\t * renaming it would move every attachment silently. */\n\treadonly key: string;\n\n\treadonly #http: Http;\n\tprivate snapshot?: KnowledgeBaseData;\n\n\tconstructor(http: Http, key: string, snapshot?: KnowledgeBaseData) {\n\t\tthis.#http = http;\n\t\tthis.key = key;\n\t\tif (snapshot) this.snapshot = snapshot;\n\t}\n\n\tprivate get base(): string {\n\t\treturn `/v1/knowledge/bases/${encodeURIComponent(this.key)}`;\n\t}\n\n\t/** The base's current record: name, description, file count, timestamps. */\n\tasync get(): Promise<KnowledgeBaseData> {\n\t\tconst res = await this.#http.get<{ base: KnowledgeBaseData }>(this.base);\n\t\tthis.snapshot = res.base;\n\t\treturn res.base;\n\t}\n\n\t/** What was known at the time this handle was made, without a round trip.\n\t * Undefined on a handle built from a bare key. */\n\tget data(): KnowledgeBaseData | undefined {\n\t\treturn this.snapshot;\n\t}\n\n\t/** Change the display name or description. The KEY is deliberately not\n\t * changeable here: agent configurations point at it. */\n\tasync update(fields: { name?: string; description?: string }): Promise<KnowledgeBase> {\n\t\tconst res = await this.#http.patch<{ base: KnowledgeBaseData }>(this.base, fields);\n\t\tthis.snapshot = res.base;\n\t\treturn new KnowledgeBase(this.#http, this.key, res.base);\n\t}\n\n\t/**\n\t * Delete the base and everything in it.\n\t *\n\t * REFUSES while any agent still attaches it, with an error naming them. That\n\t * refusal is what makes attaching safe: an agent configuration that named a\n\t * base at save time would otherwise find it missing the next morning. Call\n\t * `attachments()` first if you want to know before you try.\n\t */\n\tasync delete(): Promise<void> {\n\t\tawait this.#http.delete(this.base);\n\t}\n\n\t/**\n\t * Which agents read this base.\n\t *\n\t * The question worth asking before deleting or reorganising one: who breaks?\n\t * Read from each agent's current configuration, so it reflects what a session\n\t * would actually mount rather than what somebody typed into a draft.\n\t */\n\tasync attachments(): Promise<{ agentId: string; agentKey?: string; name?: string }[]> {\n\t\tconst res = await this.#http.get<{\n\t\t\tagents: { agentId: string; agentKey?: string; name?: string }[];\n\t\t}>(`${this.base}/attachments`);\n\t\treturn res.agents ?? [];\n\t}\n\n\t/**\n\t * The files in this base.\n\t *\n\t * Paths, not flat names, so `/pricing/tiers.md` is a file inside `pricing/`\n\t * and the session mount reproduces that shape exactly.\n\t */\n\treadonly files = {\n\t\t/**\n\t\t * Upload, or REPLACE what is already at this path. A path holds one file;\n\t\t * re-uploading it is a correction, not a second copy.\n\t\t */\n\t\tput: async (\n\t\t\tpath: string,\n\t\t\tbytes: Uint8Array | ArrayBuffer,\n\t\t\tcontentType?: string,\n\t\t): Promise<KnowledgeFileEntry> => {\n\t\t\tconst res = await this.#http.request<{ file: KnowledgeFileEntry }>(`${this.base}/files`, {\n\t\t\t\tmethod: 'PUT',\n\t\t\t\tbytes,\n\t\t\t\tquery: { path },\n\t\t\t\t...(contentType && { contentType }),\n\t\t\t});\n\t\t\treturn res.file;\n\t\t},\n\n\t\t/** List files, optionally under one prefix. Ordered by path, so a listing\n\t\t * reads like a directory rather than an upload log. */\n\t\tlist: async (prefix = '/'): Promise<KnowledgeFileEntry[]> => {\n\t\t\tconst res = await this.#http.get<{ files: KnowledgeFileEntry[] }>(`${this.base}/files`, {\n\t\t\t\t...(prefix && prefix !== '/' ? { prefix } : {}),\n\t\t\t});\n\t\t\treturn res.files ?? [];\n\t\t},\n\n\t\t/**\n\t\t * Read a file's bytes back.\n\t\t *\n\t\t * Exists because until now you could write a file and list it but never\n\t\t * read it, so a script had no way to verify its own upload except by\n\t\t * asking an agent.\n\t\t */\n\t\tget: async (path: string): Promise<Uint8Array> => {\n\t\t\t// `send`, not `request`. `request` ALWAYS parses the body as JSON; the\n\t\t\t// `raw` flag it used to be passed was declared in the options type and\n\t\t\t// read by nothing, so this returned a parsed object (or threw \"Expected\n\t\t\t// JSON\") and then called `.arrayBuffer()` on it. Every call failed.\n\t\t\tconst res = await this.#http.send(`${this.base}/files/content`, { query: { path } });\n\t\t\treturn new Uint8Array(await res.arrayBuffer());\n\t\t},\n\n\t\t/**\n\t\t * Read a file back as a string.\n\t\t *\n\t\t * `get` returns bytes, which is right for a PDF and awkward for text: you\n\t\t * decode, change something, and encode again. This is the same file\n\t\t * already decoded, for the case people actually hit, which is reading a\n\t\t * document to check it or to correct it.\n\t\t */\n\t\ttext: async (path: string): Promise<string> => {\n\t\t\t// Same fix as `get` above, and this one failed in a nastier way: for a\n\t\t\t// JSON file `request` PARSED it and returned an object, so the failure\n\t\t\t// was `.text is not a function`; for markdown it threw \"Expected JSON\n\t\t\t// from ...\". Two different errors, one cause, neither mentioning it.\n\t\t\tconst res = await this.#http.send(`${this.base}/files/content`, { query: { path } });\n\t\t\treturn res.text();\n\t\t},\n\n\t\tremove: async (path: string): Promise<void> => {\n\t\t\tawait this.#http.delete(`${this.base}/files`, { path });\n\t\t},\n\n\t\t/**\n\t\t * Prepare a file again.\n\t\t *\n\t\t * Preparation happens by itself when a file is uploaded, so this is for\n\t\t * the times it did not work: a scan that came out blank, or a document\n\t\t * that failed before we could read its format and can now.\n\t\t *\n\t\t * It also hands back a file you have edited by hand. Anything you write\n\t\t * yourself is left alone by later runs, and calling this returns it to\n\t\t * being generated.\n\t\t *\n\t\t * Returns immediately. Watch `status` on the file listing to see it\n\t\t * finish.\n\t\t */\n\t\tretry: async (path: string): Promise<void> => {\n\t\t\tawait this.#http.post(`${this.base}/files/retry`, { path });\n\t\t},\n\n\t\t/**\n\t\t * Upload a whole directory, preserving its structure under `prefix`.\n\t\t *\n\t\t * Sugar over repeated `put`, but it is the operation people actually\n\t\t * perform, and hand-rolling the walk is where mistakes about path prefixes\n\t\t * get made. Node and Bun only: it reads the local filesystem.\n\t\t *\n\t\t * Sequential on purpose. A knowledge base is uploaded once in a while, not\n\t\t * in a hot path, and a burst of parallel writes against one base is a good\n\t\t * way to meet a rate limit for no gain.\n\t\t */\n\t\tputDirectory: async (\n\t\t\tdir: string,\n\t\t\toptions: { prefix?: string; include?: string[] } = {},\n\t\t): Promise<KnowledgeFileEntry[]> => {\n\t\t\tconst { readdir, readFile } = await import('node:fs/promises');\n\t\t\tconst { join, relative, posix, sep } = await import('node:path');\n\n\t\t\tconst prefix = (options.prefix ?? '/').replace(/\\/+$/, '');\n\t\t\tconst matches = (rel: string): boolean => {\n\t\t\t\tif (!options.include || options.include.length === 0) return true;\n\t\t\t\treturn options.include.some((pattern) => globMatches(pattern, rel));\n\t\t\t};\n\n\t\t\tconst walk = async (current: string): Promise<string[]> => {\n\t\t\t\tconst entries = await readdir(current, { withFileTypes: true });\n\t\t\t\tconst out: string[] = [];\n\t\t\t\tfor (const e of entries) {\n\t\t\t\t\tconst full = join(current, e.name);\n\t\t\t\t\tif (e.isDirectory()) out.push(...(await walk(full)));\n\t\t\t\t\telse if (e.isFile()) out.push(full);\n\t\t\t\t}\n\t\t\t\treturn out;\n\t\t\t};\n\n\t\t\tconst uploaded: KnowledgeFileEntry[] = [];\n\t\t\tfor (const full of await walk(dir)) {\n\t\t\t\tconst rel = relative(dir, full).split(sep).join(posix.sep);\n\t\t\t\tif (!matches(rel)) continue;\n\t\t\t\tconst bytes = await readFile(full);\n\t\t\t\tuploaded.push(\n\t\t\t\t\tawait this.files.put(\n\t\t\t\t\t\t`${prefix}/${rel}`,\n\t\t\t\t\t\tnew Uint8Array(bytes),\n\t\t\t\t\t\tcontentTypeFor(rel),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn uploaded;\n\t\t},\n\t};\n\n\t/**\n\t * The agent that looks after this base.\n\t *\n\t * Every base has one. It reads the documents you upload, turns the ones an\n\t * agent cannot open into text, and keeps a list of what is in here. That\n\t * happens by itself; these methods are for asking it to do something\n\t * specific.\n\t *\n\t * It lives in your own workspace, so its runs cost turns like any other\n\t * agent's, and you can see what it did.\n\t */\n\treadonly librarian = {\n\t\t/**\n\t\t * Work through the whole base: read what has not been read, and rewrite\n\t\t * the list of what is in here.\n\t\t *\n\t\t * The same routine every time, which is the difference between this and\n\t\t * `ask`. Reach for it after uploading a lot at once, or when something\n\t\t * looks out of date.\n\t\t *\n\t\t * RETURNS IMMEDIATELY. The work is queued and runs within about a minute,\n\t\t * because reading a base can take several minutes and no HTTP call should\n\t\t * be held open that long. Watch `status` on the file listing to see it\n\t\t * finish, exactly as with `files.retry`.\n\t\t */\n\t\torganize: async (): Promise<void> => {\n\t\t\tawait this.#http.post(`${this.base}/librarian/organize`, {});\n\t\t},\n\n\t\t/**\n\t\t * Ask the librarian to do something to this base.\n\t\t *\n\t\t * 'split the handbook by chapter'\n\t\t * 'the text from that scan came out garbled, read it again'\n\t\t * 'what is in here about pricing?'\n\t\t *\n\t\t * It can add and change files, which is its job. Your documents are not\n\t\t * sacred to it, so ask it for what you want rather than working around it.\n\t\t */\n\t\task: async (text: string): Promise<{ reply: string }> => {\n\t\t\treturn this.#http.post<{ reply: string }>(`${this.base}/librarian/ask`, { text });\n\t\t},\n\t};\n}\n\n/**\n * Enough glob for `**\\/*.md` and `docs/*.txt`, which is what `include` is for.\n *\n * Deliberately not a glob library. Pulling a dependency into a published SDK so\n * that one convenience method can match filenames is a poor trade, and the\n * patterns people pass here are simple. Anything more complex: list the files\n * yourself and call `put`.\n */\nfunction globMatches(pattern: string, path: string): boolean {\n\tconst rx = pattern\n\t\t.split('/')\n\t\t.map((seg) => {\n\t\t\tif (seg === '**') return '.*';\n\t\t\treturn seg.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&').replace(/\\*/g, '[^/]*');\n\t\t})\n\t\t.join('/')\n\t\t// `**/` should also match zero directories, so `**\\/*.md` finds `a.md`.\n\t\t.replace(/\\.\\*\\//g, '(?:.*/)?');\n\treturn new RegExp(`^${rx}$`).test(path);\n}\n\n/** A content type from the extension, so uploads are not all octet-stream. */\nfunction contentTypeFor(path: string): string {\n\tconst ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase();\n\tconst known: Record<string, string> = {\n\t\tmd: 'text/markdown',\n\t\tmarkdown: 'text/markdown',\n\t\ttxt: 'text/plain',\n\t\tcsv: 'text/csv',\n\t\tjson: 'application/json',\n\t\thtml: 'text/html',\n\t\tpdf: 'application/pdf',\n\t};\n\treturn known[ext] ?? 'application/octet-stream';\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Filesystem, SpecInput } from './types.ts';
|
|
2
|
+
type AgentRole = string;
|
|
3
|
+
type SpecSandbox = Filesystem;
|
|
4
|
+
type SpecToolGroup = string;
|
|
5
|
+
export interface LoadAgentDirOptions {
|
|
6
|
+
/** Dev-chosen, stable key, required. Used by the backend to upsert this spec. */
|
|
7
|
+
key: string;
|
|
8
|
+
/** The role this agent plays. Default `'freelancer'` is applied server-side
|
|
9
|
+
* when omitted. See {@link AgentRole} for the full vocabulary. */
|
|
10
|
+
role?: AgentRole;
|
|
11
|
+
/** Platform tool groups to grant. Overrides `agent.json`. */
|
|
12
|
+
tools?: SpecToolGroup[];
|
|
13
|
+
/** Filesystem for this agent's turns. Overrides `agent.json`. */
|
|
14
|
+
sandbox?: SpecSandbox;
|
|
15
|
+
}
|
|
16
|
+
export declare function loadAgentDir(path: string, opts: LoadAgentDirOptions): SpecInput;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// `loadAgentDir` reads a Claude-Code-style folder and returns a SpecInput
|
|
2
|
+
// suitable for `an.specs.push(...)`. Layout it expects:
|
|
3
|
+
//
|
|
4
|
+
// <path>/
|
|
5
|
+
// ├── AGENT.md # required: core instructions (CLAUDE.md equivalent)
|
|
6
|
+
// ├── skills/<name>/SKILL.md # optional, repeated
|
|
7
|
+
// ├── subagents/<name>.md # optional, repeated (advanced)
|
|
8
|
+
// ├── workflows/<name>.md # optional, repeated
|
|
9
|
+
// ├── commands/<name>.md # IGNORED (warned): specs carry no commands
|
|
10
|
+
// ├── http-tools.json # optional, declarative HTTP tool array
|
|
11
|
+
// └── agent.json # optional, capabilities (tools, sandbox, role)
|
|
12
|
+
//
|
|
13
|
+
// Files inside `skills/` use the Claude Code convention: one folder per skill,
|
|
14
|
+
// containing `SKILL.md` with YAML frontmatter. The folder name is the skill name.
|
|
15
|
+
// `subagents/` and `workflows/` are flat directories of `<name>.md`. A
|
|
16
|
+
// `commands/` directory is ignored with a warning: specs no longer carry them.
|
|
17
|
+
//
|
|
18
|
+
// `agent.json` is what makes a folder self-describing: it carries the spec-level
|
|
19
|
+
// capabilities (`tools`, `sandbox`, `role`) alongside the markdown, so an agent
|
|
20
|
+
// directory can be copied between workspaces and still be the same agent. Values
|
|
21
|
+
// passed in `opts` win over the file, so a caller can still override in code.
|
|
22
|
+
//
|
|
23
|
+
// Knowledge base files are NOT loaded by this helper. Use `an.knowledge.upload`
|
|
24
|
+
// for those. Everything else in the bundle is markdown.
|
|
25
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
export function loadAgentDir(path, opts) {
|
|
28
|
+
if (!opts.key) {
|
|
29
|
+
throw new Error('[loadAgentDir] opts.key is required');
|
|
30
|
+
}
|
|
31
|
+
if (!existsSync(path) || !statSync(path).isDirectory()) {
|
|
32
|
+
throw new Error(`[loadAgentDir] not a directory: ${path}`);
|
|
33
|
+
}
|
|
34
|
+
const agentMdPath = join(path, 'AGENT.md');
|
|
35
|
+
if (!existsSync(agentMdPath)) {
|
|
36
|
+
throw new Error(`[loadAgentDir] missing required file: ${agentMdPath}. ` +
|
|
37
|
+
`Every spec needs an AGENT.md (the CLAUDE.md equivalent: core instructions).`);
|
|
38
|
+
}
|
|
39
|
+
const instructions = readFileSync(agentMdPath, 'utf8');
|
|
40
|
+
const skills = loadSkillsDir(join(path, 'skills'));
|
|
41
|
+
const subagents = loadFlatMdDir(join(path, 'subagents'));
|
|
42
|
+
const workflows = loadFlatMdDir(join(path, 'workflows'));
|
|
43
|
+
// `commands/` is read only to WARN about it. A spec has no commands field any
|
|
44
|
+
// more: slash commands had one writer (the dedicated-vm translator) and no
|
|
45
|
+
// reader on the default runtime.
|
|
46
|
+
//
|
|
47
|
+
// Deliberately not folded into workflows, which would be the tempting
|
|
48
|
+
// one-liner. A Claude Code slash command is invoked BY A PERSON; a workflow is
|
|
49
|
+
// invoked by the model whenever it looks relevant. Folding them would make
|
|
50
|
+
// every /command fire unbidden, which is a behaviour change disguised as a
|
|
51
|
+
// migration. Warned rather than dropped in silence, and rather than thrown,
|
|
52
|
+
// so an existing agent directory still loads.
|
|
53
|
+
const commands = loadFlatMdDir(join(path, 'commands'));
|
|
54
|
+
if (commands.length > 0) {
|
|
55
|
+
console.warn(`[loadAgentDir] ignoring ${commands.length} file(s) in commands/: agent specs no longer ` +
|
|
56
|
+
'carry slash commands. Move any that should run automatically into workflows/.');
|
|
57
|
+
}
|
|
58
|
+
const out = { key: opts.key, instructions };
|
|
59
|
+
if (skills.length > 0)
|
|
60
|
+
out.skills = skills;
|
|
61
|
+
if (subagents.length > 0)
|
|
62
|
+
out.subagents = subagents;
|
|
63
|
+
if (workflows.length > 0)
|
|
64
|
+
out.workflows = workflows;
|
|
65
|
+
const httpTools = loadHttpTools(join(path, 'http-tools.json'));
|
|
66
|
+
if (httpTools && httpTools.length > 0)
|
|
67
|
+
out.httpTools = httpTools;
|
|
68
|
+
const widgets = loadWidgets(join(path, 'widgets.json'));
|
|
69
|
+
if (widgets && widgets.length > 0)
|
|
70
|
+
out.widgets = widgets;
|
|
71
|
+
const mcpServers = loadMcpServers(join(path, 'mcp-servers.json'));
|
|
72
|
+
if (mcpServers && mcpServers.length > 0)
|
|
73
|
+
out.mcpServers = mcpServers;
|
|
74
|
+
const manifest = loadManifest(join(path, 'agent.json'));
|
|
75
|
+
const tools = opts.tools ?? manifest.tools;
|
|
76
|
+
if (tools && tools.length > 0)
|
|
77
|
+
out.tools = tools;
|
|
78
|
+
const sandbox = opts.sandbox ?? manifest.sandbox;
|
|
79
|
+
if (sandbox)
|
|
80
|
+
out.sandbox = sandbox;
|
|
81
|
+
const role = opts.role ?? manifest.role;
|
|
82
|
+
if (role)
|
|
83
|
+
out.role = role;
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
/** agent.json, optional. Absent is the common case and means "all defaults". */
|
|
87
|
+
function loadManifest(filePath) {
|
|
88
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile())
|
|
89
|
+
return {};
|
|
90
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(raw);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
throw new Error(`[loadAgentDir] agent.json: invalid JSON (${e.message})`);
|
|
97
|
+
}
|
|
98
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
99
|
+
throw new Error('[loadAgentDir] agent.json: must be an object');
|
|
100
|
+
}
|
|
101
|
+
const m = parsed;
|
|
102
|
+
if (m.tools !== undefined && !Array.isArray(m.tools)) {
|
|
103
|
+
throw new Error('[loadAgentDir] agent.json: "tools" must be an array');
|
|
104
|
+
}
|
|
105
|
+
if (m.sandbox !== undefined && m.sandbox !== 'drive' && m.sandbox !== 'none') {
|
|
106
|
+
throw new Error(`[loadAgentDir] agent.json: "sandbox" must be 'drive' or 'none'`);
|
|
107
|
+
}
|
|
108
|
+
// Group names are NOT validated here on purpose: the platform is the
|
|
109
|
+
// authority on which groups exist and rejects unknown ones at push time with
|
|
110
|
+
// the current list, so the SDK never goes stale against a newer platform.
|
|
111
|
+
return m;
|
|
112
|
+
}
|
|
113
|
+
function loadHttpTools(filePath) {
|
|
114
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile())
|
|
115
|
+
return undefined;
|
|
116
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = JSON.parse(raw);
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
throw new Error(`[loadAgentDir] http-tools.json: invalid JSON (${e.message})`);
|
|
123
|
+
}
|
|
124
|
+
if (!Array.isArray(parsed)) {
|
|
125
|
+
throw new Error('[loadAgentDir] http-tools.json: must be an array of tool defs');
|
|
126
|
+
}
|
|
127
|
+
return parsed;
|
|
128
|
+
}
|
|
129
|
+
function loadWidgets(filePath) {
|
|
130
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile())
|
|
131
|
+
return undefined;
|
|
132
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
133
|
+
let parsed;
|
|
134
|
+
try {
|
|
135
|
+
parsed = JSON.parse(raw);
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
throw new Error(`[loadAgentDir] widgets.json: invalid JSON (${e.message})`);
|
|
139
|
+
}
|
|
140
|
+
if (!Array.isArray(parsed)) {
|
|
141
|
+
throw new Error('[loadAgentDir] widgets.json: must be an array of widget defs');
|
|
142
|
+
}
|
|
143
|
+
return parsed;
|
|
144
|
+
}
|
|
145
|
+
function loadMcpServers(filePath) {
|
|
146
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile())
|
|
147
|
+
return undefined;
|
|
148
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
149
|
+
let parsed;
|
|
150
|
+
try {
|
|
151
|
+
parsed = JSON.parse(raw);
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
throw new Error(`[loadAgentDir] mcp-servers.json: invalid JSON (${e.message})`);
|
|
155
|
+
}
|
|
156
|
+
if (!Array.isArray(parsed)) {
|
|
157
|
+
throw new Error('[loadAgentDir] mcp-servers.json: must be an array of MCP server defs');
|
|
158
|
+
}
|
|
159
|
+
return parsed;
|
|
160
|
+
}
|
|
161
|
+
/** skills/<name>/SKILL.md: name comes from the directory. */
|
|
162
|
+
function loadSkillsDir(dir) {
|
|
163
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
164
|
+
return [];
|
|
165
|
+
const out = [];
|
|
166
|
+
for (const name of readdirSync(dir).sort()) {
|
|
167
|
+
if (name.startsWith('.'))
|
|
168
|
+
continue;
|
|
169
|
+
const skillPath = join(dir, name, 'SKILL.md');
|
|
170
|
+
if (existsSync(skillPath) && statSync(skillPath).isFile()) {
|
|
171
|
+
out.push({ name, content: readFileSync(skillPath, 'utf8') });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
/** subagents/, workflows/: flat directories of <name>.md. */
|
|
177
|
+
function loadFlatMdDir(dir) {
|
|
178
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
179
|
+
return [];
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const file of readdirSync(dir).sort()) {
|
|
182
|
+
if (!file.endsWith('.md') || file.startsWith('.'))
|
|
183
|
+
continue;
|
|
184
|
+
const filePath = join(dir, file);
|
|
185
|
+
if (!statSync(filePath).isFile())
|
|
186
|
+
continue;
|
|
187
|
+
const name = file.replace(/\.md$/, '');
|
|
188
|
+
out.push({ name, content: readFileSync(filePath, 'utf8') });
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=load-agent-dir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-agent-dir.js","sourceRoot":"","sources":["../src/load-agent-dir.ts"],"names":[],"mappings":"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","sourcesContent":["// `loadAgentDir` reads a Claude-Code-style folder and returns a SpecInput\n// suitable for `an.specs.push(...)`. Layout it expects:\n//\n// <path>/\n// ├── AGENT.md # required: core instructions (CLAUDE.md equivalent)\n// ├── skills/<name>/SKILL.md # optional, repeated\n// ├── subagents/<name>.md # optional, repeated (advanced)\n// ├── workflows/<name>.md # optional, repeated\n// ├── commands/<name>.md # IGNORED (warned): specs carry no commands\n// ├── http-tools.json # optional, declarative HTTP tool array\n// └── agent.json # optional, capabilities (tools, sandbox, role)\n//\n// Files inside `skills/` use the Claude Code convention: one folder per skill,\n// containing `SKILL.md` with YAML frontmatter. The folder name is the skill name.\n// `subagents/` and `workflows/` are flat directories of `<name>.md`. A\n// `commands/` directory is ignored with a warning: specs no longer carry them.\n//\n// `agent.json` is what makes a folder self-describing: it carries the spec-level\n// capabilities (`tools`, `sandbox`, `role`) alongside the markdown, so an agent\n// directory can be copied between workspaces and still be the same agent. Values\n// passed in `opts` win over the file, so a caller can still override in code.\n//\n// Knowledge base files are NOT loaded by this helper. Use `an.knowledge.upload`\n// for those. Everything else in the bundle is markdown.\n\nimport { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Filesystem, HttpTool, McpServer, SpecFile, SpecInput } from './types.ts';\n\n// Local aliases, so the body of this file reads the way it was written against\n// the retired package's names. Nothing outside this module uses them.\ntype HttpToolDef = HttpTool;\ntype McpServerSpec = McpServer;\ntype AgentRole = string;\ntype SpecSandbox = Filesystem;\ntype SpecToolGroup = string;\ntype WidgetDef = { name: string; [k: string]: unknown };\n\nexport interface LoadAgentDirOptions {\n\t/** Dev-chosen, stable key, required. Used by the backend to upsert this spec. */\n\tkey: string;\n\t/** The role this agent plays. Default `'freelancer'` is applied server-side\n\t * when omitted. See {@link AgentRole} for the full vocabulary. */\n\trole?: AgentRole;\n\t/** Platform tool groups to grant. Overrides `agent.json`. */\n\ttools?: SpecToolGroup[];\n\t/** Filesystem for this agent's turns. Overrides `agent.json`. */\n\tsandbox?: SpecSandbox;\n}\n\n/** The optional `agent.json` manifest, spec-level capabilities that belong with\n * the markdown rather than in whatever script happens to push it. */\ninterface AgentManifest {\n\ttools?: SpecToolGroup[];\n\tsandbox?: SpecSandbox;\n\trole?: AgentRole;\n}\n\nexport function loadAgentDir(path: string, opts: LoadAgentDirOptions): SpecInput {\n\tif (!opts.key) {\n\t\tthrow new Error('[loadAgentDir] opts.key is required');\n\t}\n\tif (!existsSync(path) || !statSync(path).isDirectory()) {\n\t\tthrow new Error(`[loadAgentDir] not a directory: ${path}`);\n\t}\n\n\tconst agentMdPath = join(path, 'AGENT.md');\n\tif (!existsSync(agentMdPath)) {\n\t\tthrow new Error(\n\t\t\t`[loadAgentDir] missing required file: ${agentMdPath}. ` +\n\t\t\t\t`Every spec needs an AGENT.md (the CLAUDE.md equivalent: core instructions).`,\n\t\t);\n\t}\n\tconst instructions = readFileSync(agentMdPath, 'utf8');\n\n\tconst skills = loadSkillsDir(join(path, 'skills'));\n\tconst subagents = loadFlatMdDir(join(path, 'subagents'));\n\tconst workflows = loadFlatMdDir(join(path, 'workflows'));\n\t// `commands/` is read only to WARN about it. A spec has no commands field any\n\t// more: slash commands had one writer (the dedicated-vm translator) and no\n\t// reader on the default runtime.\n\t//\n\t// Deliberately not folded into workflows, which would be the tempting\n\t// one-liner. A Claude Code slash command is invoked BY A PERSON; a workflow is\n\t// invoked by the model whenever it looks relevant. Folding them would make\n\t// every /command fire unbidden, which is a behaviour change disguised as a\n\t// migration. Warned rather than dropped in silence, and rather than thrown,\n\t// so an existing agent directory still loads.\n\tconst commands = loadFlatMdDir(join(path, 'commands'));\n\tif (commands.length > 0) {\n\t\tconsole.warn(\n\t\t\t`[loadAgentDir] ignoring ${commands.length} file(s) in commands/: agent specs no longer ` +\n\t\t\t\t'carry slash commands. Move any that should run automatically into workflows/.',\n\t\t);\n\t}\n\n\tconst out: SpecInput = { key: opts.key, instructions };\n\tif (skills.length > 0) out.skills = skills;\n\tif (subagents.length > 0) out.subagents = subagents;\n\tif (workflows.length > 0) out.workflows = workflows;\n\tconst httpTools = loadHttpTools(join(path, 'http-tools.json'));\n\tif (httpTools && httpTools.length > 0) out.httpTools = httpTools;\n\tconst widgets = loadWidgets(join(path, 'widgets.json'));\n\tif (widgets && widgets.length > 0) out.widgets = widgets;\n\tconst mcpServers = loadMcpServers(join(path, 'mcp-servers.json'));\n\tif (mcpServers && mcpServers.length > 0) out.mcpServers = mcpServers;\n\n\tconst manifest = loadManifest(join(path, 'agent.json'));\n\tconst tools = opts.tools ?? manifest.tools;\n\tif (tools && tools.length > 0) out.tools = tools;\n\tconst sandbox = opts.sandbox ?? manifest.sandbox;\n\tif (sandbox) out.sandbox = sandbox;\n\tconst role = opts.role ?? manifest.role;\n\tif (role) out.role = role;\n\treturn out;\n}\n\n/** agent.json, optional. Absent is the common case and means \"all defaults\". */\nfunction loadManifest(filePath: string): AgentManifest {\n\tif (!existsSync(filePath) || !statSync(filePath).isFile()) return {};\n\tconst raw = readFileSync(filePath, 'utf8');\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch (e) {\n\t\tthrow new Error(`[loadAgentDir] agent.json: invalid JSON (${(e as Error).message})`);\n\t}\n\tif (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n\t\tthrow new Error('[loadAgentDir] agent.json: must be an object');\n\t}\n\tconst m = parsed as Record<string, unknown>;\n\tif (m.tools !== undefined && !Array.isArray(m.tools)) {\n\t\tthrow new Error('[loadAgentDir] agent.json: \"tools\" must be an array');\n\t}\n\tif (m.sandbox !== undefined && m.sandbox !== 'drive' && m.sandbox !== 'none') {\n\t\tthrow new Error(`[loadAgentDir] agent.json: \"sandbox\" must be 'drive' or 'none'`);\n\t}\n\t// Group names are NOT validated here on purpose: the platform is the\n\t// authority on which groups exist and rejects unknown ones at push time with\n\t// the current list, so the SDK never goes stale against a newer platform.\n\treturn m as AgentManifest;\n}\n\nfunction loadHttpTools(filePath: string): HttpToolDef[] | undefined {\n\tif (!existsSync(filePath) || !statSync(filePath).isFile()) return undefined;\n\tconst raw = readFileSync(filePath, 'utf8');\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch (e) {\n\t\tthrow new Error(`[loadAgentDir] http-tools.json: invalid JSON (${(e as Error).message})`);\n\t}\n\tif (!Array.isArray(parsed)) {\n\t\tthrow new Error('[loadAgentDir] http-tools.json: must be an array of tool defs');\n\t}\n\treturn parsed as HttpToolDef[];\n}\n\nfunction loadWidgets(filePath: string): WidgetDef[] | undefined {\n\tif (!existsSync(filePath) || !statSync(filePath).isFile()) return undefined;\n\tconst raw = readFileSync(filePath, 'utf8');\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch (e) {\n\t\tthrow new Error(`[loadAgentDir] widgets.json: invalid JSON (${(e as Error).message})`);\n\t}\n\tif (!Array.isArray(parsed)) {\n\t\tthrow new Error('[loadAgentDir] widgets.json: must be an array of widget defs');\n\t}\n\treturn parsed as WidgetDef[];\n}\n\nfunction loadMcpServers(filePath: string): McpServerSpec[] | undefined {\n\tif (!existsSync(filePath) || !statSync(filePath).isFile()) return undefined;\n\tconst raw = readFileSync(filePath, 'utf8');\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch (e) {\n\t\tthrow new Error(`[loadAgentDir] mcp-servers.json: invalid JSON (${(e as Error).message})`);\n\t}\n\tif (!Array.isArray(parsed)) {\n\t\tthrow new Error('[loadAgentDir] mcp-servers.json: must be an array of MCP server defs');\n\t}\n\treturn parsed as McpServerSpec[];\n}\n\n/** skills/<name>/SKILL.md: name comes from the directory. */\nfunction loadSkillsDir(dir: string): SpecFile[] {\n\tif (!existsSync(dir) || !statSync(dir).isDirectory()) return [];\n\tconst out: SpecFile[] = [];\n\tfor (const name of readdirSync(dir).sort()) {\n\t\tif (name.startsWith('.')) continue;\n\t\tconst skillPath = join(dir, name, 'SKILL.md');\n\t\tif (existsSync(skillPath) && statSync(skillPath).isFile()) {\n\t\t\tout.push({ name, content: readFileSync(skillPath, 'utf8') });\n\t\t}\n\t}\n\treturn out;\n}\n\n/** subagents/, workflows/: flat directories of <name>.md. */\nfunction loadFlatMdDir(dir: string): SpecFile[] {\n\tif (!existsSync(dir) || !statSync(dir).isDirectory()) return [];\n\tconst out: SpecFile[] = [];\n\tfor (const file of readdirSync(dir).sort()) {\n\t\tif (!file.endsWith('.md') || file.startsWith('.')) continue;\n\t\tconst filePath = join(dir, file);\n\t\tif (!statSync(filePath).isFile()) continue;\n\t\tconst name = file.replace(/\\.md$/, '');\n\t\tout.push({ name, content: readFileSync(filePath, 'utf8') });\n\t}\n\treturn out;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cerebro-labs/agentnava-sdk",
|
|
3
|
+
"version": "0.26.0",
|
|
4
|
+
"description": "The AgentNava SDK. Configure an agent, start a conversation, and work with it.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/cerebro-labs/agentnava.git",
|
|
9
|
+
"directory": "packages/agentnava-sdk"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://docs.agentnava.com",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./index.js",
|
|
14
|
+
"types": "./index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./index.d.ts",
|
|
18
|
+
"default": "./index.js"
|
|
19
|
+
},
|
|
20
|
+
"./templates": {
|
|
21
|
+
"types": "./templates.d.ts",
|
|
22
|
+
"default": "./templates.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18",
|
|
27
|
+
"bun": ">=1.1.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|