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/commands.mjs
CHANGED
|
@@ -16,11 +16,19 @@
|
|
|
16
16
|
import { spawn, spawnSync } from "node:child_process";
|
|
17
17
|
|
|
18
18
|
import { createRegistry } from "./registry.mjs";
|
|
19
|
-
import { cliVerb, aiVerb } from "./cli.mjs";
|
|
19
|
+
import { cliVerb, aiVerb, runMoshcode } from "./cli.mjs";
|
|
20
20
|
import { ingestApproval, pollApproval } from "./notify.mjs";
|
|
21
|
-
import { capture, killSession, sendPrompt } from "./herd.mjs";
|
|
22
|
-
import { herdStart, roster,
|
|
21
|
+
import { capture, killSession, remoteStatus, sendPrompt } from "./herd.mjs";
|
|
22
|
+
import { herdStart, isRemoteMember, roster, waitForMany, waitMember } from "./herd-cli.mjs";
|
|
23
|
+
import { endTask, findTask, readTasks, startTask } from "./herd-tasks.mjs";
|
|
23
24
|
import { shellInvocation } from "./shell.mjs";
|
|
25
|
+
import { identity, loginAuto, logout as forgetCreds } from "./auth.mjs";
|
|
26
|
+
import { expandAlias, getAlias, loadAliases, removeAlias, setAlias } from "./aliases.mjs";
|
|
27
|
+
import { CORE_CLI_COMMAND_NAMES, PIT_COMMANDS } from "./cli-schema.mjs";
|
|
28
|
+
import { fetchAdvisor, stocksArgs } from "./advisor.mjs";
|
|
29
|
+
import { cryptoArgs, fetchCrypto } from "./crypto.mjs";
|
|
30
|
+
import { collectNews, loadListFeeds, readingList } from "./news.mjs";
|
|
31
|
+
import { resolveList } from "./news-sources.mjs";
|
|
24
32
|
|
|
25
33
|
// The moshcoding pit-anthem playlist. mosh() blasts this URL and, on a desktop
|
|
26
34
|
// with a GUI, tries to open it in the default browser.
|
|
@@ -64,6 +72,61 @@ function expectNoArgs(name, args) {
|
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Whether a name already belongs to moshcode, for alias().
|
|
77
|
+
*
|
|
78
|
+
* The pit asks the dispatcher this by resolving; a script has no dispatcher, so
|
|
79
|
+
* it asks the schema instead — the CLI commands and the pit's own verbs, which
|
|
80
|
+
* is what an alias could collide with. Kept as a predicate (the shape
|
|
81
|
+
* setAlias() takes) rather than an exported list, so this stays the caller's
|
|
82
|
+
* answer and not a second roster to drift from the first.
|
|
83
|
+
*/
|
|
84
|
+
function isReserved(name) {
|
|
85
|
+
const key = String(name).toLowerCase();
|
|
86
|
+
return CORE_CLI_COMMAND_NAMES.includes(key)
|
|
87
|
+
|| PIT_COMMANDS.some((c) => (typeof c === "string" ? c : c.name) === key);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The shell verb, named so runAlias() can execute an expanded alias line
|
|
91
|
+
// through exactly the same path a script's own shell() call takes — one
|
|
92
|
+
// invocation, one dry-run story, one { ok, code } contract. It is registered in
|
|
93
|
+
// COMMANDS below like every other verb.
|
|
94
|
+
const SHELL = {
|
|
95
|
+
name: "shell",
|
|
96
|
+
summary: "run a shell command (blocking, cmd.exe on Windows or $SHELL elsewhere)",
|
|
97
|
+
usage: "shell(cmd)",
|
|
98
|
+
detail: "runs cmd in $SHELL, loading your rc file where it can; returns { ok, code, signal }",
|
|
99
|
+
// The moshscript system verb for arbitrary shell commands. Blocking
|
|
100
|
+
// (spawnSync + inherited stdio) so it runs inline in the no-`await` style,
|
|
101
|
+
// and the child owns the terminal for interactive commands. Returns
|
|
102
|
+
// { ok, code } so scripts can branch on the exit status:
|
|
103
|
+
// const r = shell("npm test"); if (!r.ok) say("tests failed");
|
|
104
|
+
run(ctx, ...args) {
|
|
105
|
+
const cmd = args.join(" ");
|
|
106
|
+
if (!cmd) throw new Error("moshscript: shell() requires a command string");
|
|
107
|
+
if (ctx.dryRun) {
|
|
108
|
+
ctx.out(` ▶ shell(${JSON.stringify(cmd)}) → would run: $SHELL ${shellInvocation(cmd).flags} ${JSON.stringify(cmd)}`);
|
|
109
|
+
// Same R8 contract as the comment above: `code` is always present, so a
|
|
110
|
+
// script branching on the exit status behaves the same under --dry-run.
|
|
111
|
+
return { ok: true, code: 0, dryRun: true };
|
|
112
|
+
}
|
|
113
|
+
// Same invocation the pit's own `!cmd` uses, so a command that works when
|
|
114
|
+
// typed works when scripted: interactive where a terminal is attached, so
|
|
115
|
+
// the user's rc file — and the aliases in it — are loaded. src/shell.mjs
|
|
116
|
+
// has the reasoning, including why a headless run stays non-interactive.
|
|
117
|
+
const { shell: sh, args: shArgs } = shellInvocation(cmd);
|
|
118
|
+
ctx.out(` ▶ shell: ${cmd}`);
|
|
119
|
+
const res = spawnSync(sh, shArgs, { stdio: "inherit" });
|
|
120
|
+
if (res.error) throw res.error;
|
|
121
|
+
const code = res.status ?? 1;
|
|
122
|
+
if (code !== 0) {
|
|
123
|
+
ctx.out(` ✗ shell() exited ${res.signal || code}`);
|
|
124
|
+
return { ok: false, code, signal: res.signal || null };
|
|
125
|
+
}
|
|
126
|
+
return { ok: true, code: 0 };
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
67
130
|
// The vocabulary, in registration order. mosh() is the worked example of the
|
|
68
131
|
// command shape; the rest follow the same pattern.
|
|
69
132
|
const COMMANDS = [
|
|
@@ -189,39 +252,227 @@ const COMMANDS = [
|
|
|
189
252
|
},
|
|
190
253
|
},
|
|
191
254
|
|
|
255
|
+
SHELL,
|
|
256
|
+
|
|
257
|
+
// The account. Local rather than cliVerbs for the same reason the herd is:
|
|
258
|
+
// `moshcode whoami` prints, and a script needs to *branch* on the answer —
|
|
259
|
+
// "am I logged in?", "whose account is this?", "are there credits left?".
|
|
260
|
+
// Shelling out returns { ok, code }, so the only way to read the account
|
|
261
|
+
// would be to re-parse stdout. src/auth.mjs owns the flows either way; these
|
|
262
|
+
// are a second caller of the same identity(), not a second implementation.
|
|
192
263
|
{
|
|
193
|
-
name: "
|
|
194
|
-
summary: "
|
|
195
|
-
usage: "
|
|
196
|
-
detail: "
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
264
|
+
name: "whoami",
|
|
265
|
+
summary: "the logged-in account, as a value (verified against app.moshcode.sh)",
|
|
266
|
+
usage: "whoami()",
|
|
267
|
+
detail: "returns { status, verified, api, user: { id, email, name, credits } }. needs await",
|
|
268
|
+
// Never throws — an unreachable app is a `status`, not an exception,
|
|
269
|
+
// because the caller is usually deciding whether to start work.
|
|
270
|
+
async run(ctx) {
|
|
271
|
+
if (ctx.dryRun) {
|
|
272
|
+
ctx.out(" 👤 whoami() → would check app.moshcode.sh");
|
|
273
|
+
return { status: "dry_run", verified: false, api: null, user: null, dryRun: true };
|
|
274
|
+
}
|
|
275
|
+
const me = await identity();
|
|
276
|
+
const who = me.user?.email || me.user?.name;
|
|
277
|
+
ctx.out(me.verified
|
|
278
|
+
? ` 👤 whoami() → ${who || "moshcoder"} (${me.user.credits ?? "?"} credits)`
|
|
279
|
+
: ` 👤 whoami() → ${me.status}${who ? ` (${who}, unverified)` : ""}`);
|
|
280
|
+
return me;
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "login",
|
|
285
|
+
summary: "authenticate this machine against app.moshcode.sh",
|
|
286
|
+
usage: "login({ device, browser, force })",
|
|
287
|
+
detail: "no-op when already authenticated unless force; returns { ok, email, already }. needs await",
|
|
288
|
+
// Idempotent by default. A script that opens with login() should be safe to
|
|
289
|
+
// re-run all day without throwing a browser tab at an operator who is
|
|
290
|
+
// already signed in — so the verified case returns early. `force` re-runs
|
|
291
|
+
// the flow anyway (switching accounts), and device/browser pin the flow
|
|
292
|
+
// rather than letting loginAuto sniff for SSH.
|
|
293
|
+
async run(ctx, opts = {}) {
|
|
294
|
+
if (ctx.dryRun) {
|
|
295
|
+
ctx.out(" 🔑 login() → would authenticate against app.moshcode.sh");
|
|
296
|
+
return { ok: true, email: null, already: false, dryRun: true };
|
|
297
|
+
}
|
|
298
|
+
if (!opts.force) {
|
|
299
|
+
const me = await identity();
|
|
300
|
+
if (me.verified) {
|
|
301
|
+
ctx.out(` 🔑 login() → already signed in as ${me.user.email || me.user.name || "moshcoder"}`);
|
|
302
|
+
return { ok: true, email: me.user.email ?? null, already: true };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const r = await loginAuto({ device: Boolean(opts.device), browser: Boolean(opts.browser) });
|
|
307
|
+
ctx.out(` 🔑 login() → signed in as ${r.email || "moshcoder"} 🤘`);
|
|
308
|
+
return { ok: true, email: r.email ?? null, already: false };
|
|
309
|
+
} catch (e) {
|
|
310
|
+
// R8: hand back the failure instead of throwing, so a script can fall
|
|
311
|
+
// back (skip the notify, run read-only) rather than die at line 1.
|
|
312
|
+
ctx.out(` ✗ login() failed — ${e.message}`);
|
|
313
|
+
return { ok: false, email: null, already: false, error: e.message };
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "requireLogin",
|
|
319
|
+
summary: "BLOCK until this machine is authenticated — the gate for scripts that need an account",
|
|
320
|
+
usage: "requireLogin({ device, browser })",
|
|
321
|
+
detail: "returns the verified { id, email, name, credits }; THROWS if it can't authenticate. needs await",
|
|
322
|
+
// The one verb here that throws, and on purpose: "require" means the script
|
|
323
|
+
// must not continue unauthenticated. Everything downstream (notify, ask,
|
|
324
|
+
// credits) would fail one call at a time and much less legibly, so a script
|
|
325
|
+
// that needs an account says so once, at the top.
|
|
326
|
+
async run(ctx, opts = {}) {
|
|
327
|
+
if (ctx.dryRun) {
|
|
328
|
+
ctx.out(" 🔒 requireLogin() → would require an authenticated account");
|
|
329
|
+
return { id: null, email: null, name: null, credits: null, dryRun: true };
|
|
330
|
+
}
|
|
331
|
+
let me = await identity();
|
|
332
|
+
if (!me.verified) {
|
|
333
|
+
ctx.out(` 🔒 requireLogin() → ${me.status} — starting the login flow…`);
|
|
334
|
+
try { await loginAuto({ device: Boolean(opts.device), browser: Boolean(opts.browser) }); }
|
|
335
|
+
catch (e) { throw new Error(`moshscript: requireLogin() could not authenticate — ${e.message}`); }
|
|
336
|
+
me = await identity();
|
|
337
|
+
}
|
|
338
|
+
if (!me.verified) {
|
|
339
|
+
throw new Error(`moshscript: requireLogin() could not authenticate (${me.status}) — run \`moshcode login\``);
|
|
340
|
+
}
|
|
341
|
+
ctx.out(` 🔒 requireLogin() → ${me.user.email || me.user.name || "moshcoder"} 🤘`);
|
|
342
|
+
return me.user;
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "logout",
|
|
347
|
+
summary: "forget this machine's credentials",
|
|
348
|
+
usage: "logout()",
|
|
349
|
+
detail: "returns { ok }",
|
|
202
350
|
run(ctx, ...args) {
|
|
203
|
-
|
|
204
|
-
if (
|
|
351
|
+
expectNoArgs("logout", args);
|
|
352
|
+
if (ctx.dryRun) { ctx.out(" 🚪 logout() → would forget the local credentials"); return { ok: true, dryRun: true }; }
|
|
353
|
+
forgetCreds();
|
|
354
|
+
return { ok: true };
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
// Aliases. The pit already keeps named shortcuts (src/aliases.mjs) and they
|
|
359
|
+
// are the operator's own vocabulary — the things *they* retype. A script that
|
|
360
|
+
// cannot reach them has to re-spell every one of those lines, so the same
|
|
361
|
+
// store is readable and writable here, and runAlias() executes one.
|
|
362
|
+
{
|
|
363
|
+
name: "alias",
|
|
364
|
+
summary: "read, list, or define pit aliases",
|
|
365
|
+
usage: 'alias() | alias(name) | alias(name, line)',
|
|
366
|
+
detail: "no args → the whole map; one arg → that line or null; two → defines it, returns { ok, name, value, previous }",
|
|
367
|
+
run(ctx, name, value) {
|
|
368
|
+
if (name === undefined) return loadAliases();
|
|
369
|
+
if (value === undefined) return getAlias(String(name));
|
|
205
370
|
if (ctx.dryRun) {
|
|
206
|
-
ctx.out(`
|
|
207
|
-
|
|
208
|
-
// script branching on the exit status behaves the same under --dry-run.
|
|
209
|
-
return { ok: true, code: 0, dryRun: true };
|
|
371
|
+
ctx.out(` 🔖 alias(${name}) → would define: ${value}`);
|
|
372
|
+
return { ok: true, name: String(name), value: String(value), previous: null, dryRun: true };
|
|
210
373
|
}
|
|
211
|
-
// Same
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
374
|
+
// Same reservation rule the pit enforces: a shortcut that collides with a
|
|
375
|
+
// built-in would be silently dead, so it is refused rather than shadowed.
|
|
376
|
+
const r = setAlias(name, value, { isReserved });
|
|
377
|
+
ctx.out(r.ok ? ` 🔖 alias(${r.name}) → ${r.value}` : ` ✗ alias() — ${r.error}`);
|
|
378
|
+
return r;
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: "unalias",
|
|
383
|
+
summary: "forget a pit alias",
|
|
384
|
+
usage: "unalias(name)",
|
|
385
|
+
detail: "returns { ok, name, value } — { ok: false } when there was no such alias",
|
|
386
|
+
run(ctx, name) {
|
|
387
|
+
if (!name) throw new Error("moshscript: unalias(name) requires an alias name");
|
|
388
|
+
if (ctx.dryRun) { ctx.out(` 🔖 unalias(${name}) → would forget it`); return { ok: true, name: String(name), dryRun: true }; }
|
|
389
|
+
const r = removeAlias(name);
|
|
390
|
+
ctx.out(r.ok ? ` 🔖 unalias(${r.name})` : ` ✗ unalias() — ${r.error}`);
|
|
391
|
+
return r;
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: "runAlias",
|
|
396
|
+
summary: "run a pit alias by name, with extra arguments appended",
|
|
397
|
+
usage: "runAlias(name, ...args)",
|
|
398
|
+
detail: "returns { ok, code } like shell()/CLI verbs; { ok: false, code: 127 } when undefined",
|
|
399
|
+
// The expansion rule is the pit's (src/aliases.mjs): a leading `/` is a pit
|
|
400
|
+
// command, anything else is a shell line, and typed arguments are appended
|
|
401
|
+
// rather than substituted. A pit command routes to its CLI twin here —
|
|
402
|
+
// moshscript is not the pit, but `/agents claude` and `moshcode agents
|
|
403
|
+
// claude` are the same capability, which is the whole cliVerb premise.
|
|
404
|
+
run(ctx, name, ...args) {
|
|
405
|
+
if (!name) throw new Error("moshscript: runAlias(name) requires an alias name");
|
|
406
|
+
const value = getAlias(String(name));
|
|
407
|
+
if (value == null) {
|
|
408
|
+
ctx.out(` ✗ runAlias(${name}) — no alias named "${name}"`);
|
|
409
|
+
return { ok: false, code: 127 };
|
|
410
|
+
}
|
|
411
|
+
const line = expandAlias(value, args.map(String).join(" "));
|
|
412
|
+
if (line.startsWith("/")) {
|
|
413
|
+
const [verb, ...rest] = line.slice(1).split(/\s+/).filter(Boolean);
|
|
414
|
+
return runMoshcode(verb, rest, ctx);
|
|
415
|
+
}
|
|
416
|
+
return SHELL.run(ctx, line.replace(/^!/, ""));
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
// Reading the tools, not just running them. `stocks report NVDA` prints a
|
|
421
|
+
// table; a script wants the score. These call the same advis0r/feed layer the
|
|
422
|
+
// CLI renders from, so a verb here can never drift from its printed twin.
|
|
423
|
+
{
|
|
424
|
+
name: "stocksRead",
|
|
425
|
+
summary: "run a stocks query and RETURN its JSON (advis0r)",
|
|
426
|
+
usage: 'stocksRead("report", "NVDA")',
|
|
427
|
+
detail: "same arguments as stocks(); returns the parsed data, or null on error. needs await",
|
|
428
|
+
async run(ctx, ...args) {
|
|
429
|
+
const request = stocksArgs(args.map(String));
|
|
430
|
+
if (request.error) throw new Error(`moshscript: stocksRead() — ${request.error}`);
|
|
431
|
+
if (ctx.dryRun) { ctx.out(` 📈 stocksRead(${args.join(" ")}) → would query advis0r`); return null; }
|
|
432
|
+
ctx.out(` 📈 stocksRead(${args.join(" ")})`);
|
|
433
|
+
const res = await fetchAdvisor(request);
|
|
434
|
+
if (!res.ok) { ctx.out(` ! ${res.error || `advis0r returned ${res.status}`}`); return null; }
|
|
435
|
+
return res.data;
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "cryptoRead",
|
|
440
|
+
summary: "run a crypto query and RETURN its JSON (advis0r)",
|
|
441
|
+
usage: 'cryptoRead("report", "BTC/USD")',
|
|
442
|
+
detail: "same arguments as crypto(); returns the parsed data, or null on error. needs await",
|
|
443
|
+
async run(ctx, ...args) {
|
|
444
|
+
const request = cryptoArgs(args.map(String));
|
|
445
|
+
if (request.error) throw new Error(`moshscript: cryptoRead() — ${request.error}`);
|
|
446
|
+
if (ctx.dryRun) { ctx.out(` 🪙 cryptoRead(${args.join(" ")}) → would query advis0r`); return null; }
|
|
447
|
+
ctx.out(` 🪙 cryptoRead(${args.join(" ")})`);
|
|
448
|
+
const res = await fetchCrypto(request);
|
|
449
|
+
if (!res.ok) { ctx.out(` ! ${res.error || `advis0r returned ${res.status}`}`); return null; }
|
|
450
|
+
return res.data;
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
name: "newsRead",
|
|
455
|
+
summary: "fetch the news feeds and RETURN the headlines",
|
|
456
|
+
usage: 'newsRead({ list, limit })',
|
|
457
|
+
detail: "returns [{ title, link, source, date }, …] — your subscriptions, or a named list. needs await",
|
|
458
|
+
// `list` names one of the built-in feed lists; omit it for the operator's
|
|
459
|
+
// own subscriptions (the same reading list `/news` shows).
|
|
460
|
+
async run(ctx, opts = {}) {
|
|
461
|
+
const limit = Number(opts.limit) || 20;
|
|
462
|
+
if (ctx.dryRun) { ctx.out(` 📰 newsRead() → would fetch ${opts.list || "your"} feeds`); return []; }
|
|
463
|
+
let feeds;
|
|
464
|
+
if (opts.list) {
|
|
465
|
+
const list = resolveList(String(opts.list));
|
|
466
|
+
if (!list) throw new Error(`moshscript: newsRead() — no feed list named "${opts.list}"`);
|
|
467
|
+
const loaded = await loadListFeeds(list);
|
|
468
|
+
if (!loaded.ok) { ctx.out(` ! couldn't load ${opts.list}`); return []; }
|
|
469
|
+
feeds = loaded.feeds;
|
|
470
|
+
} else {
|
|
471
|
+
feeds = readingList().feeds;
|
|
223
472
|
}
|
|
224
|
-
|
|
473
|
+
ctx.out(` 📰 newsRead() → reading ${feeds.length} feed(s)…`);
|
|
474
|
+
const { items } = await collectNews(feeds);
|
|
475
|
+
return items.slice(0, limit).map(({ title, link, source, date }) => ({ title, link, source, date }));
|
|
225
476
|
},
|
|
226
477
|
},
|
|
227
478
|
|
|
@@ -261,29 +512,62 @@ const COMMANDS = [
|
|
|
261
512
|
},
|
|
262
513
|
{
|
|
263
514
|
name: "herdPrompt",
|
|
264
|
-
summary: "type a prompt into a herd session",
|
|
515
|
+
summary: "type a prompt into a herd session (local or remote)",
|
|
265
516
|
usage: "herdPrompt(name, text)",
|
|
266
|
-
detail: "returns { ok }; does not wait — use herdWait() to join",
|
|
517
|
+
detail: "returns { ok, task }; does not wait — use herdWait() to join",
|
|
267
518
|
run(ctx, name, ...words) {
|
|
268
519
|
const text = words.join(" ");
|
|
269
520
|
if (!name || !text) throw new Error("moshscript: herdPrompt(name, text) requires both");
|
|
270
521
|
if (ctx.dryRun) { ctx.out(` 💬 herdPrompt(${name}) → would send: ${text}`); return { ok: true, dryRun: true }; }
|
|
271
522
|
ctx.out(` 💬 herdPrompt(${name}) → ${text.slice(0, 60)}${text.length > 60 ? "…" : ""}`);
|
|
272
|
-
const
|
|
273
|
-
|
|
523
|
+
const session = String(name);
|
|
524
|
+
|
|
525
|
+
// A remote member takes the same call with the same arguments — the point
|
|
526
|
+
// of PRD 0011 R12 is that a script fanning across a local pty and a
|
|
527
|
+
// deployed agent contains no `if (remote)`. This one stays synchronous
|
|
528
|
+
// like every other local verb, so the no-`await` style keeps working: the
|
|
529
|
+
// request is in flight when it returns, and herdWait() is how a script
|
|
530
|
+
// joins on it, exactly as for a local session.
|
|
531
|
+
if (isRemoteMember(session)) {
|
|
532
|
+
const task = startTask(session, text, { screen: "" });
|
|
533
|
+
import("./herd-remote.mjs")
|
|
534
|
+
.then((remote) => remote.promptRemote(session, text)
|
|
535
|
+
.then((sent) => endTask(session, task, { state: sent.state || "done", artifact: sent.artifact || String(sent.error?.message || "") })))
|
|
536
|
+
.catch(() => endTask(session, task, { state: "done", artifact: "the request never left this machine" }));
|
|
537
|
+
return { ok: true, task, remote: true };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const task = startTask(session, text, { screen: capture(session, { lines: 60 }) });
|
|
541
|
+
const sent = sendPrompt(session, text);
|
|
542
|
+
if (!sent.ok) endTask(session, task, { state: "done", artifact: String(sent.error?.message || sent.error) });
|
|
543
|
+
return { ok: Boolean(sent.ok), task };
|
|
274
544
|
},
|
|
275
545
|
},
|
|
276
546
|
{
|
|
277
547
|
name: "herdWait",
|
|
278
548
|
summary: "BLOCK until a herd session is blocked, done, or idle",
|
|
279
|
-
usage: "herdWait(name, { states, timeout })",
|
|
280
|
-
detail: "returns the state it reached. needs await",
|
|
549
|
+
usage: "herdWait(name | [names], { states, timeout, any })",
|
|
550
|
+
detail: "one name returns the state it reached; a list returns the winner's name (any) or every result (all). needs await",
|
|
281
551
|
async run(ctx, name, opts = {}) {
|
|
282
552
|
if (!name) throw new Error("moshscript: herdWait(name) requires a session name");
|
|
283
553
|
const states = opts.states || ["blocked", "done", "idle"];
|
|
554
|
+
const timeout = opts.timeout ? { timeoutMs: Number(opts.timeout) } : {};
|
|
555
|
+
|
|
556
|
+
// A list of names is a join (PRD 0011 R8) — the thing every fan-out
|
|
557
|
+
// script so far has spelled out by hand as a polling loop.
|
|
558
|
+
if (Array.isArray(name)) {
|
|
559
|
+
const names = name.map(String);
|
|
560
|
+
const mode = opts.any ? "any" : "all";
|
|
561
|
+
if (ctx.dryRun) { ctx.out(` ⏳ herdWait([${names.join(", ")}]) → would wait for ${mode} of them to reach ${states.join("/")}`); return mode === "any" ? names[0] : names.map((n) => ({ name: n, state: "idle" })); }
|
|
562
|
+
ctx.out(` ⏳ herdWait([${names.join(", ")}]) → waiting for ${mode}…`);
|
|
563
|
+
const result = await waitForMany(names, states, { mode, ...timeout });
|
|
564
|
+
ctx.out(` ${result.outcome === "matched" ? "✅" : "⌛"} ${mode === "any" ? `${result.winner} first` : `${result.outcome}`}`);
|
|
565
|
+
return mode === "any" ? result.winner : result.results;
|
|
566
|
+
}
|
|
567
|
+
|
|
284
568
|
if (ctx.dryRun) { ctx.out(` ⏳ herdWait(${name}) → would wait for ${states.join("/")}`); return "idle"; }
|
|
285
569
|
ctx.out(` ⏳ herdWait(${name}) → waiting for ${states.join("/")}…`);
|
|
286
|
-
const result = await
|
|
570
|
+
const result = await waitMember(String(name), states, timeout);
|
|
287
571
|
ctx.out(` ${result.outcome === "matched" ? "✅" : "⌛"} ${name} is ${result.state}`);
|
|
288
572
|
return result.state;
|
|
289
573
|
},
|
|
@@ -296,7 +580,11 @@ const COMMANDS = [
|
|
|
296
580
|
run(ctx, name, opts = {}) {
|
|
297
581
|
if (!name) throw new Error("moshscript: herdRead(name) requires a session name");
|
|
298
582
|
if (ctx.dryRun) { ctx.out(` 📖 herdRead(${name}) → would read its screen`); return ""; }
|
|
299
|
-
|
|
583
|
+
const session = String(name);
|
|
584
|
+
// A remote has no screen; what it has is the last thing it said, and
|
|
585
|
+
// that is what `read` means for it (PRD 0011 R12).
|
|
586
|
+
if (isRemoteMember(session)) return String(remoteStatus(session)?.artifact || "");
|
|
587
|
+
return capture(session, { lines: Number(opts.lines) || 60 });
|
|
300
588
|
},
|
|
301
589
|
},
|
|
302
590
|
{
|
|
@@ -322,6 +610,41 @@ const COMMANDS = [
|
|
|
322
610
|
},
|
|
323
611
|
},
|
|
324
612
|
|
|
613
|
+
// The ledger (PRD 0011 R6). Same contract as herdRead/herdList and for the
|
|
614
|
+
// same reason: a script fans work out and then has to read what came back.
|
|
615
|
+
// `[]`/`null` on anything missing, never a throw — a script joining on four
|
|
616
|
+
// agents must not die because one of them has no history yet.
|
|
617
|
+
//
|
|
618
|
+
// herdPrompt("api", "port the auth routes");
|
|
619
|
+
// await herdWait("api");
|
|
620
|
+
// const [last] = herdTasks("api").slice(-1);
|
|
621
|
+
// say(herdTask(last.id).artifact);
|
|
622
|
+
{
|
|
623
|
+
name: "herdTasks",
|
|
624
|
+
summary: "every prompt submitted to a session, and what came of it",
|
|
625
|
+
usage: "herdTasks(name)",
|
|
626
|
+
detail: "returns [{ id, text, state, status, submitted, durationMs }, …], oldest first",
|
|
627
|
+
run(ctx, name) {
|
|
628
|
+
if (!name) throw new Error("moshscript: herdTasks(name) requires a session name");
|
|
629
|
+
if (ctx.dryRun) { ctx.out(` 📒 herdTasks(${name}) → would read the ledger`); return []; }
|
|
630
|
+
try {
|
|
631
|
+
return readTasks(String(name)).map(({ id, text, state, status, submitted, endedAt, durationMs }) =>
|
|
632
|
+
({ id, text, state, status, submitted, endedAt, durationMs }));
|
|
633
|
+
} catch { return []; }
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: "herdTask",
|
|
638
|
+
summary: "one task by id — its transitions and its output",
|
|
639
|
+
usage: "herdTask(id)",
|
|
640
|
+
detail: "returns { id, session, text, transitions, artifact, state } or null",
|
|
641
|
+
run(ctx, id) {
|
|
642
|
+
if (!id) throw new Error("moshscript: herdTask(id) requires a task id");
|
|
643
|
+
if (ctx.dryRun) { ctx.out(` 📒 herdTask(${id}) → would read the ledger`); return null; }
|
|
644
|
+
try { return findTask(String(id)); } catch { return null; }
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
|
|
325
648
|
// CLI verbs — each is `moshcode <name> ...args`. This is the whole point:
|
|
326
649
|
// scripting the CLI. Add a capability by adding a line here.
|
|
327
650
|
//
|
|
@@ -334,6 +657,7 @@ const COMMANDS = [
|
|
|
334
657
|
cliVerb("agents", "launch an autonomous agent session (moshcode agents <engine>)"),
|
|
335
658
|
cliVerb("herd", "drive the herd (moshcode herd <verb>) — see herdStart/herdWait for values"),
|
|
336
659
|
cliVerb("ps", "print the herd roster"),
|
|
660
|
+
cliVerb("cost", "print what the herd is spending (moshcode cost [name] [--all])"),
|
|
337
661
|
cliVerb("start", "raw-launch an engine (moshcode start <engine>)"),
|
|
338
662
|
cliVerb("install", "install an engine or workflow tool"),
|
|
339
663
|
cliVerb("upgrade", "upgrade moshcode, engines, and tools"),
|
|
@@ -350,6 +674,7 @@ const COMMANDS = [
|
|
|
350
674
|
cliVerb("supabase", "drive the Supabase CLI (local stack, migrations, functions)"),
|
|
351
675
|
cliVerb("doppler", "drive the Doppler CLI (secrets, env injection)"),
|
|
352
676
|
cliVerb("doctl", "drive the DigitalOcean CLI (droplets, apps, databases)"),
|
|
677
|
+
cliVerb("gradient", "drive the DigitalOcean Gradient ADK (init, run, deploy, logs, evaluate)"),
|
|
353
678
|
cliVerb("turso", "drive the Turso CLI (auth, databases, replicas)"),
|
|
354
679
|
cliVerb("tailscale", "drive the Tailscale CLI (mesh VPN: up, status, ssh, serve)"),
|
|
355
680
|
cliVerb("coral", "drive the Coral CLI (SQL over APIs, databases, and internal systems)"),
|
|
@@ -357,6 +682,31 @@ const COMMANDS = [
|
|
|
357
682
|
cliVerb("mcpjam", "drive the MCPJam CLI (test, debug, and validate MCP servers)"),
|
|
358
683
|
cliVerb("trade", "look up tickers, inspect markets, and preview/place Alpaca orders"),
|
|
359
684
|
cliVerb("pwd", "print the current repo/location"),
|
|
685
|
+
|
|
686
|
+
// Research and feeds. The *Read() verbs above return the data; these are the
|
|
687
|
+
// rendered CLI, for when a script wants the table on the operator's screen.
|
|
688
|
+
cliVerb("stocks", "research tickers via advis0r (report, discover, signals, research)"),
|
|
689
|
+
cliVerb("crypto", "research crypto pairs via advis0r (quote, report, bars, book)"),
|
|
690
|
+
cliVerb("advisor", "query advis0r directly"),
|
|
691
|
+
cliVerb("news", "read, search, and subscribe to news feeds"),
|
|
692
|
+
cliVerb("rss", "manage RSS subscriptions and reading lists"),
|
|
693
|
+
|
|
694
|
+
// Extending moshcode from a script — the same fan-out `mcp`/`skill` do.
|
|
695
|
+
cliVerb("plugin", "install/manage moshcode plugins from the marketplace"),
|
|
696
|
+
cliVerb("engines", "list coding engines and whether they're installed"),
|
|
697
|
+
cliVerb("tools", "list the adjacent workflow CLIs and whether they're installed"),
|
|
698
|
+
|
|
699
|
+
// Hosting: the Moshpit side of the CLI, so a deploy script can claim a name,
|
|
700
|
+
// serve a site, and bring the resolver up without dropping to $SHELL.
|
|
701
|
+
cliVerb("dns", "drive the Moshpit DNS bridge (enable, status, resolve)"),
|
|
702
|
+
cliVerb("doh", "run/inspect the DNS-over-HTTPS endpoint"),
|
|
703
|
+
cliVerb("site", "scaffold and publish a site"),
|
|
704
|
+
cliVerb("serve", "serve a directory over HTTP"),
|
|
705
|
+
cliVerb("template", "scaffold from a moshcode template"),
|
|
706
|
+
|
|
707
|
+
// Settings sync (PRD 0010) — needs an account, so pair with requireLogin().
|
|
708
|
+
cliVerb("save", "push local settings to your moshcode account"),
|
|
709
|
+
cliVerb("load", "pull settings from your moshcode account"),
|
|
360
710
|
];
|
|
361
711
|
|
|
362
712
|
/** A fresh registry preloaded with the built-in vocabulary. */
|