@anna-ai/cli 0.1.12 → 0.1.16
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/dist/agent-DUmINbo4.js +372 -0
- package/dist/{apps-CDe6Fjq2.js → apps-Bt9CT5Sl.js} +1 -1
- package/dist/bridge-AJilXBw2.js +3 -0
- package/dist/{bridge-BQUo6ehX.js → bridge-nqQ3-j-t.js} +1 -1
- package/dist/cli.js +78 -9
- package/dist/dev-BRlFgo2I.js +3 -0
- package/dist/{dev-DoY58pBM.js → dev-C6v5yRV2.js} +4 -4
- package/dist/dev-account-DCyjamBa.js +44 -0
- package/dist/dev-app-cache-DAHcq46m.js +175 -0
- package/dist/dev-app-cache-DGF2Kuzd.js +4 -0
- package/dist/{doctor-DP2UB10l.js → doctor-C8MWfLt8.js} +1 -1
- package/dist/executa-dev-4FZ7AJHR.js +259 -0
- package/dist/executa-init-COEmKDOE.js +68 -0
- package/dist/executa-register-66WKIwQQ.js +47 -0
- package/dist/host_upload-C_pGOS6p.js +136 -0
- package/dist/image-bwolX7pa.js +131 -0
- package/dist/mascot-wlYTJqMs.js +218 -0
- package/dist/runner-DmGLdat0.js +322 -0
- package/dist/sampling-CJUDG-mf.js +155 -0
- package/dist/storage-EQJA_0UW.js +316 -0
- package/package.json +1 -1
- package/templates/executa/go/README.md +10 -0
- package/templates/executa/go/executa.json +4 -0
- package/templates/executa/go/go.mod +3 -0
- package/templates/executa/go/main.go +148 -0
- package/templates/executa/node/README.md +12 -0
- package/templates/executa/node/executa.json +4 -0
- package/templates/executa/node/package.json +12 -0
- package/templates/executa/node/plugin.mjs +126 -0
- package/templates/executa/node/sampling-fixture.jsonl +1 -0
- package/templates/executa/python/README.md +23 -0
- package/templates/executa/python/__SLUG_PY___plugin.py +146 -0
- package/templates/executa/python/executa.json +4 -0
- package/templates/executa/python/pyproject.toml +15 -0
- package/templates/executa/python/sampling-fixture.jsonl +4 -0
- package/templates/minimal/bundle/app.js +2 -0
- package/templates/minimal/bundle/index.html +1 -2
- package/dist/bridge-BEHyfpPI.js +0 -3
- package/dist/dev-app-cache-BMfOlTHd.js +0 -93
- package/dist/dev-app-cache-cXvO2XwQ.js +0 -4
- /package/dist/{credentials-CIOYq2Lm.js → credentials-DDqx6XMQ.js} +0 -0
- /package/dist/{fixture-BEu4LXLG.js → fixture-CATHyLLI.js} +0 -0
- /package/dist/{login-dl1Zfny8.js → login-CsIVbrmf.js} +0 -0
- /package/dist/{logout-DablvlFs.js → logout-gfmKQxMj.js} +0 -0
- /package/dist/{server-NXmiWJjX.js → server-D8R6ppOp.js} +0 -0
- /package/dist/{whoami-giXOY415.js → whoami-BS5wy-Nh.js} +0 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { canonicalHost } from "./credentials-BTv2IfUZ.js";
|
|
2
|
+
import { hostOf, requireAccount, withCode } from "./dev-account-DCyjamBa.js";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
//#region src/executa/storage.ts
|
|
7
|
+
/** JSON-RPC error codes from matrix/src/executa/protocol.py. */
|
|
8
|
+
const STORAGE_ERR_INVALID_REQUEST = -32020;
|
|
9
|
+
const STORAGE_ERR_NOT_GRANTED = -32021;
|
|
10
|
+
const STORAGE_ERR_NOT_FOUND = -32022;
|
|
11
|
+
const STORAGE_ERR_PRECONDITION_FAILED = -32023;
|
|
12
|
+
const STORAGE_ERR_UPSTREAM = -32026;
|
|
13
|
+
/** Method → (http_method, sub_path, payload_kind). Mirrors matrix/storage.py. */
|
|
14
|
+
const ROUTING = {
|
|
15
|
+
"storage/get": [
|
|
16
|
+
"GET",
|
|
17
|
+
"/kv",
|
|
18
|
+
"query"
|
|
19
|
+
],
|
|
20
|
+
"storage/set": [
|
|
21
|
+
"PUT",
|
|
22
|
+
"/kv",
|
|
23
|
+
"body"
|
|
24
|
+
],
|
|
25
|
+
"storage/delete": [
|
|
26
|
+
"DELETE",
|
|
27
|
+
"/kv",
|
|
28
|
+
"query"
|
|
29
|
+
],
|
|
30
|
+
"storage/list": [
|
|
31
|
+
"GET",
|
|
32
|
+
"/list",
|
|
33
|
+
"query"
|
|
34
|
+
],
|
|
35
|
+
"files/upload_begin": [
|
|
36
|
+
"POST",
|
|
37
|
+
"/files/init",
|
|
38
|
+
"body"
|
|
39
|
+
],
|
|
40
|
+
"files/upload_complete": [
|
|
41
|
+
"POST",
|
|
42
|
+
"/files/finalize",
|
|
43
|
+
"body"
|
|
44
|
+
],
|
|
45
|
+
"files/download_url": [
|
|
46
|
+
"GET",
|
|
47
|
+
"/files/url",
|
|
48
|
+
"query"
|
|
49
|
+
],
|
|
50
|
+
"files/list": [
|
|
51
|
+
"GET",
|
|
52
|
+
"/files",
|
|
53
|
+
"query"
|
|
54
|
+
],
|
|
55
|
+
"files/delete": [
|
|
56
|
+
"DELETE",
|
|
57
|
+
"/files",
|
|
58
|
+
"query"
|
|
59
|
+
]
|
|
60
|
+
};
|
|
61
|
+
const VALID_SCOPES = new Set([
|
|
62
|
+
"user",
|
|
63
|
+
"app",
|
|
64
|
+
"tool"
|
|
65
|
+
]);
|
|
66
|
+
var StorageBridge = class {
|
|
67
|
+
mocks = [];
|
|
68
|
+
/** In-memory KV: `${scope}/${key}` → row. */
|
|
69
|
+
kv = new Map();
|
|
70
|
+
etagCounter = 0;
|
|
71
|
+
cachedToken = null;
|
|
72
|
+
constructor(opts) {
|
|
73
|
+
this.opts = opts;
|
|
74
|
+
if (opts.mode === "mock" && opts.mockFile) {
|
|
75
|
+
const path = resolve(opts.mockFile);
|
|
76
|
+
if (existsSync(path)) for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
77
|
+
if (!line.trim() || line.startsWith("#")) continue;
|
|
78
|
+
try {
|
|
79
|
+
this.mocks.push(JSON.parse(line));
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async call(method, params) {
|
|
85
|
+
if (!(method in ROUTING)) throw withCode(new Error(`unknown storage method: ${method}`), STORAGE_ERR_INVALID_REQUEST);
|
|
86
|
+
if (this.opts.mode === "off") throw withCode(new Error("storage disabled — rerun with `--storage memory|mock|real`"), STORAGE_ERR_NOT_GRANTED);
|
|
87
|
+
if (this.opts.mode === "mock") return this.mockCall(method, params);
|
|
88
|
+
if (this.opts.mode === "memory") return this.memoryCall(method, params);
|
|
89
|
+
return this.realCall(method, params);
|
|
90
|
+
}
|
|
91
|
+
mockCall(method, params) {
|
|
92
|
+
const [ns, shortName] = method.split("/", 2);
|
|
93
|
+
const content = String(params.key ?? params.path ?? params.prefix ?? "");
|
|
94
|
+
const matched = this.mocks.find((m) => m.ns === ns && m.method === shortName && (!m.match?.contentIncludes || content.includes(m.match.contentIncludes))) ?? this.mocks.find((m) => m.ns === ns && m.method === shortName);
|
|
95
|
+
if (matched) {
|
|
96
|
+
if (matched.error) throw withCode(new Error(matched.error.message), matched.error.code);
|
|
97
|
+
if (matched.result !== void 0) return matched.result;
|
|
98
|
+
}
|
|
99
|
+
if (method === "storage/get") throw withCode(new Error("key not found (mock)"), STORAGE_ERR_NOT_FOUND);
|
|
100
|
+
if (method === "storage/set") return {
|
|
101
|
+
ok: true,
|
|
102
|
+
etag: "mock-etag"
|
|
103
|
+
};
|
|
104
|
+
if (method === "storage/delete" || method === "files/delete") return { deleted: true };
|
|
105
|
+
if (method === "storage/list" || method === "files/list") return {
|
|
106
|
+
entries: [],
|
|
107
|
+
cursor: null
|
|
108
|
+
};
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
memoryCall(method, params) {
|
|
112
|
+
if (method.startsWith("files/")) throw withCode(new Error("memory storage backend does not support file objects — use `--storage mock <fixture>` (for canned responses) or `--storage real` (forwards to nexus)"), STORAGE_ERR_NOT_GRANTED);
|
|
113
|
+
const scope = coerceScope(params);
|
|
114
|
+
if (method === "storage/get") return this.memoryGet(scope, params);
|
|
115
|
+
if (method === "storage/set") return this.memorySet(scope, params);
|
|
116
|
+
if (method === "storage/delete") return this.memoryDelete(scope, params);
|
|
117
|
+
if (method === "storage/list") return this.memoryList(scope, params);
|
|
118
|
+
throw withCode(new Error(`unsupported storage method in memory mode: ${method}`), STORAGE_ERR_INVALID_REQUEST);
|
|
119
|
+
}
|
|
120
|
+
memoryGet(scope, params) {
|
|
121
|
+
const key = requireKey(params);
|
|
122
|
+
const row = this.kv.get(`${scope}/${key}`);
|
|
123
|
+
if (!row || expired(row)) {
|
|
124
|
+
if (row) this.kv.delete(`${scope}/${key}`);
|
|
125
|
+
return {
|
|
126
|
+
key,
|
|
127
|
+
value: null,
|
|
128
|
+
etag: null,
|
|
129
|
+
exists: false
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
key,
|
|
134
|
+
value: row.value,
|
|
135
|
+
etag: row.etag,
|
|
136
|
+
exists: true
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
memorySet(scope, params) {
|
|
140
|
+
const key = requireKey(params);
|
|
141
|
+
if (!("value" in params)) throw withCode(new Error("'value' is required for storage/set"), STORAGE_ERR_INVALID_REQUEST);
|
|
142
|
+
const existing = this.kv.get(`${scope}/${key}`);
|
|
143
|
+
const ifMatch = params.if_match;
|
|
144
|
+
if (ifMatch !== void 0) {
|
|
145
|
+
const want = String(ifMatch);
|
|
146
|
+
const have = existing && !expired(existing) ? existing.etag : null;
|
|
147
|
+
if (have !== want && !(want === "" && !have)) throw withCode(new Error(`if_match mismatch (have=${have ?? "null"} want=${want})`), STORAGE_ERR_PRECONDITION_FAILED);
|
|
148
|
+
}
|
|
149
|
+
this.etagCounter += 1;
|
|
150
|
+
const etag = `mem-${this.etagCounter.toString(16)}`;
|
|
151
|
+
const ttlSec = Number(params.ttl_seconds ?? 0);
|
|
152
|
+
const row = {
|
|
153
|
+
value: params.value,
|
|
154
|
+
etag,
|
|
155
|
+
ttl_at: ttlSec > 0 ? Math.floor(Date.now() / 1e3) + ttlSec : null
|
|
156
|
+
};
|
|
157
|
+
this.kv.set(`${scope}/${key}`, row);
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
key,
|
|
161
|
+
etag
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
memoryDelete(scope, params) {
|
|
165
|
+
const key = requireKey(params);
|
|
166
|
+
const existed = this.kv.delete(`${scope}/${key}`);
|
|
167
|
+
return { deleted: existed };
|
|
168
|
+
}
|
|
169
|
+
memoryList(scope, params) {
|
|
170
|
+
const prefix = String(params.prefix ?? "");
|
|
171
|
+
const limit = Math.min(1e3, Math.max(1, Number(params.limit ?? 100)));
|
|
172
|
+
const entries = [];
|
|
173
|
+
const wantPrefix = `${scope}/${prefix}`;
|
|
174
|
+
for (const [k, v] of this.kv.entries()) {
|
|
175
|
+
if (!k.startsWith(wantPrefix)) continue;
|
|
176
|
+
if (expired(v)) continue;
|
|
177
|
+
entries.push({
|
|
178
|
+
key: k.slice(scope.length + 1),
|
|
179
|
+
etag: v.etag
|
|
180
|
+
});
|
|
181
|
+
if (entries.length >= limit) break;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
entries,
|
|
185
|
+
cursor: null
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Slug used for the synthetic executa app (real mode). */
|
|
189
|
+
resolvedSlug() {
|
|
190
|
+
if (this.opts.appSlug) return this.opts.appSlug;
|
|
191
|
+
const tool = this.opts.pluginName;
|
|
192
|
+
if (!tool) throw withCode(new Error("storage bridge has no app_slug AND no plugin tool_id — pass `--app-slug <slug>` or run from a directory whose executa spec exposes a tool_id"), -32602);
|
|
193
|
+
return tool.startsWith("executa-") ? tool : `executa-${tool}`;
|
|
194
|
+
}
|
|
195
|
+
async registerExecuta(host, pat, slug, tool) {
|
|
196
|
+
const url = `${host}/api/v1/anna-apps/dev/executas/register`;
|
|
197
|
+
const res = await fetch(url, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: { "content-type": "application/json" },
|
|
200
|
+
body: JSON.stringify({
|
|
201
|
+
pat,
|
|
202
|
+
tool_id: tool,
|
|
203
|
+
slug
|
|
204
|
+
})
|
|
205
|
+
});
|
|
206
|
+
if (res.status === 404) throw withCode(new Error("your nexus does not expose POST /api/v1/anna-apps/dev/executas/register — upgrade nexus (matrix-nexus ≥ this CLI release) or use `--storage memory|mock`"), STORAGE_ERR_NOT_GRANTED);
|
|
207
|
+
if (!res.ok) {
|
|
208
|
+
const text = await res.text().catch(() => "");
|
|
209
|
+
throw withCode(new Error(`dev/executas/register failed: HTTP ${res.status}: ${text}`), STORAGE_ERR_UPSTREAM);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async mintStorageToken() {
|
|
213
|
+
if (this.cachedToken && this.cachedToken.expiresAt - 30 > Math.floor(Date.now() / 1e3)) return this.cachedToken;
|
|
214
|
+
const acc = requireAccount(this.opts.account);
|
|
215
|
+
const slug = this.resolvedSlug();
|
|
216
|
+
const toolId = this.opts.pluginName ?? `dev-${slug}`;
|
|
217
|
+
const scopes = (this.opts.scopes ?? [
|
|
218
|
+
"user",
|
|
219
|
+
"app",
|
|
220
|
+
"tool"
|
|
221
|
+
]).filter((s) => VALID_SCOPES.has(s));
|
|
222
|
+
const body = {
|
|
223
|
+
pat: acc.pat,
|
|
224
|
+
app_slug: slug,
|
|
225
|
+
executa_tool_id: toolId,
|
|
226
|
+
allowed_scopes: scopes,
|
|
227
|
+
ttl_seconds: 600
|
|
228
|
+
};
|
|
229
|
+
const host = hostOf(acc);
|
|
230
|
+
const url = `${host}/api/v1/anna-apps/dev/storage/mint`;
|
|
231
|
+
const doMint = async () => fetch(url, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: { "content-type": "application/json" },
|
|
234
|
+
body: JSON.stringify(body)
|
|
235
|
+
});
|
|
236
|
+
let res = await doMint();
|
|
237
|
+
if (res.status === 404) {
|
|
238
|
+
const peek = await res.clone().text().catch(() => "");
|
|
239
|
+
if (peek.includes("not found") && peek.includes(slug)) {
|
|
240
|
+
await this.registerExecuta(host, acc.pat, slug, toolId);
|
|
241
|
+
res = await doMint();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (res.status === 404) throw withCode(new Error("your nexus does not expose POST /api/v1/anna-apps/dev/storage/mint — upgrade nexus (matrix-nexus ≥ this CLI release) or use `--storage memory|mock`"), STORAGE_ERR_NOT_GRANTED);
|
|
245
|
+
if (!res.ok) {
|
|
246
|
+
const text = await res.text().catch(() => "");
|
|
247
|
+
throw withCode(new Error(`dev/storage/mint failed: HTTP ${res.status}: ${text}`), STORAGE_ERR_UPSTREAM);
|
|
248
|
+
}
|
|
249
|
+
const out = await res.json();
|
|
250
|
+
this.cachedToken = {
|
|
251
|
+
token: out.storage_token,
|
|
252
|
+
expiresAt: Math.floor(Date.now() / 1e3) + (out.expires_in || 600),
|
|
253
|
+
allowedScopes: out.allowed_scopes ?? scopes
|
|
254
|
+
};
|
|
255
|
+
return this.cachedToken;
|
|
256
|
+
}
|
|
257
|
+
async realCall(method, params) {
|
|
258
|
+
const acc = requireAccount(this.opts.account);
|
|
259
|
+
const token = await this.mintStorageToken();
|
|
260
|
+
const route = ROUTING[method];
|
|
261
|
+
if (!route) throw withCode(new Error(`unknown storage method: ${method}`), STORAGE_ERR_INVALID_REQUEST);
|
|
262
|
+
const [httpMethod, subPath, kind] = route;
|
|
263
|
+
const scope = coerceScope(params);
|
|
264
|
+
const inner = {};
|
|
265
|
+
for (const [k, v] of Object.entries(params)) {
|
|
266
|
+
if (k === "scope") continue;
|
|
267
|
+
if (v === void 0) continue;
|
|
268
|
+
inner[k] = v;
|
|
269
|
+
}
|
|
270
|
+
const url = new URL(`${hostOf(acc)}/api/v1/storage${subPath}`);
|
|
271
|
+
url.searchParams.set("scope", scope);
|
|
272
|
+
let jsonBody;
|
|
273
|
+
if (kind === "query") for (const [k, v] of Object.entries(inner)) {
|
|
274
|
+
if (v === null) continue;
|
|
275
|
+
url.searchParams.set(k, typeof v === "string" ? v : JSON.stringify(v));
|
|
276
|
+
}
|
|
277
|
+
else jsonBody = JSON.stringify(inner);
|
|
278
|
+
const headers = { authorization: `Bearer ${token.token}` };
|
|
279
|
+
if (jsonBody !== void 0) headers["content-type"] = "application/json";
|
|
280
|
+
const res = await fetch(url, {
|
|
281
|
+
method: httpMethod,
|
|
282
|
+
headers,
|
|
283
|
+
body: jsonBody
|
|
284
|
+
});
|
|
285
|
+
if (!res.ok) {
|
|
286
|
+
const text = await res.text().catch(() => "");
|
|
287
|
+
const code = mapHttpStatusToStorageCode(res.status);
|
|
288
|
+
throw withCode(new Error(`HTTP ${res.status}: ${text}`), code);
|
|
289
|
+
}
|
|
290
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
291
|
+
if ((method === "storage/delete" || method === "files/delete") && (res.status === 204 || !ct.includes("json"))) return { deleted: true };
|
|
292
|
+
return await res.json();
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
function coerceScope(params) {
|
|
296
|
+
const s = params.scope;
|
|
297
|
+
if (typeof s === "string" && VALID_SCOPES.has(s)) return s;
|
|
298
|
+
return "app";
|
|
299
|
+
}
|
|
300
|
+
function requireKey(params) {
|
|
301
|
+
const k = params.key;
|
|
302
|
+
if (typeof k !== "string" || !k) throw withCode(new Error("'key' is required (non-empty string)"), STORAGE_ERR_INVALID_REQUEST);
|
|
303
|
+
return k;
|
|
304
|
+
}
|
|
305
|
+
function expired(row) {
|
|
306
|
+
return row.ttl_at !== null && row.ttl_at < Math.floor(Date.now() / 1e3);
|
|
307
|
+
}
|
|
308
|
+
function mapHttpStatusToStorageCode(status) {
|
|
309
|
+
if (status === 401 || status === 403) return STORAGE_ERR_NOT_GRANTED;
|
|
310
|
+
if (status === 404) return STORAGE_ERR_NOT_FOUND;
|
|
311
|
+
if (status === 409 || status === 412) return STORAGE_ERR_PRECONDITION_FAILED;
|
|
312
|
+
return STORAGE_ERR_UPSTREAM;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
//#endregion
|
|
316
|
+
export { StorageBridge };
|
package/package.json
CHANGED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Minimal stdio Executa plugin scaffold (Go).
|
|
2
|
+
//
|
|
3
|
+
// Speaks Executa JSON-RPC 2.0 over stdin/stdout. Implements the v2
|
|
4
|
+
// `initialize` handshake declaring sampling capability.
|
|
5
|
+
//
|
|
6
|
+
// Build & run:
|
|
7
|
+
//
|
|
8
|
+
// anna-app executa dev --describe
|
|
9
|
+
package main
|
|
10
|
+
|
|
11
|
+
import (
|
|
12
|
+
"bufio"
|
|
13
|
+
"crypto/rand"
|
|
14
|
+
"encoding/hex"
|
|
15
|
+
"encoding/json"
|
|
16
|
+
"fmt"
|
|
17
|
+
"os"
|
|
18
|
+
"sync"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
type envelope struct {
|
|
22
|
+
Jsonrpc string `json:"jsonrpc"`
|
|
23
|
+
ID json.RawMessage `json:"id,omitempty"`
|
|
24
|
+
Method string `json:"method,omitempty"`
|
|
25
|
+
Params json.RawMessage `json:"params,omitempty"`
|
|
26
|
+
Result json.RawMessage `json:"result,omitempty"`
|
|
27
|
+
Error *rpcError `json:"error,omitempty"`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type rpcError struct {
|
|
31
|
+
Code int `json:"code"`
|
|
32
|
+
Message string `json:"message"`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var (
|
|
36
|
+
manifest = map[string]any{
|
|
37
|
+
"name": "__TOOL_ID__",
|
|
38
|
+
"display_name": "__TOOL_ID__",
|
|
39
|
+
"version": "0.1.0",
|
|
40
|
+
"description": "A minimal Go Executa plugin scaffold.",
|
|
41
|
+
"host_capabilities": []string{"llm.sample"},
|
|
42
|
+
"tools": []any{
|
|
43
|
+
map[string]any{
|
|
44
|
+
"name": "ping",
|
|
45
|
+
"description": "Smoke-test method.",
|
|
46
|
+
"parameters": []any{},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
writeMu sync.Mutex
|
|
52
|
+
pending = struct {
|
|
53
|
+
sync.Mutex
|
|
54
|
+
m map[string]chan envelope
|
|
55
|
+
}{m: map[string]chan envelope{}}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
func writeEnv(e envelope) {
|
|
59
|
+
writeMu.Lock()
|
|
60
|
+
defer writeMu.Unlock()
|
|
61
|
+
b, _ := json.Marshal(e)
|
|
62
|
+
fmt.Fprintln(os.Stdout, string(b))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func newID() string {
|
|
66
|
+
b := make([]byte, 8)
|
|
67
|
+
_, _ = rand.Read(b)
|
|
68
|
+
return hex.EncodeToString(b)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func invoke(method string, args map[string]any) map[string]any {
|
|
72
|
+
switch method {
|
|
73
|
+
case "ping":
|
|
74
|
+
return map[string]any{"success": true, "data": map[string]any{"pong": true}}
|
|
75
|
+
default:
|
|
76
|
+
return map[string]any{"success": false, "error": "unknown method: " + method}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func dispatch(e envelope) {
|
|
81
|
+
id := e.ID
|
|
82
|
+
var result any
|
|
83
|
+
var err error
|
|
84
|
+
switch e.Method {
|
|
85
|
+
case "initialize":
|
|
86
|
+
result = map[string]any{
|
|
87
|
+
"protocolVersion": "2.0",
|
|
88
|
+
"server_info": map[string]any{"name": manifest["name"], "version": manifest["version"]},
|
|
89
|
+
"capabilities": map[string]any{"sampling": map[string]any{}},
|
|
90
|
+
}
|
|
91
|
+
case "describe":
|
|
92
|
+
result = manifest
|
|
93
|
+
case "health":
|
|
94
|
+
result = map[string]any{"status": "ok"}
|
|
95
|
+
case "invoke":
|
|
96
|
+
var p struct {
|
|
97
|
+
Tool string `json:"tool"`
|
|
98
|
+
Arguments map[string]any `json:"arguments"`
|
|
99
|
+
}
|
|
100
|
+
_ = json.Unmarshal(e.Params, &p)
|
|
101
|
+
if p.Arguments == nil {
|
|
102
|
+
p.Arguments = map[string]any{}
|
|
103
|
+
}
|
|
104
|
+
result = invoke(p.Tool, p.Arguments)
|
|
105
|
+
default:
|
|
106
|
+
err = fmt.Errorf("unknown rpc: %s", e.Method)
|
|
107
|
+
}
|
|
108
|
+
out := envelope{Jsonrpc: "2.0", ID: id}
|
|
109
|
+
if err != nil {
|
|
110
|
+
out.Error = &rpcError{Code: -32601, Message: err.Error()}
|
|
111
|
+
} else {
|
|
112
|
+
raw, _ := json.Marshal(result)
|
|
113
|
+
out.Result = raw
|
|
114
|
+
}
|
|
115
|
+
writeEnv(out)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func main() {
|
|
119
|
+
scanner := bufio.NewScanner(os.Stdin)
|
|
120
|
+
scanner.Buffer(make([]byte, 1<<20), 1<<24)
|
|
121
|
+
for scanner.Scan() {
|
|
122
|
+
line := scanner.Bytes()
|
|
123
|
+
if len(line) == 0 {
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
var e envelope
|
|
127
|
+
if err := json.Unmarshal(line, &e); err != nil {
|
|
128
|
+
fmt.Fprintln(os.Stderr, "bad json:", err)
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
// Response to one of our reverse RPCs?
|
|
132
|
+
if e.Method == "" && len(e.ID) > 0 {
|
|
133
|
+
var key string
|
|
134
|
+
_ = json.Unmarshal(e.ID, &key)
|
|
135
|
+
pending.Lock()
|
|
136
|
+
ch, ok := pending.m[key]
|
|
137
|
+
if ok {
|
|
138
|
+
delete(pending.m, key)
|
|
139
|
+
}
|
|
140
|
+
pending.Unlock()
|
|
141
|
+
if ok {
|
|
142
|
+
ch <- e
|
|
143
|
+
}
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
go dispatch(e)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# __SLUG__
|
|
2
|
+
|
|
3
|
+
A standalone Node.js Executa plugin scaffolded by `anna-app executa init`.
|
|
4
|
+
|
|
5
|
+
## Develop locally
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
anna-app executa dev --describe
|
|
9
|
+
anna-app executa dev --invoke ping
|
|
10
|
+
anna-app executa dev --mock-sampling ./sampling-fixture.jsonl \
|
|
11
|
+
--invoke summarize --args '{"text":"…"}'
|
|
12
|
+
```
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Minimal stdio Executa plugin scaffold (Node.js).
|
|
4
|
+
*
|
|
5
|
+
* Reads one JSON-RPC envelope per line on stdin, writes responses on
|
|
6
|
+
* stdout. Implements the v2 `initialize` handshake declaring sampling
|
|
7
|
+
* capability so the host can forward reverse-RPC calls.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createInterface } from "node:readline";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import process from "node:process";
|
|
13
|
+
|
|
14
|
+
const MANIFEST = {
|
|
15
|
+
name: "__TOOL_ID__",
|
|
16
|
+
display_name: "__TOOL_ID__",
|
|
17
|
+
version: "0.1.0",
|
|
18
|
+
description: "A minimal Node.js Executa plugin scaffold.",
|
|
19
|
+
host_capabilities: ["llm.sample"],
|
|
20
|
+
tools: [
|
|
21
|
+
{
|
|
22
|
+
name: "ping",
|
|
23
|
+
description: "Smoke-test method.",
|
|
24
|
+
parameters: [],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "summarize",
|
|
28
|
+
description: "Summarize text via host sampling.",
|
|
29
|
+
parameters: [
|
|
30
|
+
{ name: "text", type: "string", description: "Text to summarize.", required: true },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const pending = new Map(); // reverse-RPC id -> {resolve, reject}
|
|
37
|
+
|
|
38
|
+
function write(env) {
|
|
39
|
+
process.stdout.write(`${JSON.stringify(env)}\n`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function requestSampling(prompt) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const id = randomUUID();
|
|
45
|
+
pending.set(id, { resolve, reject });
|
|
46
|
+
write({
|
|
47
|
+
jsonrpc: "2.0",
|
|
48
|
+
id,
|
|
49
|
+
method: "sampling/createMessage",
|
|
50
|
+
params: {
|
|
51
|
+
messages: [{ role: "user", content: { type: "text", text: prompt } }],
|
|
52
|
+
maxTokens: 400,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function invoke(method, args) {
|
|
59
|
+
if (method === "ping") return { success: true, data: { pong: true } };
|
|
60
|
+
if (method === "summarize") {
|
|
61
|
+
const text = args?.text;
|
|
62
|
+
if (!text) return { success: false, error: "text is required" };
|
|
63
|
+
try {
|
|
64
|
+
const out = await requestSampling(`Summarize in one sentence:\n${text}`);
|
|
65
|
+
return { success: true, data: { summary: out?.content?.text ?? "" } };
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return { success: false, error: `sampling failed: ${e.message}` };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { success: false, error: `unknown method: ${method}` };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function dispatch(env) {
|
|
74
|
+
const { method, id, params = {} } = env;
|
|
75
|
+
try {
|
|
76
|
+
let result;
|
|
77
|
+
if (method === "initialize") {
|
|
78
|
+
result = {
|
|
79
|
+
protocolVersion: "2.0",
|
|
80
|
+
server_info: { name: MANIFEST.name, version: MANIFEST.version },
|
|
81
|
+
capabilities: { sampling: {} },
|
|
82
|
+
};
|
|
83
|
+
} else if (method === "describe") {
|
|
84
|
+
result = MANIFEST;
|
|
85
|
+
} else if (method === "health") {
|
|
86
|
+
result = { status: "ok" };
|
|
87
|
+
} else if (method === "invoke") {
|
|
88
|
+
result = await invoke(params.tool, params.arguments ?? {});
|
|
89
|
+
} else {
|
|
90
|
+
throw Object.assign(new Error(`unknown rpc: ${method}`), { code: -32601 });
|
|
91
|
+
}
|
|
92
|
+
write({ jsonrpc: "2.0", id, result });
|
|
93
|
+
} catch (e) {
|
|
94
|
+
write({
|
|
95
|
+
jsonrpc: "2.0",
|
|
96
|
+
id,
|
|
97
|
+
error: { code: e.code ?? -32000, message: e.message },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function handleLine(line) {
|
|
103
|
+
const env = JSON.parse(line);
|
|
104
|
+
// Response to one of our reverse-RPC requests?
|
|
105
|
+
if (env.method == null && pending.has(env.id)) {
|
|
106
|
+
const { resolve, reject } = pending.get(env.id);
|
|
107
|
+
pending.delete(env.id);
|
|
108
|
+
if (env.error) reject(new Error(env.error.message));
|
|
109
|
+
else resolve(env.result);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Host → plugin request.
|
|
113
|
+
if (env.method) {
|
|
114
|
+
void dispatch(env);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
createInterface({ input: process.stdin }).on("line", (raw) => {
|
|
119
|
+
const line = raw.trim();
|
|
120
|
+
if (!line) return;
|
|
121
|
+
try {
|
|
122
|
+
handleLine(line);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
process.stderr.write(`bad json: ${e.message}\n`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"ns":"sampling","method":"createMessage","result":{"role":"assistant","content":{"type":"text","text":"(mock) a one-line summary"},"model":"mock-model","stopReason":"endTurn"}}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# __SLUG__
|
|
2
|
+
|
|
3
|
+
A standalone Executa plugin scaffolded by `anna-app executa init`.
|
|
4
|
+
|
|
5
|
+
## Develop locally
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
# Smoke-test (no LLM):
|
|
9
|
+
anna-app executa dev --describe
|
|
10
|
+
anna-app executa dev --invoke ping
|
|
11
|
+
|
|
12
|
+
# With host sampling against a mock fixture:
|
|
13
|
+
anna-app executa dev \
|
|
14
|
+
--mock-sampling ./sampling-fixture.jsonl \
|
|
15
|
+
--invoke summarize --args '{"text":"Anna is a multi-app workspace."}'
|
|
16
|
+
|
|
17
|
+
# With real nexus sampling (after `anna-app login --host <nexus>`):
|
|
18
|
+
anna-app executa dev \
|
|
19
|
+
--app-slug my-app \
|
|
20
|
+
--invoke summarize --args '{"text":"…"}'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
See `__SLUG_PY___plugin.py` for the JSON-RPC protocol.
|