@bli-cockpit/cli 0.1.17 → 0.1.19

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 CHANGED
@@ -40,17 +40,25 @@ staging, a custom dashboard, or deliberately forcing a different dashboard
40
40
  pairing. Already-onboarded users update with
41
41
  `npm install -g @bli-cockpit/cli@latest`, then run
42
42
  `cockpit sync --workspace "$PWD" --json`. `--repo <path>` remains supported
43
- for older prompts and the Codex ticket-binding guardrail.
43
+ for older prompts and the agent ticket-binding guardrail.
44
44
 
45
- On machines where Codex agents will do ticketed work, install the user-scope
46
- agent rule once:
45
+ On machines where Codex or Claude agents will do ticketed work, interactive
46
+ `cockpit onboard` checks `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` after
47
+ harvest proof. If equivalent Cockpit ticket-binding guidance already exists, it
48
+ leaves the files alone. If guidance is missing or clearly stale, it asks whether
49
+ to install or replace it.
47
50
 
48
51
  ```bash
52
+ # Repair/manual path, or headless/json onboarding where Cockpit cannot prompt.
49
53
  cockpit agent-rules install
50
54
  ```
51
55
 
52
- That updates `~/.codex/AGENTS.md` with the Cockpit rule to bind known Linear
53
- tickets before edits, or ask once when the ticket ID is missing.
56
+ The direct command updates `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` with
57
+ the Cockpit rule to bind known Linear tickets before edits, or ask once when
58
+ the ticket ID is missing. That binding starts attributing the session's work to
59
+ the specific ticket in Cockpit. It checks for managed or equivalent guidance
60
+ first, so current files are left unchanged and stale Cockpit ticket-binding
61
+ sections are replaced.
54
62
 
55
63
  Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
56
64
  repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
@@ -4,56 +4,96 @@ import path from "node:path";
4
4
  const MANAGED_BLOCK_START = "<!-- BLI_COCKPIT_AGENT_RULES:START -->";
5
5
  const MANAGED_BLOCK_END = "<!-- BLI_COCKPIT_AGENT_RULES:END -->";
6
6
  export async function installCodexAgentRules(options = {}) {
7
- const agentsFile = codexAgentsFile(options.homeDir);
7
+ return installAgentRulesForHost("codex", options);
8
+ }
9
+ export async function installClaudeAgentRules(options = {}) {
10
+ return installAgentRulesForHost("claude", options);
11
+ }
12
+ export async function installAgentRules(options = {}) {
13
+ const targets = await Promise.all(targetHosts(options.hosts).map((host) => installAgentRulesForHost(host, options)));
14
+ return aggregateAgentRulesResult(targets);
15
+ }
16
+ async function installAgentRulesForHost(host, options = {}) {
17
+ const rulesFile = agentRulesFile(host, options.homeDir);
8
18
  const block = cockpitAgentRulesBlock();
9
19
  let existing = "";
10
20
  let existed = true;
11
21
  try {
12
- existing = await readFile(agentsFile, "utf8");
22
+ existing = await readFile(rulesFile, "utf8");
13
23
  }
14
24
  catch {
15
25
  existed = false;
16
26
  }
17
- const next = upsertManagedBlock(existing, block);
27
+ const prepared = prepareManagedBlockInstall(existing, block);
28
+ if (!prepared.next) {
29
+ return agentRulesResult(host, rulesFile, "unchanged", block, prepared.state);
30
+ }
31
+ const next = prepared.next;
18
32
  if (next === existing) {
19
- return { status: "unchanged", agents_file: agentsFile, block };
33
+ return agentRulesResult(host, rulesFile, "unchanged", block, prepared.state);
20
34
  }
21
- await mkdir(path.dirname(agentsFile), { recursive: true });
22
- await writeFile(agentsFile, next, "utf8");
23
- return { status: existed ? "updated" : "created", agents_file: agentsFile, block };
35
+ await mkdir(path.dirname(rulesFile), { recursive: true });
36
+ await writeFile(rulesFile, next, "utf8");
37
+ return agentRulesResult(host, rulesFile, existed ? "updated" : "created", block, "managed", prepared.state === "stale");
24
38
  }
