@rucher/cli 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +13 -2
- package/dist/cli.mjs +1027 -0
- package/package.json +52 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Florian Varrin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @rucher/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `rucher` binary: status, journal and sessions of your Claude Code sub-agents, from the terminal. Built on [`@rucher/carnet`](../carnet).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
rucher init --name shop --repos ~/code/shop-api,~/code/shop-web
|
|
7
|
+
rucher status --watch # the status pane
|
|
8
|
+
rucher log # the journal
|
|
9
|
+
rucher sessions # named sessions
|
|
10
|
+
rucher resume shop-auth # back into a named session
|
|
11
|
+
rucher serve # the Carnet MCP server, started by the Claude Code plugin
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
See the repository [README](../../README.md) for install and the plugin, and `docs/carnet-spec.md` for every command.
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,1027 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import tab from "@bomb.sh/tab/citty";
|
|
6
|
+
import { CarnetError, LEGEND, addGroupRepo, claudeCode, formatClock, handleSessionStart, handleUserPromptSubmit, hostById, initGroup, isGroupRoot, locateGroup, openCarnet, readRegistry, removeGroupRepo, renderSections, repoPathFor, shortId, unregisterGroups } from "@rucher/carnet";
|
|
7
|
+
import { styleText } from "node:util";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { serveCarnet } from "@rucher/carnet/mcp";
|
|
10
|
+
// src/completion.ts
|
|
11
|
+
const hiddenCommands = /* @__PURE__ */ new Set(["hook", "serve"]);
|
|
12
|
+
function quietly(suggest) {
|
|
13
|
+
try {
|
|
14
|
+
suggest();
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
function suggestSessions(context, complete) {
|
|
18
|
+
quietly(() => {
|
|
19
|
+
const carnet = openCarnet(context);
|
|
20
|
+
try {
|
|
21
|
+
const sessions = carnet.sessions.list({ includeTemporary: true });
|
|
22
|
+
const suggestions = [...sessions.flatMap((session) => session.name ? [{
|
|
23
|
+
value: session.name,
|
|
24
|
+
description: session.purpose ?? session.status
|
|
25
|
+
}] : []), ...sessions.flatMap((session) => session.name ? [] : [{
|
|
26
|
+
value: shortId(session.id),
|
|
27
|
+
description: session.status
|
|
28
|
+
}])];
|
|
29
|
+
for (const suggestion of suggestions) if (!context.typedWords.slice(0, -1).includes(suggestion.value)) complete(suggestion.value, suggestion.description);
|
|
30
|
+
} finally {
|
|
31
|
+
carnet.close();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function suggestGroups(context, complete) {
|
|
36
|
+
quietly(() => {
|
|
37
|
+
const groups = readRegistry(context.env);
|
|
38
|
+
for (const group of groups) complete(groups.filter((other) => other.name === group.name).length > 1 ? group.path : group.name, group.path);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function suggestRepos(context, complete) {
|
|
42
|
+
quietly(() => {
|
|
43
|
+
for (const repo of locateGroup(context).repos) complete(repo, "");
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function suggestDirectories(context, complete) {
|
|
47
|
+
quietly(() => {
|
|
48
|
+
const partial = context.typedWords.at(-1) ?? "";
|
|
49
|
+
const base = partial.endsWith("/") ? partial : dirname(partial) === "." ? "" : `${dirname(partial)}/`;
|
|
50
|
+
for (const entry of readdirSync(resolve(context.cwd, base || "."), { withFileTypes: true })) if (entry.isDirectory() && !entry.name.startsWith(".")) complete(join(base, entry.name), "");
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function processContext() {
|
|
54
|
+
const separator = process.argv.indexOf("--");
|
|
55
|
+
const typedWords = separator === -1 ? [] : process.argv.slice(separator + 1);
|
|
56
|
+
return {
|
|
57
|
+
cwd: process.cwd(),
|
|
58
|
+
env: process.env,
|
|
59
|
+
typedWords
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const sessions = (complete) => suggestSessions(processContext(), complete);
|
|
63
|
+
const groups = (complete) => suggestGroups(processContext(), complete);
|
|
64
|
+
const repos = (complete) => suggestRepos(processContext(), complete);
|
|
65
|
+
const directories = (complete) => suggestDirectories(processContext(), complete);
|
|
66
|
+
const completionConfig = { subCommands: {
|
|
67
|
+
status: { options: { session: sessions } },
|
|
68
|
+
log: { options: { session: sessions } },
|
|
69
|
+
init: { args: { directory: directories } },
|
|
70
|
+
resume: { args: { name: sessions } },
|
|
71
|
+
sessions: { subCommands: { merge: { args: {
|
|
72
|
+
from: sessions,
|
|
73
|
+
into: sessions
|
|
74
|
+
} } } },
|
|
75
|
+
groups: { subCommands: { forget: { args: { reference: groups } } } },
|
|
76
|
+
group: { subCommands: {
|
|
77
|
+
add: { args: { path: directories } },
|
|
78
|
+
remove: { args: { path: repos } }
|
|
79
|
+
} }
|
|
80
|
+
} };
|
|
81
|
+
async function setupCompletion(main) {
|
|
82
|
+
const subCommands = await (typeof main.subCommands === "function" ? main.subCommands() : main.subCommands);
|
|
83
|
+
const visible = Object.fromEntries(Object.entries(subCommands ?? {}).filter(([name]) => !hiddenCommands.has(name)));
|
|
84
|
+
await tab({
|
|
85
|
+
...main,
|
|
86
|
+
subCommands: visible
|
|
87
|
+
}, completionConfig);
|
|
88
|
+
const complete = visible["complete"];
|
|
89
|
+
if (complete) main.subCommands = {
|
|
90
|
+
...subCommands,
|
|
91
|
+
complete
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// package.json
|
|
95
|
+
var version = "0.1.0";
|
|
96
|
+
// src/color.ts
|
|
97
|
+
function paint(io) {
|
|
98
|
+
const style = (name) => (text) => io.color ? styleText(name, text, { validateStream: false }) : text;
|
|
99
|
+
return {
|
|
100
|
+
bold: style("bold"),
|
|
101
|
+
dim: style("dim"),
|
|
102
|
+
yellow: style("yellow"),
|
|
103
|
+
red: style("red"),
|
|
104
|
+
cyan: style("cyan")
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// src/commands/init.ts
|
|
108
|
+
function syncContextFiles(group, mode) {
|
|
109
|
+
return group.hosts.flatMap((id) => hostById(id).contextFiles.map((file) => ({
|
|
110
|
+
path: file.path,
|
|
111
|
+
result: file.sync(group, mode)
|
|
112
|
+
})));
|
|
113
|
+
}
|
|
114
|
+
function init(options, io) {
|
|
115
|
+
const colors = paint(io);
|
|
116
|
+
const root = resolve(io.cwd, options.directory ?? ".");
|
|
117
|
+
let group;
|
|
118
|
+
try {
|
|
119
|
+
group = initGroup(root, {
|
|
120
|
+
name: options.name,
|
|
121
|
+
repos: options.repos?.map((repo) => resolve(io.cwd, repo)),
|
|
122
|
+
env: io.env
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error instanceof CarnetError) {
|
|
126
|
+
io.stderr(`error: ${error.message}\n`);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
const lines = [
|
|
132
|
+
`${colors.bold(group.name)} initialised in ${group.root}`,
|
|
133
|
+
` repos: ${group.repos.length ? group.repos.join(", ") : "none yet, add them with `rucher group add <path>`"}`,
|
|
134
|
+
` database: ${group.dbPath}`
|
|
135
|
+
];
|
|
136
|
+
for (const { path, result } of syncContextFiles(group, "merge")) {
|
|
137
|
+
if (result === "unchanged") continue;
|
|
138
|
+
lines.push(result === "no-markers" ? ` kept existing ${path} ${colors.dim("(add the rucher:repos markers to keep its repo list in sync, and a line: multi-agent work uses the rucher tools)")}` : ` ${result} ${path}`);
|
|
139
|
+
}
|
|
140
|
+
const hosts = group.hosts.map((id) => hostById(id).displayName).join(" or ");
|
|
141
|
+
lines.push("", `Next: start ${hosts} in this directory, then \`rucher status --watch\` in a side pane.`);
|
|
142
|
+
io.stdout(`${lines.join("\n")}\n`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
// src/commands/group.ts
|
|
146
|
+
function groupShow(io) {
|
|
147
|
+
const colors = paint(io);
|
|
148
|
+
const group = locateGroup({
|
|
149
|
+
cwd: io.cwd,
|
|
150
|
+
env: io.env
|
|
151
|
+
});
|
|
152
|
+
const repos = group.repos.length ? group.repos.map((r) => ` ${basename(r)} ${colors.dim(r)}`).join("\n") : ` ${colors.dim("none")}`;
|
|
153
|
+
io.stdout(`${colors.bold(group.name)} ${colors.dim(group.root)}\nrepos:\n${repos}\n`);
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
function syncNotes(group, mode) {
|
|
157
|
+
return syncContextFiles(group, mode).filter(({ result }) => result !== "unchanged").map(({ path, result }) => result === "no-markers" ? `${path} has no rucher:repos markers, update its repo list yourself` : `${result} ${path}`);
|
|
158
|
+
}
|
|
159
|
+
function withNotes(line, notes) {
|
|
160
|
+
return `${line}${notes.length ? ` (${notes.join(", ")})` : ""}\n`;
|
|
161
|
+
}
|
|
162
|
+
function carnetErrors(io, run) {
|
|
163
|
+
try {
|
|
164
|
+
return run();
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (error instanceof CarnetError) {
|
|
167
|
+
io.stderr(`error: ${error.message}\n`);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function groupAdd(repo, io) {
|
|
174
|
+
const found = locateGroup({
|
|
175
|
+
cwd: io.cwd,
|
|
176
|
+
env: io.env
|
|
177
|
+
});
|
|
178
|
+
return carnetErrors(io, () => {
|
|
179
|
+
const path = resolve(io.cwd, repo);
|
|
180
|
+
const group = addGroupRepo(found.root, path);
|
|
181
|
+
io.stdout(withNotes(`added ${basename(path)} to ${group.name}`, syncNotes(group, "merge")));
|
|
182
|
+
return 0;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function groupRemove(repo, io) {
|
|
186
|
+
const found = locateGroup({
|
|
187
|
+
cwd: io.cwd,
|
|
188
|
+
env: io.env
|
|
189
|
+
});
|
|
190
|
+
return carnetErrors(io, () => {
|
|
191
|
+
const path = resolve(io.cwd, repo);
|
|
192
|
+
const group = removeGroupRepo(found.root, path);
|
|
193
|
+
io.stdout(withNotes(`removed ${basename(path)} from ${group.name}`, syncNotes(group, "exact")));
|
|
194
|
+
return 0;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function groupSync(io) {
|
|
198
|
+
const group = locateGroup({
|
|
199
|
+
cwd: io.cwd,
|
|
200
|
+
env: io.env
|
|
201
|
+
});
|
|
202
|
+
const notes = syncNotes(group, "exact");
|
|
203
|
+
io.stdout(notes.length ? withNotes(`synced ${group.name}`, notes) : `${group.name} already in sync\n`);
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
// src/commands/groups.ts
|
|
207
|
+
function missing(reference) {
|
|
208
|
+
return !isGroupRoot(reference.path);
|
|
209
|
+
}
|
|
210
|
+
function groupsList(io) {
|
|
211
|
+
const colors = paint(io);
|
|
212
|
+
const groups = readRegistry(io.env);
|
|
213
|
+
if (groups.length === 0) {
|
|
214
|
+
io.stdout(`${colors.dim("no registered group")}\n`);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
const width = Math.max(...groups.map((group) => group.name.length));
|
|
218
|
+
const lines = groups.map((group) => `${colors.bold(group.name.padEnd(width))} ${group.path}${missing(group) ? ` ${colors.yellow("missing")}` : ""}`);
|
|
219
|
+
io.stdout(`${lines.join("\n")}\n`);
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
function groupsForget(reference, io) {
|
|
223
|
+
const groups = readRegistry(io.env);
|
|
224
|
+
const byPath = groups.filter((group) => group.path === resolve(io.cwd, reference));
|
|
225
|
+
const matches = byPath.length ? byPath : groups.filter((group) => group.name === reference);
|
|
226
|
+
const [match] = matches;
|
|
227
|
+
if (!match) {
|
|
228
|
+
io.stderr(`error: no registered group named or located at "${reference}"\n`);
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
if (matches.length > 1) {
|
|
232
|
+
io.stderr(`error: several groups are named "${reference}", forget one by path:\n${matches.map((group) => ` ${group.path}`).join("\n")}\n`);
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
unregisterGroups([match.path], io.env);
|
|
236
|
+
io.stdout(`forgot ${match.name} (${match.path}); its meta directory is untouched\n`);
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
function groupsPrune(io) {
|
|
240
|
+
const gone = readRegistry(io.env).filter(missing);
|
|
241
|
+
if (gone.length === 0) {
|
|
242
|
+
io.stdout("nothing to prune\n");
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
unregisterGroups(gone.map((group) => group.path), io.env);
|
|
246
|
+
io.stdout(`pruned ${gone.length}:\n${gone.map((g) => ` ${g.name} ${g.path}`).join("\n")}\n`);
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
// src/commands/hook.ts
|
|
250
|
+
const host = claudeCode;
|
|
251
|
+
function runHook(io, run) {
|
|
252
|
+
try {
|
|
253
|
+
run();
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error instanceof CarnetError && error.code === "NO_GROUP") return 0;
|
|
256
|
+
io.stderr(`rucher hook: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
257
|
+
}
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
function withCarnet(cwd, io, use) {
|
|
261
|
+
const carnet = openCarnet({
|
|
262
|
+
cwd: cwd ?? io.cwd,
|
|
263
|
+
env: io.env
|
|
264
|
+
});
|
|
265
|
+
try {
|
|
266
|
+
use(carnet);
|
|
267
|
+
} finally {
|
|
268
|
+
carnet.close();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function sessionStart(stdinText, io) {
|
|
272
|
+
return runHook(io, () => {
|
|
273
|
+
const payload = host.parseSessionPayload(stdinText);
|
|
274
|
+
withCarnet(payload.cwd, io, (carnet) => {
|
|
275
|
+
const { additionalContext } = handleSessionStart(carnet, payload, {
|
|
276
|
+
host: host.id,
|
|
277
|
+
processId: host.processId(io.env),
|
|
278
|
+
RUCHER_PURGE_DAYS: io.env.RUCHER_PURGE_DAYS
|
|
279
|
+
});
|
|
280
|
+
if (additionalContext) {
|
|
281
|
+
const hookSpecificOutput = {
|
|
282
|
+
hookEventName: "SessionStart",
|
|
283
|
+
additionalContext
|
|
284
|
+
};
|
|
285
|
+
io.stdout(`${JSON.stringify({ hookSpecificOutput })}\n`);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function subagentHook(action, stdinText, io) {
|
|
291
|
+
return runHook(io, () => {
|
|
292
|
+
const payload = host.parseSubagentPayload(stdinText);
|
|
293
|
+
withCarnet(payload.cwd, io, (carnet) => {
|
|
294
|
+
carnet.agents[action](payload.session_id, payload.agent_id);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
function subagentStart(stdinText, io) {
|
|
299
|
+
return subagentHook("pairSubagent", stdinText, io);
|
|
300
|
+
}
|
|
301
|
+
function subagentStop(stdinText, io) {
|
|
302
|
+
return subagentHook("stopSubagent", stdinText, io);
|
|
303
|
+
}
|
|
304
|
+
function userPromptSubmit(stdinText, io) {
|
|
305
|
+
return runHook(io, () => {
|
|
306
|
+
const payload = host.parsePromptPayload(stdinText);
|
|
307
|
+
withCarnet(payload.cwd, io, (carnet) => {
|
|
308
|
+
handleUserPromptSubmit(carnet, host, payload);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
// src/session.ts
|
|
313
|
+
var CliError = class extends Error {
|
|
314
|
+
hint;
|
|
315
|
+
constructor(message, hint) {
|
|
316
|
+
super(message);
|
|
317
|
+
this.hint = hint;
|
|
318
|
+
this.name = "CliError";
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
function findSession(carnet, reference) {
|
|
322
|
+
try {
|
|
323
|
+
return carnet.sessions.find(reference);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (!(error instanceof CarnetError) || error.code !== "SESSION_NOT_FOUND") throw error;
|
|
326
|
+
const known = carnet.sessions.list().map((session) => session.name).filter(Boolean);
|
|
327
|
+
throw new CliError(error.message, known.length ? `known sessions: ${known.join(", ")}` : "pass a session name, id or id prefix, see rucher sessions --all");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function resolveSession(carnet, reference) {
|
|
331
|
+
if (reference) return findSession(carnet, reference);
|
|
332
|
+
const session = carnet.sessions.default();
|
|
333
|
+
if (!session) throw new CliError("No session in this group yet", "start a Claude Code session inside the group");
|
|
334
|
+
return session;
|
|
335
|
+
}
|
|
336
|
+
// src/commands/log.ts
|
|
337
|
+
function formatEvents(events, io) {
|
|
338
|
+
const colors = paint(io);
|
|
339
|
+
if (events.length === 0) return colors.dim("no events yet");
|
|
340
|
+
return events.map((event) => {
|
|
341
|
+
return [
|
|
342
|
+
colors.dim(formatClock(new Date(event.at))),
|
|
343
|
+
colors.bold(event.type.padEnd(9)),
|
|
344
|
+
event.agent ?? "",
|
|
345
|
+
event.link ?? event.note ?? "",
|
|
346
|
+
event.commit ? colors.cyan(event.commit.slice(0, 7)) : ""
|
|
347
|
+
].filter(Boolean).join(" ");
|
|
348
|
+
}).join("\n");
|
|
349
|
+
}
|
|
350
|
+
function formatDay(date) {
|
|
351
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
352
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
353
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
354
|
+
}
|
|
355
|
+
function markdownEvent(event) {
|
|
356
|
+
const parts = [`${formatClock(new Date(event.at))} **${event.type}**`];
|
|
357
|
+
if (event.agent) parts.push(`\`${event.agent}\``);
|
|
358
|
+
const detail = [
|
|
359
|
+
event.note,
|
|
360
|
+
event.link ? `[report](${event.link})` : null,
|
|
361
|
+
event.commit ? `\`${event.commit.slice(0, 7)}\`` : null
|
|
362
|
+
].filter(Boolean).join(" · ");
|
|
363
|
+
return `- ${parts.join(" ")}${detail ? `: ${detail}` : ""}`;
|
|
364
|
+
}
|
|
365
|
+
function formatEventsMarkdown(events, title) {
|
|
366
|
+
const lines = [`# Journal: ${title}`];
|
|
367
|
+
if (events.length === 0) return `${lines[0]}\n\nNo events yet.`;
|
|
368
|
+
let day = "";
|
|
369
|
+
for (const event of events) {
|
|
370
|
+
const current = formatDay(new Date(event.at));
|
|
371
|
+
if (current !== day) {
|
|
372
|
+
day = current;
|
|
373
|
+
lines.push("", `## ${day}`, "");
|
|
374
|
+
}
|
|
375
|
+
lines.push(markdownEvent(event));
|
|
376
|
+
}
|
|
377
|
+
return lines.join("\n");
|
|
378
|
+
}
|
|
379
|
+
function sessionTitle(carnet, session) {
|
|
380
|
+
return `${carnet.group.name} · ${session.name ?? session.id.slice(0, 8)}`;
|
|
381
|
+
}
|
|
382
|
+
function logOnce(carnet, io, options) {
|
|
383
|
+
const session = resolveSession(carnet, options.session);
|
|
384
|
+
const events = carnet.events.read(session.id, options.count);
|
|
385
|
+
io.stdout(`${options.md ? formatEventsMarkdown(events, sessionTitle(carnet, session)) : formatEvents(events, io)}\n`);
|
|
386
|
+
}
|
|
387
|
+
// src/commands/resume.ts
|
|
388
|
+
function resumeTarget(carnet, reference, io) {
|
|
389
|
+
const session = findSession(carnet, reference);
|
|
390
|
+
if (session.status === "dropped") io.stderr(`warning: session ${reference} was dropped\n`);
|
|
391
|
+
const cwd = session.cwd && existsSync(session.cwd) ? session.cwd : io.cwd;
|
|
392
|
+
return {
|
|
393
|
+
...hostById(session.host).resumeCommand(session),
|
|
394
|
+
cwd
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function runTarget({ command, args, cwd }) {
|
|
398
|
+
const result = spawnSync(command, args, {
|
|
399
|
+
cwd,
|
|
400
|
+
stdio: "inherit"
|
|
401
|
+
});
|
|
402
|
+
if (result.error) throw result.error;
|
|
403
|
+
return result.status ?? 1;
|
|
404
|
+
}
|
|
405
|
+
// src/commands/serve.ts
|
|
406
|
+
function serve(io) {
|
|
407
|
+
serveCarnet({
|
|
408
|
+
cwd: io.cwd,
|
|
409
|
+
env: io.env
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
// src/commands/status.ts
|
|
413
|
+
const SECTION_HEADERS = /* @__PURE__ */ new Set([
|
|
414
|
+
"On the table",
|
|
415
|
+
"Waiting on you",
|
|
416
|
+
"Running"
|
|
417
|
+
]);
|
|
418
|
+
function truncate(line, columns) {
|
|
419
|
+
const chars = Array.from(line);
|
|
420
|
+
return chars.length > columns ? `${chars.slice(0, Math.max(columns - 1, 1)).join("")}…` : line;
|
|
421
|
+
}
|
|
422
|
+
function colorizeLine(line, io) {
|
|
423
|
+
const colors = paint(io);
|
|
424
|
+
if (line === LEGEND) return colors.dim(line);
|
|
425
|
+
const cut = truncate(line, io.columns);
|
|
426
|
+
if (SECTION_HEADERS.has(line)) return colors.bold(cut);
|
|
427
|
+
if (line.startsWith(" ⚙️")) return colors.yellow(cut);
|
|
428
|
+
if (line === " nothing") return colors.dim(cut);
|
|
429
|
+
return cut;
|
|
430
|
+
}
|
|
431
|
+
function sessionWhere(carnet, session) {
|
|
432
|
+
return repoPathFor(carnet.group, session.cwd) ?? session.repo;
|
|
433
|
+
}
|
|
434
|
+
function sessionLabel(carnet, session, columns) {
|
|
435
|
+
const name = session.name ?? session.id.slice(0, 8);
|
|
436
|
+
const where = sessionWhere(carnet, session);
|
|
437
|
+
if (!where) return name;
|
|
438
|
+
const room = columns - Array.from(name).length - 3;
|
|
439
|
+
return room > 1 ? `${name} · ${truncate(where, room)}` : name;
|
|
440
|
+
}
|
|
441
|
+
function sessionHeader(carnet, session, io) {
|
|
442
|
+
return paint(io).cyan(sessionLabel(carnet, session, io.columns));
|
|
443
|
+
}
|
|
444
|
+
function sessionLines(carnet, session, view, io) {
|
|
445
|
+
if (session.status === "dropped") return [paint(io).dim(" dropped, see rucher sessions --all")];
|
|
446
|
+
return renderSections(view).split("\n").map((line) => colorizeLine(line, io));
|
|
447
|
+
}
|
|
448
|
+
function hasSomethingToShow(carnet, session) {
|
|
449
|
+
return carnet.agents.list(session.id).length > 0 || carnet.table.get(session.id) !== void 0;
|
|
450
|
+
}
|
|
451
|
+
function sessionBlock(carnet, session, view, io) {
|
|
452
|
+
return [sessionHeader(carnet, session, io), ...sessionLines(carnet, session, view, io)].join("\n");
|
|
453
|
+
}
|
|
454
|
+
function singleView(carnet, session, io) {
|
|
455
|
+
const colors = paint(io);
|
|
456
|
+
const view = carnet.status.view(session);
|
|
457
|
+
const separator = " - ";
|
|
458
|
+
const label = sessionLabel(carnet, session, io.columns - view.time.length - 3);
|
|
459
|
+
return [
|
|
460
|
+
`${colors.dim(view.time)}${separator}${colors.cyan(label)}`,
|
|
461
|
+
"",
|
|
462
|
+
...sessionLines(carnet, session, view, io),
|
|
463
|
+
"",
|
|
464
|
+
colors.dim(LEGEND)
|
|
465
|
+
].join("\n");
|
|
466
|
+
}
|
|
467
|
+
function renderStatusView(carnet, io, options) {
|
|
468
|
+
const sessions = options.all ? carnet.sessions.list({ includeTemporary: true }).filter((session) => hasSomethingToShow(carnet, session)) : [];
|
|
469
|
+
if (sessions.length === 0) return singleView(carnet, resolveSession(carnet, options.session), io);
|
|
470
|
+
let time = "";
|
|
471
|
+
const blocks = sessions.map((session) => {
|
|
472
|
+
const view = carnet.status.view(session);
|
|
473
|
+
time ||= view.time;
|
|
474
|
+
return sessionBlock(carnet, session, view, io);
|
|
475
|
+
});
|
|
476
|
+
return [
|
|
477
|
+
paint(io).dim(time),
|
|
478
|
+
"",
|
|
479
|
+
blocks.join("\n\n"),
|
|
480
|
+
"",
|
|
481
|
+
paint(io).dim(LEGEND)
|
|
482
|
+
].join("\n");
|
|
483
|
+
}
|
|
484
|
+
function statusOnce(carnet, io, options) {
|
|
485
|
+
io.stdout(`${renderStatusView(carnet, io, options)}\n`);
|
|
486
|
+
}
|
|
487
|
+
const CLEAR = "\x1B[H\x1B[2J";
|
|
488
|
+
const HIDE_CURSOR = "\x1B[?25l";
|
|
489
|
+
const SHOW_CURSOR = "\x1B[?25h";
|
|
490
|
+
function statusWatch(carnet, io, options) {
|
|
491
|
+
return new Promise((resolve) => {
|
|
492
|
+
let last = "";
|
|
493
|
+
const repaint = () => {
|
|
494
|
+
let text;
|
|
495
|
+
try {
|
|
496
|
+
text = renderStatusView(carnet, io, options);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
text = paint(io).dim(error instanceof Error ? error.message : String(error));
|
|
499
|
+
}
|
|
500
|
+
if (text === last) return;
|
|
501
|
+
last = text;
|
|
502
|
+
io.stdout(`${CLEAR}${text}\n`);
|
|
503
|
+
};
|
|
504
|
+
const timer = setInterval(repaint, options.intervalMilliseconds);
|
|
505
|
+
const stop = () => {
|
|
506
|
+
clearInterval(timer);
|
|
507
|
+
io.stdout(SHOW_CURSOR);
|
|
508
|
+
resolve();
|
|
509
|
+
};
|
|
510
|
+
options.signal?.addEventListener("abort", stop, { once: true });
|
|
511
|
+
io.stdout(HIDE_CURSOR);
|
|
512
|
+
repaint();
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
// src/commands/sessions.ts
|
|
516
|
+
function lastActive(at, now) {
|
|
517
|
+
const date = new Date(at);
|
|
518
|
+
return date.toDateString() === now.toDateString() ? formatClock(date) : date.toISOString().slice(0, 10);
|
|
519
|
+
}
|
|
520
|
+
function row(carnet, session, now) {
|
|
521
|
+
const open = carnet.agents.list(session.id).length;
|
|
522
|
+
return [
|
|
523
|
+
session.name ?? session.id.slice(0, 8),
|
|
524
|
+
session.status,
|
|
525
|
+
sessionWhere(carnet, session) ?? "",
|
|
526
|
+
lastActive(session.lastActiveAt, now),
|
|
527
|
+
open ? `${open} agent${open > 1 ? "s" : ""}` : "",
|
|
528
|
+
session.purpose ?? ""
|
|
529
|
+
];
|
|
530
|
+
}
|
|
531
|
+
function sessionsOnce(carnet, io, options = {}) {
|
|
532
|
+
const colors = paint(io);
|
|
533
|
+
const sessions = carnet.sessions.list({
|
|
534
|
+
includeTemporary: options.all,
|
|
535
|
+
includeDropped: options.all
|
|
536
|
+
});
|
|
537
|
+
if (sessions.length === 0) {
|
|
538
|
+
const hint = options.all ? "" : " (temporary ones are hidden, use --all)";
|
|
539
|
+
io.stdout(`${colors.dim(`no named session yet${hint}`)}\n`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
543
|
+
const rows = sessions.map((session) => ({
|
|
544
|
+
session,
|
|
545
|
+
cells: row(carnet, session, now)
|
|
546
|
+
}));
|
|
547
|
+
const widths = rows.reduce((current, { cells }) => cells.map((cell, column) => Math.max(current[column] ?? 0, cell.length)), []);
|
|
548
|
+
const lines = rows.map(({ session, cells }) => {
|
|
549
|
+
return cells.map((cell, column) => {
|
|
550
|
+
const padded = column < cells.length - 1 ? cell.padEnd(widths[column] ?? 0) : cell;
|
|
551
|
+
if (column === 0) return colors.bold(padded);
|
|
552
|
+
if (column === 1 && session.status !== "active") return colors.dim(padded);
|
|
553
|
+
return padded;
|
|
554
|
+
}).join(" ").trimEnd();
|
|
555
|
+
});
|
|
556
|
+
io.stdout(`${lines.join("\n")}\n`);
|
|
557
|
+
}
|
|
558
|
+
function sessionsMerge(carnet, fromReference, intoReference, io) {
|
|
559
|
+
const from = findSession(carnet, fromReference);
|
|
560
|
+
const into = findSession(carnet, intoReference);
|
|
561
|
+
const result = carnet.sessions.merge(from.id, into.id);
|
|
562
|
+
const label = (session) => session.name ?? session.id.slice(0, 8);
|
|
563
|
+
const lines = [
|
|
564
|
+
`merged ${label(from)} into ${label(result.session)}`,
|
|
565
|
+
` agents moved: ${result.movedAgents.join(", ") || "none"}`,
|
|
566
|
+
...result.closedAgents.length ? [` agents closed (name already used): ${result.closedAgents.join(", ")}`] : [],
|
|
567
|
+
` events moved: ${result.movedEvents}`,
|
|
568
|
+
` table line moved: ${result.movedTable ? "yes" : "no"}`
|
|
569
|
+
];
|
|
570
|
+
io.stdout(`${lines.join("\n")}\n`);
|
|
571
|
+
}
|
|
572
|
+
// src/io.ts
|
|
573
|
+
function processIo(options = {}) {
|
|
574
|
+
const tty = process.stdout.isTTY === true;
|
|
575
|
+
const colorWanted = options.color ?? true;
|
|
576
|
+
return {
|
|
577
|
+
stdout: (text) => process.stdout.write(text),
|
|
578
|
+
stderr: (text) => process.stderr.write(text),
|
|
579
|
+
cwd: process.cwd(),
|
|
580
|
+
env: process.env,
|
|
581
|
+
color: tty && colorWanted && !process.env.NO_COLOR,
|
|
582
|
+
columns: process.stdout.columns ?? 80
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function readStdin() {
|
|
586
|
+
return new Promise((resolve) => {
|
|
587
|
+
let data = "";
|
|
588
|
+
process.stdin.setEncoding("utf8");
|
|
589
|
+
process.stdin.on("data", (chunk) => {
|
|
590
|
+
data += chunk;
|
|
591
|
+
});
|
|
592
|
+
process.stdin.on("end", () => resolve(data));
|
|
593
|
+
process.stdin.on("error", () => resolve(data));
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
// src/main.ts
|
|
597
|
+
function fail(io, error) {
|
|
598
|
+
if (error instanceof CliError) io.stderr(`error: ${error.message}${error.hint ? `\n ${error.hint}` : ""}\n`);
|
|
599
|
+
else if (error instanceof CarnetError) io.stderr(`error: ${error.message}\n`);
|
|
600
|
+
else io.stderr(`error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
const colorArgument = { color: {
|
|
604
|
+
type: "boolean",
|
|
605
|
+
default: true,
|
|
606
|
+
description: "Colored output (--no-color to disable)"
|
|
607
|
+
} };
|
|
608
|
+
const sessionArgument = { session: {
|
|
609
|
+
type: "string",
|
|
610
|
+
alias: "s",
|
|
611
|
+
description: "Session name, id or id prefix (default: most recently active)"
|
|
612
|
+
} };
|
|
613
|
+
const initCommand = defineCommand({
|
|
614
|
+
meta: {
|
|
615
|
+
name: "init",
|
|
616
|
+
description: "Make the current directory a Rucher group (meta directory)"
|
|
617
|
+
},
|
|
618
|
+
args: {
|
|
619
|
+
directory: {
|
|
620
|
+
type: "positional",
|
|
621
|
+
description: "Meta directory (default: current directory)",
|
|
622
|
+
required: false
|
|
623
|
+
},
|
|
624
|
+
name: {
|
|
625
|
+
type: "string",
|
|
626
|
+
description: "Group name (default: directory name)"
|
|
627
|
+
},
|
|
628
|
+
repos: {
|
|
629
|
+
type: "string",
|
|
630
|
+
description: "Comma-separated repo paths (more later with `rucher group add`)"
|
|
631
|
+
},
|
|
632
|
+
...colorArgument
|
|
633
|
+
},
|
|
634
|
+
run({ args }) {
|
|
635
|
+
const io = processIo({ color: args.color });
|
|
636
|
+
const repos = args.repos ? args.repos.split(",").map((repo) => repo.trim()).filter(Boolean) : void 0;
|
|
637
|
+
process.exitCode = init({
|
|
638
|
+
directory: args.directory,
|
|
639
|
+
name: args.name,
|
|
640
|
+
repos
|
|
641
|
+
}, io);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
const statusCommand = defineCommand({
|
|
645
|
+
meta: {
|
|
646
|
+
name: "status",
|
|
647
|
+
description: "Show agent statuses of a session"
|
|
648
|
+
},
|
|
649
|
+
args: {
|
|
650
|
+
...sessionArgument,
|
|
651
|
+
all: {
|
|
652
|
+
type: "boolean",
|
|
653
|
+
default: false,
|
|
654
|
+
description: "Every session with activity"
|
|
655
|
+
},
|
|
656
|
+
watch: {
|
|
657
|
+
type: "boolean",
|
|
658
|
+
alias: "w",
|
|
659
|
+
default: false,
|
|
660
|
+
description: "Refresh continuously"
|
|
661
|
+
},
|
|
662
|
+
interval: {
|
|
663
|
+
type: "string",
|
|
664
|
+
default: "1",
|
|
665
|
+
description: "Refresh interval in seconds with --watch"
|
|
666
|
+
},
|
|
667
|
+
...colorArgument
|
|
668
|
+
},
|
|
669
|
+
async run({ args }) {
|
|
670
|
+
const io = processIo({ color: args.color });
|
|
671
|
+
try {
|
|
672
|
+
const carnet = openCarnet({
|
|
673
|
+
cwd: io.cwd,
|
|
674
|
+
env: io.env
|
|
675
|
+
});
|
|
676
|
+
const options = {
|
|
677
|
+
session: args.session,
|
|
678
|
+
all: args.all
|
|
679
|
+
};
|
|
680
|
+
if (!args.watch) {
|
|
681
|
+
statusOnce(carnet, io, options);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const controller = new AbortController();
|
|
685
|
+
process.on("SIGINT", () => controller.abort());
|
|
686
|
+
process.on("SIGTERM", () => controller.abort());
|
|
687
|
+
const intervalMilliseconds = Math.max(Number(args.interval) || 1, .2) * 1e3;
|
|
688
|
+
await statusWatch(carnet, io, {
|
|
689
|
+
...options,
|
|
690
|
+
intervalMilliseconds,
|
|
691
|
+
signal: controller.signal
|
|
692
|
+
});
|
|
693
|
+
} catch (error) {
|
|
694
|
+
fail(io, error);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
const logCommand = defineCommand({
|
|
699
|
+
meta: {
|
|
700
|
+
name: "log",
|
|
701
|
+
description: "Show the last journal events of a session"
|
|
702
|
+
},
|
|
703
|
+
args: {
|
|
704
|
+
...sessionArgument,
|
|
705
|
+
count: {
|
|
706
|
+
type: "string",
|
|
707
|
+
alias: "n",
|
|
708
|
+
default: "20",
|
|
709
|
+
description: "Number of events"
|
|
710
|
+
},
|
|
711
|
+
md: {
|
|
712
|
+
type: "boolean",
|
|
713
|
+
default: false,
|
|
714
|
+
description: "Markdown output, for archiving"
|
|
715
|
+
},
|
|
716
|
+
...colorArgument
|
|
717
|
+
},
|
|
718
|
+
run({ args }) {
|
|
719
|
+
const io = processIo({ color: args.color });
|
|
720
|
+
try {
|
|
721
|
+
logOnce(openCarnet({
|
|
722
|
+
cwd: io.cwd,
|
|
723
|
+
env: io.env
|
|
724
|
+
}), io, {
|
|
725
|
+
session: args.session,
|
|
726
|
+
count: Number(args.count) || 20,
|
|
727
|
+
md: args.md
|
|
728
|
+
});
|
|
729
|
+
} catch (error) {
|
|
730
|
+
fail(io, error);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
const sessionsListCommand = defineCommand({
|
|
735
|
+
meta: {
|
|
736
|
+
name: "list",
|
|
737
|
+
description: "List the named sessions of the group"
|
|
738
|
+
},
|
|
739
|
+
args: {
|
|
740
|
+
all: {
|
|
741
|
+
type: "boolean",
|
|
742
|
+
default: false,
|
|
743
|
+
description: "Include temporary and dropped sessions"
|
|
744
|
+
},
|
|
745
|
+
...colorArgument
|
|
746
|
+
},
|
|
747
|
+
run({ args }) {
|
|
748
|
+
const io = processIo({ color: args.color });
|
|
749
|
+
try {
|
|
750
|
+
sessionsOnce(openCarnet({
|
|
751
|
+
cwd: io.cwd,
|
|
752
|
+
env: io.env
|
|
753
|
+
}), io, { all: args.all });
|
|
754
|
+
} catch (error) {
|
|
755
|
+
fail(io, error);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
const sessionsMergeCommand = defineCommand({
|
|
760
|
+
meta: {
|
|
761
|
+
name: "merge",
|
|
762
|
+
description: "Move the agents, table line and journal of one session into another, then drop it"
|
|
763
|
+
},
|
|
764
|
+
args: {
|
|
765
|
+
from: {
|
|
766
|
+
type: "positional",
|
|
767
|
+
description: "Session to empty (name, id or id prefix)",
|
|
768
|
+
required: true
|
|
769
|
+
},
|
|
770
|
+
into: {
|
|
771
|
+
type: "positional",
|
|
772
|
+
description: "Session that keeps everything (name, id or id prefix)",
|
|
773
|
+
required: true
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
run({ args }) {
|
|
777
|
+
const io = processIo();
|
|
778
|
+
try {
|
|
779
|
+
const carnet = openCarnet({
|
|
780
|
+
cwd: io.cwd,
|
|
781
|
+
env: io.env
|
|
782
|
+
});
|
|
783
|
+
sessionsMerge(carnet, args.from, args.into, io);
|
|
784
|
+
carnet.close();
|
|
785
|
+
} catch (error) {
|
|
786
|
+
fail(io, error);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
const sessionsCommand = defineCommand({
|
|
791
|
+
meta: {
|
|
792
|
+
name: "sessions",
|
|
793
|
+
description: "List or merge the sessions of the group"
|
|
794
|
+
},
|
|
795
|
+
default: "list",
|
|
796
|
+
subCommands: {
|
|
797
|
+
list: sessionsListCommand,
|
|
798
|
+
merge: sessionsMergeCommand
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
const resumeCommand = defineCommand({
|
|
802
|
+
meta: {
|
|
803
|
+
name: "resume",
|
|
804
|
+
description: "Resume a named session in its host (Claude Code today)"
|
|
805
|
+
},
|
|
806
|
+
args: { name: {
|
|
807
|
+
type: "positional",
|
|
808
|
+
description: "Session name, id or id prefix",
|
|
809
|
+
required: true
|
|
810
|
+
} },
|
|
811
|
+
run({ args }) {
|
|
812
|
+
const io = processIo();
|
|
813
|
+
try {
|
|
814
|
+
const carnet = openCarnet({
|
|
815
|
+
cwd: io.cwd,
|
|
816
|
+
env: io.env
|
|
817
|
+
});
|
|
818
|
+
const target = resumeTarget(carnet, args.name, io);
|
|
819
|
+
carnet.close();
|
|
820
|
+
process.exitCode = runTarget(target);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
fail(io, error);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
const groupCommand = defineCommand({
|
|
827
|
+
meta: {
|
|
828
|
+
name: "group",
|
|
829
|
+
description: "Show the current group, add or remove repos, sync its settings"
|
|
830
|
+
},
|
|
831
|
+
subCommands: {
|
|
832
|
+
show: defineCommand({
|
|
833
|
+
meta: {
|
|
834
|
+
name: "show",
|
|
835
|
+
description: "Name, meta directory and repos of the current group"
|
|
836
|
+
},
|
|
837
|
+
args: { ...colorArgument },
|
|
838
|
+
run({ args }) {
|
|
839
|
+
const io = processIo({ color: args.color });
|
|
840
|
+
try {
|
|
841
|
+
process.exitCode = groupShow(io);
|
|
842
|
+
} catch (error) {
|
|
843
|
+
fail(io, error);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}),
|
|
847
|
+
add: defineCommand({
|
|
848
|
+
meta: {
|
|
849
|
+
name: "add",
|
|
850
|
+
description: "Add a repo path to the current group"
|
|
851
|
+
},
|
|
852
|
+
args: {
|
|
853
|
+
path: {
|
|
854
|
+
type: "positional",
|
|
855
|
+
description: "Repo path",
|
|
856
|
+
required: true
|
|
857
|
+
},
|
|
858
|
+
...colorArgument
|
|
859
|
+
},
|
|
860
|
+
run({ args }) {
|
|
861
|
+
const io = processIo({ color: args.color });
|
|
862
|
+
try {
|
|
863
|
+
process.exitCode = groupAdd(args.path, io);
|
|
864
|
+
} catch (error) {
|
|
865
|
+
fail(io, error);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}),
|
|
869
|
+
remove: defineCommand({
|
|
870
|
+
meta: {
|
|
871
|
+
name: "remove",
|
|
872
|
+
description: "Remove a repo path from the current group"
|
|
873
|
+
},
|
|
874
|
+
args: {
|
|
875
|
+
path: {
|
|
876
|
+
type: "positional",
|
|
877
|
+
description: "Repo path",
|
|
878
|
+
required: true
|
|
879
|
+
},
|
|
880
|
+
...colorArgument
|
|
881
|
+
},
|
|
882
|
+
run({ args }) {
|
|
883
|
+
const io = processIo({ color: args.color });
|
|
884
|
+
try {
|
|
885
|
+
process.exitCode = groupRemove(args.path, io);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
fail(io, error);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}),
|
|
891
|
+
sync: defineCommand({
|
|
892
|
+
meta: {
|
|
893
|
+
name: "sync",
|
|
894
|
+
description: "Rewrite the hosts' context files (repo list, directory access) from group.json"
|
|
895
|
+
},
|
|
896
|
+
args: { ...colorArgument },
|
|
897
|
+
run({ args }) {
|
|
898
|
+
const io = processIo({ color: args.color });
|
|
899
|
+
try {
|
|
900
|
+
process.exitCode = groupSync(io);
|
|
901
|
+
} catch (error) {
|
|
902
|
+
fail(io, error);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
})
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
const groupsCommand = defineCommand({
|
|
909
|
+
meta: {
|
|
910
|
+
name: "groups",
|
|
911
|
+
description: "Registered groups of this machine"
|
|
912
|
+
},
|
|
913
|
+
subCommands: {
|
|
914
|
+
list: defineCommand({
|
|
915
|
+
meta: {
|
|
916
|
+
name: "list",
|
|
917
|
+
description: "Registered groups, missing ones flagged"
|
|
918
|
+
},
|
|
919
|
+
args: { ...colorArgument },
|
|
920
|
+
run({ args }) {
|
|
921
|
+
process.exitCode = groupsList(processIo({ color: args.color }));
|
|
922
|
+
}
|
|
923
|
+
}),
|
|
924
|
+
forget: defineCommand({
|
|
925
|
+
meta: {
|
|
926
|
+
name: "forget",
|
|
927
|
+
description: "Unregister a group by name or meta directory path"
|
|
928
|
+
},
|
|
929
|
+
args: {
|
|
930
|
+
reference: {
|
|
931
|
+
type: "positional",
|
|
932
|
+
description: "Group name or path",
|
|
933
|
+
required: true
|
|
934
|
+
},
|
|
935
|
+
...colorArgument
|
|
936
|
+
},
|
|
937
|
+
run({ args }) {
|
|
938
|
+
process.exitCode = groupsForget(args.reference, processIo({ color: args.color }));
|
|
939
|
+
}
|
|
940
|
+
}),
|
|
941
|
+
prune: defineCommand({
|
|
942
|
+
meta: {
|
|
943
|
+
name: "prune",
|
|
944
|
+
description: "Unregister groups whose meta directory is gone"
|
|
945
|
+
},
|
|
946
|
+
args: { ...colorArgument },
|
|
947
|
+
run({ args }) {
|
|
948
|
+
process.exitCode = groupsPrune(processIo({ color: args.color }));
|
|
949
|
+
}
|
|
950
|
+
})
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
const hookCommand = defineCommand({
|
|
954
|
+
meta: {
|
|
955
|
+
name: "hook",
|
|
956
|
+
description: "Agent host hook entry points (Claude Code, JSON payload on stdin)"
|
|
957
|
+
},
|
|
958
|
+
subCommands: {
|
|
959
|
+
"session-start": defineCommand({
|
|
960
|
+
meta: {
|
|
961
|
+
name: "session-start",
|
|
962
|
+
description: "SessionStart hook: records the session"
|
|
963
|
+
},
|
|
964
|
+
async run() {
|
|
965
|
+
process.exitCode = sessionStart(await readStdin(), processIo());
|
|
966
|
+
}
|
|
967
|
+
}),
|
|
968
|
+
"user-prompt-submit": defineCommand({
|
|
969
|
+
meta: {
|
|
970
|
+
name: "user-prompt-submit",
|
|
971
|
+
description: "UserPromptSubmit hook: names the session after a /rename"
|
|
972
|
+
},
|
|
973
|
+
async run() {
|
|
974
|
+
process.exitCode = userPromptSubmit(await readStdin(), processIo());
|
|
975
|
+
}
|
|
976
|
+
}),
|
|
977
|
+
"subagent-start": defineCommand({
|
|
978
|
+
meta: {
|
|
979
|
+
name: "subagent-start",
|
|
980
|
+
description: "SubagentStart hook: pairs the fork with its agent"
|
|
981
|
+
},
|
|
982
|
+
async run() {
|
|
983
|
+
process.exitCode = subagentStart(await readStdin(), processIo());
|
|
984
|
+
}
|
|
985
|
+
}),
|
|
986
|
+
"subagent-stop": defineCommand({
|
|
987
|
+
meta: {
|
|
988
|
+
name: "subagent-stop",
|
|
989
|
+
description: "SubagentStop hook: flags a fork that never called agent_done"
|
|
990
|
+
},
|
|
991
|
+
async run() {
|
|
992
|
+
process.exitCode = subagentStop(await readStdin(), processIo());
|
|
993
|
+
}
|
|
994
|
+
})
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
const serveCommand = defineCommand({
|
|
998
|
+
meta: {
|
|
999
|
+
name: "serve",
|
|
1000
|
+
description: "Run the Carnet MCP server over stdio"
|
|
1001
|
+
},
|
|
1002
|
+
run() {
|
|
1003
|
+
serve(processIo());
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
const main = defineCommand({
|
|
1007
|
+
meta: {
|
|
1008
|
+
name: "rucher",
|
|
1009
|
+
version,
|
|
1010
|
+
description: "Status, journal and sessions of your coding agents (Claude Code today)"
|
|
1011
|
+
},
|
|
1012
|
+
subCommands: {
|
|
1013
|
+
init: initCommand,
|
|
1014
|
+
status: statusCommand,
|
|
1015
|
+
log: logCommand,
|
|
1016
|
+
sessions: sessionsCommand,
|
|
1017
|
+
resume: resumeCommand,
|
|
1018
|
+
group: groupCommand,
|
|
1019
|
+
groups: groupsCommand,
|
|
1020
|
+
hook: hookCommand,
|
|
1021
|
+
serve: serveCommand
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
// src/cli.ts
|
|
1025
|
+
await setupCompletion(main);
|
|
1026
|
+
runMain(main);
|
|
1027
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rucher/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "rucher: status, journal and sessions of your Claude Code sub-agents, from the terminal",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agents",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"cli",
|
|
9
|
+
"multi-agent",
|
|
10
|
+
"status"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/Florian-Varrin/Rucher/tree/master/packages/cli#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Florian-Varrin/Rucher/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Florian Varrin",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Florian-Varrin/Rucher.git",
|
|
21
|
+
"directory": "packages/cli"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"rucher": "./dist/cli.mjs"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@bomb.sh/tab": "0.0.22",
|
|
38
|
+
"@rucher/carnet": "0.1.0",
|
|
39
|
+
"citty": "^0.2.2"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^24",
|
|
43
|
+
"typescript": "^7.0.2",
|
|
44
|
+
"vite-plus": "1.4.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=24"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "vp pack",
|
|
51
|
+
"dev": "vp pack --watch",
|
|
52
|
+
"test": "vp test",
|
|
53
|
+
"check": "vp check"
|
|
54
|
+
}
|
|
6
55
|
}
|