@bli-cockpit/cli 0.1.17 → 0.1.18

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,20 @@ 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, install the
46
+ user-scope agent rule once:
47
47
 
48
48
  ```bash
49
49
  cockpit agent-rules install
50
50
  ```
51
51
 
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.
52
+ That updates `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` with the Cockpit
53
+ rule to bind known Linear tickets before edits, or ask once when the ticket ID
54
+ is missing. That binding starts attributing the session's work to the specific
55
+ ticket in Cockpit. It checks for the managed block first, so current files are
56
+ left unchanged and stale managed blocks are replaced.
54
57
 
55
58
  Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
56
59
  repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
@@ -4,56 +4,88 @@ 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
27
  const next = upsertManagedBlock(existing, block);
18
28
  if (next === existing) {
19
- return { status: "unchanged", agents_file: agentsFile, block };
29
+ return agentRulesResult(host, rulesFile, "unchanged", block);
20
30
  }
21
- await mkdir(path.dirname(agentsFile), { recursive: true });
22
- await writeFile(agentsFile, next, "utf8");
23
- return { status: existed ? "updated" : "created", agents_file: agentsFile, block };
31
+ await mkdir(path.dirname(rulesFile), { recursive: true });
32
+ await writeFile(rulesFile, next, "utf8");
33
+ return agentRulesResult(host, rulesFile, existed ? "updated" : "created", block);
24
34
  }
25
35
  export async function uninstallCodexAgentRules(options = {}) {
26
- const agentsFile = codexAgentsFile(options.homeDir);
36
+ return uninstallAgentRulesForHost("codex", options);
37
+ }
38
+ export async function uninstallClaudeAgentRules(options = {}) {
39
+ return uninstallAgentRulesForHost("claude", options);
40
+ }
41
+ export async function uninstallAgentRules(options = {}) {
42
+ const targets = await Promise.all(targetHosts(options.hosts).map((host) => uninstallAgentRulesForHost(host, options)));
43
+ return aggregateAgentRulesResult(targets);
44
+ }
45
+ async function uninstallAgentRulesForHost(host, options = {}) {
46
+ const rulesFile = agentRulesFile(host, options.homeDir);
27
47
  const block = cockpitAgentRulesBlock();
28
48
  let existing = "";
29
49
  try {
30
- existing = await readFile(agentsFile, "utf8");
50
+ existing = await readFile(rulesFile, "utf8");
31
51
  }
32
52
  catch {
33
- return { status: "missing", agents_file: agentsFile, block };
53
+ return agentRulesResult(host, rulesFile, "missing", block);
34
54
  }
35
55
  const next = removeManagedBlock(existing);
36
56
  if (next === existing) {
37
- return { status: "missing", agents_file: agentsFile, block };
57
+ return agentRulesResult(host, rulesFile, "missing", block);
38
58
  }
39
- await writeFile(agentsFile, next, "utf8");
40
- return { status: "updated", agents_file: agentsFile, block };
59
+ await writeFile(rulesFile, next, "utf8");
60
+ return agentRulesResult(host, rulesFile, "updated", block);
41
61
  }
42
62
  export async function inspectCodexAgentRules(options = {}) {
43
- const agentsFile = codexAgentsFile(options.homeDir);
63
+ return inspectAgentRulesForHost("codex", options);
64
+ }
65
+ export async function inspectClaudeAgentRules(options = {}) {
66
+ return inspectAgentRulesForHost("claude", options);
67
+ }
68
+ export async function inspectAgentRules(options = {}) {
69
+ const targets = await Promise.all(targetHosts(options.hosts).map((host) => inspectAgentRulesForHost(host, options)));
70
+ return {
71
+ ...aggregateAgentRulesResult(targets),
72
+ installed: targets.every((target) => target.installed),
73
+ };
74
+ }
75
+ async function inspectAgentRulesForHost(host, options = {}) {
76
+ const rulesFile = agentRulesFile(host, options.homeDir);
44
77
  const block = cockpitAgentRulesBlock();
45
78
  let existing = "";
46
79
  try {
47
- existing = await readFile(agentsFile, "utf8");
80
+ existing = await readFile(rulesFile, "utf8");
48
81
  }
49
82
  catch {
50
- return { status: "missing", agents_file: agentsFile, block, installed: false };
83
+ return { ...agentRulesResult(host, rulesFile, "missing", block), installed: false };
51
84
  }
85
+ const installed = hasManagedBlock(existing);
52
86
  return {
53
- status: hasManagedBlock(existing) ? "unchanged" : "missing",
54
- agents_file: agentsFile,
55
- block,
56
- installed: hasManagedBlock(existing),
87
+ ...agentRulesResult(host, rulesFile, installed ? "unchanged" : "missing", block),
88
+ installed,
57
89
  };
58
90
  }
59
91
  export function cockpitAgentRulesBlock() {
@@ -61,7 +93,7 @@ export function cockpitAgentRulesBlock() {
61
93
  MANAGED_BLOCK_START,
62
94
  "## Cockpit Ticket Binding",
63
95
  "",
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.",
96
+ "- 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
97
  "- Use `--ticket`; do not invent `--ticketId` or other flag shapes.",
66
98
  "- 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
99
  "- If there is truly no ticket, state that the work remains in general ambient capture and do not invent a ticket.",
@@ -86,9 +118,36 @@ export function removeManagedBlock(contents) {
86
118
  return contents;
87
119
  return contents.replace(managedBlockPattern(), "").replace(/\n{3,}/gu, "\n\n").trimEnd() + "\n";
88
120
  }
89
- function codexAgentsFile(homeDir = os.homedir()) {
121
+ function agentRulesFile(host, homeDir = os.homedir()) {
122
+ if (host === "claude") {
123
+ return path.join(homeDir, ".claude", "CLAUDE.md");
124
+ }
90
125
  return path.join(homeDir, ".codex", "AGENTS.md");
91
126
  }
127
+ function agentRulesResult(host, rulesFile, status, block) {
128
+ return {
129
+ host,
130
+ status,
131
+ rules_file: rulesFile,
132
+ agents_file: rulesFile,
133
+ block,
134
+ };
135
+ }
136
+ function targetHosts(hosts) {
137
+ return hosts && hosts.length > 0 ? hosts : ["codex", "claude"];
138
+ }
139
+ function aggregateAgentRulesResult(targets) {
140
+ if (targets.every((target) => target.status === "unchanged")) {
141
+ return { status: "unchanged", targets };
142
+ }
143
+ if (targets.every((target) => target.status === "missing")) {
144
+ return { status: "missing", targets };
145
+ }
146
+ if (targets.every((target) => target.status === "created")) {
147
+ return { status: "created", targets };
148
+ }
149
+ return { status: "updated", targets };
150
+ }
92
151
  function managedBlockPattern() {
93
152
  return new RegExp(`${escapeRegExp(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(MANAGED_BLOCK_END)}`, "u");
94
153
  }
@@ -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");
@@ -208,11 +208,13 @@ function localSubcommandHelp(command) {
208
208
  [
209
209
  "agent-rules",
210
210
  [
211
- "Usage: cockpit agent-rules [install|uninstall|status] [--json]",
211
+ "Usage: cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--json]",
212
212
  "",
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.",
213
+ "Installs a managed Cockpit Ticket Binding block into ~/.codex/AGENTS.md",
214
+ "and ~/.claude/CLAUDE.md by default. Pass --host to manage only one.",
215
+ "This gives Codex and Claude agents a user-scope rule to run",
216
+ "`cockpit start --ticket <id>` before ticketed implementation work, or ask",
217
+ "once when the ticket ID is missing.",
216
218
  "Action defaults to `install`.",
217
219
  ],
218
220
  ],
@@ -1032,39 +1034,47 @@ async function runAutostart(command, io) {
1032
1034
  return result.status === "unsupported" ? 1 : 0;
1033
1035
  }
1034
1036
  async function runAgentRules(command, io) {
1037
+ const hosts = agentRuleHosts(command.host);
1035
1038
  const result = command.action === "install"
1036
- ? await installCodexAgentRules({ homeDir: command.homeDir })
1039
+ ? await installAgentRules({ homeDir: command.homeDir, hosts })
1037
1040
  : command.action === "uninstall"
1038
- ? await uninstallCodexAgentRules({ homeDir: command.homeDir })
1039
- : await inspectCodexAgentRules({ homeDir: command.homeDir });
1041
+ ? await uninstallAgentRules({ homeDir: command.homeDir, hosts })
1042
+ : await inspectAgentRules({ homeDir: command.homeDir, hosts });
1040
1043
  if (command.json) {
1041
1044
  writeLine(io.stdout, JSON.stringify(result, null, 2));
1042
1045
  return 0;
1043
1046
  }
1044
1047
  if ("installed" in result) {
1045
1048
  writeLine(io.stdout, result.installed
1046
- ? "Cockpit Codex agent rules are installed."
1047
- : "Cockpit Codex agent rules are not installed.");
1049
+ ? "Cockpit agent rules are installed."
1050
+ : "Cockpit agent rules are not installed.");
1048
1051
  }
1049
1052
  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
- }
1053
+ writeLine(io.stdout, agentRulesStatusLine(result));
1054
+ }
1055
+ for (const target of result.targets) {
1056
+ writeLine(io.stdout, `${agentRuleHostLabel(target.host)}: ${target.rules_file}`);
1064
1057
  }
1065
- writeLine(io.stdout, `AGENTS.md: ${result.agents_file}`);
1066
1058
  return 0;
1067
1059
  }
1060
+ function agentRuleHosts(host) {
1061
+ return host === "all" ? ["codex", "claude"] : [host];
1062
+ }
1063
+ function agentRulesStatusLine(result) {
1064
+ switch (result.status) {
1065
+ case "created":
1066
+ return "Cockpit agent rules installed.";
1067
+ case "updated":
1068
+ return "Cockpit agent rules updated.";
1069
+ case "unchanged":
1070
+ return "Cockpit agent rules already current.";
1071
+ case "missing":
1072
+ return "Cockpit agent rules were not installed.";
1073
+ }
1074
+ }
1075
+ function agentRuleHostLabel(host) {
1076
+ return host === "claude" ? "CLAUDE.md" : "AGENTS.md";
1077
+ }
1068
1078
  function writeAutostartResult(io, result) {
1069
1079
  switch (result.status) {
1070
1080
  case "installed":
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.18",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {