moshcode 0.33.0 → 0.34.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 +58 -0
- package/package.json +1 -1
- package/src/cli-schema.mjs +31 -5
- package/src/help.mjs +5 -1
- package/src/herd-cli.mjs +76 -2
- package/src/herd-state.mjs +19 -1
package/README.md
CHANGED
|
@@ -125,10 +125,68 @@ moshcode attach api # step in; Ctrl-b d steps back out
|
|
|
125
125
|
moshcode kill api # end it
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
### A workspace: a few shells and an agent
|
|
129
|
+
|
|
130
|
+
This is what most people actually want — a couple of shells to work in and an
|
|
131
|
+
agent or two running beside them, none of which die when the terminal does:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
cd ~/src/coinpay
|
|
135
|
+
moshcode herd shell --name work # a plain $SHELL
|
|
136
|
+
moshcode herd shell --name logs # another
|
|
137
|
+
moshcode agents claude -d --name api # and an agent
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
$ moshcode ps
|
|
142
|
+
api claude blocked ~/src/coinpay 3m
|
|
143
|
+
logs shell idle ~/src/coinpay 3m
|
|
144
|
+
work shell idle ~/src/coinpay 3m
|
|
145
|
+
|
|
146
|
+
⚠ 1 waiting on you — moshcode attach api
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`moshcode attach work` puts you in one; `Ctrl-b s` hops between all three;
|
|
150
|
+
`Ctrl-b d` leaves the lot running. Close the laptop and they are still there.
|
|
151
|
+
|
|
152
|
+
### Agents moshcode does not ship
|
|
153
|
+
|
|
154
|
+
`start` only knows the engines moshcode installs. `run` takes anything —
|
|
155
|
+
an agent with no install spec here, a build, a script:
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
moshcode herd run --name cur -- cursor-agent
|
|
159
|
+
moshcode herd run --name build -- npm run watch
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Everything after `--` is the command, flags and all. These get a roster entry
|
|
163
|
+
and the same state detection as a known engine: the shared rules match what a
|
|
164
|
+
terminal *draws* — a y/n prompt, a numbered menu, "esc to interrupt" — not
|
|
165
|
+
anything engine-specific, so an agent moshcode has never heard of still shows up
|
|
166
|
+
`blocked` when it stops to ask you something.
|
|
167
|
+
|
|
128
168
|
Close the terminal, drop the SSH link, come back tomorrow — `moshcode ps` still
|
|
129
169
|
answers, and `moshcode attach` puts you back inside. In the pit the same verbs
|
|
130
170
|
are `/ps`, `/attach`, `/kill`, and the roster prints on the way in.
|
|
131
171
|
|
|
172
|
+
### Switching between them without leaving
|
|
173
|
+
|
|
174
|
+
Every session lives in one tmux server that moshcode owns, so once you are
|
|
175
|
+
attached to any of them you can move around the whole herd without going back to
|
|
176
|
+
the pit:
|
|
177
|
+
|
|
178
|
+
| key | |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `Ctrl-b s` | pick from a list of every session |
|
|
181
|
+
| `Ctrl-b )` / `Ctrl-b (` | next / previous session |
|
|
182
|
+
| `Ctrl-b L` | back to the one you were just in |
|
|
183
|
+
| `Ctrl-b d` | detach — the session keeps running |
|
|
184
|
+
|
|
185
|
+
That server is started without your `~/.tmux.conf`, so these are the stock
|
|
186
|
+
bindings whatever your own tmux does with the prefix. Under the no-tmux fallback
|
|
187
|
+
there is no switcher: detach with `Ctrl-]` and `moshcode attach <name>` the next
|
|
188
|
+
one.
|
|
189
|
+
|
|
132
190
|
### Which one needs you
|
|
133
191
|
|
|
134
192
|
Every session carries a state: `working`, `blocked`, `done`, `idle`, or
|
package/package.json
CHANGED
package/src/cli-schema.mjs
CHANGED
|
@@ -79,12 +79,25 @@ export const CORE_CLI_COMMANDS = [
|
|
|
79
79
|
verbs: "HERD_VERBS",
|
|
80
80
|
flags: [["--json", "machine-readable, on every verb", ""]],
|
|
81
81
|
examples: [
|
|
82
|
-
["
|
|
83
|
-
["moshcode herd
|
|
84
|
-
["moshcode herd
|
|
82
|
+
["# a workspace: two shells and an agent, none of which die with this terminal", ""],
|
|
83
|
+
["moshcode herd shell --name work", "a plain $SHELL you can come back to"],
|
|
84
|
+
["moshcode herd shell --name logs", "another one"],
|
|
85
|
+
["moshcode agents claude -d --name api", "an agent, detached"],
|
|
86
|
+
["moshcode ps", "all three, and which one is blocked"],
|
|
87
|
+
["moshcode attach api", "step in · Ctrl-b s switches · Ctrl-b d leaves it running"],
|
|
88
|
+
["", ""],
|
|
89
|
+
["# an agent moshcode has no install spec for", ""],
|
|
90
|
+
["moshcode herd run --name cur -- cursor-agent", "anything at all runs in the herd"],
|
|
91
|
+
["", ""],
|
|
92
|
+
["# driving one without attaching", ""],
|
|
93
|
+
["moshcode herd prompt api \"run the tests\" --wait", "hand it work, block until it lands"],
|
|
94
|
+
["moshcode herd read api --lines 40", "read its screen"],
|
|
85
95
|
],
|
|
86
96
|
seeAlso: ["ps", "attach", "wait", "restore", "start"],
|
|
87
|
-
note: "
|
|
97
|
+
note: "`start` is for the engines moshcode installs; `run` and `shell` take anything else, "
|
|
98
|
+
+ "so an agent it has never heard of still gets a roster entry and blocked/idle detection. "
|
|
99
|
+
+ "sessions live in a tmux server moshcode owns, or under script(1) when there is no tmux. "
|
|
100
|
+
+ "with neither, launches stay in the foreground and say so.",
|
|
88
101
|
},
|
|
89
102
|
{
|
|
90
103
|
name: "ps",
|
|
@@ -103,7 +116,10 @@ export const CORE_CLI_COMMANDS = [
|
|
|
103
116
|
synopsis: [["moshcode attach <name>", "detach again with Ctrl-b d (or Ctrl-] without tmux)"]],
|
|
104
117
|
examples: [["moshcode attach api", ""]],
|
|
105
118
|
seeAlso: ["ps", "herd", "kill"],
|
|
106
|
-
note: "detaching leaves the session running
|
|
119
|
+
note: "detaching leaves the session running; ending it is `moshcode kill`. "
|
|
120
|
+
+ "the whole herd shares one tmux server, so from inside any session Ctrl-b s picks another, "
|
|
121
|
+
+ "Ctrl-b ) and Ctrl-b ( step through them, and Ctrl-b L goes back to the last one — "
|
|
122
|
+
+ "no switcher under the no-tmux fallback, where Ctrl-] detaches instead.",
|
|
107
123
|
},
|
|
108
124
|
{
|
|
109
125
|
name: "kill",
|
|
@@ -720,6 +736,16 @@ export const HERD_VERBS = [
|
|
|
720
736
|
["--agent", "autonomous mode — bypasses the engine's approvals", ""],
|
|
721
737
|
["--json", "machine-readable", ""],
|
|
722
738
|
] },
|
|
739
|
+
{ name: "run", description: "run ANY command in the herd — an agent moshcode does not ship, a build, a script",
|
|
740
|
+
synopsis: [["moshcode herd run [--name <slug>] -- <command…>", "everything after -- is the command"]],
|
|
741
|
+
flags: [
|
|
742
|
+
["--name <slug>", "session name", "<command>-<dir>"],
|
|
743
|
+
["--cwd <dir>", "where to run it", "this directory"],
|
|
744
|
+
["--json", "machine-readable", ""],
|
|
745
|
+
] },
|
|
746
|
+
{ name: "shell", description: "a plain $SHELL in the herd",
|
|
747
|
+
synopsis: [["moshcode herd shell [--name <slug>]", ""]],
|
|
748
|
+
flags: [["--name <slug>", "session name", "shell-<dir>"], ["--cwd <dir>", "where to run it", "this directory"]] },
|
|
723
749
|
{ name: "attach", description: "put this terminal inside a session",
|
|
724
750
|
synopsis: [["moshcode herd attach <name>", ""]] },
|
|
725
751
|
{ name: "kill", description: "end a session",
|
package/src/help.mjs
CHANGED
|
@@ -167,7 +167,11 @@ export function wrap(text, indent = 0, width = WIDTH) {
|
|
|
167
167
|
|
|
168
168
|
/** ` name description`, wrapped, for a two-column list. */
|
|
169
169
|
function row(left, right, pad = 22) {
|
|
170
|
-
|
|
170
|
+
// padEnd does nothing when the left column is already wider than the pad, so
|
|
171
|
+
// a long example used to butt straight against its comment:
|
|
172
|
+
// `… "run the tests" --wait# hand it work`. One guaranteed space keeps the
|
|
173
|
+
// two readable however long the command gets.
|
|
174
|
+
const head = left.length >= pad ? ` ${left} ` : ` ${left.padEnd(pad)}`;
|
|
171
175
|
if (!right) return head.trimEnd();
|
|
172
176
|
const wrapped = wrap(right, head.length, WIDTH);
|
|
173
177
|
return `${head}${wrapped}`;
|
package/src/herd-cli.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import path from "node:path";
|
|
|
11
11
|
import {
|
|
12
12
|
attachSession, capture, defaultName, detectSubstrate, forgetSession, HERD_SOCKET,
|
|
13
13
|
herdDir, killSession, listSessions, readManifest, rememberSession, sendKeys, sendPrompt,
|
|
14
|
-
startSession, stopRuntime, substrateNote, validName, NAME_RE,
|
|
14
|
+
slugifyName, startSession, stopRuntime, substrateNote, validName, NAME_RE,
|
|
15
15
|
} from "./herd.mjs";
|
|
16
16
|
import { clearReport, reportState, STATES, withState } from "./herd-state.mjs";
|
|
17
17
|
import { ENGINES, resolveEngine, resolveExecutable, agentLaunchArgs } from "./engines.mjs";
|
|
@@ -182,6 +182,79 @@ export function herdStart(argv, { write = console.log } = {}) {
|
|
|
182
182
|
return EXIT.matched;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Run anything at all in the herd — a shell, or an agent moshcode does not ship
|
|
187
|
+
* an install spec for.
|
|
188
|
+
*
|
|
189
|
+
* `herd start` is deliberately limited to the engines in ENGINES, because it
|
|
190
|
+
* does engine-specific things: agent-mode flags, env stripping, resume args.
|
|
191
|
+
* That made the herd useless for the two most common things people actually
|
|
192
|
+
* want in it — a couple of shells, and whichever agent they use that moshcode
|
|
193
|
+
* has never heard of (cursor-agent, copilot, amp, a local script).
|
|
194
|
+
*
|
|
195
|
+
* So this is the escape hatch, and it is the same model herdr uses: a session
|
|
196
|
+
* holds a process, and an agent is just a process we happen to recognise.
|
|
197
|
+
* Detection still works, because the shared rules in herd-state.mjs match what
|
|
198
|
+
* a terminal draws — a y/n prompt, a numbered menu, "esc to interrupt" — rather
|
|
199
|
+
* than anything engine-specific. An unknown agent that stops to ask a question
|
|
200
|
+
* shows up `blocked` without moshcode knowing what it is.
|
|
201
|
+
*/
|
|
202
|
+
export function herdRun(argv, { write = console.log, shell = false } = {}) {
|
|
203
|
+
const substrate = requireSubstrate(write);
|
|
204
|
+
if (!substrate) return EXIT.usage;
|
|
205
|
+
|
|
206
|
+
const flags = { name: null, cwd: process.cwd(), json: false };
|
|
207
|
+
const command = [];
|
|
208
|
+
let afterSeparator = false;
|
|
209
|
+
for (let i = 0; i < argv.length; i++) {
|
|
210
|
+
const a = argv[i];
|
|
211
|
+
if (afterSeparator) { command.push(a); continue; }
|
|
212
|
+
// Everything after `--` belongs to the command, flags included — otherwise
|
|
213
|
+
// `herd run -- claude --json` would have its --json eaten by us.
|
|
214
|
+
if (a === "--") { afterSeparator = true; }
|
|
215
|
+
else if (a === "--name") flags.name = argv[++i];
|
|
216
|
+
else if (a.startsWith("--name=")) flags.name = a.slice(7);
|
|
217
|
+
else if (a === "--cwd") flags.cwd = path.resolve(argv[++i] || ".");
|
|
218
|
+
else if (a === "--json") flags.json = true;
|
|
219
|
+
else command.push(a);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (shell && !command.length) {
|
|
223
|
+
command.push(process.env.SHELL || (process.platform === "win32" ? "cmd.exe" : "/bin/sh"));
|
|
224
|
+
}
|
|
225
|
+
if (!command.length) {
|
|
226
|
+
write(err('usage: moshcode herd run [--name <slug>] -- <command…>'));
|
|
227
|
+
write(info(`e.g. ${acid('moshcode herd run --name build -- npm run watch')}`));
|
|
228
|
+
return EXIT.usage;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const [bin, ...args] = command;
|
|
232
|
+
const label = shell ? "shell" : path.basename(bin);
|
|
233
|
+
const taken = listSessions().map((s) => s.name);
|
|
234
|
+
const name = flags.name || defaultName(slugifyName(label), flags.cwd, taken);
|
|
235
|
+
if (!validName(name)) {
|
|
236
|
+
write(err(`invalid name ${JSON.stringify(name)} — must match ${NAME_RE}`));
|
|
237
|
+
return EXIT.usage;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const started = startSession({ name, engine: label, bin, args, cwd: flags.cwd, substrate });
|
|
241
|
+
if (!started.ok) {
|
|
242
|
+
write(err(String(started.error?.message || started.error)));
|
|
243
|
+
return EXIT.usage;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (flags.json) {
|
|
247
|
+
write(JSON.stringify({ name, engine: label, cwd: flags.cwd, substrate }, null, 2));
|
|
248
|
+
return EXIT.matched;
|
|
249
|
+
}
|
|
250
|
+
write(ok(`${bone(name)} — ${label} running in the herd. the prompt is yours.`));
|
|
251
|
+
write(info(`attach: ${acid(`moshcode attach ${name}`)} · roster: ${acid("moshcode ps")}`));
|
|
252
|
+
return EXIT.matched;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** A plain $SHELL in the herd — the common case of herdRun. */
|
|
256
|
+
export const herdShell = (argv, options = {}) => herdRun(argv, { ...options, shell: true });
|
|
257
|
+
|
|
185
258
|
/**
|
|
186
259
|
* Pull the herd flags out of an engine launch (PRD 0009 R3).
|
|
187
260
|
*
|
|
@@ -646,7 +719,8 @@ export function herdStop(argv, { write = console.log } = {}) {
|
|
|
646
719
|
|
|
647
720
|
const VERBS = {
|
|
648
721
|
ps: herdPs, list: herdPs, status: herdStatus,
|
|
649
|
-
start: herdStart,
|
|
722
|
+
start: herdStart, run: herdRun, shell: herdShell,
|
|
723
|
+
attach: herdAttach, kill: herdKill, prune: herdPrune,
|
|
650
724
|
read: herdRead, prompt: herdPrompt, "send-keys": herdSendKeys,
|
|
651
725
|
wait: herdWait, restore: herdRestore, report: herdReport,
|
|
652
726
|
notify: herdNotify, watch: herdWatch, stop: herdStop,
|
package/src/herd-state.mjs
CHANGED
|
@@ -80,7 +80,25 @@ export const COMMON_RULES = {
|
|
|
80
80
|
/\bctrl\+c to (?:stop|cancel|interrupt)\b/i,
|
|
81
81
|
/\bpress esc to cancel\b/i,
|
|
82
82
|
],
|
|
83
|
-
|
|
83
|
+
// A prompt sitting at the very end of the screen, waiting for a keystroke —
|
|
84
|
+
// a shell's `$`, a root `#`, zsh/starship's `❯`/`➜`, an agent's `>` composer.
|
|
85
|
+
// Anchored to the end of the *capture* rather than to any line, because a `$`
|
|
86
|
+
// in the middle of output is a dollar sign and not an invitation.
|
|
87
|
+
//
|
|
88
|
+
// Checked after blocked and working, so the cost of a false positive is only
|
|
89
|
+
// `idle` where `unknown` was already the honest answer. This is what stops a
|
|
90
|
+
// couple of shells in the herd reading `unknown` forever.
|
|
91
|
+
idle: [
|
|
92
|
+
// The glyph last: `… $`, `… #`, `… ❯`.
|
|
93
|
+
/[$%#>❯➜»]\s*$/,
|
|
94
|
+
// The glyph first, with the path after it: `➜ ~/src/api `, which is what
|
|
95
|
+
// zsh and starship actually draw. Restricted to ❯ and ➜ because those are
|
|
96
|
+
// prompt characters and almost nothing else; `>` and `#` in that position
|
|
97
|
+
// are markdown quotes and headings, which agents print all the time.
|
|
98
|
+
// Claude Code's `❯ 2. Dark mode` selector reaches the blocked rule first,
|
|
99
|
+
// so a menu still reads blocked rather than idle.
|
|
100
|
+
/(?:^|\n)[^\n]*[❯➜][^\n]*$/,
|
|
101
|
+
],
|
|
84
102
|
};
|
|
85
103
|
|
|
86
104
|
/**
|