moshcode 0.58.0 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +299 -6
- package/bin/moshcode.mjs +3 -3
- package/examples/account.mosh +48 -0
- package/examples/aliases.mosh +56 -0
- package/examples/research-desk.mosh +49 -0
- package/package.json +1 -1
- package/prd/0011-herd-agent-protocol.md +391 -0
- package/prd/README.md +1 -0
- package/src/auth.mjs +59 -64
- package/src/cli-schema.mjs +133 -4
- package/src/commands.mjs +389 -39
- package/src/cost-cli.mjs +232 -0
- package/src/cost-pricing.mjs +159 -0
- package/src/cost.mjs +753 -0
- package/src/engines.mjs +32 -0
- package/src/games-breakout.mjs +64 -10
- package/src/games-paddle.mjs +128 -0
- package/src/games-pong.mjs +53 -4
- package/src/games.mjs +164 -12
- package/src/herd-cli.mjs +816 -20
- package/src/herd-eval.mjs +301 -0
- package/src/herd-hooks.mjs +285 -0
- package/src/herd-remote.mjs +365 -0
- package/src/herd-serve.mjs +515 -0
- package/src/herd-state.mjs +167 -10
- package/src/herd-tasks.mjs +377 -0
- package/src/herd.mjs +89 -7
- package/src/templates.mjs +32 -5
- package/src/tools.mjs +43 -0
- package/src/tui.mjs +1 -0
package/src/herd.mjs
CHANGED
|
@@ -142,6 +142,54 @@ export function forgetSession(name) {
|
|
|
142
142
|
return true;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Remote members — the last thing a URL told us (PRD 0011 R11)
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
//
|
|
149
|
+
// A remote member has no pane to capture and no pid to signal, so its state can
|
|
150
|
+
// only come from a request. Requests are slow and the roster is drawn on every
|
|
151
|
+
// pit start, so what `ps` reads is a *cache*: whatever the last call to that
|
|
152
|
+
// remote observed, with the time it was observed at. The alternative — a roster
|
|
153
|
+
// that opens N sockets before it prints a line — makes `moshcode ps` as fast as
|
|
154
|
+
// the slowest agent someone registered, which is not a trade worth making for a
|
|
155
|
+
// column that already says `authority: remote`.
|
|
156
|
+
//
|
|
157
|
+
// It lives here rather than in herd-remote.mjs so that herd-state.mjs can read
|
|
158
|
+
// it without importing the module that makes network calls.
|
|
159
|
+
|
|
160
|
+
const remoteFile = (name) => path.join(herdDir(), "remote", `${name}.json`);
|
|
161
|
+
|
|
162
|
+
/** Record what a remote just told us. Best effort: a failed write loses a poll. */
|
|
163
|
+
export function recordRemoteStatus(name, status = {}) {
|
|
164
|
+
try {
|
|
165
|
+
fs.mkdirSync(path.join(herdDir(), "remote"), { recursive: true, mode: 0o700 });
|
|
166
|
+
const file = remoteFile(name);
|
|
167
|
+
fs.writeFileSync(file, JSON.stringify({ ...status, at: status.at ?? Date.now() }), { mode: 0o600 });
|
|
168
|
+
fs.chmodSync(file, 0o600);
|
|
169
|
+
return true;
|
|
170
|
+
} catch { return false; }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The cached status of a remote member, or null when it has never answered.
|
|
175
|
+
*
|
|
176
|
+
* Deliberately un-expiring. A hook report has a TTL because a stale one would
|
|
177
|
+
* outrank a screen that could be read instead; there is nothing better to fall
|
|
178
|
+
* back to here, and "it was idle an hour ago" beats "unknown" as long as the
|
|
179
|
+
* age travels with it — which it does, in `--json` and in `herd remote list`.
|
|
180
|
+
*/
|
|
181
|
+
export function remoteStatus(name) {
|
|
182
|
+
try {
|
|
183
|
+
const raw = JSON.parse(fs.readFileSync(remoteFile(name), "utf8"));
|
|
184
|
+
return raw && typeof raw === "object" ? raw : null;
|
|
185
|
+
} catch { return null; }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function clearRemoteStatus(name) {
|
|
189
|
+
try { fs.rmSync(remoteFile(name), { force: true }); return true; }
|
|
190
|
+
catch { return false; }
|
|
191
|
+
}
|
|
192
|
+
|
|
145
193
|
// ---------------------------------------------------------------------------
|
|
146
194
|
// Substrate detection
|
|
147
195
|
// ---------------------------------------------------------------------------
|
|
@@ -226,10 +274,17 @@ export function tmux(args, { runner = spawnSync, env = process.env, encoding = "
|
|
|
226
274
|
* (an inherited ANTHROPIC_API_KEY hijacks its stored login — see ENGINES), and
|
|
227
275
|
* `-e KEY=` sets an empty value, which is not the same as unset.
|
|
228
276
|
*/
|
|
229
|
-
export function sessionCommand({ bin, args = [], stripEnv = [], exec = true }) {
|
|
277
|
+
export function sessionCommand({ bin, args = [], stripEnv = [], setEnv = {}, exec = true }) {
|
|
230
278
|
const unset = stripEnv.flatMap((key) => ["-u", key]);
|
|
279
|
+
// Set through the same `env` prefix rather than tmux's `-e`, for two reasons:
|
|
280
|
+
// `-e` is a 3.2+ flag and the pty substrate has no equivalent at all, so one
|
|
281
|
+
// prefix is the only spelling both substrates can share.
|
|
282
|
+
const set = Object.entries(setEnv)
|
|
283
|
+
.filter(([key, value]) => key && value !== undefined && value !== null)
|
|
284
|
+
.map(([key, value]) => `${key}=${String(value)}`);
|
|
231
285
|
const command = [bin, ...args].map(shQuote).join(" ");
|
|
232
|
-
const
|
|
286
|
+
const prefix = [...unset, ...set];
|
|
287
|
+
const withEnv = prefix.length ? `env ${prefix.map(shQuote).join(" ")} ${command}` : command;
|
|
233
288
|
// `exec` so the engine replaces the shell rather than sitting under it — one
|
|
234
289
|
// less process between a signal and the thing meant to receive it. The pty
|
|
235
290
|
// substrate passes exec:false because it needs the shell to outlive the
|
|
@@ -302,6 +357,24 @@ export function pidAlive(pid) {
|
|
|
302
357
|
* never sees EOF and waits for input forever, which is what an idle agent
|
|
303
358
|
* should do.
|
|
304
359
|
*/
|
|
360
|
+
/**
|
|
361
|
+
* What every session knows about itself (PRD 0011 R2).
|
|
362
|
+
*
|
|
363
|
+
* A lifecycle hook fires inside the engine's own process tree and has to name
|
|
364
|
+
* the session it is reporting for. Nothing else in that tree knows the name, so
|
|
365
|
+
* the herd puts it there at launch. `MOSHCODE_HERD_DIR` rides along because a
|
|
366
|
+
* hook shells out to `moshcode herd report`, and a box whose herd lives
|
|
367
|
+
* somewhere non-default would otherwise have its reports written to the default
|
|
368
|
+
* directory nobody is reading.
|
|
369
|
+
*
|
|
370
|
+
* A hook fired outside a herd session sees neither, which is the signal to exit
|
|
371
|
+
* quietly — see `herdReport`. Hooks must never break an engine that is not in
|
|
372
|
+
* the herd.
|
|
373
|
+
*/
|
|
374
|
+
export function sessionEnv(name) {
|
|
375
|
+
return { MOSHCODE_HERD_NAME: name, MOSHCODE_HERD_DIR: herdDir() };
|
|
376
|
+
}
|
|
377
|
+
|
|
305
378
|
function ptyStart({ name, cwd, bin, args, stripEnv, env, spawner = spawn, runner = spawnSync, size = {} }) {
|
|
306
379
|
ensureDir();
|
|
307
380
|
const cols = Number(size.cols) || Number(env.COLUMNS) || process.stdout.columns || 80;
|
|
@@ -328,7 +401,7 @@ function ptyStart({ name, cwd, bin, args, stripEnv, env, spawner = spawn, runner
|
|
|
328
401
|
// that a session which finishes on its own leaves proof it finished.
|
|
329
402
|
const command = [
|
|
330
403
|
`stty rows ${rows} cols ${cols} 2>/dev/null`,
|
|
331
|
-
sessionCommand({ bin, args, stripEnv, exec: false }),
|
|
404
|
+
sessionCommand({ bin, args, stripEnv, setEnv: sessionEnv(name), exec: false }),
|
|
332
405
|
`printf '%s' "$?" > ${shQuote(exit)}`,
|
|
333
406
|
].join("; ");
|
|
334
407
|
// Reuse ptySpec's flag knowledge rather than re-deriving it: util-linux and
|
|
@@ -347,7 +420,7 @@ function ptyStart({ name, cwd, bin, args, stripEnv, env, spawner = spawn, runner
|
|
|
347
420
|
cwd,
|
|
348
421
|
// Belt and braces with the stty above: some toolkits read COLUMNS/LINES
|
|
349
422
|
// before they ever ask the terminal.
|
|
350
|
-
env: { ...env, COLUMNS: String(cols), LINES: String(rows), MOSHCODE_HERD_SESSION: name },
|
|
423
|
+
env: { ...env, COLUMNS: String(cols), LINES: String(rows), MOSHCODE_HERD_SESSION: name, ...sessionEnv(name) },
|
|
351
424
|
stdio: [stdin, "ignore", "ignore"],
|
|
352
425
|
detached: true,
|
|
353
426
|
});
|
|
@@ -565,7 +638,7 @@ export function startSession({
|
|
|
565
638
|
};
|
|
566
639
|
|
|
567
640
|
if (substrate === "tmux") {
|
|
568
|
-
const command = sessionCommand({ bin, args, stripEnv });
|
|
641
|
+
const command = sessionCommand({ bin, args, stripEnv, setEnv: sessionEnv(name) });
|
|
569
642
|
const started = tmux(tmuxStartPlan({ name, cwd, command }), { runner, env });
|
|
570
643
|
if (!started.ok) {
|
|
571
644
|
return { ok: false, error: new Error(started.stderr.trim() || started.error?.message || "tmux could not start the session") };
|
|
@@ -802,12 +875,21 @@ export function listSessions({ substrate = detectSubstrate(), runner = spawnSync
|
|
|
802
875
|
const names = [...new Set([...live, ...Object.keys(manifest.sessions)])].sort();
|
|
803
876
|
return names.map((name) => {
|
|
804
877
|
const meta = manifest.sessions[name] || {};
|
|
805
|
-
|
|
806
|
-
|
|
878
|
+
// A remote member (PRD 0011 R11) has no pane and no pid — it is a URL. The
|
|
879
|
+
// substrate can only ever report it as absent, so liveness for those rows
|
|
880
|
+
// means "still registered", and whether the far end answers is a question
|
|
881
|
+
// for herd-remote.mjs's cache rather than for tmux.
|
|
882
|
+
const remote = meta.kind === "remote";
|
|
883
|
+
const alive = remote ? true : live.has(name);
|
|
884
|
+
const exited = remote ? false
|
|
885
|
+
: !alive ? null
|
|
807
886
|
: panes ? Boolean(panes.get(name)?.dead)
|
|
808
887
|
: sessionExited(name, { substrate, runner });
|
|
809
888
|
return {
|
|
810
889
|
name,
|
|
890
|
+
kind: meta.kind || "local",
|
|
891
|
+
url: meta.url || null,
|
|
892
|
+
remoteKind: meta.remoteKind || null,
|
|
811
893
|
engine: meta.engine || "?",
|
|
812
894
|
// Sessions started before herds existed have none. They belong to `main`
|
|
813
895
|
// rather than to a group rendered as "undefined".
|
package/src/templates.mjs
CHANGED
|
@@ -41,6 +41,23 @@ function isOwnManifest(relative) {
|
|
|
41
41
|
|
|
42
42
|
/* ------------------------------------------------------------------ listing */
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Template collections worth knowing about that this repo does not ship
|
|
46
|
+
* (PRD 0011 R15).
|
|
47
|
+
*
|
|
48
|
+
* A pointer rather than a copy, deliberately. `template install` already takes
|
|
49
|
+
* an `owner/repo`, so vendoring somebody else's templates would mean carrying
|
|
50
|
+
* their updates by hand forever — and pinning a stale copy of an SDK's starter
|
|
51
|
+
* kit is worse than not having one. What is missing is only that nobody knows
|
|
52
|
+
* the name to type, so the listing says it.
|
|
53
|
+
*/
|
|
54
|
+
export const TEMPLATE_POINTERS = [
|
|
55
|
+
{
|
|
56
|
+
spec: "digitalocean/gradient-adk-templates",
|
|
57
|
+
description: "DigitalOcean Gradient ADK — agent starters (A2A-capable; pairs with `moshcode install gradient`)",
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
44
61
|
/** The bundled templates, each with whatever its manifest says about it. */
|
|
45
62
|
export async function listTemplates(dir = BUNDLED_DIR) {
|
|
46
63
|
let entries;
|
|
@@ -347,19 +364,29 @@ export async function templateCommand(
|
|
|
347
364
|
}
|
|
348
365
|
const templates = await listTemplates();
|
|
349
366
|
if (rest.includes("--json")) {
|
|
367
|
+
// The bundled list stays an array, because that is what it has always
|
|
368
|
+
// been and something is parsing it. Pointers are named separately rather
|
|
369
|
+
// than mixed in: `template install <name>` works for one and not the
|
|
370
|
+
// other, and a listing that hid that difference would be a listing that
|
|
371
|
+
// lies about what it can do.
|
|
350
372
|
out(JSON.stringify(templates, null, 2));
|
|
351
373
|
return 0;
|
|
352
374
|
}
|
|
353
375
|
if (!templates.length) {
|
|
354
376
|
out("no templates bundled with this install");
|
|
355
|
-
|
|
377
|
+
} else {
|
|
378
|
+
const width = Math.max(...templates.map((t) => t.name.length));
|
|
379
|
+
for (const { name, description } of templates) {
|
|
380
|
+
out(` ${name.padEnd(width)} ${description}`);
|
|
381
|
+
}
|
|
356
382
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
383
|
+
out("");
|
|
384
|
+
out("elsewhere:");
|
|
385
|
+
for (const { spec, description } of TEMPLATE_POINTERS) {
|
|
386
|
+
out(` ${spec} ${description}`);
|
|
360
387
|
}
|
|
361
388
|
out("");
|
|
362
|
-
out("install one with: moshcode template install <name>");
|
|
389
|
+
out("install one with: moshcode template install <name|owner/repo|url>");
|
|
363
390
|
return 0;
|
|
364
391
|
}
|
|
365
392
|
|
package/src/tools.mjs
CHANGED
|
@@ -160,6 +160,49 @@ export const TOOLS = {
|
|
|
160
160
|
},
|
|
161
161
|
installHelp: "Go is required to install Alpaca; install Go, then retry `moshcode install alpaca`.",
|
|
162
162
|
},
|
|
163
|
+
gradient: {
|
|
164
|
+
desc: "DigitalOcean Gradient ADK — build, run, deploy and evaluate agents (A2A-capable)",
|
|
165
|
+
bin: "gradient",
|
|
166
|
+
// The one tool here that is not a self-contained binary. gradient-adk is a
|
|
167
|
+
// Python package, and moshcode stays Node: the tool owns its runtime, the
|
|
168
|
+
// same way CoinPay owns Node 20. So the install spec checks for a Python
|
|
169
|
+
// the package can actually run on and NAMES the requirement when it is
|
|
170
|
+
// missing, rather than letting pip fail three screens later with a
|
|
171
|
+
// resolution error nobody reads. `--user` keeps it out of the system
|
|
172
|
+
// site-packages, which is also the only place a non-root install can go on
|
|
173
|
+
// a modern distro.
|
|
174
|
+
install: {
|
|
175
|
+
cmd: "sh",
|
|
176
|
+
args: [
|
|
177
|
+
"-c",
|
|
178
|
+
'python3 -c "import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)" 2>/dev/null '
|
|
179
|
+
+ '|| { echo "gradient-adk needs Python 3.10 or newer on PATH as python3 — install it, then re-run: moshcode install gradient" >&2; exit 1; }; '
|
|
180
|
+
+ "python3 -m pip install --user --upgrade gradient-adk",
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
installHelp: "gradient-adk is a Python package: it needs python3 (3.10+) and pip. moshcode does not install Python for you.",
|
|
184
|
+
// pip --user drops console scripts here, and appends nothing to PATH for
|
|
185
|
+
// the shell that ran the install — the same gap turso and kimi have.
|
|
186
|
+
binDirs: [path.join(homedir(), ".local", "bin")],
|
|
187
|
+
// How the ADK's dev server reads in the herd (PRD 0011 R15). `gradient
|
|
188
|
+
// agent run --dev` is uvicorn underneath, and its startup banner is a clear
|
|
189
|
+
// "I am up and waiting", which is `idle`.
|
|
190
|
+
//
|
|
191
|
+
// There is deliberately no `working` rule. uvicorn writes its access line
|
|
192
|
+
// when a request has FINISHED, so a screen showing one is a screen showing
|
|
193
|
+
// a server that is free again — a rule matching it would pin the tile to
|
|
194
|
+
// `working` from the first request until the line scrolled away, which is
|
|
195
|
+
// the exact kind of rot the sub-kinds and hooks exist to get away from. So
|
|
196
|
+
// the completed request counts as idle too, and it is right both times.
|
|
197
|
+
// Watching a *deployed* agent's state is what `herd remote add` is for.
|
|
198
|
+
state: {
|
|
199
|
+
idle: [
|
|
200
|
+
/\buvicorn running on\b/i,
|
|
201
|
+
/\bapplication startup complete\b/i,
|
|
202
|
+
/"(?:POST|GET|PUT) \/[^"]*" \d{3}\b/,
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
163
206
|
mcpjam: {
|
|
164
207
|
desc: "MCPJam — test, debug, and validate MCP servers (health, OAuth, tool-surface diffs)",
|
|
165
208
|
bin: "mcpjam",
|
package/src/tui.mjs
CHANGED
|
@@ -833,6 +833,7 @@ export async function tui() {
|
|
|
833
833
|
// sessions run somewhere that outlives it.
|
|
834
834
|
if (cmd === "herd") { await herdCommand(rest); continue; }
|
|
835
835
|
if (cmd === "ps") { await herdCommand(["ps", ...rest]); continue; }
|
|
836
|
+
if (cmd === "cost" || cmd === "usage") { await herdCommand(["cost", ...rest]); continue; }
|
|
836
837
|
if (cmd === "kill") { await herdCommand(["kill", ...rest]); continue; }
|
|
837
838
|
if (cmd === "wait") { await herdCommand(["wait", ...rest]); continue; }
|
|
838
839
|
if (cmd === "restore") { await herdCommand(["restore", ...rest]); continue; }
|