25
39
  export async function uninstallCodexAgentRules(options = {}) {
26
- const agentsFile = codexAgentsFile(options.homeDir);
40
+ return uninstallAgentRulesForHost("codex", options);
41
+ }
42
+ export async function uninstallClaudeAgentRules(options = {}) {
43
+ return uninstallAgentRulesForHost("claude", options);
44
+ }
45
+ export async function uninstallAgentRules(options = {}) {
46
+ const targets = await Promise.all(targetHosts(options.hosts).map((host) => uninstallAgentRulesForHost(host, options)));
47
+ return aggregateAgentRulesResult(targets);
48
+ }
49
+ async function uninstallAgentRulesForHost(host, options = {}) {
50
+ const rulesFile = agentRulesFile(host, options.homeDir);
27
51
  const block = cockpitAgentRulesBlock();
28
52
  let existing = "";
29
53
  try {
30
- existing = await readFile(agentsFile, "utf8");
54
+ existing = await readFile(rulesFile, "utf8");
31
55
  }
32
56
  catch {
33
- return { status: "missing", agents_file: agentsFile, block };
57
+ return agentRulesResult(host, rulesFile, "missing", block, "missing");
34
58
  }
35
59
  const next = removeManagedBlock(existing);
36
60
  if (next === existing) {
37
- return { status: "missing", agents_file: agentsFile, block };
61
+ return agentRulesResult(host, rulesFile, "missing", block, "missing");
38
62
  }
39
- await writeFile(agentsFile, next, "utf8");
40
- return { status: "updated", agents_file: agentsFile, block };
63
+ await writeFile(rulesFile, next, "utf8");
64
+ return agentRulesResult(host, rulesFile, "updated", block, "missing");
41
65
  }
42
66
  export async function inspectCodexAgentRules(options = {}) {
43
- const agentsFile = codexAgentsFile(options.homeDir);
67
+ return inspectAgentRulesForHost("codex", options);
68
+ }
69
+ export async function inspectClaudeAgentRules(options = {}) {
70
+ return inspectAgentRulesForHost("claude", options);
71
+ }
72
+ export async function inspectAgentRules(options = {}) {
73
+ const targets = await Promise.all(targetHosts(options.hosts).map((host) => inspectAgentRulesForHost(host, options)));
74
+ return {
75
+ ...aggregateAgentRulesResult(targets),
76
+ installed: targets.every((target) => target.installed),
77
+ };
78
+ }
79
+ async function inspectAgentRulesForHost(host, options = {}) {
80
+ const rulesFile = agentRulesFile(host, options.homeDir);
44
81
  const block = cockpitAgentRulesBlock();
45
82
  let existing = "";
46
83
  try {
47
- existing = await readFile(agentsFile, "utf8");
84
+ existing = await readFile(rulesFile, "utf8");
48
85
  }
49
86
  catch {
50
- return { status: "missing", agents_file: agentsFile, block, installed: false };
87
+ return {
88
+ ...agentRulesResult(host, rulesFile, "missing", block, "missing"),
89
+ installed: false,
90
+ };
51
91
  }
92
+ const state = inspectAgentRulesContents(existing);
93
+ const installed = state === "managed" || state === "equivalent";
52
94
  return {
53
- status: hasManagedBlock(existing) ? "unchanged" : "missing",
54
- agents_file: agentsFile,
55
- block,
56
- installed: hasManagedBlock(existing),
95
+ ...agentRulesResult(host, rulesFile, installed ? "unchanged" : "missing", block, state),
96
+ installed,
57
97
  };
58
98
  }
