@moda-ai/cli 1.55.10 → 1.55.12
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-rd9deae9.js +459 -0
- package/dist/cli-09rr5smg.js +30 -0
- package/dist/cli-34zdvcpg.js +811 -0
- package/dist/cli-4qe6n8am.js +345 -0
- package/dist/cli-4x9w6ey7.js +70 -0
- package/dist/{cli-d3c95kks.js → cli-90hb4ng0.js} +18 -1733
- package/dist/cli-97fnztpy.js +140 -0
- package/dist/{cli-7s48d6ty.js → cli-bfmn3bsk.js} +2 -2
- package/dist/cli-k7vwt56h.js +496 -0
- package/dist/{cli-0qnghzg4.js → cli-kwz900s4.js} +95 -85
- package/dist/{cli-v27q5dvh.js → cli-m0fa54ny.js} +189 -970
- package/dist/cli-ryd7nzke.js +159 -0
- package/dist/{cli-4f0p8erk.js → cli-ssqb8bfw.js} +5 -7
- package/dist/cli-y8gbbe9k.js +946 -0
- package/dist/cli.js +1461 -7021
- package/dist/doctor-8yb6r66e.js +23 -0
- package/dist/fix-bt5zn07e.js +1444 -0
- package/dist/{harness-0tnqw849.js → harness-9psndqtm.js} +7 -2
- package/dist/{harness-github-actions-wpkr7jjr.js → harness-github-actions-nph5t2s7.js} +7 -2
- package/dist/{index-jqexf7kn.js → index-qjaprhrv.js} +31 -20
- package/dist/production-intelligence-mg3qp2dg.js +1802 -0
- package/dist/profile-commands-2g5bjzj2.js +481 -0
- package/dist/prompt-source-mcp.js +7 -2
- package/dist/prompts-mzazn8ce.js +40 -0
- package/dist/{provision-5pkn25hh.js → provision-rm0yc08f.js} +7 -5
- package/dist/registry-mbz65bjy.js +1201 -0
- package/dist/skills-sagxq9sc.js +19 -0
- package/dist/sync-gkwexz68.js +54 -0
- package/package.json +6 -6
- package/skills/integration/index.json +2 -2
- package/skills/moda-cli/SKILL.md +10 -4
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertFlags
|
|
3
|
+
} from "./cli-k7vwt56h.js";
|
|
4
|
+
import {
|
|
5
|
+
callControlAPI
|
|
6
|
+
} from "./cli-ryd7nzke.js";
|
|
7
|
+
import"./cli-09rr5smg.js";
|
|
8
|
+
import {
|
|
9
|
+
CliInputError,
|
|
10
|
+
explicitOutputMode,
|
|
11
|
+
resolveApiKey
|
|
12
|
+
} from "./cli-kwz900s4.js";
|
|
13
|
+
import"./cli-0v6na3yp.js";
|
|
14
|
+
|
|
15
|
+
// src/registry.ts
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
19
|
+
|
|
20
|
+
// src/registry-harness.ts
|
|
21
|
+
var HARNESS_SECTIONS = ["runtime", "agents", "routing", "context", "mcpServers"];
|
|
22
|
+
var ROUTING_STRATEGIES = ["single", "trace_attribute", "handoff", "router", "rules"];
|
|
23
|
+
var HISTORY_STRATEGIES = ["full", "window", "summary", "truncate_tokens"];
|
|
24
|
+
var SKILL_LOADING_MODES = ["tool", "system_prompt", "none"];
|
|
25
|
+
var CONTEXT_SOURCES = ["static", "clock", "trace", "tool", "memory", "retrieval", "unknown"];
|
|
26
|
+
var CONTEXT_TARGETS = ["prompt_variable", "system_append"];
|
|
27
|
+
var CLOCK_FORMATS = ["date", "datetime", "iso"];
|
|
28
|
+
var MCP_TRANSPORTS = ["stdio", "http", "sse", "streamable_http"];
|
|
29
|
+
var AGENT_KEY_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
30
|
+
var TOOL_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
31
|
+
var VARIABLE_PATTERN = /^[A-Za-z_][A-Za-z0-9_.]{0,127}$/;
|
|
32
|
+
var MAX_AGENTS = 100;
|
|
33
|
+
var MAX_CONTEXT = 200;
|
|
34
|
+
var MAX_MCP_SERVERS = 50;
|
|
35
|
+
var MAX_HARNESS_DOCUMENT_BYTES = 1e6;
|
|
36
|
+
var AGENT_FIELDS = {
|
|
37
|
+
key: "string",
|
|
38
|
+
name: "string",
|
|
39
|
+
description: "string",
|
|
40
|
+
entry: "boolean",
|
|
41
|
+
prompt: "string",
|
|
42
|
+
model: "object",
|
|
43
|
+
tools: "string[]",
|
|
44
|
+
skills: "string[]",
|
|
45
|
+
handoffs: "object[]",
|
|
46
|
+
subagents: "object[]",
|
|
47
|
+
context: "string[]",
|
|
48
|
+
maxToolRoundsPerTurn: "number",
|
|
49
|
+
responseSchema: "object",
|
|
50
|
+
match: "object"
|
|
51
|
+
};
|
|
52
|
+
var MODEL_FIELDS = {
|
|
53
|
+
id: "string",
|
|
54
|
+
provider: "string",
|
|
55
|
+
temperature: "number",
|
|
56
|
+
maxTokens: "number",
|
|
57
|
+
topP: "number",
|
|
58
|
+
reasoningEffort: "string"
|
|
59
|
+
};
|
|
60
|
+
var HANDOFF_FIELDS = { to: "string", tool: "string", description: "string" };
|
|
61
|
+
var SUBAGENT_FIELDS = { agent: "string", tool: "string", description: "string" };
|
|
62
|
+
var MATCH_FIELDS = { agentNames: "string[]", promptKeys: "string[]" };
|
|
63
|
+
var ROUTING_FIELDS = { strategy: "string", default: "string", notes: "string" };
|
|
64
|
+
var RUNTIME_FIELDS = {
|
|
65
|
+
framework: "string",
|
|
66
|
+
language: "string",
|
|
67
|
+
maxToolRoundsPerTurn: "number",
|
|
68
|
+
parallelToolCalls: "boolean",
|
|
69
|
+
history: "object",
|
|
70
|
+
skillLoading: "string",
|
|
71
|
+
notes: "string"
|
|
72
|
+
};
|
|
73
|
+
var HISTORY_FIELDS = { strategy: "string", maxMessages: "number", maxTokens: "number" };
|
|
74
|
+
var CONTEXT_FIELDS = {
|
|
75
|
+
key: "string",
|
|
76
|
+
variable: "string",
|
|
77
|
+
source: "string",
|
|
78
|
+
value: "string",
|
|
79
|
+
format: "string",
|
|
80
|
+
target: "string",
|
|
81
|
+
description: "string"
|
|
82
|
+
};
|
|
83
|
+
var MCP_FIELDS = {
|
|
84
|
+
name: "string",
|
|
85
|
+
transport: "string",
|
|
86
|
+
url: "string",
|
|
87
|
+
command: "string",
|
|
88
|
+
tools: "string[]",
|
|
89
|
+
description: "string"
|
|
90
|
+
};
|
|
91
|
+
function handoffToolName(handoff) {
|
|
92
|
+
return handoff.tool?.trim() || `transfer_to_${handoff.to.replace(/[^A-Za-z0-9_-]/g, "_")}`.slice(0, 64);
|
|
93
|
+
}
|
|
94
|
+
function hasHarnessSections(input) {
|
|
95
|
+
return HARNESS_SECTIONS.some((section) => input[section] !== undefined);
|
|
96
|
+
}
|
|
97
|
+
function validateHarnessSections(input, known, errors, warnings) {
|
|
98
|
+
if (!hasHarnessSections(input))
|
|
99
|
+
return;
|
|
100
|
+
const document = {};
|
|
101
|
+
if (input.runtime !== undefined) {
|
|
102
|
+
if (checkObject("runtime", input.runtime, RUNTIME_FIELDS, errors)) {
|
|
103
|
+
const runtime = input.runtime;
|
|
104
|
+
checkPositiveInt("runtime.maxToolRoundsPerTurn", runtime.maxToolRoundsPerTurn, errors);
|
|
105
|
+
checkEnum("runtime.skillLoading", runtime.skillLoading, SKILL_LOADING_MODES, errors);
|
|
106
|
+
if (runtime.history !== undefined && checkObject("runtime.history", runtime.history, HISTORY_FIELDS, errors)) {
|
|
107
|
+
checkEnum("runtime.history.strategy", runtime.history.strategy, HISTORY_STRATEGIES, errors);
|
|
108
|
+
checkPositiveInt("runtime.history.maxMessages", runtime.history.maxMessages, errors);
|
|
109
|
+
checkPositiveInt("runtime.history.maxTokens", runtime.history.maxTokens, errors);
|
|
110
|
+
}
|
|
111
|
+
document.runtime = runtime;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const contextKeys = new Set;
|
|
115
|
+
if (input.context !== undefined) {
|
|
116
|
+
const entries = listOf("context", input.context, MAX_CONTEXT, errors);
|
|
117
|
+
document.context = [];
|
|
118
|
+
entries.forEach((raw, index) => {
|
|
119
|
+
const label = `context[${index}]`;
|
|
120
|
+
if (!checkObject(label, raw, CONTEXT_FIELDS, errors))
|
|
121
|
+
return;
|
|
122
|
+
const entry = raw;
|
|
123
|
+
if (!nonEmpty(entry.key))
|
|
124
|
+
errors.push(`${label}.key is required`);
|
|
125
|
+
else if (contextKeys.has(entry.key))
|
|
126
|
+
errors.push(`${label}: duplicate context key "${entry.key}"`);
|
|
127
|
+
else
|
|
128
|
+
contextKeys.add(entry.key);
|
|
129
|
+
if (!nonEmpty(entry.source))
|
|
130
|
+
errors.push(`${label}.source is required (one of ${CONTEXT_SOURCES.join(", ")})`);
|
|
131
|
+
else
|
|
132
|
+
checkEnum(`${label}.source`, entry.source, CONTEXT_SOURCES, errors);
|
|
133
|
+
checkEnum(`${label}.target`, entry.target, CONTEXT_TARGETS, errors);
|
|
134
|
+
checkEnum(`${label}.format`, entry.format, CLOCK_FORMATS, errors);
|
|
135
|
+
const target = entry.target ?? "prompt_variable";
|
|
136
|
+
if (target === "prompt_variable") {
|
|
137
|
+
if (!nonEmpty(entry.variable))
|
|
138
|
+
errors.push(`${label}.variable is required for target "prompt_variable"`);
|
|
139
|
+
else if (!VARIABLE_PATTERN.test(entry.variable))
|
|
140
|
+
errors.push(`${label}.variable "${entry.variable}" is not a valid template variable name`);
|
|
141
|
+
}
|
|
142
|
+
if (entry.source === "static" && typeof entry.value !== "string")
|
|
143
|
+
errors.push(`${label}.value is required for source "static"`);
|
|
144
|
+
if (entry.source !== "static" && entry.value !== undefined) {
|
|
145
|
+
errors.push(`${label}.value is only allowed for source "static" (record runtime values in traces instead)`);
|
|
146
|
+
}
|
|
147
|
+
document.context.push(entry);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const agentKeys = new Set;
|
|
151
|
+
const agents = input.agents === undefined ? [] : listOf("agents", input.agents, MAX_AGENTS, errors);
|
|
152
|
+
for (const raw of agents) {
|
|
153
|
+
if (isRecord(raw) && nonEmpty(raw.key) && AGENT_KEY_PATTERN.test(raw.key))
|
|
154
|
+
agentKeys.add(raw.key);
|
|
155
|
+
}
|
|
156
|
+
if (input.agents !== undefined) {
|
|
157
|
+
document.agents = [];
|
|
158
|
+
const seen = new Set;
|
|
159
|
+
let entries = 0;
|
|
160
|
+
agents.forEach((raw, index) => {
|
|
161
|
+
const label = `agents[${index}]`;
|
|
162
|
+
if (!checkObject(label, raw, AGENT_FIELDS, errors))
|
|
163
|
+
return;
|
|
164
|
+
const agent = raw;
|
|
165
|
+
if (!nonEmpty(agent.key))
|
|
166
|
+
errors.push(`${label}.key is required`);
|
|
167
|
+
else if (!AGENT_KEY_PATTERN.test(agent.key))
|
|
168
|
+
errors.push(`${label}.key "${agent.key}" may only contain letters, digits, ".", "_" and "-" (max 128)`);
|
|
169
|
+
else if (seen.has(agent.key))
|
|
170
|
+
errors.push(`${label}: duplicate agent key "${agent.key}"`);
|
|
171
|
+
else
|
|
172
|
+
seen.add(agent.key);
|
|
173
|
+
if (agent.entry === true)
|
|
174
|
+
entries += 1;
|
|
175
|
+
if (agent.model !== undefined && checkObject(`${label}.model`, agent.model, MODEL_FIELDS, errors)) {
|
|
176
|
+
const model = agent.model;
|
|
177
|
+
if (typeof model.temperature === "number" && (model.temperature < 0 || model.temperature > 2)) {
|
|
178
|
+
errors.push(`${label}.model.temperature must be between 0 and 2`);
|
|
179
|
+
}
|
|
180
|
+
if (typeof model.topP === "number" && (model.topP <= 0 || model.topP > 1))
|
|
181
|
+
errors.push(`${label}.model.topP must be in (0, 1]`);
|
|
182
|
+
checkPositiveInt(`${label}.model.maxTokens`, model.maxTokens, errors);
|
|
183
|
+
}
|
|
184
|
+
checkPositiveInt(`${label}.maxToolRoundsPerTurn`, agent.maxToolRoundsPerTurn, errors);
|
|
185
|
+
if (agent.match !== undefined)
|
|
186
|
+
checkObject(`${label}.match`, agent.match, MATCH_FIELDS, errors);
|
|
187
|
+
const handoffTools = new Set;
|
|
188
|
+
(Array.isArray(agent.handoffs) ? agent.handoffs : []).forEach((handoff, hIndex) => {
|
|
189
|
+
const hLabel = `${label}.handoffs[${hIndex}]`;
|
|
190
|
+
if (!checkObject(hLabel, handoff, HANDOFF_FIELDS, errors))
|
|
191
|
+
return;
|
|
192
|
+
if (!nonEmpty(handoff.to)) {
|
|
193
|
+
errors.push(`${hLabel}.to is required`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!agentKeys.has(handoff.to))
|
|
197
|
+
errors.push(`${hLabel}.to "${handoff.to}" is not an agent key in this manifest`);
|
|
198
|
+
if (handoff.to === agent.key)
|
|
199
|
+
errors.push(`${hLabel}: an agent cannot hand off to itself`);
|
|
200
|
+
const tool = handoffToolName(handoff);
|
|
201
|
+
if (!TOOL_NAME_PATTERN.test(tool))
|
|
202
|
+
errors.push(`${hLabel}.tool "${tool}" must match ${TOOL_NAME_PATTERN.source}`);
|
|
203
|
+
else if (handoffTools.has(tool))
|
|
204
|
+
errors.push(`${hLabel}: duplicate handoff tool "${tool}"`);
|
|
205
|
+
else if ((agent.tools ?? []).includes(tool))
|
|
206
|
+
errors.push(`${hLabel}: handoff tool "${tool}" collides with a tool in ${label}.tools`);
|
|
207
|
+
handoffTools.add(tool);
|
|
208
|
+
});
|
|
209
|
+
(Array.isArray(agent.subagents) ? agent.subagents : []).forEach((subagent, sIndex) => {
|
|
210
|
+
const sLabel = `${label}.subagents[${sIndex}]`;
|
|
211
|
+
if (!checkObject(sLabel, subagent, SUBAGENT_FIELDS, errors))
|
|
212
|
+
return;
|
|
213
|
+
if (!nonEmpty(subagent.agent))
|
|
214
|
+
errors.push(`${sLabel}.agent is required`);
|
|
215
|
+
else if (!agentKeys.has(subagent.agent))
|
|
216
|
+
errors.push(`${sLabel}.agent "${subagent.agent}" is not an agent key in this manifest`);
|
|
217
|
+
if (!nonEmpty(subagent.tool))
|
|
218
|
+
errors.push(`${sLabel}.tool is required (the tool name the parent calls)`);
|
|
219
|
+
else if (!(agent.tools ?? []).includes(subagent.tool)) {
|
|
220
|
+
errors.push(`${sLabel}.tool "${subagent.tool}" must also be listed in ${label}.tools (with its schema in tools[])`);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
for (const key of agent.context ?? []) {
|
|
224
|
+
if (!contextKeys.has(key))
|
|
225
|
+
errors.push(`${label}.context "${key}" is not a key in context[]`);
|
|
226
|
+
}
|
|
227
|
+
if (nonEmpty(agent.prompt) && !known.prompts.has(agent.prompt)) {
|
|
228
|
+
warnings.push(`${label}.prompt "${agent.prompt}" is not in prompts[]; it must already exist in Moda`);
|
|
229
|
+
}
|
|
230
|
+
for (const tool of agent.tools ?? []) {
|
|
231
|
+
if (!known.tools.has(tool))
|
|
232
|
+
warnings.push(`${label}.tools "${tool}" is not in tools[]; it must already exist in Moda`);
|
|
233
|
+
}
|
|
234
|
+
for (const skill of agent.skills ?? []) {
|
|
235
|
+
if (!known.skills.has(skill))
|
|
236
|
+
warnings.push(`${label}.skills "${skill}" is not in skills[]; it must already exist in Moda`);
|
|
237
|
+
}
|
|
238
|
+
document.agents.push(agent);
|
|
239
|
+
});
|
|
240
|
+
if (entries > 1)
|
|
241
|
+
errors.push('agents: at most one agent may set "entry": true');
|
|
242
|
+
}
|
|
243
|
+
if (input.routing !== undefined) {
|
|
244
|
+
if (checkObject("routing", input.routing, ROUTING_FIELDS, errors)) {
|
|
245
|
+
const routing = input.routing;
|
|
246
|
+
checkEnum("routing.strategy", routing.strategy, ROUTING_STRATEGIES, errors);
|
|
247
|
+
if (routing.default !== undefined && !agentKeys.has(routing.default)) {
|
|
248
|
+
errors.push(`routing.default "${routing.default}" is not an agent key in agents[]`);
|
|
249
|
+
}
|
|
250
|
+
document.routing = routing;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (input.mcpServers !== undefined) {
|
|
254
|
+
document.mcpServers = [];
|
|
255
|
+
const names = new Set;
|
|
256
|
+
listOf("mcpServers", input.mcpServers, MAX_MCP_SERVERS, errors).forEach((raw, index) => {
|
|
257
|
+
const label = `mcpServers[${index}]`;
|
|
258
|
+
if (!checkObject(label, raw, MCP_FIELDS, errors))
|
|
259
|
+
return;
|
|
260
|
+
const server = raw;
|
|
261
|
+
if (!nonEmpty(server.name))
|
|
262
|
+
errors.push(`${label}.name is required`);
|
|
263
|
+
else if (names.has(server.name))
|
|
264
|
+
errors.push(`${label}: duplicate MCP server name "${server.name}"`);
|
|
265
|
+
else
|
|
266
|
+
names.add(server.name);
|
|
267
|
+
checkEnum(`${label}.transport`, server.transport, MCP_TRANSPORTS, errors);
|
|
268
|
+
if (server.url !== undefined && hasUrlCredentials(server.url)) {
|
|
269
|
+
errors.push(`${label}.url must not contain credentials (user info or token query parameters)`);
|
|
270
|
+
}
|
|
271
|
+
document.mcpServers.push(server);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (Buffer.byteLength(JSON.stringify(document), "utf8") > MAX_HARNESS_DOCUMENT_BYTES) {
|
|
275
|
+
errors.push(`harness sections exceed ${MAX_HARNESS_DOCUMENT_BYTES} bytes`);
|
|
276
|
+
}
|
|
277
|
+
return document;
|
|
278
|
+
}
|
|
279
|
+
function hasUrlCredentials(raw) {
|
|
280
|
+
try {
|
|
281
|
+
const url = new URL(raw);
|
|
282
|
+
if (url.username || url.password)
|
|
283
|
+
return true;
|
|
284
|
+
return [...url.searchParams.keys()].some((key) => /token|key|secret|password|auth/i.test(key));
|
|
285
|
+
} catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function listOf(label, value, max, errors) {
|
|
290
|
+
if (!Array.isArray(value)) {
|
|
291
|
+
errors.push(`${label} must be an array`);
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
if (value.length > max)
|
|
295
|
+
errors.push(`${label} has ${value.length} entries; the limit is ${max}`);
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
function checkObject(label, value, spec, errors) {
|
|
299
|
+
if (!isRecord(value)) {
|
|
300
|
+
errors.push(`${label} must be an object`);
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
let typed = true;
|
|
304
|
+
for (const [key, item] of Object.entries(value)) {
|
|
305
|
+
const expected = spec[key];
|
|
306
|
+
if (!expected) {
|
|
307
|
+
errors.push(`${label}: unknown field "${key}" (allowed: ${Object.keys(spec).join(", ")})`);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const ok = expected === "string[]" ? Array.isArray(item) && item.every((entry) => typeof entry === "string") : expected === "object[]" ? Array.isArray(item) && item.every(isRecord) : expected === "object" ? isRecord(item) : expected === "number" ? typeof item === "number" && Number.isFinite(item) : typeof item === expected;
|
|
311
|
+
if (!ok) {
|
|
312
|
+
errors.push(`${label}.${key} must be ${describe(expected)}`);
|
|
313
|
+
typed = false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return typed;
|
|
317
|
+
}
|
|
318
|
+
function describe(type) {
|
|
319
|
+
if (type === "string[]")
|
|
320
|
+
return "an array of strings";
|
|
321
|
+
if (type === "object[]")
|
|
322
|
+
return "an array of objects";
|
|
323
|
+
return type === "object" ? "an object" : `a ${type}`;
|
|
324
|
+
}
|
|
325
|
+
function checkEnum(label, value, allowed, errors) {
|
|
326
|
+
if (value === undefined || typeof value !== "string")
|
|
327
|
+
return;
|
|
328
|
+
if (!allowed.includes(value))
|
|
329
|
+
errors.push(`${label} must be one of ${allowed.join(", ")} (got "${value}")`);
|
|
330
|
+
}
|
|
331
|
+
function checkPositiveInt(label, value, errors) {
|
|
332
|
+
if (value === undefined || typeof value !== "number")
|
|
333
|
+
return;
|
|
334
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
335
|
+
errors.push(`${label} must be a positive integer`);
|
|
336
|
+
}
|
|
337
|
+
function isRecord(value) {
|
|
338
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
339
|
+
}
|
|
340
|
+
function nonEmpty(value) {
|
|
341
|
+
return typeof value === "string" && value.trim() !== "";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/registry-prompt.ts
|
|
345
|
+
var REGISTRY_AGENT_BRIEF = `# Register this repo's AI harness with Moda for faithful replays
|
|
346
|
+
|
|
347
|
+
You are working in the repository that runs our production AI agent. Moda
|
|
348
|
+
replays recorded production conversations against our agent to test prompt,
|
|
349
|
+
tool, and skill changes. A replay is only as faithful as what Moda knows about
|
|
350
|
+
how our agent actually runs. Your job: describe our harness to Moda exactly,
|
|
351
|
+
push it with the \`moda\` CLI, and make sure our production traces say which
|
|
352
|
+
agent and prompt handled each LLM call.
|
|
353
|
+
|
|
354
|
+
## Ground rules
|
|
355
|
+
|
|
356
|
+
- Read the code; do not guess. Every value you register must come from the
|
|
357
|
+
repo (cite the file in \`sourcePath\` or in a note). If something is decided
|
|
358
|
+
at runtime and you can't find it, say so in your final report.
|
|
359
|
+
- Copy prompt text, tool names, tool descriptions, and parameter schemas
|
|
360
|
+
verbatim. For templated prompts, keep the template with its placeholders
|
|
361
|
+
(\`{{name}}\`, \`{name}\`, \`\${name}\`). Never register a rendered example.
|
|
362
|
+
- Never put secrets in the manifest: no API keys, tokens, auth headers, env var
|
|
363
|
+
values, or customer data.
|
|
364
|
+
- Do not change runtime behaviour. Part 5 adds trace metadata only. Show the
|
|
365
|
+
diff and get my OK before committing it.
|
|
366
|
+
- Parts 0 to 6 upload only \`.moda/registry.json\` (plus the files it
|
|
367
|
+
references). Part 7 uploads a bundle of the repo, but only after I review
|
|
368
|
+
its dry run. Secrets, env var values, and git-ignored installs are never
|
|
369
|
+
included.
|
|
370
|
+
|
|
371
|
+
## Part 0: setup
|
|
372
|
+
|
|
373
|
+
1. Run \`moda registry --help\` and confirm it lists all of \`push\`,
|
|
374
|
+
\`status\`, \`coverage\`, \`log\`, \`show\`, and \`prompt\` (the full set
|
|
375
|
+
first shipped in 1.45.0). If any is missing or the command fails, run
|
|
376
|
+
\`npm install -g @moda-ai/cli@latest\` and check again. If they're still
|
|
377
|
+
missing, stop and tell me which ones; nothing below works without them.
|
|
378
|
+
Part 7 also needs \`moda harness register --help\` to work; if it does
|
|
379
|
+
not, finish Parts 1 to 6 and tell me.
|
|
380
|
+
2. Auth: \`MODA_API_KEY\` must be set, or run \`moda auth login --api-key=<key>\`.
|
|
381
|
+
Ask me for the key if neither is available. Check with \`moda doctor\`.
|
|
382
|
+
3. Run \`moda registry template\` to see a complete example manifest, and
|
|
383
|
+
\`moda registry status\` to see what is already registered.
|
|
384
|
+
|
|
385
|
+
## Part 1: inventory the harness
|
|
386
|
+
|
|
387
|
+
Find every place the code calls an LLM: direct SDK calls (OpenAI, Anthropic,
|
|
388
|
+
Gemini, Bedrock, OpenRouter), framework agents, and anything else. For each
|
|
389
|
+
call site, write down:
|
|
390
|
+
|
|
391
|
+
- **Agent**: the logical agent it belongs to, with a stable key such as
|
|
392
|
+
\`triage\` or \`billing\`. Different system prompts, tool sets, or models mean
|
|
393
|
+
different agents. A one-off call (summariser, classifier, title generator)
|
|
394
|
+
is its own agent if it runs during a conversation.
|
|
395
|
+
- **Prompt**: the system prompt or messages it sends, how they're built
|
|
396
|
+
(static text, template plus variables, or code that assembles them), and
|
|
397
|
+
the file.
|
|
398
|
+
- **Model and settings**: model id, provider, temperature, max tokens, top_p,
|
|
399
|
+
and reasoning effort, wherever they're set (code, config, env defaults).
|
|
400
|
+
- **Tools**: every tool the model can call at that site, with the exact JSON
|
|
401
|
+
schema the model sees. Include MCP tools and agents exposed as tools.
|
|
402
|
+
- **Skills**: SKILL.md files or instruction docs the agent loads, and HOW it
|
|
403
|
+
loads them (a tool such as skill_read or read_file, inlined into the system
|
|
404
|
+
prompt, or not at all).
|
|
405
|
+
- **Loop limits**: max tool rounds or steps per turn, parallel tool calls, and
|
|
406
|
+
how history is truncated or summarised.
|
|
407
|
+
|
|
408
|
+
Framework hints:
|
|
409
|
+
|
|
410
|
+
- **OpenAI Agents SDK**: \`Agent(name, instructions, model, model_settings,
|
|
411
|
+
tools, handoffs)\`. \`name\` is the trace agent name. Handoffs become
|
|
412
|
+
\`transfer_to_<agent>\` tools.
|
|
413
|
+
- **Claude Agent SDK / Claude Code SDK**: \`ClaudeAgentOptions(system_prompt,
|
|
414
|
+
model, allowed_tools, mcp_servers, agents=...)\`. Subagents are \`agents\`,
|
|
415
|
+
and skills live in \`.claude/skills/*/SKILL.md\`.
|
|
416
|
+
- **LangGraph / LangChain**: each node that binds an LLM (\`bind_tools\`,
|
|
417
|
+
\`create_react_agent\`) is an agent. Conditional edges are the routing.
|
|
418
|
+
- **Vercel AI SDK**: \`generateText\` / \`streamText({ system, tools, model,
|
|
419
|
+
maxSteps | stopWhen })\`.
|
|
420
|
+
- **Raw SDK calls**: the system message, \`tools=\`, and \`model=\` arguments.
|
|
421
|
+
|
|
422
|
+
## Part 2: work out prompt switching
|
|
423
|
+
|
|
424
|
+
Work out how the harness decides which agent and prompt answers a given turn.
|
|
425
|
+
That becomes \`routing\` and the agents' \`handoffs\`:
|
|
426
|
+
|
|
427
|
+
- **single**: one agent always answers. Prompt versions still change over
|
|
428
|
+
time; Part 5 handles that.
|
|
429
|
+
- **handoff**: the model transfers control by calling a tool (OpenAI Agents
|
|
430
|
+
handoffs, a "transfer_to_*" function, or a supervisor delegating). Add
|
|
431
|
+
\`handoffs: [{ "to": "<agent key>", "tool": "<exact tool name>",
|
|
432
|
+
"description": "<when>" }]\` to the source agent.
|
|
433
|
+
- **router / rules**: code picks the agent (by channel, intent classifier,
|
|
434
|
+
conversation stage, user plan, or feature flag). Describe the rule in
|
|
435
|
+
\`routing.notes\` and make sure Part 5's attribution names the chosen agent
|
|
436
|
+
on every call.
|
|
437
|
+
- **trace_attribute**: the agent is already identified on every trace.
|
|
438
|
+
- **subagents** (agents as tools): the parent calls a tool that runs another
|
|
439
|
+
agent and returns its result. Register that tool's schema in \`tools[]\`,
|
|
440
|
+
list it in the parent's \`tools\`, and add \`subagents: [{ "agent": "<child
|
|
441
|
+
key>", "tool": "<tool name>" }]\`.
|
|
442
|
+
|
|
443
|
+
Set \`routing.default\` to the agent that answers when nothing else decides,
|
|
444
|
+
and mark the first agent a conversation reaches with \`"entry": true\`.
|
|
445
|
+
|
|
446
|
+
## Part 3: list the injected context
|
|
447
|
+
|
|
448
|
+
For every placeholder in every prompt, and anything appended to the prompt at
|
|
449
|
+
runtime, add a \`context[]\` entry:
|
|
450
|
+
|
|
451
|
+
- \`source: "static"\`: the same value every time (put it in \`value\`).
|
|
452
|
+
- \`source: "clock"\`: the current date or time (\`format\`: date, datetime, or
|
|
453
|
+
iso). Replay uses the original conversation's time.
|
|
454
|
+
- \`source: "trace"\`: a per-user or per-conversation value, such as the
|
|
455
|
+
profile, account, plan, or locale.
|
|
456
|
+
- \`source: "memory" | "retrieval" | "tool"\`: memory recall, RAG results, or a
|
|
457
|
+
tool fetched before the call.
|
|
458
|
+
- \`target: "prompt_variable"\` (with \`variable\` set to the placeholder name,
|
|
459
|
+
without braces) for template variables, or \`target: "system_append"\` for
|
|
460
|
+
blocks the code appends.
|
|
461
|
+
|
|
462
|
+
List context keys on an agent with \`context: [...]\` when only some agents get
|
|
463
|
+
them.
|
|
464
|
+
|
|
465
|
+
## Part 4: write, validate, and push the manifest
|
|
466
|
+
|
|
467
|
+
Write \`.moda/registry.json\` with \`"schema": "moda_registry.v2"\`:
|
|
468
|
+
|
|
469
|
+
- \`prompts[]\`: \`{ key, file | content | systemPrompt | messages, name,
|
|
470
|
+
description, variables, modelConfig, responseSchema, sourcePath }\`. If the
|
|
471
|
+
repo already uses \`moda prompts\` (it has a \`.moda/prompts.lock.json\`),
|
|
472
|
+
reuse those keys.
|
|
473
|
+
- \`tools[]\`: \`{ schema: { name, description, parameters } }\`, exactly as
|
|
474
|
+
sent to the model.
|
|
475
|
+
- \`skills[]\`: \`{ key, file: "path/to/SKILL.md" }\`. Set \`"live": true\` only
|
|
476
|
+
if I ask.
|
|
477
|
+
- \`agents[]\`: \`{ key, name, entry, prompt, model: { id, provider,
|
|
478
|
+
temperature, maxTokens, topP, reasoningEffort }, tools, skills, handoffs,
|
|
479
|
+
subagents, context, maxToolRoundsPerTurn, responseSchema, match: {
|
|
480
|
+
agentNames, promptKeys } }\`. \`match.agentNames\` are the names this agent
|
|
481
|
+
carries in traces; \`match.promptKeys\` are prompt keys that identify it.
|
|
482
|
+
- \`routing\`: \`{ strategy, default, notes }\`.
|
|
483
|
+
- \`runtime\`: \`{ framework, language, maxToolRoundsPerTurn,
|
|
484
|
+
parallelToolCalls, history: { strategy, maxMessages, maxTokens },
|
|
485
|
+
skillLoading: "tool" | "system_prompt" | "none" }\`.
|
|
486
|
+
- \`context[]\`: from Part 3.
|
|
487
|
+
- \`mcpServers[]\`: \`{ name, transport, url | command, tools, description }\`,
|
|
488
|
+
with no credentials. Their tools still go in \`tools[]\`.
|
|
489
|
+
|
|
490
|
+
Then run:
|
|
491
|
+
|
|
492
|
+
moda registry validate # local; fix every error and read every warning
|
|
493
|
+
moda registry push --dry-run # server-side preview, writes nothing
|
|
494
|
+
moda registry push --message="<what you registered or changed>"
|
|
495
|
+
|
|
496
|
+
Re-pushing an unchanged manifest is a no-op. A changed prompt adds a new
|
|
497
|
+
version, and a changed harness adds a new harness version.
|
|
498
|
+
|
|
499
|
+
## Part 5: make traces name the agent and prompt version
|
|
500
|
+
|
|
501
|
+
Replay picks the agent and prompt version that handled the replayed turn from
|
|
502
|
+
attributes on each LLM call. Auto-instrumented calls don't carry them, so add
|
|
503
|
+
them at each call site from Part 1:
|
|
504
|
+
|
|
505
|
+
| What | OTel span attribute | Vercel AI SDK \`experimental_telemetry.metadata\` | \`/v1/ingest\` event field |
|
|
506
|
+
|---|---|---|---|
|
|
507
|
+
| Agent | \`gen_ai.agent.name\` (or \`moda.agent_name\`) | \`moda.agent_name\` | \`agent_name\` |
|
|
508
|
+
| Prompt key | \`moda.prompt_key\` | \`moda.prompt_key\` | \`prompt_name\` |
|
|
509
|
+
| Prompt version | \`moda.prompt_version_id\` | \`moda.prompt_version_id\` | \`prompt_version_id\` |
|
|
510
|
+
|
|
511
|
+
- The agent value must be the agent's \`key\` or one of its
|
|
512
|
+
\`match.agentNames\`. Frameworks that already set \`gen_ai.agent.name\`
|
|
513
|
+
(OpenAI Agents SDK, many OpenInference/OpenLLMetry instrumentations) only
|
|
514
|
+
need \`match.agentNames\` to list those names.
|
|
515
|
+
- Get the prompt version id from \`.moda/prompts.lock.json\` (written by
|
|
516
|
+
\`moda prompts sync\`), or leave it out. Replay then uses the prompt's prod
|
|
517
|
+
version.
|
|
518
|
+
- Node with the Moda SDK: \`Moda.withLLMCall(..., ({ span }) =>
|
|
519
|
+
span.rawSpan.setAttribute(...))\`, or pass Vercel metadata through
|
|
520
|
+
\`Moda.getVercelAITelemetry({ metadata })\`.
|
|
521
|
+
- Python: set the attributes in your OpenTelemetry instrumentation on the LLM
|
|
522
|
+
span, or send \`agent_name\` / \`prompt_*\` on \`/v1/ingest\` events.
|
|
523
|
+
- Show me the diff before committing it.
|
|
524
|
+
|
|
525
|
+
## Part 6: verify
|
|
526
|
+
|
|
527
|
+
moda registry status
|
|
528
|
+
moda registry coverage --days=7
|
|
529
|
+
moda registry log # each push is a commit; review what changed
|
|
530
|
+
|
|
531
|
+
Coverage compares the last N days of production traces with what you
|
|
532
|
+
registered: the share of LLM calls naming an agent or prompt, agent names and
|
|
533
|
+
prompt keys that match no registered agent, tools used in production but not
|
|
534
|
+
registered, and registered agents never seen. Fix the manifest (or the
|
|
535
|
+
attribution) and push again until there is nothing unexplained left. Attribution
|
|
536
|
+
added in Part 5 only shows up on traces created after it ships.
|
|
537
|
+
|
|
538
|
+
## Part 7: register the runnable harness
|
|
539
|
+
|
|
540
|
+
Replay Environments run our real harness in a sandbox with outbound network
|
|
541
|
+
denied, so Moda needs the code itself, not just the manifest. \`moda harness
|
|
542
|
+
register\` collects the git repository into a bundle. It keeps everything a
|
|
543
|
+
build could plausibly need and records the name and reason for everything it
|
|
544
|
+
leaves out: secret files, files that contain credentials, VCS internals,
|
|
545
|
+
installed dependencies a lockfile reproduces, build output, and paths in
|
|
546
|
+
\`.modaignore\`. Env var values are never read; only names are recorded.
|
|
547
|
+
|
|
548
|
+
1. Preview it. Nothing is uploaded:
|
|
549
|
+
|
|
550
|
+
moda harness register --dry-run
|
|
551
|
+
|
|
552
|
+
2. Review the report with me before going further:
|
|
553
|
+
- **Excluded**: every path left out and why. If a file a build needs was
|
|
554
|
+
withheld (for example \`secret_content\` on a fixture with a fake key),
|
|
555
|
+
tell me; don't move secrets around to get it in.
|
|
556
|
+
- **Git-ignored but present**: generated clients, local config, or model
|
|
557
|
+
files the build may need. Ask me before un-ignoring anything.
|
|
558
|
+
- **Environment variables**: each name with a proposed classification
|
|
559
|
+
(\`secret\`, \`config\`, \`dependency_endpoint\`, \`model_endpoint\`,
|
|
560
|
+
\`model_credential\`, \`unknown\`). Read the code for every \`unknown\`
|
|
561
|
+
and tell me what it is.
|
|
562
|
+
- **Dependencies and migrations**: the databases, queues, and APIs it
|
|
563
|
+
found, the adapter Moda proposes for each, and the migrations it would
|
|
564
|
+
run against a disposable database.
|
|
565
|
+
- **Gaps** and **Moda will still need from you**.
|
|
566
|
+
3. Declare the entrypoint and the model-routing env vars. The entrypoint is
|
|
567
|
+
the exact command that starts the harness the way production does (from
|
|
568
|
+
the Dockerfile, Procfile, start script, or deploy config). The routing vars
|
|
569
|
+
are the names of the env vars that set the model base URL, the model id,
|
|
570
|
+
and the API key, so replay can send inference to Moda's approved lane.
|
|
571
|
+
Pass names, never values:
|
|
572
|
+
|
|
573
|
+
moda harness register <name> --dry-run \\
|
|
574
|
+
--model-base-url-env=OPENAI_BASE_URL --model-env=OPENAI_MODEL \\
|
|
575
|
+
--model-api-key-env=OPENAI_API_KEY \\
|
|
576
|
+
--entry -- node dist/server.js
|
|
577
|
+
|
|
578
|
+
\`--entry\` goes last: everything after \`--\` is the command. If the code
|
|
579
|
+
hardcodes the model base URL instead of reading an env var, say so in your
|
|
580
|
+
report; do not change it.
|
|
581
|
+
4. When I approve the dry run, register for real with the same flags and no
|
|
582
|
+
\`--dry-run\`. It prints a registration id, a status, and a dashboard link.
|
|
583
|
+
Then run:
|
|
584
|
+
|
|
585
|
+
moda harness registration status <id>
|
|
586
|
+
|
|
587
|
+
\`needs_input\` means the dashboard checklist still has items for me:
|
|
588
|
+
confirming env classifications, approving dependency adapters and
|
|
589
|
+
migrations, and acknowledging gaps. List the blocking ones in your report.
|
|
590
|
+
I make those decisions; you don't.
|
|
591
|
+
|
|
592
|
+
## Final report
|
|
593
|
+
|
|
594
|
+
Reply with:
|
|
595
|
+
|
|
596
|
+
1. The agents you registered: key, prompt, model, tool count, skill count, and
|
|
597
|
+
handoffs.
|
|
598
|
+
2. The routing strategy, and how you verified it.
|
|
599
|
+
3. Context entries whose values replay can't reproduce (\`trace\`, \`memory\`,
|
|
600
|
+
\`retrieval\`, \`tool\`).
|
|
601
|
+
4. The attribution diff (or that it's pending my OK).
|
|
602
|
+
5. The \`moda registry coverage\` result, plus anything unexplained you
|
|
603
|
+
couldn't resolve.
|
|
604
|
+
6. Anything you found but couldn't express in the manifest.
|
|
605
|
+
7. The registration id and status, the entrypoint and model-routing env vars
|
|
606
|
+
you declared, exclusions or ignored paths a build may need, and the
|
|
607
|
+
checklist items still open.
|
|
608
|
+
`;
|
|
609
|
+
|
|
610
|
+
// src/registry.ts
|
|
611
|
+
var REGISTRY_SCHEMA = "moda_registry.v2";
|
|
612
|
+
var REGISTRY_SCHEMA_V1 = "moda_registry.v1";
|
|
613
|
+
var DEFAULT_REGISTRY_PATH = ".moda/registry.json";
|
|
614
|
+
var SKILL_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
615
|
+
var MAX_ITEMS_PER_SECTION = 500;
|
|
616
|
+
var REGISTRY_TEMPLATE = {
|
|
617
|
+
schema: REGISTRY_SCHEMA,
|
|
618
|
+
prompts: [
|
|
619
|
+
{
|
|
620
|
+
key: "support.triage",
|
|
621
|
+
name: "Support triage system prompt",
|
|
622
|
+
description: "Routes inbound support messages",
|
|
623
|
+
content: "You are a support triage agent for {{company_name}}. Today is {{current_date}}. Classify the request and ...",
|
|
624
|
+
sourcePath: "src/agents/triage/prompt.ts"
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
key: "support.reply",
|
|
628
|
+
file: "prompts/reply.md"
|
|
629
|
+
}
|
|
630
|
+
],
|
|
631
|
+
skills: [
|
|
632
|
+
{
|
|
633
|
+
key: "refund-policy",
|
|
634
|
+
description: "How to process refund requests",
|
|
635
|
+
skillMd: `---
|
|
636
|
+
name: refund-policy
|
|
637
|
+
description: How to process refund requests
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
# Refund policy
|
|
641
|
+
...`
|
|
642
|
+
}
|
|
643
|
+
],
|
|
644
|
+
tools: [
|
|
645
|
+
{
|
|
646
|
+
schema: {
|
|
647
|
+
name: "lookup_customer",
|
|
648
|
+
description: "Find a customer by email",
|
|
649
|
+
parameters: {
|
|
650
|
+
type: "object",
|
|
651
|
+
properties: { email: { type: "string" } },
|
|
652
|
+
required: ["email"]
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
],
|
|
657
|
+
runtime: {
|
|
658
|
+
framework: "openai-agents",
|
|
659
|
+
language: "python",
|
|
660
|
+
maxToolRoundsPerTurn: 6,
|
|
661
|
+
history: { strategy: "window", maxMessages: 40 },
|
|
662
|
+
skillLoading: "tool"
|
|
663
|
+
},
|
|
664
|
+
agents: [
|
|
665
|
+
{
|
|
666
|
+
key: "triage",
|
|
667
|
+
name: "Triage",
|
|
668
|
+
entry: true,
|
|
669
|
+
prompt: "support.triage",
|
|
670
|
+
model: { id: "openai/gpt-5.1", temperature: 0.2, maxTokens: 1024 },
|
|
671
|
+
tools: ["lookup_customer"],
|
|
672
|
+
handoffs: [{ to: "reply", description: "Hand off once the request is classified" }],
|
|
673
|
+
match: { agentNames: ["TriageAgent"] }
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
key: "reply",
|
|
677
|
+
name: "Reply writer",
|
|
678
|
+
prompt: "support.reply",
|
|
679
|
+
model: { id: "anthropic/claude-sonnet-5-5", temperature: 0.4, maxTokens: 2048 },
|
|
680
|
+
skills: ["refund-policy"],
|
|
681
|
+
match: { agentNames: ["ReplyAgent"] }
|
|
682
|
+
}
|
|
683
|
+
],
|
|
684
|
+
routing: { strategy: "handoff", default: "triage" },
|
|
685
|
+
context: [
|
|
686
|
+
{ key: "today", variable: "current_date", source: "clock", format: "date" },
|
|
687
|
+
{ key: "company", variable: "company_name", source: "static", value: "Acme" }
|
|
688
|
+
]
|
|
689
|
+
};
|
|
690
|
+
var REGISTRY_SUBCOMMAND_FLAGS = {
|
|
691
|
+
template: ["out", "dry-run"],
|
|
692
|
+
validate: ["file", "manifest"],
|
|
693
|
+
push: ["file", "manifest", "live", "dry-run", "message"],
|
|
694
|
+
log: ["limit"],
|
|
695
|
+
show: [],
|
|
696
|
+
status: [],
|
|
697
|
+
coverage: ["days"],
|
|
698
|
+
prompt: []
|
|
699
|
+
};
|
|
700
|
+
async function runRegistryCommand(context) {
|
|
701
|
+
const subcommand = context.positionals[0] ?? "push";
|
|
702
|
+
assertFlags(`registry ${subcommand}`, context.flags, REGISTRY_SUBCOMMAND_FLAGS[subcommand] ?? []);
|
|
703
|
+
switch (subcommand) {
|
|
704
|
+
case "template":
|
|
705
|
+
return writeTemplate(context);
|
|
706
|
+
case "validate": {
|
|
707
|
+
const registry = loadRegistry(context);
|
|
708
|
+
printWarnings(context, registry.warnings);
|
|
709
|
+
context.output.writeData({
|
|
710
|
+
schema: registry.schema,
|
|
711
|
+
manifest: displayPath(context, registry.manifestPath),
|
|
712
|
+
valid: true,
|
|
713
|
+
counts: countsOf(registry),
|
|
714
|
+
warnings: registry.warnings
|
|
715
|
+
});
|
|
716
|
+
return { exitCode: 0 };
|
|
717
|
+
}
|
|
718
|
+
case "push":
|
|
719
|
+
return pushRegistry(context);
|
|
720
|
+
case "status":
|
|
721
|
+
return readRegistry(context, "/harness-registry");
|
|
722
|
+
case "log": {
|
|
723
|
+
const limit = context.flags.limit;
|
|
724
|
+
if (limit !== undefined && !/^\d+$/.test(limit)) {
|
|
725
|
+
throw new CliInputError("--limit must be a whole number.", "Usage: moda registry log [--limit=20]");
|
|
726
|
+
}
|
|
727
|
+
return readRegistry(context, `/registry/commits${limit ? `?limit=${limit}` : ""}`);
|
|
728
|
+
}
|
|
729
|
+
case "show": {
|
|
730
|
+
const ref = context.positionals[1];
|
|
731
|
+
if (!ref) {
|
|
732
|
+
throw new CliInputError("Missing commit.", 'Usage: moda registry show <commit> (a hash from `moda registry log`, or "working")');
|
|
733
|
+
}
|
|
734
|
+
return readRegistry(context, ref === "working" ? "/registry/compare?to=working" : `/registry/commits/${encodeURIComponent(ref)}`);
|
|
735
|
+
}
|
|
736
|
+
case "coverage": {
|
|
737
|
+
const days = context.flags.days;
|
|
738
|
+
if (days !== undefined && !/^\d+$/.test(days)) {
|
|
739
|
+
throw new CliInputError("--days must be a whole number of days.", "Usage: moda registry coverage [--days=7]");
|
|
740
|
+
}
|
|
741
|
+
return readRegistry(context, `/harness-registry/coverage${days ? `?days=${days}` : ""}`);
|
|
742
|
+
}
|
|
743
|
+
case "prompt": {
|
|
744
|
+
const mode = explicitOutputMode(context.flags, context.env);
|
|
745
|
+
if (mode && mode !== "human")
|
|
746
|
+
context.output.writeData({ prompt: REGISTRY_AGENT_BRIEF });
|
|
747
|
+
else if (!context.quiet)
|
|
748
|
+
process.stdout.write(`${REGISTRY_AGENT_BRIEF}
|
|
749
|
+
`);
|
|
750
|
+
return { exitCode: 0 };
|
|
751
|
+
}
|
|
752
|
+
default:
|
|
753
|
+
throw new CliInputError(`Unknown registry command '${subcommand}'.`, "Usage: moda registry template|validate|push|status|coverage|log|show|prompt [--file=.moda/registry.json] [--dry-run] [--live]");
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function writeTemplate(context) {
|
|
757
|
+
const out = context.flags.out;
|
|
758
|
+
if (out === "true") {
|
|
759
|
+
throw new CliInputError("Missing output path for --out.", "Usage: moda registry template --out=.moda/registry.json");
|
|
760
|
+
}
|
|
761
|
+
if (!out) {
|
|
762
|
+
context.output.writeData(REGISTRY_TEMPLATE);
|
|
763
|
+
return { exitCode: 0 };
|
|
764
|
+
}
|
|
765
|
+
const target = resolve(context.cwd, out);
|
|
766
|
+
if (existsSync(target) && !context.yes) {
|
|
767
|
+
throw new CliInputError(`${out} already exists; refusing to overwrite.`, "Pass --yes to overwrite it.");
|
|
768
|
+
}
|
|
769
|
+
if (!context.dryRun) {
|
|
770
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
771
|
+
writeFileSync(target, `${JSON.stringify(REGISTRY_TEMPLATE, null, 2)}
|
|
772
|
+
`);
|
|
773
|
+
}
|
|
774
|
+
context.output.writeData({ written: context.dryRun ? [] : [displayPath(context, target)], dryRun: context.dryRun });
|
|
775
|
+
return { exitCode: 0 };
|
|
776
|
+
}
|
|
777
|
+
async function pushRegistry(context) {
|
|
778
|
+
const message = context.flags.message;
|
|
779
|
+
if (message === "true")
|
|
780
|
+
throw new CliInputError("Missing text for --message.", 'Usage: moda registry push --message="Add billing agent"');
|
|
781
|
+
const registry = loadRegistry(context);
|
|
782
|
+
const profileOptions = { profile: context.profile, env: context.env };
|
|
783
|
+
const hasKey = Boolean(resolveApiKey(profileOptions));
|
|
784
|
+
if (!hasKey && !context.dryRun) {
|
|
785
|
+
throw new CliInputError("No Moda API key found for registry push.", "Set MODA_API_KEY, run `moda auth login --api-key=<key>`, or select a profile with an API key.");
|
|
786
|
+
}
|
|
787
|
+
const sections = [];
|
|
788
|
+
const push = async (section, endpoint, count, body) => {
|
|
789
|
+
if (count === 0) {
|
|
790
|
+
sections.push({ section, status: "skipped", count, endpoint });
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (context.dryRun && !hasKey) {
|
|
794
|
+
sections.push({ section, status: "validated", count, endpoint });
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
const response = await callControlAPI(endpoint, { method: "POST", body: JSON.stringify({ ...body, ...context.dryRun ? { dryRun: true } : {} }) }, profileOptions);
|
|
799
|
+
sections.push({ section, status: context.dryRun ? "dry_run" : "synced", count, endpoint, response });
|
|
800
|
+
} catch (error) {
|
|
801
|
+
sections.push({
|
|
802
|
+
section,
|
|
803
|
+
status: "error",
|
|
804
|
+
count,
|
|
805
|
+
endpoint,
|
|
806
|
+
error: error instanceof Error ? error.message : String(error)
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
await push("prompts", "/prompts/sync", registry.prompts.length, { prompts: registry.prompts });
|
|
811
|
+
await push("skills", "/skills/sync", registry.skills.length, { skills: registry.skills });
|
|
812
|
+
await push("tools", "/tool-definitions/sync", registry.tools.length, { tools: registry.tools });
|
|
813
|
+
if (registry.harness) {
|
|
814
|
+
await push("harness", "/harness-registry/sync", 1, { schema: registry.schema, document: registry.harness });
|
|
815
|
+
}
|
|
816
|
+
let commit;
|
|
817
|
+
let commitError;
|
|
818
|
+
if (!context.dryRun && sections.some((section) => section.status === "synced")) {
|
|
819
|
+
try {
|
|
820
|
+
commit = await callControlAPI("/registry/commits", { method: "POST", body: JSON.stringify({ ...message ? { message } : {}, ...gitInfo(context.cwd) }) }, profileOptions);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
commitError = error instanceof Error ? error.message : String(error);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const failed = sections.filter((section) => section.status === "error");
|
|
826
|
+
printWarnings(context, registry.warnings);
|
|
827
|
+
if (context.outputMode === "human" && !context.quiet) {
|
|
828
|
+
for (const section of sections) {
|
|
829
|
+
const detail = section.status === "error" ? ` — ${section.error}` : describeActions(section.response);
|
|
830
|
+
console.error(` ${section.section}: ${section.status} (${section.count})${detail}`);
|
|
831
|
+
}
|
|
832
|
+
if (sections.some((section) => section.section === "tools" && section.status === "synced")) {
|
|
833
|
+
console.error(" Replays now give your agent these tools with their real schemas and descriptions.");
|
|
834
|
+
}
|
|
835
|
+
if (sections.some((section) => section.section === "harness" && section.status === "synced")) {
|
|
836
|
+
console.error(" Replays now rebuild the agent live at each replayed turn. Check `moda registry coverage`.");
|
|
837
|
+
}
|
|
838
|
+
const recorded = isRecord2(commit) && isRecord2(commit.commit) ? commit.commit : undefined;
|
|
839
|
+
if (recorded) {
|
|
840
|
+
const label = commit && isRecord2(commit) && commit.action === "unchanged" ? "no registry changes since" : "commit";
|
|
841
|
+
console.error(` ${label} ${String(recorded.id).slice(0, 7)} (#${String(recorded.seq)}) ${String(recorded.summary ?? "")}`);
|
|
842
|
+
} else if (commitError) {
|
|
843
|
+
console.error(` commit: not recorded — ${commitError}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
context.output.writeData({
|
|
847
|
+
schema: registry.schema,
|
|
848
|
+
manifest: displayPath(context, registry.manifestPath),
|
|
849
|
+
dryRun: context.dryRun,
|
|
850
|
+
counts: countsOf(registry),
|
|
851
|
+
sections,
|
|
852
|
+
warnings: registry.warnings,
|
|
853
|
+
...commit !== undefined ? { commit } : {},
|
|
854
|
+
...commitError ? { commitError } : {}
|
|
855
|
+
}, {
|
|
856
|
+
status: failed.length > 0 ? "error" : "success",
|
|
857
|
+
...failed.length > 0 ? {
|
|
858
|
+
errors: failed.map((section) => ({
|
|
859
|
+
code: "registry_section_failed",
|
|
860
|
+
message: `${section.section}: ${section.error}`,
|
|
861
|
+
blocking: false
|
|
862
|
+
}))
|
|
863
|
+
} : {}
|
|
864
|
+
});
|
|
865
|
+
return { exitCode: failed.length > 0 ? 1 : 0 };
|
|
866
|
+
}
|
|
867
|
+
function gitInfo(cwd) {
|
|
868
|
+
const git = (...args) => execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).toString().trim();
|
|
869
|
+
try {
|
|
870
|
+
const gitCommit = git("rev-parse", "HEAD");
|
|
871
|
+
const branch = git("rev-parse", "--abbrev-ref", "HEAD");
|
|
872
|
+
return {
|
|
873
|
+
gitCommit,
|
|
874
|
+
...branch && branch !== "HEAD" ? { gitBranch: branch } : {},
|
|
875
|
+
gitDirty: git("status", "--porcelain").length > 0
|
|
876
|
+
};
|
|
877
|
+
} catch {
|
|
878
|
+
return {};
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async function readRegistry(context, endpoint) {
|
|
882
|
+
const profileOptions = { profile: context.profile, env: context.env };
|
|
883
|
+
if (!resolveApiKey(profileOptions)) {
|
|
884
|
+
throw new CliInputError("No Moda API key found.", "Set MODA_API_KEY, run `moda auth login --api-key=<key>`, or select a profile with an API key.");
|
|
885
|
+
}
|
|
886
|
+
context.output.writeData(await callControlAPI(endpoint, { method: "GET" }, profileOptions));
|
|
887
|
+
return { exitCode: 0 };
|
|
888
|
+
}
|
|
889
|
+
function printWarnings(context, warnings) {
|
|
890
|
+
if (context.outputMode !== "human" || context.quiet)
|
|
891
|
+
return;
|
|
892
|
+
for (const warning of warnings)
|
|
893
|
+
console.error(` warning: ${warning}`);
|
|
894
|
+
}
|
|
895
|
+
function describeActions(response) {
|
|
896
|
+
const synced = isRecord2(response) && Array.isArray(response.synced) ? response.synced : [];
|
|
897
|
+
const counts = new Map;
|
|
898
|
+
for (const item of synced) {
|
|
899
|
+
if (!isRecord2(item))
|
|
900
|
+
continue;
|
|
901
|
+
const action = typeof item.action === "string" ? `${item.action}${typeof item.reason === "string" ? ` (${item.reason})` : ""}` : item.changed === false ? "unchanged" : item.policyChanged === true && item.changed === true ? "changed (live setting)" : item.changed === true ? "changed" : "synced";
|
|
902
|
+
counts.set(action, (counts.get(action) ?? 0) + 1);
|
|
903
|
+
}
|
|
904
|
+
return counts.size > 0 ? ` — ${[...counts].map(([action, count]) => `${count} ${action}`).join(", ")}` : "";
|
|
905
|
+
}
|
|
906
|
+
function loadRegistry(context) {
|
|
907
|
+
const fileFlag = context.flags.file ?? context.flags.manifest;
|
|
908
|
+
if (fileFlag === "true") {
|
|
909
|
+
throw new CliInputError("Missing path for --file.", "Usage: moda registry push --file=.moda/registry.json");
|
|
910
|
+
}
|
|
911
|
+
const manifestPath = resolve(context.cwd, fileFlag ?? DEFAULT_REGISTRY_PATH);
|
|
912
|
+
if (!existsSync(manifestPath)) {
|
|
913
|
+
throw new CliInputError(`Registry manifest not found: ${fileFlag ?? DEFAULT_REGISTRY_PATH}`, "Create one with `moda registry template --out=.moda/registry.json`, fill it in, then rerun.");
|
|
914
|
+
}
|
|
915
|
+
let parsed;
|
|
916
|
+
try {
|
|
917
|
+
parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
918
|
+
} catch (error) {
|
|
919
|
+
throw new CliInputError(`Registry manifest is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
920
|
+
}
|
|
921
|
+
return resolveRegistry(parsed, {
|
|
922
|
+
manifestPath,
|
|
923
|
+
cwd: context.cwd,
|
|
924
|
+
forceLive: context.flags.live === "true"
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
var PROMPT_FIELDS = {
|
|
928
|
+
key: "string",
|
|
929
|
+
name: "string",
|
|
930
|
+
description: "string",
|
|
931
|
+
category: "string",
|
|
932
|
+
content: "string",
|
|
933
|
+
systemPrompt: "string",
|
|
934
|
+
messages: "array",
|
|
935
|
+
variables: "array",
|
|
936
|
+
variablesSchema: "object",
|
|
937
|
+
modelConfig: "object",
|
|
938
|
+
toolRefs: "array",
|
|
939
|
+
responseSchema: "object",
|
|
940
|
+
sourcePath: "string",
|
|
941
|
+
file: "string"
|
|
942
|
+
};
|
|
943
|
+
var SKILL_FIELDS = {
|
|
944
|
+
key: "string",
|
|
945
|
+
name: "string",
|
|
946
|
+
description: "string",
|
|
947
|
+
skillMd: "string",
|
|
948
|
+
sourcePath: "string",
|
|
949
|
+
file: "string",
|
|
950
|
+
live: "boolean"
|
|
951
|
+
};
|
|
952
|
+
var TOOL_FIELDS = {
|
|
953
|
+
name: "string",
|
|
954
|
+
description: "string",
|
|
955
|
+
schema: "object",
|
|
956
|
+
isActive: "boolean"
|
|
957
|
+
};
|
|
958
|
+
var MAX_SKILL_KEY_LENGTH = 256;
|
|
959
|
+
var MAX_SKILL_BYTES = 1e6;
|
|
960
|
+
function checkFields(label, item, spec, errors) {
|
|
961
|
+
for (const [key, value] of Object.entries(item)) {
|
|
962
|
+
const expected = spec[key];
|
|
963
|
+
if (!expected) {
|
|
964
|
+
errors.push(`${label}: unknown field "${key}" (allowed: ${Object.keys(spec).join(", ")})`);
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const actual = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
968
|
+
if (actual !== expected)
|
|
969
|
+
errors.push(`${label}.${key} must be ${expected === "array" || expected === "object" ? "an" : "a"} ${expected} (got ${actual})`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
function resolveRegistry(input, options) {
|
|
973
|
+
const errors = [];
|
|
974
|
+
const warnings = [];
|
|
975
|
+
if (!isRecord2(input)) {
|
|
976
|
+
throw new CliInputError("Registry manifest must be a JSON object with prompts, skills, and/or tools arrays.");
|
|
977
|
+
}
|
|
978
|
+
if (input.schema !== undefined && input.schema !== REGISTRY_SCHEMA && input.schema !== REGISTRY_SCHEMA_V1) {
|
|
979
|
+
errors.push(`schema must be "${REGISTRY_SCHEMA}" or "${REGISTRY_SCHEMA_V1}" (got ${JSON.stringify(input.schema)})`);
|
|
980
|
+
}
|
|
981
|
+
if (input.schema === REGISTRY_SCHEMA_V1 && hasHarnessSections(input)) {
|
|
982
|
+
errors.push(`${HARNESS_SECTIONS.join(", ")} need schema "${REGISTRY_SCHEMA}"`);
|
|
983
|
+
}
|
|
984
|
+
const known = new Set(["schema", "prompts", "skills", "tools", ...HARNESS_SECTIONS]);
|
|
985
|
+
for (const key of Object.keys(input)) {
|
|
986
|
+
if (!known.has(key))
|
|
987
|
+
errors.push(`unknown top-level field "${key}"`);
|
|
988
|
+
}
|
|
989
|
+
const readRef = (label, file) => {
|
|
990
|
+
if (typeof file !== "string" || file.trim() === "") {
|
|
991
|
+
errors.push(`${label}.file must be a non-empty string`);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const target = resolve(options.cwd, file);
|
|
995
|
+
if (!isInside(options.cwd, target)) {
|
|
996
|
+
errors.push(`${label}.file "${file}" resolves outside the project directory`);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (!existsSync(target)) {
|
|
1000
|
+
errors.push(`${label}.file "${file}" does not exist`);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (!isInside(realpathSync(options.cwd), realpathSync(target))) {
|
|
1004
|
+
errors.push(`${label}.file "${file}" is a symlink to outside the project directory`);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (!statSync(target).isFile()) {
|
|
1008
|
+
errors.push(`${label}.file "${file}" is not a file`);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
return readFileSync(target, "utf8");
|
|
1012
|
+
};
|
|
1013
|
+
const sourcePathFor = (file) => relative(options.cwd, resolve(options.cwd, file)).split("\\").join("/");
|
|
1014
|
+
const prompts = arrayOf(input.prompts, "prompts", errors).map((raw, index) => {
|
|
1015
|
+
const label = `prompts[${index}]`;
|
|
1016
|
+
if (!isRecord2(raw)) {
|
|
1017
|
+
errors.push(`${label} must be an object`);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
checkFields(label, raw, PROMPT_FIELDS, errors);
|
|
1021
|
+
const { file, ...rest } = raw;
|
|
1022
|
+
if (!nonEmptyString(rest.key))
|
|
1023
|
+
errors.push(`${label}.key is required`);
|
|
1024
|
+
const prompt = { ...rest };
|
|
1025
|
+
if (file !== undefined) {
|
|
1026
|
+
if (rest.content !== undefined)
|
|
1027
|
+
errors.push(`${label}: set content or file, not both`);
|
|
1028
|
+
const content = readRef(label, file);
|
|
1029
|
+
if (content !== undefined)
|
|
1030
|
+
prompt.content = content;
|
|
1031
|
+
if (typeof file === "string" && !prompt.sourcePath)
|
|
1032
|
+
prompt.sourcePath = sourcePathFor(file);
|
|
1033
|
+
}
|
|
1034
|
+
if (!nonEmptyString(prompt.content) && !nonEmptyString(prompt.systemPrompt) && !(Array.isArray(prompt.messages) && prompt.messages.length > 0)) {
|
|
1035
|
+
errors.push(`${label}: one of content, file, systemPrompt, or messages is required`);
|
|
1036
|
+
}
|
|
1037
|
+
return prompt;
|
|
1038
|
+
});
|
|
1039
|
+
const skills = arrayOf(input.skills, "skills", errors).map((raw, index) => {
|
|
1040
|
+
const label = `skills[${index}]`;
|
|
1041
|
+
if (!isRecord2(raw)) {
|
|
1042
|
+
errors.push(`${label} must be an object`);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
checkFields(label, raw, SKILL_FIELDS, errors);
|
|
1046
|
+
const skill = raw;
|
|
1047
|
+
if (!nonEmptyString(skill.key)) {
|
|
1048
|
+
errors.push(`${label}.key is required`);
|
|
1049
|
+
} else if (!SKILL_KEY_PATTERN.test(skill.key)) {
|
|
1050
|
+
errors.push(`${label}.key "${skill.key}" may only contain letters, digits, "_" and "-"`);
|
|
1051
|
+
} else if (skill.key.length > MAX_SKILL_KEY_LENGTH) {
|
|
1052
|
+
errors.push(`${label}.key must be ${MAX_SKILL_KEY_LENGTH} characters or fewer`);
|
|
1053
|
+
}
|
|
1054
|
+
let skillMd = skill.skillMd;
|
|
1055
|
+
let sourcePath = skill.sourcePath;
|
|
1056
|
+
if (skill.file !== undefined) {
|
|
1057
|
+
if (skill.skillMd !== undefined)
|
|
1058
|
+
errors.push(`${label}: set skillMd or file, not both`);
|
|
1059
|
+
skillMd = readRef(label, skill.file);
|
|
1060
|
+
if (typeof skill.file === "string" && !sourcePath)
|
|
1061
|
+
sourcePath = sourcePathFor(skill.file);
|
|
1062
|
+
}
|
|
1063
|
+
if (!nonEmptyString(skillMd)) {
|
|
1064
|
+
errors.push(`${label}: skillMd or file is required`);
|
|
1065
|
+
} else if (Buffer.byteLength(skillMd, "utf8") > MAX_SKILL_BYTES) {
|
|
1066
|
+
errors.push(`${label}: skill content exceeds ${MAX_SKILL_BYTES} bytes`);
|
|
1067
|
+
}
|
|
1068
|
+
if (skill.live !== undefined && typeof skill.live !== "boolean")
|
|
1069
|
+
errors.push(`${label}.live must be a boolean`);
|
|
1070
|
+
const live = options.forceLive || skill.live === true;
|
|
1071
|
+
return {
|
|
1072
|
+
key: skill.key,
|
|
1073
|
+
...skill.name !== undefined ? { name: skill.name } : {},
|
|
1074
|
+
...skill.description !== undefined ? { description: skill.description } : {},
|
|
1075
|
+
skillMd: skillMd ?? "",
|
|
1076
|
+
...sourcePath !== undefined ? { sourcePath } : {},
|
|
1077
|
+
...live ? { surfacePolicy: "include" } : skill.live === false ? { surfacePolicy: "local_only" } : {}
|
|
1078
|
+
};
|
|
1079
|
+
});
|
|
1080
|
+
const toolNames = new Set;
|
|
1081
|
+
const tools = arrayOf(input.tools, "tools", errors).map((raw, index) => {
|
|
1082
|
+
const label = `tools[${index}]`;
|
|
1083
|
+
if (!isRecord2(raw) || !isRecord2(raw.schema)) {
|
|
1084
|
+
errors.push(`${label}.schema must be an object (OpenAI function schema)`);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
checkFields(label, raw, TOOL_FIELDS, errors);
|
|
1088
|
+
const tool = raw;
|
|
1089
|
+
const inner = tool.schema.type === "function" && isRecord2(tool.schema.function) ? tool.schema.function : tool.schema;
|
|
1090
|
+
const schemaName = typeof inner.name === "string" ? inner.name.trim() : undefined;
|
|
1091
|
+
const name = (typeof tool.name === "string" ? tool.name.trim() : "") || schemaName;
|
|
1092
|
+
if (typeof tool.name === "string" && tool.name.trim() && schemaName && tool.name.trim() !== schemaName) {
|
|
1093
|
+
errors.push(`${label}: name "${tool.name}" does not match schema.name "${schemaName}"`);
|
|
1094
|
+
}
|
|
1095
|
+
if (!nonEmptyString(name)) {
|
|
1096
|
+
errors.push(`${label}: name or schema.name is required`);
|
|
1097
|
+
} else if (toolNames.has(name)) {
|
|
1098
|
+
errors.push(`${label}: duplicate tool name "${name}"`);
|
|
1099
|
+
} else {
|
|
1100
|
+
toolNames.add(name);
|
|
1101
|
+
}
|
|
1102
|
+
if (inner.name !== undefined && typeof inner.name !== "string")
|
|
1103
|
+
errors.push(`${label}: schema.name must be a string`);
|
|
1104
|
+
if (inner.description !== undefined && typeof inner.description !== "string") {
|
|
1105
|
+
errors.push(`${label}: schema.description must be a string`);
|
|
1106
|
+
}
|
|
1107
|
+
if (inner.parameters !== undefined) {
|
|
1108
|
+
if (!isRecord2(inner.parameters) || inner.parameters.type !== "object") {
|
|
1109
|
+
errors.push(`${label}: schema.parameters.type must be "object"`);
|
|
1110
|
+
} else if (inner.parameters.properties !== undefined && !isRecord2(inner.parameters.properties)) {
|
|
1111
|
+
errors.push(`${label}: schema.parameters.properties must be an object`);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return tool;
|
|
1115
|
+
});
|
|
1116
|
+
const promptKeys = new Set;
|
|
1117
|
+
for (const prompt of prompts) {
|
|
1118
|
+
if (prompt && nonEmptyString(prompt.key)) {
|
|
1119
|
+
if (promptKeys.has(prompt.key))
|
|
1120
|
+
errors.push(`duplicate prompt key "${prompt.key}"`);
|
|
1121
|
+
promptKeys.add(prompt.key);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const skillKeys = new Set;
|
|
1125
|
+
for (const skill of skills) {
|
|
1126
|
+
if (skill && nonEmptyString(skill.key)) {
|
|
1127
|
+
if (skillKeys.has(skill.key))
|
|
1128
|
+
errors.push(`duplicate skill key "${skill.key}"`);
|
|
1129
|
+
skillKeys.add(skill.key);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
const harness = validateHarnessSections(input, { prompts: promptKeys, tools: toolNames, skills: skillKeys }, errors, warnings);
|
|
1133
|
+
if (errors.length > 0) {
|
|
1134
|
+
throw new CliInputError(`Registry manifest is invalid:
|
|
1135
|
+
- ${errors.join(`
|
|
1136
|
+
- `)}`, "Fix the manifest and rerun `moda registry validate`. See `moda registry template` for the format.");
|
|
1137
|
+
}
|
|
1138
|
+
const resolved = {
|
|
1139
|
+
manifestPath: options.manifestPath,
|
|
1140
|
+
schema: typeof input.schema === "string" ? input.schema : harness ? REGISTRY_SCHEMA : REGISTRY_SCHEMA_V1,
|
|
1141
|
+
prompts: prompts.filter(isDefined),
|
|
1142
|
+
skills: skills.filter(isDefined),
|
|
1143
|
+
tools: tools.filter(isDefined),
|
|
1144
|
+
...harness ? { harness } : {},
|
|
1145
|
+
warnings
|
|
1146
|
+
};
|
|
1147
|
+
if (resolved.prompts.length + resolved.skills.length + resolved.tools.length === 0 && !harness) {
|
|
1148
|
+
throw new CliInputError("Registry manifest has no prompts, skills, tools, or agents to push.");
|
|
1149
|
+
}
|
|
1150
|
+
return resolved;
|
|
1151
|
+
}
|
|
1152
|
+
function arrayOf(value, label, errors) {
|
|
1153
|
+
if (value === undefined)
|
|
1154
|
+
return [];
|
|
1155
|
+
if (!Array.isArray(value)) {
|
|
1156
|
+
errors.push(`${label} must be an array`);
|
|
1157
|
+
return [];
|
|
1158
|
+
}
|
|
1159
|
+
if (value.length > MAX_ITEMS_PER_SECTION) {
|
|
1160
|
+
errors.push(`${label} has ${value.length} entries; the limit is ${MAX_ITEMS_PER_SECTION} per push`);
|
|
1161
|
+
}
|
|
1162
|
+
return value;
|
|
1163
|
+
}
|
|
1164
|
+
function countsOf(registry) {
|
|
1165
|
+
return {
|
|
1166
|
+
prompts: registry.prompts.length,
|
|
1167
|
+
skills: registry.skills.length,
|
|
1168
|
+
tools: registry.tools.length,
|
|
1169
|
+
...registry.harness ? {
|
|
1170
|
+
agents: registry.harness.agents?.length ?? 0,
|
|
1171
|
+
context: registry.harness.context?.length ?? 0,
|
|
1172
|
+
mcpServers: registry.harness.mcpServers?.length ?? 0
|
|
1173
|
+
} : {}
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
function displayPath(context, path) {
|
|
1177
|
+
const rel = relative(context.cwd, path);
|
|
1178
|
+
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel.split("\\").join("/") : path;
|
|
1179
|
+
}
|
|
1180
|
+
function isInside(root, target) {
|
|
1181
|
+
const rel = relative(resolve(root), target);
|
|
1182
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1183
|
+
}
|
|
1184
|
+
function isRecord2(value) {
|
|
1185
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1186
|
+
}
|
|
1187
|
+
function nonEmptyString(value) {
|
|
1188
|
+
return typeof value === "string" && value.trim() !== "";
|
|
1189
|
+
}
|
|
1190
|
+
function isDefined(value) {
|
|
1191
|
+
return value !== undefined;
|
|
1192
|
+
}
|
|
1193
|
+
export {
|
|
1194
|
+
runRegistryCommand,
|
|
1195
|
+
resolveRegistry,
|
|
1196
|
+
loadRegistry,
|
|
1197
|
+
REGISTRY_TEMPLATE,
|
|
1198
|
+
REGISTRY_SCHEMA_V1,
|
|
1199
|
+
REGISTRY_SCHEMA,
|
|
1200
|
+
DEFAULT_REGISTRY_PATH
|
|
1201
|
+
};
|