@moda-ai/cli 1.44.1 → 1.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -6
- package/dist/{cli-5tsqk3jt.js → cli-3xh2w7hb.js} +2 -2
- package/dist/{cli-8c60k2p4.js → cli-bvnw7585.js} +4 -1
- package/dist/{cli-rycpwqzm.js → cli-dy2aavbe.js} +1 -1
- package/dist/{cli-j2rdgd3p.js → cli-nnwegmzh.js} +4 -4
- package/dist/{cli-j2fapt3g.js → cli-phgzghwq.js} +2 -2
- package/dist/cli.js +756 -41
- package/dist/{harness-sqngqjrd.js → harness-9qpbj2qj.js} +2 -2
- package/dist/{harness-github-actions-pg0azf3h.js → harness-github-actions-ky4ym5n3.js} +2 -2
- package/dist/{index-nepr7cw7.js → index-bcn5chmf.js} +5 -5
- package/dist/prompt-source-mcp.js +2 -2
- package/dist/{provision-02wqtkvm.js → provision-mqw8s0jb.js} +3 -3
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
- package/skills/moda-cli/SKILL.md +92 -26
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
runPromptsCommand,
|
|
9
9
|
runSkillsCommand,
|
|
10
10
|
runStatusCommand
|
|
11
|
-
} from "./cli-
|
|
11
|
+
} from "./cli-nnwegmzh.js";
|
|
12
12
|
import {
|
|
13
13
|
ApiError,
|
|
14
14
|
HARNESS_REPORT_APPROVAL_PATH,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
terminalStyles,
|
|
38
38
|
unknownSignalsSubcommandError,
|
|
39
39
|
validateHarnessReport
|
|
40
|
-
} from "./cli-
|
|
40
|
+
} from "./cli-3xh2w7hb.js";
|
|
41
41
|
import"./cli-ssfq84k5.js";
|
|
42
42
|
import {
|
|
43
43
|
authFetch,
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
loadAuthSession,
|
|
47
47
|
login,
|
|
48
48
|
revokeCliSessionToken
|
|
49
|
-
} from "./cli-
|
|
49
|
+
} from "./cli-dy2aavbe.js";
|
|
50
50
|
import {
|
|
51
51
|
AGENT_ERROR_CODES,
|
|
52
52
|
CliAccessError,
|
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
describeBaseUrlSource,
|
|
73
73
|
embeddedTenantIdFromApiKey,
|
|
74
74
|
ensureSecretFilePermissions,
|
|
75
|
+
explicitOutputMode,
|
|
75
76
|
getActiveProfile,
|
|
76
77
|
getModaDir,
|
|
77
78
|
getModaPaths,
|
|
@@ -95,7 +96,7 @@ import {
|
|
|
95
96
|
validateConfig,
|
|
96
97
|
wasOutputCancelled,
|
|
97
98
|
writeSecretJsonFile
|
|
98
|
-
} from "./cli-
|
|
99
|
+
} from "./cli-bvnw7585.js";
|
|
99
100
|
import {
|
|
100
101
|
__require
|
|
101
102
|
} from "./cli-0v6na3yp.js";
|
|
@@ -2062,9 +2063,539 @@ async function runSyncCommand(context) {
|
|
|
2062
2063
|
}
|
|
2063
2064
|
|
|
2064
2065
|
// src/registry.ts
|
|
2066
|
+
import { execFileSync } from "node:child_process";
|
|
2065
2067
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2066
2068
|
import { dirname as dirname2, isAbsolute as isAbsolute2, relative, resolve as resolve2 } from "node:path";
|
|
2067
|
-
|
|
2069
|
+
|
|
2070
|
+
// src/registry-harness.ts
|
|
2071
|
+
var HARNESS_SECTIONS = ["runtime", "agents", "routing", "context", "mcpServers"];
|
|
2072
|
+
var ROUTING_STRATEGIES = ["single", "trace_attribute", "handoff", "router", "rules"];
|
|
2073
|
+
var HISTORY_STRATEGIES = ["full", "window", "summary", "truncate_tokens"];
|
|
2074
|
+
var SKILL_LOADING_MODES = ["tool", "system_prompt", "none"];
|
|
2075
|
+
var CONTEXT_SOURCES = ["static", "clock", "trace", "tool", "memory", "retrieval", "unknown"];
|
|
2076
|
+
var CONTEXT_TARGETS = ["prompt_variable", "system_append"];
|
|
2077
|
+
var CLOCK_FORMATS = ["date", "datetime", "iso"];
|
|
2078
|
+
var MCP_TRANSPORTS = ["stdio", "http", "sse", "streamable_http"];
|
|
2079
|
+
var AGENT_KEY_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
2080
|
+
var TOOL_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
2081
|
+
var VARIABLE_PATTERN = /^[A-Za-z_][A-Za-z0-9_.]{0,127}$/;
|
|
2082
|
+
var MAX_AGENTS = 100;
|
|
2083
|
+
var MAX_CONTEXT = 200;
|
|
2084
|
+
var MAX_MCP_SERVERS = 50;
|
|
2085
|
+
var MAX_HARNESS_DOCUMENT_BYTES = 1e6;
|
|
2086
|
+
var AGENT_FIELDS = {
|
|
2087
|
+
key: "string",
|
|
2088
|
+
name: "string",
|
|
2089
|
+
description: "string",
|
|
2090
|
+
entry: "boolean",
|
|
2091
|
+
prompt: "string",
|
|
2092
|
+
model: "object",
|
|
2093
|
+
tools: "string[]",
|
|
2094
|
+
skills: "string[]",
|
|
2095
|
+
handoffs: "object[]",
|
|
2096
|
+
subagents: "object[]",
|
|
2097
|
+
context: "string[]",
|
|
2098
|
+
maxToolRoundsPerTurn: "number",
|
|
2099
|
+
responseSchema: "object",
|
|
2100
|
+
match: "object"
|
|
2101
|
+
};
|
|
2102
|
+
var MODEL_FIELDS = {
|
|
2103
|
+
id: "string",
|
|
2104
|
+
provider: "string",
|
|
2105
|
+
temperature: "number",
|
|
2106
|
+
maxTokens: "number",
|
|
2107
|
+
topP: "number",
|
|
2108
|
+
reasoningEffort: "string"
|
|
2109
|
+
};
|
|
2110
|
+
var HANDOFF_FIELDS = { to: "string", tool: "string", description: "string" };
|
|
2111
|
+
var SUBAGENT_FIELDS = { agent: "string", tool: "string", description: "string" };
|
|
2112
|
+
var MATCH_FIELDS = { agentNames: "string[]", promptKeys: "string[]" };
|
|
2113
|
+
var ROUTING_FIELDS = { strategy: "string", default: "string", notes: "string" };
|
|
2114
|
+
var RUNTIME_FIELDS = {
|
|
2115
|
+
framework: "string",
|
|
2116
|
+
language: "string",
|
|
2117
|
+
maxToolRoundsPerTurn: "number",
|
|
2118
|
+
parallelToolCalls: "boolean",
|
|
2119
|
+
history: "object",
|
|
2120
|
+
skillLoading: "string",
|
|
2121
|
+
notes: "string"
|
|
2122
|
+
};
|
|
2123
|
+
var HISTORY_FIELDS = { strategy: "string", maxMessages: "number", maxTokens: "number" };
|
|
2124
|
+
var CONTEXT_FIELDS = {
|
|
2125
|
+
key: "string",
|
|
2126
|
+
variable: "string",
|
|
2127
|
+
source: "string",
|
|
2128
|
+
value: "string",
|
|
2129
|
+
format: "string",
|
|
2130
|
+
target: "string",
|
|
2131
|
+
description: "string"
|
|
2132
|
+
};
|
|
2133
|
+
var MCP_FIELDS = {
|
|
2134
|
+
name: "string",
|
|
2135
|
+
transport: "string",
|
|
2136
|
+
url: "string",
|
|
2137
|
+
command: "string",
|
|
2138
|
+
tools: "string[]",
|
|
2139
|
+
description: "string"
|
|
2140
|
+
};
|
|
2141
|
+
function handoffToolName(handoff) {
|
|
2142
|
+
return handoff.tool?.trim() || `transfer_to_${handoff.to.replace(/[^A-Za-z0-9_-]/g, "_")}`.slice(0, 64);
|
|
2143
|
+
}
|
|
2144
|
+
function hasHarnessSections(input) {
|
|
2145
|
+
return HARNESS_SECTIONS.some((section) => input[section] !== undefined);
|
|
2146
|
+
}
|
|
2147
|
+
function validateHarnessSections(input, known, errors, warnings) {
|
|
2148
|
+
if (!hasHarnessSections(input))
|
|
2149
|
+
return;
|
|
2150
|
+
const document = {};
|
|
2151
|
+
if (input.runtime !== undefined) {
|
|
2152
|
+
if (checkObject("runtime", input.runtime, RUNTIME_FIELDS, errors)) {
|
|
2153
|
+
const runtime = input.runtime;
|
|
2154
|
+
checkPositiveInt("runtime.maxToolRoundsPerTurn", runtime.maxToolRoundsPerTurn, errors);
|
|
2155
|
+
checkEnum("runtime.skillLoading", runtime.skillLoading, SKILL_LOADING_MODES, errors);
|
|
2156
|
+
if (runtime.history !== undefined && checkObject("runtime.history", runtime.history, HISTORY_FIELDS, errors)) {
|
|
2157
|
+
checkEnum("runtime.history.strategy", runtime.history.strategy, HISTORY_STRATEGIES, errors);
|
|
2158
|
+
checkPositiveInt("runtime.history.maxMessages", runtime.history.maxMessages, errors);
|
|
2159
|
+
checkPositiveInt("runtime.history.maxTokens", runtime.history.maxTokens, errors);
|
|
2160
|
+
}
|
|
2161
|
+
document.runtime = runtime;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
const contextKeys = new Set;
|
|
2165
|
+
if (input.context !== undefined) {
|
|
2166
|
+
const entries = listOf("context", input.context, MAX_CONTEXT, errors);
|
|
2167
|
+
document.context = [];
|
|
2168
|
+
entries.forEach((raw, index) => {
|
|
2169
|
+
const label = `context[${index}]`;
|
|
2170
|
+
if (!checkObject(label, raw, CONTEXT_FIELDS, errors))
|
|
2171
|
+
return;
|
|
2172
|
+
const entry = raw;
|
|
2173
|
+
if (!nonEmpty(entry.key))
|
|
2174
|
+
errors.push(`${label}.key is required`);
|
|
2175
|
+
else if (contextKeys.has(entry.key))
|
|
2176
|
+
errors.push(`${label}: duplicate context key "${entry.key}"`);
|
|
2177
|
+
else
|
|
2178
|
+
contextKeys.add(entry.key);
|
|
2179
|
+
if (!nonEmpty(entry.source))
|
|
2180
|
+
errors.push(`${label}.source is required (one of ${CONTEXT_SOURCES.join(", ")})`);
|
|
2181
|
+
else
|
|
2182
|
+
checkEnum(`${label}.source`, entry.source, CONTEXT_SOURCES, errors);
|
|
2183
|
+
checkEnum(`${label}.target`, entry.target, CONTEXT_TARGETS, errors);
|
|
2184
|
+
checkEnum(`${label}.format`, entry.format, CLOCK_FORMATS, errors);
|
|
2185
|
+
const target = entry.target ?? "prompt_variable";
|
|
2186
|
+
if (target === "prompt_variable") {
|
|
2187
|
+
if (!nonEmpty(entry.variable))
|
|
2188
|
+
errors.push(`${label}.variable is required for target "prompt_variable"`);
|
|
2189
|
+
else if (!VARIABLE_PATTERN.test(entry.variable))
|
|
2190
|
+
errors.push(`${label}.variable "${entry.variable}" is not a valid template variable name`);
|
|
2191
|
+
}
|
|
2192
|
+
if (entry.source === "static" && typeof entry.value !== "string")
|
|
2193
|
+
errors.push(`${label}.value is required for source "static"`);
|
|
2194
|
+
if (entry.source !== "static" && entry.value !== undefined) {
|
|
2195
|
+
errors.push(`${label}.value is only allowed for source "static" (record runtime values in traces instead)`);
|
|
2196
|
+
}
|
|
2197
|
+
document.context.push(entry);
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
const agentKeys = new Set;
|
|
2201
|
+
const agents = input.agents === undefined ? [] : listOf("agents", input.agents, MAX_AGENTS, errors);
|
|
2202
|
+
for (const raw of agents) {
|
|
2203
|
+
if (isRecord(raw) && nonEmpty(raw.key) && AGENT_KEY_PATTERN.test(raw.key))
|
|
2204
|
+
agentKeys.add(raw.key);
|
|
2205
|
+
}
|
|
2206
|
+
if (input.agents !== undefined) {
|
|
2207
|
+
document.agents = [];
|
|
2208
|
+
const seen = new Set;
|
|
2209
|
+
let entries = 0;
|
|
2210
|
+
agents.forEach((raw, index) => {
|
|
2211
|
+
const label = `agents[${index}]`;
|
|
2212
|
+
if (!checkObject(label, raw, AGENT_FIELDS, errors))
|
|
2213
|
+
return;
|
|
2214
|
+
const agent = raw;
|
|
2215
|
+
if (!nonEmpty(agent.key))
|
|
2216
|
+
errors.push(`${label}.key is required`);
|
|
2217
|
+
else if (!AGENT_KEY_PATTERN.test(agent.key))
|
|
2218
|
+
errors.push(`${label}.key "${agent.key}" may only contain letters, digits, ".", "_" and "-" (max 128)`);
|
|
2219
|
+
else if (seen.has(agent.key))
|
|
2220
|
+
errors.push(`${label}: duplicate agent key "${agent.key}"`);
|
|
2221
|
+
else
|
|
2222
|
+
seen.add(agent.key);
|
|
2223
|
+
if (agent.entry === true)
|
|
2224
|
+
entries += 1;
|
|
2225
|
+
if (agent.model !== undefined && checkObject(`${label}.model`, agent.model, MODEL_FIELDS, errors)) {
|
|
2226
|
+
const model = agent.model;
|
|
2227
|
+
if (typeof model.temperature === "number" && (model.temperature < 0 || model.temperature > 2)) {
|
|
2228
|
+
errors.push(`${label}.model.temperature must be between 0 and 2`);
|
|
2229
|
+
}
|
|
2230
|
+
if (typeof model.topP === "number" && (model.topP <= 0 || model.topP > 1))
|
|
2231
|
+
errors.push(`${label}.model.topP must be in (0, 1]`);
|
|
2232
|
+
checkPositiveInt(`${label}.model.maxTokens`, model.maxTokens, errors);
|
|
2233
|
+
}
|
|
2234
|
+
checkPositiveInt(`${label}.maxToolRoundsPerTurn`, agent.maxToolRoundsPerTurn, errors);
|
|
2235
|
+
if (agent.match !== undefined)
|
|
2236
|
+
checkObject(`${label}.match`, agent.match, MATCH_FIELDS, errors);
|
|
2237
|
+
const handoffTools = new Set;
|
|
2238
|
+
(Array.isArray(agent.handoffs) ? agent.handoffs : []).forEach((handoff, hIndex) => {
|
|
2239
|
+
const hLabel = `${label}.handoffs[${hIndex}]`;
|
|
2240
|
+
if (!checkObject(hLabel, handoff, HANDOFF_FIELDS, errors))
|
|
2241
|
+
return;
|
|
2242
|
+
if (!nonEmpty(handoff.to)) {
|
|
2243
|
+
errors.push(`${hLabel}.to is required`);
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
if (!agentKeys.has(handoff.to))
|
|
2247
|
+
errors.push(`${hLabel}.to "${handoff.to}" is not an agent key in this manifest`);
|
|
2248
|
+
if (handoff.to === agent.key)
|
|
2249
|
+
errors.push(`${hLabel}: an agent cannot hand off to itself`);
|
|
2250
|
+
const tool = handoffToolName(handoff);
|
|
2251
|
+
if (!TOOL_NAME_PATTERN.test(tool))
|
|
2252
|
+
errors.push(`${hLabel}.tool "${tool}" must match ${TOOL_NAME_PATTERN.source}`);
|
|
2253
|
+
else if (handoffTools.has(tool))
|
|
2254
|
+
errors.push(`${hLabel}: duplicate handoff tool "${tool}"`);
|
|
2255
|
+
else if ((agent.tools ?? []).includes(tool))
|
|
2256
|
+
errors.push(`${hLabel}: handoff tool "${tool}" collides with a tool in ${label}.tools`);
|
|
2257
|
+
handoffTools.add(tool);
|
|
2258
|
+
});
|
|
2259
|
+
(Array.isArray(agent.subagents) ? agent.subagents : []).forEach((subagent, sIndex) => {
|
|
2260
|
+
const sLabel = `${label}.subagents[${sIndex}]`;
|
|
2261
|
+
if (!checkObject(sLabel, subagent, SUBAGENT_FIELDS, errors))
|
|
2262
|
+
return;
|
|
2263
|
+
if (!nonEmpty(subagent.agent))
|
|
2264
|
+
errors.push(`${sLabel}.agent is required`);
|
|
2265
|
+
else if (!agentKeys.has(subagent.agent))
|
|
2266
|
+
errors.push(`${sLabel}.agent "${subagent.agent}" is not an agent key in this manifest`);
|
|
2267
|
+
if (!nonEmpty(subagent.tool))
|
|
2268
|
+
errors.push(`${sLabel}.tool is required (the tool name the parent calls)`);
|
|
2269
|
+
else if (!(agent.tools ?? []).includes(subagent.tool)) {
|
|
2270
|
+
errors.push(`${sLabel}.tool "${subagent.tool}" must also be listed in ${label}.tools (with its schema in tools[])`);
|
|
2271
|
+
}
|
|
2272
|
+
});
|
|
2273
|
+
for (const key of agent.context ?? []) {
|
|
2274
|
+
if (!contextKeys.has(key))
|
|
2275
|
+
errors.push(`${label}.context "${key}" is not a key in context[]`);
|
|
2276
|
+
}
|
|
2277
|
+
if (nonEmpty(agent.prompt) && !known.prompts.has(agent.prompt)) {
|
|
2278
|
+
warnings.push(`${label}.prompt "${agent.prompt}" is not in prompts[]; it must already exist in Moda`);
|
|
2279
|
+
}
|
|
2280
|
+
for (const tool of agent.tools ?? []) {
|
|
2281
|
+
if (!known.tools.has(tool))
|
|
2282
|
+
warnings.push(`${label}.tools "${tool}" is not in tools[]; it must already exist in Moda`);
|
|
2283
|
+
}
|
|
2284
|
+
for (const skill of agent.skills ?? []) {
|
|
2285
|
+
if (!known.skills.has(skill))
|
|
2286
|
+
warnings.push(`${label}.skills "${skill}" is not in skills[]; it must already exist in Moda`);
|
|
2287
|
+
}
|
|
2288
|
+
document.agents.push(agent);
|
|
2289
|
+
});
|
|
2290
|
+
if (entries > 1)
|
|
2291
|
+
errors.push('agents: at most one agent may set "entry": true');
|
|
2292
|
+
}
|
|
2293
|
+
if (input.routing !== undefined) {
|
|
2294
|
+
if (checkObject("routing", input.routing, ROUTING_FIELDS, errors)) {
|
|
2295
|
+
const routing = input.routing;
|
|
2296
|
+
checkEnum("routing.strategy", routing.strategy, ROUTING_STRATEGIES, errors);
|
|
2297
|
+
if (routing.default !== undefined && !agentKeys.has(routing.default)) {
|
|
2298
|
+
errors.push(`routing.default "${routing.default}" is not an agent key in agents[]`);
|
|
2299
|
+
}
|
|
2300
|
+
document.routing = routing;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
if (input.mcpServers !== undefined) {
|
|
2304
|
+
document.mcpServers = [];
|
|
2305
|
+
const names = new Set;
|
|
2306
|
+
listOf("mcpServers", input.mcpServers, MAX_MCP_SERVERS, errors).forEach((raw, index) => {
|
|
2307
|
+
const label = `mcpServers[${index}]`;
|
|
2308
|
+
if (!checkObject(label, raw, MCP_FIELDS, errors))
|
|
2309
|
+
return;
|
|
2310
|
+
const server = raw;
|
|
2311
|
+
if (!nonEmpty(server.name))
|
|
2312
|
+
errors.push(`${label}.name is required`);
|
|
2313
|
+
else if (names.has(server.name))
|
|
2314
|
+
errors.push(`${label}: duplicate MCP server name "${server.name}"`);
|
|
2315
|
+
else
|
|
2316
|
+
names.add(server.name);
|
|
2317
|
+
checkEnum(`${label}.transport`, server.transport, MCP_TRANSPORTS, errors);
|
|
2318
|
+
if (server.url !== undefined && hasUrlCredentials(server.url)) {
|
|
2319
|
+
errors.push(`${label}.url must not contain credentials (user info or token query parameters)`);
|
|
2320
|
+
}
|
|
2321
|
+
document.mcpServers.push(server);
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
if (Buffer.byteLength(JSON.stringify(document), "utf8") > MAX_HARNESS_DOCUMENT_BYTES) {
|
|
2325
|
+
errors.push(`harness sections exceed ${MAX_HARNESS_DOCUMENT_BYTES} bytes`);
|
|
2326
|
+
}
|
|
2327
|
+
return document;
|
|
2328
|
+
}
|
|
2329
|
+
function hasUrlCredentials(raw) {
|
|
2330
|
+
try {
|
|
2331
|
+
const url = new URL(raw);
|
|
2332
|
+
if (url.username || url.password)
|
|
2333
|
+
return true;
|
|
2334
|
+
return [...url.searchParams.keys()].some((key) => /token|key|secret|password|auth/i.test(key));
|
|
2335
|
+
} catch {
|
|
2336
|
+
return false;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
function listOf(label, value, max, errors) {
|
|
2340
|
+
if (!Array.isArray(value)) {
|
|
2341
|
+
errors.push(`${label} must be an array`);
|
|
2342
|
+
return [];
|
|
2343
|
+
}
|
|
2344
|
+
if (value.length > max)
|
|
2345
|
+
errors.push(`${label} has ${value.length} entries; the limit is ${max}`);
|
|
2346
|
+
return value;
|
|
2347
|
+
}
|
|
2348
|
+
function checkObject(label, value, spec, errors) {
|
|
2349
|
+
if (!isRecord(value)) {
|
|
2350
|
+
errors.push(`${label} must be an object`);
|
|
2351
|
+
return false;
|
|
2352
|
+
}
|
|
2353
|
+
let typed = true;
|
|
2354
|
+
for (const [key, item] of Object.entries(value)) {
|
|
2355
|
+
const expected = spec[key];
|
|
2356
|
+
if (!expected) {
|
|
2357
|
+
errors.push(`${label}: unknown field "${key}" (allowed: ${Object.keys(spec).join(", ")})`);
|
|
2358
|
+
continue;
|
|
2359
|
+
}
|
|
2360
|
+
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;
|
|
2361
|
+
if (!ok) {
|
|
2362
|
+
errors.push(`${label}.${key} must be ${describe(expected)}`);
|
|
2363
|
+
typed = false;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
return typed;
|
|
2367
|
+
}
|
|
2368
|
+
function describe(type) {
|
|
2369
|
+
if (type === "string[]")
|
|
2370
|
+
return "an array of strings";
|
|
2371
|
+
if (type === "object[]")
|
|
2372
|
+
return "an array of objects";
|
|
2373
|
+
return type === "object" ? "an object" : `a ${type}`;
|
|
2374
|
+
}
|
|
2375
|
+
function checkEnum(label, value, allowed, errors) {
|
|
2376
|
+
if (value === undefined || typeof value !== "string")
|
|
2377
|
+
return;
|
|
2378
|
+
if (!allowed.includes(value))
|
|
2379
|
+
errors.push(`${label} must be one of ${allowed.join(", ")} (got "${value}")`);
|
|
2380
|
+
}
|
|
2381
|
+
function checkPositiveInt(label, value, errors) {
|
|
2382
|
+
if (value === undefined || typeof value !== "number")
|
|
2383
|
+
return;
|
|
2384
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
2385
|
+
errors.push(`${label} must be a positive integer`);
|
|
2386
|
+
}
|
|
2387
|
+
function isRecord(value) {
|
|
2388
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2389
|
+
}
|
|
2390
|
+
function nonEmpty(value) {
|
|
2391
|
+
return typeof value === "string" && value.trim() !== "";
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
// src/registry-prompt.ts
|
|
2395
|
+
var REGISTRY_AGENT_BRIEF = `# Register this repo's AI harness with Moda for faithful replays
|
|
2396
|
+
|
|
2397
|
+
You are working in the repository that runs our production AI agent. Moda
|
|
2398
|
+
replays recorded production conversations against our agent to test prompt,
|
|
2399
|
+
tool, and skill changes. A replay is only as faithful as what Moda knows about
|
|
2400
|
+
how our agent actually runs. Your job: describe our harness to Moda exactly,
|
|
2401
|
+
push it with the \`moda\` CLI, and make sure our production traces say which
|
|
2402
|
+
agent and prompt handled each LLM call.
|
|
2403
|
+
|
|
2404
|
+
## Ground rules
|
|
2405
|
+
|
|
2406
|
+
- Read the code; do not guess. Every value you register must come from the
|
|
2407
|
+
repo (cite the file in \`sourcePath\` or in a note). If something is decided
|
|
2408
|
+
at runtime and you can't find it, say so in your final report.
|
|
2409
|
+
- Copy prompt text, tool names, tool descriptions, and parameter schemas
|
|
2410
|
+
verbatim. For templated prompts, keep the template with its placeholders
|
|
2411
|
+
(\`{{name}}\`, \`{name}\`, \`\${name}\`). Never register a rendered example.
|
|
2412
|
+
- Never put secrets in the manifest: no API keys, tokens, auth headers, env var
|
|
2413
|
+
values, or customer data.
|
|
2414
|
+
- Do not change runtime behaviour. Part 5 adds trace metadata only. Show the
|
|
2415
|
+
diff and get my OK before committing it.
|
|
2416
|
+
- Only \`.moda/registry.json\` (plus the files it references) is uploaded.
|
|
2417
|
+
Nothing else in the repo leaves this machine.
|
|
2418
|
+
|
|
2419
|
+
## Part 0: setup
|
|
2420
|
+
|
|
2421
|
+
1. Run \`moda --version\`. You need 1.44.0 or newer. If it's older, run
|
|
2422
|
+
\`npm install -g @moda-ai/cli@latest\`.
|
|
2423
|
+
2. Auth: \`MODA_API_KEY\` must be set, or run \`moda auth login --api-key=<key>\`.
|
|
2424
|
+
Ask me for the key if neither is available. Check with \`moda doctor\`.
|
|
2425
|
+
3. Run \`moda registry template\` to see a complete example manifest, and
|
|
2426
|
+
\`moda registry status\` to see what is already registered.
|
|
2427
|
+
|
|
2428
|
+
## Part 1: inventory the harness
|
|
2429
|
+
|
|
2430
|
+
Find every place the code calls an LLM: direct SDK calls (OpenAI, Anthropic,
|
|
2431
|
+
Gemini, Bedrock, OpenRouter), framework agents, and anything else. For each
|
|
2432
|
+
call site, write down:
|
|
2433
|
+
|
|
2434
|
+
- **Agent**: the logical agent it belongs to, with a stable key such as
|
|
2435
|
+
\`triage\` or \`billing\`. Different system prompts, tool sets, or models mean
|
|
2436
|
+
different agents. A one-off call (summariser, classifier, title generator)
|
|
2437
|
+
is its own agent if it runs during a conversation.
|
|
2438
|
+
- **Prompt**: the system prompt or messages it sends, how they're built
|
|
2439
|
+
(static text, template plus variables, or code that assembles them), and
|
|
2440
|
+
the file.
|
|
2441
|
+
- **Model and settings**: model id, provider, temperature, max tokens, top_p,
|
|
2442
|
+
and reasoning effort, wherever they're set (code, config, env defaults).
|
|
2443
|
+
- **Tools**: every tool the model can call at that site, with the exact JSON
|
|
2444
|
+
schema the model sees. Include MCP tools and agents exposed as tools.
|
|
2445
|
+
- **Skills**: SKILL.md files or instruction docs the agent loads, and HOW it
|
|
2446
|
+
loads them (a tool such as skill_read or read_file, inlined into the system
|
|
2447
|
+
prompt, or not at all).
|
|
2448
|
+
- **Loop limits**: max tool rounds or steps per turn, parallel tool calls, and
|
|
2449
|
+
how history is truncated or summarised.
|
|
2450
|
+
|
|
2451
|
+
Framework hints:
|
|
2452
|
+
|
|
2453
|
+
- **OpenAI Agents SDK**: \`Agent(name, instructions, model, model_settings,
|
|
2454
|
+
tools, handoffs)\`. \`name\` is the trace agent name. Handoffs become
|
|
2455
|
+
\`transfer_to_<agent>\` tools.
|
|
2456
|
+
- **Claude Agent SDK / Claude Code SDK**: \`ClaudeAgentOptions(system_prompt,
|
|
2457
|
+
model, allowed_tools, mcp_servers, agents=...)\`. Subagents are \`agents\`,
|
|
2458
|
+
and skills live in \`.claude/skills/*/SKILL.md\`.
|
|
2459
|
+
- **LangGraph / LangChain**: each node that binds an LLM (\`bind_tools\`,
|
|
2460
|
+
\`create_react_agent\`) is an agent. Conditional edges are the routing.
|
|
2461
|
+
- **Vercel AI SDK**: \`generateText\` / \`streamText({ system, tools, model,
|
|
2462
|
+
maxSteps | stopWhen })\`.
|
|
2463
|
+
- **Raw SDK calls**: the system message, \`tools=\`, and \`model=\` arguments.
|
|
2464
|
+
|
|
2465
|
+
## Part 2: work out prompt switching
|
|
2466
|
+
|
|
2467
|
+
Work out how the harness decides which agent and prompt answers a given turn.
|
|
2468
|
+
That becomes \`routing\` and the agents' \`handoffs\`:
|
|
2469
|
+
|
|
2470
|
+
- **single**: one agent always answers. Prompt versions still change over
|
|
2471
|
+
time; Part 5 handles that.
|
|
2472
|
+
- **handoff**: the model transfers control by calling a tool (OpenAI Agents
|
|
2473
|
+
handoffs, a "transfer_to_*" function, or a supervisor delegating). Add
|
|
2474
|
+
\`handoffs: [{ "to": "<agent key>", "tool": "<exact tool name>",
|
|
2475
|
+
"description": "<when>" }]\` to the source agent.
|
|
2476
|
+
- **router / rules**: code picks the agent (by channel, intent classifier,
|
|
2477
|
+
conversation stage, user plan, or feature flag). Describe the rule in
|
|
2478
|
+
\`routing.notes\` and make sure Part 5's attribution names the chosen agent
|
|
2479
|
+
on every call.
|
|
2480
|
+
- **trace_attribute**: the agent is already identified on every trace.
|
|
2481
|
+
- **subagents** (agents as tools): the parent calls a tool that runs another
|
|
2482
|
+
agent and returns its result. Register that tool's schema in \`tools[]\`,
|
|
2483
|
+
list it in the parent's \`tools\`, and add \`subagents: [{ "agent": "<child
|
|
2484
|
+
key>", "tool": "<tool name>" }]\`.
|
|
2485
|
+
|
|
2486
|
+
Set \`routing.default\` to the agent that answers when nothing else decides,
|
|
2487
|
+
and mark the first agent a conversation reaches with \`"entry": true\`.
|
|
2488
|
+
|
|
2489
|
+
## Part 3: list the injected context
|
|
2490
|
+
|
|
2491
|
+
For every placeholder in every prompt, and anything appended to the prompt at
|
|
2492
|
+
runtime, add a \`context[]\` entry:
|
|
2493
|
+
|
|
2494
|
+
- \`source: "static"\`: the same value every time (put it in \`value\`).
|
|
2495
|
+
- \`source: "clock"\`: the current date or time (\`format\`: date, datetime, or
|
|
2496
|
+
iso). Replay uses the original conversation's time.
|
|
2497
|
+
- \`source: "trace"\`: a per-user or per-conversation value, such as the
|
|
2498
|
+
profile, account, plan, or locale.
|
|
2499
|
+
- \`source: "memory" | "retrieval" | "tool"\`: memory recall, RAG results, or a
|
|
2500
|
+
tool fetched before the call.
|
|
2501
|
+
- \`target: "prompt_variable"\` (with \`variable\` set to the placeholder name,
|
|
2502
|
+
without braces) for template variables, or \`target: "system_append"\` for
|
|
2503
|
+
blocks the code appends.
|
|
2504
|
+
|
|
2505
|
+
List context keys on an agent with \`context: [...]\` when only some agents get
|
|
2506
|
+
them.
|
|
2507
|
+
|
|
2508
|
+
## Part 4: write, validate, and push the manifest
|
|
2509
|
+
|
|
2510
|
+
Write \`.moda/registry.json\` with \`"schema": "moda_registry.v2"\`:
|
|
2511
|
+
|
|
2512
|
+
- \`prompts[]\`: \`{ key, file | content | systemPrompt | messages, name,
|
|
2513
|
+
description, variables, modelConfig, responseSchema, sourcePath }\`. If the
|
|
2514
|
+
repo already uses \`moda prompts\` (it has a \`.moda/prompts.lock.json\`),
|
|
2515
|
+
reuse those keys.
|
|
2516
|
+
- \`tools[]\`: \`{ schema: { name, description, parameters } }\`, exactly as
|
|
2517
|
+
sent to the model.
|
|
2518
|
+
- \`skills[]\`: \`{ key, file: "path/to/SKILL.md" }\`. Set \`"live": true\` only
|
|
2519
|
+
if I ask.
|
|
2520
|
+
- \`agents[]\`: \`{ key, name, entry, prompt, model: { id, provider,
|
|
2521
|
+
temperature, maxTokens, topP, reasoningEffort }, tools, skills, handoffs,
|
|
2522
|
+
subagents, context, maxToolRoundsPerTurn, responseSchema, match: {
|
|
2523
|
+
agentNames, promptKeys } }\`. \`match.agentNames\` are the names this agent
|
|
2524
|
+
carries in traces; \`match.promptKeys\` are prompt keys that identify it.
|
|
2525
|
+
- \`routing\`: \`{ strategy, default, notes }\`.
|
|
2526
|
+
- \`runtime\`: \`{ framework, language, maxToolRoundsPerTurn,
|
|
2527
|
+
parallelToolCalls, history: { strategy, maxMessages, maxTokens },
|
|
2528
|
+
skillLoading: "tool" | "system_prompt" | "none" }\`.
|
|
2529
|
+
- \`context[]\`: from Part 3.
|
|
2530
|
+
- \`mcpServers[]\`: \`{ name, transport, url | command, tools, description }\`,
|
|
2531
|
+
with no credentials. Their tools still go in \`tools[]\`.
|
|
2532
|
+
|
|
2533
|
+
Then run:
|
|
2534
|
+
|
|
2535
|
+
moda registry validate # local; fix every error and read every warning
|
|
2536
|
+
moda registry push --dry-run # server-side preview, writes nothing
|
|
2537
|
+
moda registry push --message="<what you registered or changed>"
|
|
2538
|
+
|
|
2539
|
+
Re-pushing an unchanged manifest is a no-op. A changed prompt adds a new
|
|
2540
|
+
version, and a changed harness adds a new harness version.
|
|
2541
|
+
|
|
2542
|
+
## Part 5: make traces name the agent and prompt version
|
|
2543
|
+
|
|
2544
|
+
Replay picks the agent and prompt version that handled the replayed turn from
|
|
2545
|
+
attributes on each LLM call. Auto-instrumented calls don't carry them, so add
|
|
2546
|
+
them at each call site from Part 1:
|
|
2547
|
+
|
|
2548
|
+
| What | OTel span attribute | Vercel AI SDK \`experimental_telemetry.metadata\` | \`/v1/ingest\` event field |
|
|
2549
|
+
|---|---|---|---|
|
|
2550
|
+
| Agent | \`gen_ai.agent.name\` (or \`moda.agent_name\`) | \`moda.agent_name\` | \`agent_name\` |
|
|
2551
|
+
| Prompt key | \`moda.prompt_key\` | \`moda.prompt_key\` | \`prompt_name\` |
|
|
2552
|
+
| Prompt version | \`moda.prompt_version_id\` | \`moda.prompt_version_id\` | \`prompt_version_id\` |
|
|
2553
|
+
|
|
2554
|
+
- The agent value must be the agent's \`key\` or one of its
|
|
2555
|
+
\`match.agentNames\`. Frameworks that already set \`gen_ai.agent.name\`
|
|
2556
|
+
(OpenAI Agents SDK, many OpenInference/OpenLLMetry instrumentations) only
|
|
2557
|
+
need \`match.agentNames\` to list those names.
|
|
2558
|
+
- Get the prompt version id from \`.moda/prompts.lock.json\` (written by
|
|
2559
|
+
\`moda prompts sync\`), or leave it out. Replay then uses the prompt's prod
|
|
2560
|
+
version.
|
|
2561
|
+
- Node with the Moda SDK: \`Moda.withLLMCall(..., ({ span }) =>
|
|
2562
|
+
span.rawSpan.setAttribute(...))\`, or pass Vercel metadata through
|
|
2563
|
+
\`Moda.getVercelAITelemetry({ metadata })\`.
|
|
2564
|
+
- Python: set the attributes in your OpenTelemetry instrumentation on the LLM
|
|
2565
|
+
span, or send \`agent_name\` / \`prompt_*\` on \`/v1/ingest\` events.
|
|
2566
|
+
- Show me the diff before committing it.
|
|
2567
|
+
|
|
2568
|
+
## Part 6: verify
|
|
2569
|
+
|
|
2570
|
+
moda registry status
|
|
2571
|
+
moda registry coverage --days=7
|
|
2572
|
+
moda registry log # each push is a commit; review what changed
|
|
2573
|
+
|
|
2574
|
+
Coverage compares the last N days of production traces with what you
|
|
2575
|
+
registered: the share of LLM calls naming an agent or prompt, agent names and
|
|
2576
|
+
prompt keys that match no registered agent, tools used in production but not
|
|
2577
|
+
registered, and registered agents never seen. Fix the manifest (or the
|
|
2578
|
+
attribution) and push again until there is nothing unexplained left. Attribution
|
|
2579
|
+
added in Part 5 only shows up on traces created after it ships.
|
|
2580
|
+
|
|
2581
|
+
## Final report
|
|
2582
|
+
|
|
2583
|
+
Reply with:
|
|
2584
|
+
|
|
2585
|
+
1. The agents you registered: key, prompt, model, tool count, skill count, and
|
|
2586
|
+
handoffs.
|
|
2587
|
+
2. The routing strategy, and how you verified it.
|
|
2588
|
+
3. Context entries whose values replay can't reproduce (\`trace\`, \`memory\`,
|
|
2589
|
+
\`retrieval\`, \`tool\`).
|
|
2590
|
+
4. The attribution diff (or that it's pending my OK).
|
|
2591
|
+
5. The \`moda registry coverage\` result, plus anything unexplained you
|
|
2592
|
+
couldn't resolve.
|
|
2593
|
+
6. Anything you found but couldn't express in the manifest.
|
|
2594
|
+
`;
|
|
2595
|
+
|
|
2596
|
+
// src/registry.ts
|
|
2597
|
+
var REGISTRY_SCHEMA = "moda_registry.v2";
|
|
2598
|
+
var REGISTRY_SCHEMA_V1 = "moda_registry.v1";
|
|
2068
2599
|
var DEFAULT_REGISTRY_PATH = ".moda/registry.json";
|
|
2069
2600
|
var SKILL_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
2070
2601
|
var MAX_ITEMS_PER_SECTION = 500;
|
|
@@ -2075,7 +2606,7 @@ var REGISTRY_TEMPLATE = {
|
|
|
2075
2606
|
key: "support.triage",
|
|
2076
2607
|
name: "Support triage system prompt",
|
|
2077
2608
|
description: "Routes inbound support messages",
|
|
2078
|
-
content: "You are a support triage agent. Classify the request and ...",
|
|
2609
|
+
content: "You are a support triage agent for {{company_name}}. Today is {{current_date}}. Classify the request and ...",
|
|
2079
2610
|
sourcePath: "src/agents/triage/prompt.ts"
|
|
2080
2611
|
},
|
|
2081
2612
|
{
|
|
@@ -2108,12 +2639,49 @@ description: How to process refund requests
|
|
|
2108
2639
|
}
|
|
2109
2640
|
}
|
|
2110
2641
|
}
|
|
2642
|
+
],
|
|
2643
|
+
runtime: {
|
|
2644
|
+
framework: "openai-agents",
|
|
2645
|
+
language: "python",
|
|
2646
|
+
maxToolRoundsPerTurn: 6,
|
|
2647
|
+
history: { strategy: "window", maxMessages: 40 },
|
|
2648
|
+
skillLoading: "tool"
|
|
2649
|
+
},
|
|
2650
|
+
agents: [
|
|
2651
|
+
{
|
|
2652
|
+
key: "triage",
|
|
2653
|
+
name: "Triage",
|
|
2654
|
+
entry: true,
|
|
2655
|
+
prompt: "support.triage",
|
|
2656
|
+
model: { id: "openai/gpt-5.1", temperature: 0.2, maxTokens: 1024 },
|
|
2657
|
+
tools: ["lookup_customer"],
|
|
2658
|
+
handoffs: [{ to: "reply", description: "Hand off once the request is classified" }],
|
|
2659
|
+
match: { agentNames: ["TriageAgent"] }
|
|
2660
|
+
},
|
|
2661
|
+
{
|
|
2662
|
+
key: "reply",
|
|
2663
|
+
name: "Reply writer",
|
|
2664
|
+
prompt: "support.reply",
|
|
2665
|
+
model: { id: "anthropic/claude-sonnet-5-5", temperature: 0.4, maxTokens: 2048 },
|
|
2666
|
+
skills: ["refund-policy"],
|
|
2667
|
+
match: { agentNames: ["ReplyAgent"] }
|
|
2668
|
+
}
|
|
2669
|
+
],
|
|
2670
|
+
routing: { strategy: "handoff", default: "triage" },
|
|
2671
|
+
context: [
|
|
2672
|
+
{ key: "today", variable: "current_date", source: "clock", format: "date" },
|
|
2673
|
+
{ key: "company", variable: "company_name", source: "static", value: "Acme" }
|
|
2111
2674
|
]
|
|
2112
2675
|
};
|
|
2113
2676
|
var REGISTRY_SUBCOMMAND_FLAGS = {
|
|
2114
2677
|
template: ["out", "dry-run"],
|
|
2115
2678
|
validate: ["file", "manifest"],
|
|
2116
|
-
push: ["file", "manifest", "live", "dry-run"]
|
|
2679
|
+
push: ["file", "manifest", "live", "dry-run", "message"],
|
|
2680
|
+
log: ["limit"],
|
|
2681
|
+
show: [],
|
|
2682
|
+
status: [],
|
|
2683
|
+
coverage: ["days"],
|
|
2684
|
+
prompt: []
|
|
2117
2685
|
};
|
|
2118
2686
|
async function runRegistryCommand(context) {
|
|
2119
2687
|
const subcommand = context.positionals[0] ?? "push";
|
|
@@ -2123,18 +2691,52 @@ async function runRegistryCommand(context) {
|
|
|
2123
2691
|
return writeTemplate(context);
|
|
2124
2692
|
case "validate": {
|
|
2125
2693
|
const registry = loadRegistry(context);
|
|
2694
|
+
printWarnings(context, registry.warnings);
|
|
2126
2695
|
context.output.writeData({
|
|
2127
|
-
schema:
|
|
2696
|
+
schema: registry.schema,
|
|
2128
2697
|
manifest: displayPath(context, registry.manifestPath),
|
|
2129
2698
|
valid: true,
|
|
2130
|
-
counts: countsOf(registry)
|
|
2699
|
+
counts: countsOf(registry),
|
|
2700
|
+
warnings: registry.warnings
|
|
2131
2701
|
});
|
|
2132
2702
|
return { exitCode: 0 };
|
|
2133
2703
|
}
|
|
2134
2704
|
case "push":
|
|
2135
2705
|
return pushRegistry(context);
|
|
2706
|
+
case "status":
|
|
2707
|
+
return readRegistry(context, "/harness-registry");
|
|
2708
|
+
case "log": {
|
|
2709
|
+
const limit = context.flags.limit;
|
|
2710
|
+
if (limit !== undefined && !/^\d+$/.test(limit)) {
|
|
2711
|
+
throw new CliInputError("--limit must be a whole number.", "Usage: moda registry log [--limit=20]");
|
|
2712
|
+
}
|
|
2713
|
+
return readRegistry(context, `/registry/commits${limit ? `?limit=${limit}` : ""}`);
|
|
2714
|
+
}
|
|
2715
|
+
case "show": {
|
|
2716
|
+
const ref = context.positionals[1];
|
|
2717
|
+
if (!ref) {
|
|
2718
|
+
throw new CliInputError("Missing commit.", 'Usage: moda registry show <commit> (a hash from `moda registry log`, or "working")');
|
|
2719
|
+
}
|
|
2720
|
+
return readRegistry(context, ref === "working" ? "/registry/compare?to=working" : `/registry/commits/${encodeURIComponent(ref)}`);
|
|
2721
|
+
}
|
|
2722
|
+
case "coverage": {
|
|
2723
|
+
const days = context.flags.days;
|
|
2724
|
+
if (days !== undefined && !/^\d+$/.test(days)) {
|
|
2725
|
+
throw new CliInputError("--days must be a whole number of days.", "Usage: moda registry coverage [--days=7]");
|
|
2726
|
+
}
|
|
2727
|
+
return readRegistry(context, `/harness-registry/coverage${days ? `?days=${days}` : ""}`);
|
|
2728
|
+
}
|
|
2729
|
+
case "prompt": {
|
|
2730
|
+
const mode = explicitOutputMode(context.flags, context.env);
|
|
2731
|
+
if (mode && mode !== "human")
|
|
2732
|
+
context.output.writeData({ prompt: REGISTRY_AGENT_BRIEF });
|
|
2733
|
+
else if (!context.quiet)
|
|
2734
|
+
process.stdout.write(`${REGISTRY_AGENT_BRIEF}
|
|
2735
|
+
`);
|
|
2736
|
+
return { exitCode: 0 };
|
|
2737
|
+
}
|
|
2136
2738
|
default:
|
|
2137
|
-
throw new CliInputError(`Unknown registry command '${subcommand}'.`, "Usage: moda registry template|validate|push [--file=.moda/registry.json] [--dry-run] [--live]");
|
|
2739
|
+
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]");
|
|
2138
2740
|
}
|
|
2139
2741
|
}
|
|
2140
2742
|
function writeTemplate(context) {
|
|
@@ -2159,6 +2761,9 @@ function writeTemplate(context) {
|
|
|
2159
2761
|
return { exitCode: 0 };
|
|
2160
2762
|
}
|
|
2161
2763
|
async function pushRegistry(context) {
|
|
2764
|
+
const message = context.flags.message;
|
|
2765
|
+
if (message === "true")
|
|
2766
|
+
throw new CliInputError("Missing text for --message.", 'Usage: moda registry push --message="Add billing agent"');
|
|
2162
2767
|
const registry = loadRegistry(context);
|
|
2163
2768
|
const profileOptions = { profile: context.profile, env: context.env };
|
|
2164
2769
|
const hasKey = Boolean(resolveApiKey(profileOptions));
|
|
@@ -2191,7 +2796,20 @@ async function pushRegistry(context) {
|
|
|
2191
2796
|
await push("prompts", "/prompts/sync", registry.prompts.length, { prompts: registry.prompts });
|
|
2192
2797
|
await push("skills", "/skills/sync", registry.skills.length, { skills: registry.skills });
|
|
2193
2798
|
await push("tools", "/tool-definitions/sync", registry.tools.length, { tools: registry.tools });
|
|
2799
|
+
if (registry.harness) {
|
|
2800
|
+
await push("harness", "/harness-registry/sync", 1, { schema: registry.schema, document: registry.harness });
|
|
2801
|
+
}
|
|
2802
|
+
let commit;
|
|
2803
|
+
let commitError;
|
|
2804
|
+
if (!context.dryRun && sections.some((section) => section.status === "synced")) {
|
|
2805
|
+
try {
|
|
2806
|
+
commit = await callControlAPI("/registry/commits", { method: "POST", body: JSON.stringify({ ...message ? { message } : {}, ...gitInfo(context.cwd) }) }, profileOptions);
|
|
2807
|
+
} catch (error) {
|
|
2808
|
+
commitError = error instanceof Error ? error.message : String(error);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2194
2811
|
const failed = sections.filter((section) => section.status === "error");
|
|
2812
|
+
printWarnings(context, registry.warnings);
|
|
2195
2813
|
if (context.outputMode === "human" && !context.quiet) {
|
|
2196
2814
|
for (const section of sections) {
|
|
2197
2815
|
const detail = section.status === "error" ? ` — ${section.error}` : describeActions(section.response);
|
|
@@ -2200,13 +2818,26 @@ async function pushRegistry(context) {
|
|
|
2200
2818
|
if (sections.some((section) => section.section === "tools" && section.status === "synced")) {
|
|
2201
2819
|
console.error(" Replays now give your agent these tools with their real schemas and descriptions.");
|
|
2202
2820
|
}
|
|
2821
|
+
if (sections.some((section) => section.section === "harness" && section.status === "synced")) {
|
|
2822
|
+
console.error(" Replays now rebuild the agent live at each replayed turn. Check `moda registry coverage`.");
|
|
2823
|
+
}
|
|
2824
|
+
const recorded = isRecord2(commit) && isRecord2(commit.commit) ? commit.commit : undefined;
|
|
2825
|
+
if (recorded) {
|
|
2826
|
+
const label = commit && isRecord2(commit) && commit.action === "unchanged" ? "no registry changes since" : "commit";
|
|
2827
|
+
console.error(` ${label} ${String(recorded.id).slice(0, 7)} (#${String(recorded.seq)}) ${String(recorded.summary ?? "")}`);
|
|
2828
|
+
} else if (commitError) {
|
|
2829
|
+
console.error(` commit: not recorded — ${commitError}`);
|
|
2830
|
+
}
|
|
2203
2831
|
}
|
|
2204
2832
|
context.output.writeData({
|
|
2205
|
-
schema:
|
|
2833
|
+
schema: registry.schema,
|
|
2206
2834
|
manifest: displayPath(context, registry.manifestPath),
|
|
2207
2835
|
dryRun: context.dryRun,
|
|
2208
2836
|
counts: countsOf(registry),
|
|
2209
|
-
sections
|
|
2837
|
+
sections,
|
|
2838
|
+
warnings: registry.warnings,
|
|
2839
|
+
...commit !== undefined ? { commit } : {},
|
|
2840
|
+
...commitError ? { commitError } : {}
|
|
2210
2841
|
}, {
|
|
2211
2842
|
status: failed.length > 0 ? "error" : "success",
|
|
2212
2843
|
...failed.length > 0 ? {
|
|
@@ -2219,11 +2850,39 @@ async function pushRegistry(context) {
|
|
|
2219
2850
|
});
|
|
2220
2851
|
return { exitCode: failed.length > 0 ? 1 : 0 };
|
|
2221
2852
|
}
|
|
2853
|
+
function gitInfo(cwd) {
|
|
2854
|
+
const git = (...args) => execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).toString().trim();
|
|
2855
|
+
try {
|
|
2856
|
+
const gitCommit = git("rev-parse", "HEAD");
|
|
2857
|
+
const branch = git("rev-parse", "--abbrev-ref", "HEAD");
|
|
2858
|
+
return {
|
|
2859
|
+
gitCommit,
|
|
2860
|
+
...branch && branch !== "HEAD" ? { gitBranch: branch } : {},
|
|
2861
|
+
gitDirty: git("status", "--porcelain").length > 0
|
|
2862
|
+
};
|
|
2863
|
+
} catch {
|
|
2864
|
+
return {};
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
async function readRegistry(context, endpoint) {
|
|
2868
|
+
const profileOptions = { profile: context.profile, env: context.env };
|
|
2869
|
+
if (!resolveApiKey(profileOptions)) {
|
|
2870
|
+
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.");
|
|
2871
|
+
}
|
|
2872
|
+
context.output.writeData(await callControlAPI(endpoint, { method: "GET" }, profileOptions));
|
|
2873
|
+
return { exitCode: 0 };
|
|
2874
|
+
}
|
|
2875
|
+
function printWarnings(context, warnings) {
|
|
2876
|
+
if (context.outputMode !== "human" || context.quiet)
|
|
2877
|
+
return;
|
|
2878
|
+
for (const warning of warnings)
|
|
2879
|
+
console.error(` warning: ${warning}`);
|
|
2880
|
+
}
|
|
2222
2881
|
function describeActions(response) {
|
|
2223
|
-
const synced =
|
|
2882
|
+
const synced = isRecord2(response) && Array.isArray(response.synced) ? response.synced : [];
|
|
2224
2883
|
const counts = new Map;
|
|
2225
2884
|
for (const item of synced) {
|
|
2226
|
-
if (!
|
|
2885
|
+
if (!isRecord2(item))
|
|
2227
2886
|
continue;
|
|
2228
2887
|
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";
|
|
2229
2888
|
counts.set(action, (counts.get(action) ?? 0) + 1);
|
|
@@ -2298,13 +2957,17 @@ function checkFields(label, item, spec, errors) {
|
|
|
2298
2957
|
}
|
|
2299
2958
|
function resolveRegistry(input, options) {
|
|
2300
2959
|
const errors = [];
|
|
2301
|
-
|
|
2960
|
+
const warnings = [];
|
|
2961
|
+
if (!isRecord2(input)) {
|
|
2302
2962
|
throw new CliInputError("Registry manifest must be a JSON object with prompts, skills, and/or tools arrays.");
|
|
2303
2963
|
}
|
|
2304
|
-
if (input.schema !== undefined && input.schema !== REGISTRY_SCHEMA) {
|
|
2305
|
-
errors.push(`schema must be "${REGISTRY_SCHEMA}" (got ${JSON.stringify(input.schema)})`);
|
|
2964
|
+
if (input.schema !== undefined && input.schema !== REGISTRY_SCHEMA && input.schema !== REGISTRY_SCHEMA_V1) {
|
|
2965
|
+
errors.push(`schema must be "${REGISTRY_SCHEMA}" or "${REGISTRY_SCHEMA_V1}" (got ${JSON.stringify(input.schema)})`);
|
|
2966
|
+
}
|
|
2967
|
+
if (input.schema === REGISTRY_SCHEMA_V1 && hasHarnessSections(input)) {
|
|
2968
|
+
errors.push(`${HARNESS_SECTIONS.join(", ")} need schema "${REGISTRY_SCHEMA}"`);
|
|
2306
2969
|
}
|
|
2307
|
-
const known = new Set(["schema", "prompts", "skills", "tools"]);
|
|
2970
|
+
const known = new Set(["schema", "prompts", "skills", "tools", ...HARNESS_SECTIONS]);
|
|
2308
2971
|
for (const key of Object.keys(input)) {
|
|
2309
2972
|
if (!known.has(key))
|
|
2310
2973
|
errors.push(`unknown top-level field "${key}"`);
|
|
@@ -2336,7 +2999,7 @@ function resolveRegistry(input, options) {
|
|
|
2336
2999
|
const sourcePathFor = (file) => relative(options.cwd, resolve2(options.cwd, file)).split("\\").join("/");
|
|
2337
3000
|
const prompts = arrayOf(input.prompts, "prompts", errors).map((raw, index) => {
|
|
2338
3001
|
const label = `prompts[${index}]`;
|
|
2339
|
-
if (!
|
|
3002
|
+
if (!isRecord2(raw)) {
|
|
2340
3003
|
errors.push(`${label} must be an object`);
|
|
2341
3004
|
return;
|
|
2342
3005
|
}
|
|
@@ -2361,7 +3024,7 @@ function resolveRegistry(input, options) {
|
|
|
2361
3024
|
});
|
|
2362
3025
|
const skills = arrayOf(input.skills, "skills", errors).map((raw, index) => {
|
|
2363
3026
|
const label = `skills[${index}]`;
|
|
2364
|
-
if (!
|
|
3027
|
+
if (!isRecord2(raw)) {
|
|
2365
3028
|
errors.push(`${label} must be an object`);
|
|
2366
3029
|
return;
|
|
2367
3030
|
}
|
|
@@ -2403,13 +3066,13 @@ function resolveRegistry(input, options) {
|
|
|
2403
3066
|
const toolNames = new Set;
|
|
2404
3067
|
const tools = arrayOf(input.tools, "tools", errors).map((raw, index) => {
|
|
2405
3068
|
const label = `tools[${index}]`;
|
|
2406
|
-
if (!
|
|
3069
|
+
if (!isRecord2(raw) || !isRecord2(raw.schema)) {
|
|
2407
3070
|
errors.push(`${label}.schema must be an object (OpenAI function schema)`);
|
|
2408
3071
|
return;
|
|
2409
3072
|
}
|
|
2410
3073
|
checkFields(label, raw, TOOL_FIELDS, errors);
|
|
2411
3074
|
const tool = raw;
|
|
2412
|
-
const inner = tool.schema.type === "function" &&
|
|
3075
|
+
const inner = tool.schema.type === "function" && isRecord2(tool.schema.function) ? tool.schema.function : tool.schema;
|
|
2413
3076
|
const schemaName = typeof inner.name === "string" ? inner.name.trim() : undefined;
|
|
2414
3077
|
const name = (typeof tool.name === "string" ? tool.name.trim() : "") || schemaName;
|
|
2415
3078
|
if (typeof tool.name === "string" && tool.name.trim() && schemaName && tool.name.trim() !== schemaName) {
|
|
@@ -2428,9 +3091,9 @@ function resolveRegistry(input, options) {
|
|
|
2428
3091
|
errors.push(`${label}: schema.description must be a string`);
|
|
2429
3092
|
}
|
|
2430
3093
|
if (inner.parameters !== undefined) {
|
|
2431
|
-
if (!
|
|
3094
|
+
if (!isRecord2(inner.parameters) || inner.parameters.type !== "object") {
|
|
2432
3095
|
errors.push(`${label}: schema.parameters.type must be "object"`);
|
|
2433
|
-
} else if (inner.parameters.properties !== undefined && !
|
|
3096
|
+
} else if (inner.parameters.properties !== undefined && !isRecord2(inner.parameters.properties)) {
|
|
2434
3097
|
errors.push(`${label}: schema.parameters.properties must be an object`);
|
|
2435
3098
|
}
|
|
2436
3099
|
}
|
|
@@ -2452,6 +3115,7 @@ function resolveRegistry(input, options) {
|
|
|
2452
3115
|
skillKeys.add(skill.key);
|
|
2453
3116
|
}
|
|
2454
3117
|
}
|
|
3118
|
+
const harness = validateHarnessSections(input, { prompts: promptKeys, tools: toolNames, skills: skillKeys }, errors, warnings);
|
|
2455
3119
|
if (errors.length > 0) {
|
|
2456
3120
|
throw new CliInputError(`Registry manifest is invalid:
|
|
2457
3121
|
- ${errors.join(`
|
|
@@ -2459,12 +3123,15 @@ function resolveRegistry(input, options) {
|
|
|
2459
3123
|
}
|
|
2460
3124
|
const resolved = {
|
|
2461
3125
|
manifestPath: options.manifestPath,
|
|
3126
|
+
schema: typeof input.schema === "string" ? input.schema : harness ? REGISTRY_SCHEMA : REGISTRY_SCHEMA_V1,
|
|
2462
3127
|
prompts: prompts.filter(isDefined),
|
|
2463
3128
|
skills: skills.filter(isDefined),
|
|
2464
|
-
tools: tools.filter(isDefined)
|
|
3129
|
+
tools: tools.filter(isDefined),
|
|
3130
|
+
...harness ? { harness } : {},
|
|
3131
|
+
warnings
|
|
2465
3132
|
};
|
|
2466
|
-
if (resolved.prompts.length + resolved.skills.length + resolved.tools.length === 0) {
|
|
2467
|
-
throw new CliInputError("Registry manifest has no prompts, skills, or
|
|
3133
|
+
if (resolved.prompts.length + resolved.skills.length + resolved.tools.length === 0 && !harness) {
|
|
3134
|
+
throw new CliInputError("Registry manifest has no prompts, skills, tools, or agents to push.");
|
|
2468
3135
|
}
|
|
2469
3136
|
return resolved;
|
|
2470
3137
|
}
|
|
@@ -2481,7 +3148,16 @@ function arrayOf(value, label, errors) {
|
|
|
2481
3148
|
return value;
|
|
2482
3149
|
}
|
|
2483
3150
|
function countsOf(registry) {
|
|
2484
|
-
return {
|
|
3151
|
+
return {
|
|
3152
|
+
prompts: registry.prompts.length,
|
|
3153
|
+
skills: registry.skills.length,
|
|
3154
|
+
tools: registry.tools.length,
|
|
3155
|
+
...registry.harness ? {
|
|
3156
|
+
agents: registry.harness.agents?.length ?? 0,
|
|
3157
|
+
context: registry.harness.context?.length ?? 0,
|
|
3158
|
+
mcpServers: registry.harness.mcpServers?.length ?? 0
|
|
3159
|
+
} : {}
|
|
3160
|
+
};
|
|
2485
3161
|
}
|
|
2486
3162
|
function displayPath(context, path) {
|
|
2487
3163
|
const rel = relative(context.cwd, path);
|
|
@@ -2491,7 +3167,7 @@ function isInside(root, target) {
|
|
|
2491
3167
|
const rel = relative(resolve2(root), target);
|
|
2492
3168
|
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
2493
3169
|
}
|
|
2494
|
-
function
|
|
3170
|
+
function isRecord2(value) {
|
|
2495
3171
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2496
3172
|
}
|
|
2497
3173
|
function nonEmptyString(value) {
|
|
@@ -7198,7 +7874,7 @@ Commands:
|
|
|
7198
7874
|
skills sync Push local .claude/skills/**/SKILL.md up to Moda
|
|
7199
7875
|
skills sync --dry-run Show what would be pushed without writing
|
|
7200
7876
|
skills list|show|policy List pushed skills, show one, or set --live/--local
|
|
7201
|
-
registry push Push prompts, skills, and
|
|
7877
|
+
registry push Push prompts, skills, tools, and agent wiring from one JSON manifest
|
|
7202
7878
|
|
|
7203
7879
|
Options:
|
|
7204
7880
|
--help Show this help message
|
|
@@ -7681,7 +8357,8 @@ async function runTailCommand(params, context) {
|
|
|
7681
8357
|
message_count: messageCount,
|
|
7682
8358
|
environment: conversation.environment ?? null,
|
|
7683
8359
|
cluster_name: conversation.cluster_name ?? null,
|
|
7684
|
-
summary: conversation.summary ?? null
|
|
8360
|
+
summary: conversation.summary ?? null,
|
|
8361
|
+
latest_task: conversation.latest_task ?? null
|
|
7685
8362
|
});
|
|
7686
8363
|
}
|
|
7687
8364
|
};
|
|
@@ -7988,12 +8665,14 @@ async function searchFallbackToTraces(params) {
|
|
|
7988
8665
|
match_grain: "trace",
|
|
7989
8666
|
conversation_id: row.conversation_id,
|
|
7990
8667
|
summary: row.summary ?? null,
|
|
8668
|
+
matched_task: row.matched_task ?? null,
|
|
8669
|
+
latest_task: row.latest_task ?? null,
|
|
7991
8670
|
message_count: row.message_count ?? null,
|
|
7992
8671
|
first_timestamp: row.first_timestamp ?? null,
|
|
7993
8672
|
last_timestamp: row.last_timestamp ?? null
|
|
7994
8673
|
}));
|
|
7995
8674
|
const first = asString(rows[0].conversation_id);
|
|
7996
|
-
const
|
|
8675
|
+
const describe2 = literal ? "trace-level keyword matches (each trace contains the words)" : `trace-level ${traceMode}-ranked related traces (they may not contain the exact words; fallback.total is the candidate pool, not a match count)`;
|
|
7997
8676
|
const nextCommands = [];
|
|
7998
8677
|
if (first) {
|
|
7999
8678
|
nextCommands.push({ command: `moda audit ${first}`, purpose: "Open the top trace to locate the relevant message.", mutability: "read", requires_approval: false });
|
|
@@ -8018,7 +8697,7 @@ async function searchFallbackToTraces(params) {
|
|
|
8018
8697
|
...hasMore ? { next_offset: nextOffset } : {}
|
|
8019
8698
|
}
|
|
8020
8699
|
},
|
|
8021
|
-
warning: `Message-level search returned 0 results, so these are ${
|
|
8700
|
+
warning: `Message-level search returned 0 results, so these are ${describe2}, from \`moda traces --search\`. ` + `Each result is a whole trace, not a message, with no score or message index. ` + `Open one with \`moda audit <conversation_id>\` or \`moda context <conversation_id>\` to find the matching turn.` + (hasMore ? " More exist: page with the suggested `moda traces --search ... --offset` command." : ""),
|
|
8022
8701
|
nextCommands
|
|
8023
8702
|
};
|
|
8024
8703
|
}
|
|
@@ -9342,7 +10021,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
9342
10021
|
resetApiRequestCountBeforeRun: true,
|
|
9343
10022
|
telemetry: "result",
|
|
9344
10023
|
handler: async (context) => {
|
|
9345
|
-
const { runInit } = await import("./index-
|
|
10024
|
+
const { runInit } = await import("./index-bcn5chmf.js");
|
|
9346
10025
|
if (context.outputMode === "agent-stream") {
|
|
9347
10026
|
context.output.writeEvent({
|
|
9348
10027
|
event: "started",
|
|
@@ -9383,7 +10062,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
9383
10062
|
resetApiRequestCountBeforeRun: true,
|
|
9384
10063
|
telemetry: "result-and-error",
|
|
9385
10064
|
handler: async (context) => {
|
|
9386
|
-
const { runProvision } = await import("./provision-
|
|
10065
|
+
const { runProvision } = await import("./provision-mqw8s0jb.js");
|
|
9387
10066
|
return runProvision(context, {
|
|
9388
10067
|
resumeCommand: buildResumeCommand("provision", context.rawFlags)
|
|
9389
10068
|
});
|
|
@@ -9681,17 +10360,19 @@ var commandRegistry = createCommandRegistry([
|
|
|
9681
10360
|
},
|
|
9682
10361
|
{
|
|
9683
10362
|
name: "registry",
|
|
9684
|
-
description: "Push prompts, skills, and
|
|
10363
|
+
description: "Push prompts, skills, tools, and agent wiring (agents, routing, context) to Moda from one agent-written JSON manifest, so replays rebuild your harness (no repo scan)",
|
|
9685
10364
|
examples: [
|
|
10365
|
+
"moda registry prompt",
|
|
9686
10366
|
"moda registry template --out=.moda/registry.json",
|
|
9687
10367
|
"moda registry validate",
|
|
9688
10368
|
"moda registry push --dry-run",
|
|
9689
|
-
"moda registry push --file=.moda/registry.json"
|
|
10369
|
+
"moda registry push --file=.moda/registry.json",
|
|
10370
|
+
"moda registry coverage --days=7"
|
|
9690
10371
|
],
|
|
9691
10372
|
subcommands: [
|
|
9692
10373
|
{
|
|
9693
10374
|
name: "template",
|
|
9694
|
-
description: "Print (or write with --out) an example moda_registry.
|
|
10375
|
+
description: "Print (or write with --out) an example moda_registry.v2 manifest",
|
|
9695
10376
|
usage: "moda registry template [--out=<path>] [--yes]",
|
|
9696
10377
|
flags: [
|
|
9697
10378
|
{ name: "--out=<path>", description: "Write the template to a file instead of stdout (refuses to overwrite without --yes)." }
|
|
@@ -9707,18 +10388,52 @@ var commandRegistry = createCommandRegistry([
|
|
|
9707
10388
|
},
|
|
9708
10389
|
{
|
|
9709
10390
|
name: "push",
|
|
9710
|
-
description: "Upload the manifest's prompts, skills, and
|
|
9711
|
-
usage: "moda registry push [--file=<path>] [--dry-run] [--live]",
|
|
10391
|
+
description: "Upload the manifest's prompts, skills, tools, and harness sections to the API key's tenant",
|
|
10392
|
+
usage: "moda registry push [--file=<path>] [--dry-run] [--live] [--message=<text>]",
|
|
9712
10393
|
flags: [
|
|
9713
10394
|
{ name: "--file=<path>", description: "Manifest path (default .moda/registry.json)." },
|
|
10395
|
+
{ name: "--message=<text>", description: "Message for the registry commit this push records (shown in the dashboard Registry history)." },
|
|
9714
10396
|
{ name: "--dry-run", description: "Server-side preview; nothing is written. Without an API key, validates locally only." },
|
|
9715
10397
|
{ name: "--live", description: "Mark every pushed skill live (included in the served skill surface)." }
|
|
9716
10398
|
],
|
|
9717
10399
|
examples: ["moda registry push --dry-run", "moda registry push --live"]
|
|
10400
|
+
},
|
|
10401
|
+
{
|
|
10402
|
+
name: "status",
|
|
10403
|
+
description: "Show the registered harness (agents, routing, context) and its version",
|
|
10404
|
+
usage: "moda registry status",
|
|
10405
|
+
examples: ["moda registry status"]
|
|
10406
|
+
},
|
|
10407
|
+
{
|
|
10408
|
+
name: "log",
|
|
10409
|
+
description: "Registry history: one commit per push, with what changed",
|
|
10410
|
+
usage: "moda registry log [--limit=20]",
|
|
10411
|
+
flags: [{ name: "--limit=<n>", description: "Commits to list (default 50)." }],
|
|
10412
|
+
examples: ["moda registry log --limit=10"]
|
|
10413
|
+
},
|
|
10414
|
+
{
|
|
10415
|
+
name: "show",
|
|
10416
|
+
description: `A registry commit's file diffs, or "working" for changes since the last push`,
|
|
10417
|
+
usage: "moda registry show <commit|working>",
|
|
10418
|
+
examples: ["moda registry show 3f9a2c1", "moda registry show working"]
|
|
10419
|
+
},
|
|
10420
|
+
{
|
|
10421
|
+
name: "coverage",
|
|
10422
|
+
description: "Compare recent production traces with the registry: attribution rate, unmatched agents and prompts, unregistered tools",
|
|
10423
|
+
usage: "moda registry coverage [--days=7]",
|
|
10424
|
+
flags: [{ name: "--days=<n>", description: "Look-back window in days (default 7, max 30)." }],
|
|
10425
|
+
examples: ["moda registry coverage --days=7"]
|
|
10426
|
+
},
|
|
10427
|
+
{
|
|
10428
|
+
name: "prompt",
|
|
10429
|
+
description: "Print the brief to hand your coding agent so it inventories and registers your harness",
|
|
10430
|
+
usage: "moda registry prompt [--json | --agent]",
|
|
10431
|
+
flags: [{ name: "--json | --agent", description: "Return the brief inside JSON / the agent envelope. Without them it prints raw text, even when piped." }],
|
|
10432
|
+
examples: ["moda registry prompt | pbcopy", 'claude "$(moda registry prompt)"']
|
|
9718
10433
|
}
|
|
9719
10434
|
],
|
|
9720
10435
|
auth: "api-key",
|
|
9721
|
-
authNote: "template and
|
|
10436
|
+
authNote: "template, validate, and prompt are local; push, status, log, show, and coverage need the API key (push --dry-run without a key validates locally only).",
|
|
9722
10437
|
outputModes: ["human", "json", "agent"],
|
|
9723
10438
|
mutability: "write",
|
|
9724
10439
|
validateConfigBeforeRun: false,
|