59
99
  export function cockpitAgentRulesBlock() {
@@ -61,7 +101,7 @@ export function cockpitAgentRulesBlock() {
61
101
  MANAGED_BLOCK_START,
62
102
  "## Cockpit Ticket Binding",
63
103
  "",
64
- "- For implementation, debugging, review, PR, or ship work tied to a clear Linear ticket, run `cockpit start --ticket <ticket-id> --repo \"$PWD\"` before the first code edit or mutating tool call.",
104
+ "- For implementation, debugging, review, PR, or ship work tied to a clear Linear ticket, run `cockpit start --ticket <ticket-id> --repo \"$PWD\"` before the first code edit or mutating tool call. This starts attributing the session's work to that specific ticket in Cockpit.",
65
105
  "- Use `--ticket`; do not invent `--ticketId` or other flag shapes.",
66
106
  "- If the user mentions ticketed work but no ticket ID is visible, ask once for the Linear ticket ID before editing. Agents cannot reliably infer it from context.",
67
107
  "- If there is truly no ticket, state that the work remains in general ambient capture and do not invent a ticket.",
@@ -86,12 +126,124 @@ export function removeManagedBlock(contents) {
86
126
  return contents;
87
127
  return contents.replace(managedBlockPattern(), "").replace(/\n{3,}/gu, "\n\n").trimEnd() + "\n";
88
128
  }
89
- function codexAgentsFile(homeDir = os.homedir()) {
129
+ function prepareManagedBlockInstall(contents, block) {
130
+ if (!contents.trim())
131
+ return { next: `${block}\n`, state: "missing" };
132
+ if (hasManagedBlock(contents)) {
133
+ return { next: upsertManagedBlock(contents, block), state: "managed" };
134
+ }
135
+ if (hasEquivalentUnmanagedTicketBinding(contents)) {
136
+ return { next: null, state: "equivalent" };
137
+ }
138
+ const staleBlock = findStaleUnmanagedTicketBindingBlock(contents);
139
+ if (staleBlock) {
140
+ return {
141
+ next: replaceLineSpan(contents, staleBlock.startLine, staleBlock.endLine, block),
142
+ state: "stale",
143
+ };
144
+ }
145
+ return { next: `${contents.replace(/\s+$/u, "")}\n\n${block}\n`, state: "missing" };
146
+ }
147
+ function inspectAgentRulesContents(contents) {
148
+ if (hasManagedBlock(contents))
149
+ return "managed";
150
+ if (hasEquivalentUnmanagedTicketBinding(contents))
151
+ return "equivalent";
152
+ if (findStaleUnmanagedTicketBindingBlock(contents))
153
+ return "stale";
154
+ return "missing";
155
+ }
156
+ function agentRulesFile(host, homeDir = os.homedir()) {
157
+ if (host === "claude") {
158
+ return path.join(homeDir, ".claude", "CLAUDE.md");
159
+ }
90
160
  return path.join(homeDir, ".codex", "AGENTS.md");
91
161
  }
162
+ function agentRulesResult(host, rulesFile, status, block, state, staleBlockReplaced = false) {
163
+ return {
164
+ host,
165
+ status,
166
+ rules_file: rulesFile,
167
+ agents_file: rulesFile,
168
+ block,
169
+ state,
170
+ ...(staleBlockReplaced ? { stale_block_replaced: true } : {}),
171
+ };
172
+ }
173
+ function targetHosts(hosts) {
174
+ return hosts && hosts.length > 0 ? hosts : ["codex", "claude"];
175
+ }
176
+ function aggregateAgentRulesResult(targets) {
177
+ if (targets.every((target) => target.status === "unchanged")) {
178
+ return { status: "unchanged", targets };
179
+ }
180
+ if (targets.every((target) => target.status === "missing")) {
181
+ return { status: "missing", targets };
182
+ }
183
+ if (targets.every((target) => target.status === "created")) {
184
+ return { status: "created", targets };
185
+ }
186
+ return { status: "updated", targets };
187
+ }
92
188
  function managedBlockPattern() {
93
189
  return new RegExp(`${escapeRegExp(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(MANAGED_BLOCK_END)}`, "u");
94
190
  }
191
+ function hasEquivalentUnmanagedTicketBinding(contents) {
192
+ const text = normalizeRuleText(contents);
193
+ if (!hasTicketBindingCues(text))
194
+ return false;
195
+ const signals = [
196
+ /cockpit\s+start\s+--ticket\b/u,
197
+ /before\s+(?:the\s+)?first\s+code\s+edit|before\s+ticketed\s+implementation/u,
198
+ /ticket\s+id\s+(?:is\s+)?(?:missing|visible)|ask\s+once/u,
199
+ /general\s+ambient/u,
200
+ /cockpit\s+sync\s+--repo|fresh\s+ticket\/session\s+binding\s+metadata/u,
201
+ /use\s+--ticket|do\s+not\s+invent\s+--ticketid/u,
202
+ ];
203
+ const score = signals.filter((signal) => signal.test(text)).length;
204
+ return score >= 5;
205
+ }
206
+ function findStaleUnmanagedTicketBindingBlock(contents) {
207
+ const lines = contents.split("\n");
208
+ for (let index = 0; index < lines.length; index += 1) {
209
+ if (!/^#{1,6}\s+.*(?:cockpit\s+)?ticket\s+binding\b/iu.test(lines[index] ?? "")) {
210
+ continue;
211
+ }
212
+ let endLine = lines.length;
213
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
214
+ if (/^#{1,6}\s+\S/u.test(lines[cursor] ?? "")) {
215
+ endLine = cursor;
216
+ break;
217
+ }
218
+ }
219
+ const candidate = lines.slice(index, endLine).join("\n");
220
+ const normalized = normalizeRuleText(candidate);
221
+ if (hasTicketBindingCues(normalized) && !hasEquivalentUnmanagedTicketBinding(candidate)) {
222
+ return { startLine: index, endLine };
223
+ }
224
+ }
225
+ return null;
226
+ }
227
+ function replaceLineSpan(contents, startLine, endLine, replacement) {
228
+ const lines = contents.split("\n");
229
+ const before = lines.slice(0, startLine).join("\n").trimEnd();
230
+ const after = lines.slice(endLine).join("\n").trimStart();
231
+ return [before, replacement, after]
232
+ .filter((part) => part.trim().length > 0)
233
+ .join("\n\n")
234
+ .replace(/\n{3,}/gu, "\n\n")
235
+ .trimEnd() + "\n";
236
+ }
237
+ function hasTicketBindingCues(text) {
238
+ return /\bcockpit\b/u.test(text) && /\bticket\b/u.test(text) && /binding|agent|linear/u.test(text);
239
+ }
240
+ function normalizeRuleText(contents) {
241
+ return contents
242
+ .toLowerCase()
243
+ .replace(/[`"'<>]/gu, "")
244
+ .replace(/\s+/gu, " ")
245
+ .trim();
246
+ }
95
247
  function escapeRegExp(value) {
96
248
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
97
249
  }
@@ -345,8 +345,8 @@ function parseAutostartArgs(args) {
345
345
  }
346
346
  function parseAgentRulesArgs(args) {
347
347
  const values = parseNamedArgs(args, {
348
- allowedFlags: ["--home", "--json"],
349
- valueFlags: ["--home"],
348
+ allowedFlags: ["--home", "--host", "--json"],
349
+ valueFlags: ["--home", "--host"],
350
350
  });
351
351
  if (values.positionals.length > 1) {
352
352
  throw new Error("agent-rules accepts at most one action (install|uninstall|status).");
@@ -358,10 +358,17 @@ function parseAgentRulesArgs(args) {
358
358
  return {
359
359
  kind: "agent-rules",
360
360
  action,
361
+ host: parseAgentRulesHost(values.flags.get("--host")),
361
362
  homeDir: optionalNonEmpty(values.flags.get("--home")),
362
363
  json: values.booleans.has("--json"),
363
364
  };
364
365
  }
366
+ function parseAgentRulesHost(value) {
367
+ const host = value?.trim().toLowerCase() || "all";
368
+ if (host === "codex" || host === "claude" || host === "all")
369
+ return host;
370
+ throw new Error("agent-rules --host must be codex, claude, or all.");
371
+ }
365
372
  function parseNamedArgs(args, options) {
366
373
  const allowed = new Set(options.allowedFlags);
367
374
  const valueFlags = new Set(options.valueFlags);
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { createCollectorServer } from "../server.js";
5
- import { inspectCodexAgentRules, installCodexAgentRules, uninstallCodexAgentRules, } from "../agent-rules.js";
5
+ import { inspectAgentRules, installAgentRules, uninstallAgentRules, } from "../agent-rules.js";
6
6
  import { parseLocalArgs, normalizeUrl } from "./local-args.js";
7
7
  import { autostartStatus, installAutostartAgent, uninstallAutostartAgent } from "../autostart.js";
8
8
  import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext } from "../local-state.js";
@@ -86,7 +86,7 @@ export function localCommandHelp(command) {
86
86
  " cockpit sessions [--source codex|claude] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
87
87
  " cockpit serve [--port <port>] [--workspace <path>]",
88
88
  " cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
89
- " cockpit agent-rules [install|uninstall|status] [--json]",
89
+ " cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--json]",
90
90
  "",
91
91
  `Default dashboard: ${DEFAULT_DASHBOARD_URL}. Omit --dashboard-url for normal production use; pass it only for staging/custom dashboards or to force a different pairing.`,
92
92
  ].join("\n");
@@ -104,6 +104,7 @@ function localSubcommandHelp(command) {
104
104
  `Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
105
105
  "Pass --dashboard-url only for staging/custom dashboards or to force a different pairing.",
106
106
  "Run with no flags in a terminal and it prompts for the dashboard email; pass --email to skip the prompt (and on shared/reused machines, where mismatched sessions are re-paired).",
107
+ "Interactive runs also offer to add Cockpit ticket-binding rules to AGENTS.md and CLAUDE.md after readiness proof.",
107
108
  ],
108
109
  ],
109
110
  [
@@ -208,11 +209,13 @@ function localSubcommandHelp(command) {
208
209
  [
209
210
  "agent-rules",
210
211
  [
211
- "Usage: cockpit agent-rules [install|uninstall|status] [--json]",
212
+ "Usage: cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--json]",
212
213
  "",
213
- "Installs a managed Cockpit Ticket Binding block into ~/.codex/AGENTS.md.",
214
- "This gives Codex agents a user-scope rule to run `cockpit start --ticket <id>`",
215
- "before ticketed implementation work, or ask once when the ticket ID is missing.",
214
+ "Installs a managed Cockpit Ticket Binding block into ~/.codex/AGENTS.md",
215
+ "and ~/.claude/CLAUDE.md by default. Pass --host to manage only one.",
216
+ "This gives Codex and Claude agents a user-scope rule to run",
217
+ "`cockpit start --ticket <id>` before ticketed implementation work, or ask",
218
+ "once when the ticket ID is missing.",
216
219
  "Action defaults to `install`.",
217
220
  ],
218
221
  ],
@@ -308,6 +311,48 @@ async function maybeOfferAutostart(command, io) {
308
311
  : "Background autostart installed, but launchctl load reported a problem; check `cockpit autostart status`.");
309
312
  writeLine(io.stdout, `Plist: ${result.plist_path}`);
310
313
  }
314
+ async function maybeOfferAgentRules(command, io) {
315
+ if (command.json || !isInteractiveStdin(io))
316
+ return;
317
+ const current = await inspectAgentRules({ homeDir: command.homeDir });
318
+ if (current.installed) {
319
+ writeLine(io.stdout, onboardAgentRulesAlreadyInstalledLine(current));
320
+ return;
321
+ }
322
+ const answer = (await readLine(io, "Add Cockpit ticket-binding rules to AGENTS.md and CLAUDE.md? [Y/n] "))
323
+ .trim()
324
+ .toLowerCase();
325
+ if (answer === "n" || answer === "no") {
326
+ writeLine(io.stdout, "Skipped agent rules. Run `cockpit agent-rules install` anytime.");
327
+ return;
328
+ }
329
+ const result = await installAgentRules({ homeDir: command.homeDir });
330
+ writeLine(io.stdout, `Agent rules: ${onboardAgentRulesInstallLine(result)}`);
331
+ for (const target of result.targets) {
332
+ writeLine(io.stdout, `${agentRuleHostLabel(target.host)}: ${target.rules_file}`);
333
+ }
334
+ }
335
+ function onboardAgentRulesAlreadyInstalledLine(result) {
336
+ const hasEquivalent = result.targets.some((target) => target.state === "equivalent");
337
+ return hasEquivalent
338
+ ? "Agent rules: matching Cockpit ticket-binding guidance already exists."
339
+ : "Agent rules: already current in AGENTS.md and CLAUDE.md.";
340
+ }
341
+ function onboardAgentRulesInstallLine(result) {
342
+ if (result.targets.some((target) => target.stale_block_replaced)) {
343
+ return "updated; replaced stale Cockpit ticket-binding guidance.";
344
+ }
345
+ switch (result.status) {
346
+ case "created":
347
+ return "installed.";
348
+ case "updated":
349
+ return "updated.";
350
+ case "unchanged":
351
+ return "already current.";
352
+ case "missing":
353
+ return "not installed.";
354
+ }
355
+ }
311
356
  async function runOnboard(command, io) {
312
357
  let install = null;
313
358
  let pair = null;
@@ -381,8 +426,10 @@ async function runOnboard(command, io) {
381
426
  codex_sessions: multi.codex_sessions,
382
427
  }, null, 2));
383
428
  }
384
- if (multi.ok)
429
+ if (multi.ok) {
430
+ await maybeOfferAgentRules(command, io);
385
431
  await maybeOfferAutostart(command, io);
432
+ }
386
433
  return multi.ok ? 0 : 1;
387
434
  }
388
435
  const context = await startLocalWorkContext({
@@ -447,6 +494,7 @@ async function runOnboard(command, io) {
447
494
  writeLine(io.stdout, `Upload state: ${status.upload_state}`);
448
495
  writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
449
496
  writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
497
+ await maybeOfferAgentRules(command, io);
450
498
  await maybeOfferAutostart(command, io);
451
499
  return 0;
452
500
  }
@@ -1032,39 +1080,47 @@ async function runAutostart(command, io) {
1032
1080
  return result.status === "unsupported" ? 1 : 0;
1033
1081
  }
1034
1082
  async function runAgentRules(command, io) {
1083
+ const hosts = agentRuleHosts(command.host);
1035
1084
  const result = command.action === "install"
1036
- ? await installCodexAgentRules({ homeDir: command.homeDir })
1085
+ ? await installAgentRules({ homeDir: command.homeDir, hosts })
1037
1086
  : command.action === "uninstall"
1038
- ? await uninstallCodexAgentRules({ homeDir: command.homeDir })
1039
- : await inspectCodexAgentRules({ homeDir: command.homeDir });
1087
+ ? await uninstallAgentRules({ homeDir: command.homeDir, hosts })
1088
+ : await inspectAgentRules({ homeDir: command.homeDir, hosts });
1040
1089
  if (command.json) {
1041
1090
  writeLine(io.stdout, JSON.stringify(result, null, 2));
1042
1091
  return 0;
1043
1092
  }
1044
1093
  if ("installed" in result) {
1045
1094
  writeLine(io.stdout, result.installed
1046
- ? "Cockpit Codex agent rules are installed."
1047
- : "Cockpit Codex agent rules are not installed.");
1095
+ ? "Cockpit agent rules are installed."
1096
+ : "Cockpit agent rules are not installed.");
1048
1097
  }
1049
1098
  else {
1050
- switch (result.status) {
1051
- case "created":
1052
- writeLine(io.stdout, "Cockpit Codex agent rules installed.");
1053
- break;
1054
- case "updated":
1055
- writeLine(io.stdout, "Cockpit Codex agent rules updated.");
1056
- break;
1057
- case "unchanged":
1058
- writeLine(io.stdout, "Cockpit Codex agent rules already current.");
1059
- break;
1060
- case "missing":
1061
- writeLine(io.stdout, "Cockpit Codex agent rules were not installed.");
1062
- break;
1063
- }
1099
+ writeLine(io.stdout, agentRulesStatusLine(result));
1100
+ }
1101
+ for (const target of result.targets) {
1102
+ writeLine(io.stdout, `${agentRuleHostLabel(target.host)}: ${target.rules_file}`);
1064
1103
  }
1065
- writeLine(io.stdout, `AGENTS.md: ${result.agents_file}`);
1066
1104
  return 0;
1067
1105
  }
1106
+ function agentRuleHosts(host) {
1107
+ return host === "all" ? ["codex", "claude"] : [host];
1108
+ }
1109
+ function agentRulesStatusLine(result) {
1110
+ switch (result.status) {
1111
+ case "created":
1112
+ return "Cockpit agent rules installed.";
1113
+ case "updated":
1114
+ return "Cockpit agent rules updated.";
1115
+ case "unchanged":
1116
+ return "Cockpit agent rules already current.";
1117
+ case "missing":
1118
+ return "Cockpit agent rules were not installed.";
1119
+ }
1120
+ }
1121
+ function agentRuleHostLabel(host) {
1122
+ return host === "claude" ? "CLAUDE.md" : "AGENTS.md";
1123
+ }
1068
1124
  function writeAutostartResult(io, result) {
1069
1125
  switch (result.status) {
1070
1126
  case "installed":
@@ -25,7 +25,7 @@ function cockpitHelp() {
25
25
  "Install/update: `npm install -g @bli-cockpit/cli@latest`.",
26
26
  "Intern path: run `cockpit onboard` from the repo root; add `--ticket <id>` only when work already has a ticket.",
27
27
  "Already onboarded: run `cockpit sync --workspace \"$PWD\" --json`.",
28
- "Agent setup: run `cockpit agent-rules install` so Codex asks for or binds Linear tickets before edits.",
28
+ "Agent setup: interactive `cockpit onboard` offers AGENTS.md/CLAUDE.md rules; use `cockpit agent-rules install` for repair/headless setup.",
29
29
  "Dashboard URL is optional for normal production use; pass `--dashboard-url` only for staging/custom dashboards or forced re-pairing.",
30
30
  "Manual collector path: `install`, `login`, `start [--ticket <id>]`, `sync`, `status`, `agent-rules`.",
31
31
  ].join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {