@bli-cockpit/cli 0.1.14 → 0.1.16

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
@@ -41,6 +41,16 @@ pairing. Already-onboarded users update with
41
41
  `npm install -g @bli-cockpit/cli@latest`, then run
42
42
  `cockpit sync --repo "$PWD" --json`.
43
43
 
44
+ On machines where Codex agents will do ticketed work, install the user-scope
45
+ agent rule once:
46
+
47
+ ```bash
48
+ cockpit agent-rules install
49
+ ```
50
+
51
+ That updates `~/.codex/AGENTS.md` with the Cockpit rule to bind known Linear
52
+ tickets before edits, or ask once when the ticket ID is missing.
53
+
44
54
  Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
45
55
  repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
46
56
  if the repo cap truncates discovery), creates one stable work context per
@@ -0,0 +1,97 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ const MANAGED_BLOCK_START = "<!-- BLI_COCKPIT_AGENT_RULES:START -->";
5
+ const MANAGED_BLOCK_END = "<!-- BLI_COCKPIT_AGENT_RULES:END -->";
6
+ export async function installCodexAgentRules(options = {}) {
7
+ const agentsFile = codexAgentsFile(options.homeDir);
8
+ const block = cockpitAgentRulesBlock();
9
+ let existing = "";
10
+ let existed = true;
11
+ try {
12
+ existing = await readFile(agentsFile, "utf8");
13
+ }
14
+ catch {
15
+ existed = false;
16
+ }
17
+ const next = upsertManagedBlock(existing, block);
18
+ if (next === existing) {
19
+ return { status: "unchanged", agents_file: agentsFile, block };
20
+ }
21
+ await mkdir(path.dirname(agentsFile), { recursive: true });
22
+ await writeFile(agentsFile, next, "utf8");
23
+ return { status: existed ? "updated" : "created", agents_file: agentsFile, block };
24
+ }
25
+ export async function uninstallCodexAgentRules(options = {}) {
26
+ const agentsFile = codexAgentsFile(options.homeDir);
27
+ const block = cockpitAgentRulesBlock();
28
+ let existing = "";
29
+ try {
30
+ existing = await readFile(agentsFile, "utf8");
31
+ }
32
+ catch {
33
+ return { status: "missing", agents_file: agentsFile, block };
34
+ }
35
+ const next = removeManagedBlock(existing);
36
+ if (next === existing) {
37
+ return { status: "missing", agents_file: agentsFile, block };
38
+ }
39
+ await writeFile(agentsFile, next, "utf8");
40
+ return { status: "updated", agents_file: agentsFile, block };
41
+ }
42
+ export async function inspectCodexAgentRules(options = {}) {
43
+ const agentsFile = codexAgentsFile(options.homeDir);
44
+ const block = cockpitAgentRulesBlock();
45
+ let existing = "";
46
+ try {
47
+ existing = await readFile(agentsFile, "utf8");
48
+ }
49
+ catch {
50
+ return { status: "missing", agents_file: agentsFile, block, installed: false };
51
+ }
52
+ return {
53
+ status: hasManagedBlock(existing) ? "unchanged" : "missing",
54
+ agents_file: agentsFile,
55
+ block,
56
+ installed: hasManagedBlock(existing),
57
+ };
58
+ }
59
+ export function cockpitAgentRulesBlock() {
60
+ return [
61
+ MANAGED_BLOCK_START,
62
+ "## Cockpit Ticket Binding",
63
+ "",
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.",
65
+ "- Use `--ticket`; do not invent `--ticketId` or other flag shapes.",
66
+ "- 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
+ "- If there is truly no ticket, state that the work remains in general ambient capture and do not invent a ticket.",
68
+ "- After the first meaningful checkpoint, run `cockpit sync --repo \"$PWD\" --json` so Cockpit has fresh ticket/session binding metadata.",
69
+ MANAGED_BLOCK_END,
70
+ ].join("\n");
71
+ }
72
+ export function hasManagedBlock(contents) {
73
+ return contents.includes(MANAGED_BLOCK_START) && contents.includes(MANAGED_BLOCK_END);
74
+ }
75
+ export function upsertManagedBlock(contents, block) {
76
+ if (!contents.trim())
77
+ return `${block}\n`;
78
+ if (!hasManagedBlock(contents)) {
79
+ return `${contents.replace(/\s+$/u, "")}\n\n${block}\n`;
80
+ }
81
+ const pattern = managedBlockPattern();
82
+ return contents.replace(pattern, block);
83
+ }
84
+ export function removeManagedBlock(contents) {
85
+ if (!hasManagedBlock(contents))
86
+ return contents;
87
+ return contents.replace(managedBlockPattern(), "").replace(/\n{3,}/gu, "\n\n").trimEnd() + "\n";
88
+ }
89
+ function codexAgentsFile(homeDir = os.homedir()) {
90
+ return path.join(homeDir, ".codex", "AGENTS.md");
91
+ }
92
+ function managedBlockPattern() {
93
+ return new RegExp(`${escapeRegExp(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(MANAGED_BLOCK_END)}`, "u");
94
+ }
95
+ function escapeRegExp(value) {
96
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
97
+ }
package/dist/autostart.js CHANGED
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths } from "./local-state.js";
5
5
  /** launchd LaunchAgent label; matches docs/runbooks/cockpit-launchd-sync.md. */
6
6
  export const AUTOSTART_LABEL = "com.bli.cockpit.sync";
7
- const DEFAULT_INTERVAL_SECONDS = 1800;
7
+ export const DEFAULT_AUTOSTART_INTERVAL_SECONDS = 15 * 60;
8
8
  const UNSUPPORTED_MESSAGE = "macOS-only for now; see docs/runbooks/cockpit-launchd-sync.md";
9
9
  function plistPathFor(homeDir) {
10
10
  return path.join(homeDir, "Library", "LaunchAgents", `${AUTOSTART_LABEL}.plist`);
@@ -31,7 +31,7 @@ export async function installAutostartAgent(options) {
31
31
  const homeDir = options.homeDir ?? os.homedir();
32
32
  const workDir = path.resolve(options.repoRoot ?? process.cwd());
33
33
  const dashboardUrl = options.dashboardUrl ?? DEFAULT_DASHBOARD_URL;
34
- const intervalSeconds = options.intervalSeconds ?? DEFAULT_INTERVAL_SECONDS;
34
+ const intervalSeconds = options.intervalSeconds ?? DEFAULT_AUTOSTART_INTERVAL_SECONDS;
35
35
  const paths = getCollectorRuntimePaths(homeDir);
36
36
  const plistPath = plistPathFor(homeDir);
37
37
  const stdoutPath = path.join(paths.state_dir, "sync.log");
@@ -4,6 +4,7 @@
4
4
  //
5
5
  // Behavior-preserving extraction: functions moved verbatim, no logic change.
6
6
  import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
7
+ import { DEFAULT_AUTOSTART_INTERVAL_SECONDS } from "../autostart.js";
7
8
  export function parseLocalArgs(argv) {
8
9
  const command = argv[0];
9
10
  switch (command) {
@@ -28,6 +29,8 @@ export function parseLocalArgs(argv) {
28
29
  return parseServeArgs(argv.slice(1));
29
30
  case "autostart":
30
31
  return parseAutostartArgs(argv.slice(1));
32
+ case "agent-rules":
33
+ return parseAgentRulesArgs(argv.slice(1));
31
34
  default:
32
35
  throw new Error(`Unknown local command: ${command ?? ""}`);
33
36
  }
@@ -274,7 +277,26 @@ function parseAutostartArgs(args) {
274
277
  homeDir: optionalNonEmpty(values.flags.get("--home")),
275
278
  repoRoot: optionalNonEmpty(values.flags.get("--repo")),
276
279
  dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
277
- intervalSeconds: optionalPositiveInteger(values.flags.get("--interval-seconds"), "--interval-seconds") ?? 1800,
280
+ intervalSeconds: optionalPositiveInteger(values.flags.get("--interval-seconds"), "--interval-seconds") ?? DEFAULT_AUTOSTART_INTERVAL_SECONDS,
281
+ json: values.booleans.has("--json"),
282
+ };
283
+ }
284
+ function parseAgentRulesArgs(args) {
285
+ const values = parseNamedArgs(args, {
286
+ allowedFlags: ["--home", "--json"],
287
+ valueFlags: ["--home"],
288
+ });
289
+ if (values.positionals.length > 1) {
290
+ throw new Error("agent-rules accepts at most one action (install|uninstall|status).");
291
+ }
292
+ const action = values.positionals[0] ?? "install";
293
+ if (action !== "install" && action !== "uninstall" && action !== "status") {
294
+ throw new Error("agent-rules action must be install, uninstall, or status.");
295
+ }
296
+ return {
297
+ kind: "agent-rules",
298
+ action,
299
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
278
300
  json: values.booleans.has("--json"),
279
301
  };
280
302
  }
@@ -2,6 +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
6
  import { parseLocalArgs, normalizeUrl } from "./local-args.js";
6
7
  import { autostartStatus, installAutostartAgent, uninstallAutostartAgent } from "../autostart.js";
7
8
  import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext } from "../local-state.js";
@@ -22,6 +23,7 @@ export const rootCommandNames = new Set([
22
23
  "sessions",
23
24
  "serve",
24
25
  "autostart",
26
+ "agent-rules",
25
27
  ]);
26
28
  export async function runLocalCockpitCli(argv, io = defaultIo()) {
27
29
  if (isLocalHelpRequest(argv)) {
@@ -60,6 +62,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
60
62
  return await runServe(command, io);
61
63
  case "autostart":
62
64
  return await runAutostart(command, io);
65
+ case "agent-rules":
66
+ return await runAgentRules(command, io);
63
67
  }
64
68
  }
65
69
  catch (error) {
@@ -82,6 +86,7 @@ export function localCommandHelp(command) {
82
86
  " cockpit sessions [--source codex|claude] [--repo <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
83
87
  " cockpit serve [--port <port>] [--repo <path>]",
84
88
  " cockpit autostart [install|uninstall|status] [--repo <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
89
+ " cockpit agent-rules [install|uninstall|status] [--json]",
85
90
  "",
86
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.`,
87
92
  ].join("\n");
@@ -186,12 +191,23 @@ function localSubcommandHelp(command) {
186
191
  "Usage: cockpit autostart [install|uninstall|status] [--repo <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
187
192
  "",
188
193
  "Installs a macOS launchd LaunchAgent that runs `cockpit sync` at login and",
189
- "every 30 min (default), surviving reboots — so machines never drift to Stale.",
194
+ "every 15 min (default), surviving reboots — so machines never drift to Stale.",
190
195
  "Action defaults to `install`. `--repo` is the parent work folder to sync.",
191
196
  "Omit --dashboard-url for production; pass it only for staging/custom dashboards.",
192
197
  "macOS-only for now; see docs/runbooks/cockpit-launchd-sync.md.",
193
198
  ],
194
199
  ],
200
+ [
201
+ "agent-rules",
202
+ [
203
+ "Usage: cockpit agent-rules [install|uninstall|status] [--json]",
204
+ "",
205
+ "Installs a managed Cockpit Ticket Binding block into ~/.codex/AGENTS.md.",
206
+ "This gives Codex agents a user-scope rule to run `cockpit start --ticket <id>`",
207
+ "before ticketed implementation work, or ask once when the ticket ID is missing.",
208
+ "Action defaults to `install`.",
209
+ ],
210
+ ],
195
211
  ]);
196
212
  return (helpByCommand.get(command) ?? [localCommandHelp()]).join("\n");
197
213
  }
@@ -280,7 +296,7 @@ async function maybeOfferAutostart(command, io) {
280
296
  return;
281
297
  }
282
298
  writeLine(io.stdout, result.loaded
283
- ? "Background autostart installed; Cockpit syncs at login and every 30 min."
299
+ ? "Background autostart installed; Cockpit syncs at login and every 15 min."
284
300
  : "Background autostart installed, but launchctl load reported a problem; check `cockpit autostart status`.");
285
301
  writeLine(io.stdout, `Plist: ${result.plist_path}`);
286
302
  }
@@ -1007,6 +1023,40 @@ async function runAutostart(command, io) {
1007
1023
  writeAutostartResult(io, result);
1008
1024
  return result.status === "unsupported" ? 1 : 0;
1009
1025
  }
1026
+ async function runAgentRules(command, io) {
1027
+ const result = command.action === "install"
1028
+ ? await installCodexAgentRules({ homeDir: command.homeDir })
1029
+ : command.action === "uninstall"
1030
+ ? await uninstallCodexAgentRules({ homeDir: command.homeDir })
1031
+ : await inspectCodexAgentRules({ homeDir: command.homeDir });
1032
+ if (command.json) {
1033
+ writeLine(io.stdout, JSON.stringify(result, null, 2));
1034
+ return 0;
1035
+ }
1036
+ if ("installed" in result) {
1037
+ writeLine(io.stdout, result.installed
1038
+ ? "Cockpit Codex agent rules are installed."
1039
+ : "Cockpit Codex agent rules are not installed.");
1040
+ }
1041
+ else {
1042
+ switch (result.status) {
1043
+ case "created":
1044
+ writeLine(io.stdout, "Cockpit Codex agent rules installed.");
1045
+ break;
1046
+ case "updated":
1047
+ writeLine(io.stdout, "Cockpit Codex agent rules updated.");
1048
+ break;
1049
+ case "unchanged":
1050
+ writeLine(io.stdout, "Cockpit Codex agent rules already current.");
1051
+ break;
1052
+ case "missing":
1053
+ writeLine(io.stdout, "Cockpit Codex agent rules were not installed.");
1054
+ break;
1055
+ }
1056
+ }
1057
+ writeLine(io.stdout, `AGENTS.md: ${result.agents_file}`);
1058
+ return 0;
1059
+ }
1010
1060
  function writeAutostartResult(io, result) {
1011
1061
  switch (result.status) {
1012
1062
  case "installed":
@@ -25,8 +25,9 @@ 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 --repo \"$PWD\" --json`.",
28
+ "Agent setup: run `cockpit agent-rules install` so Codex asks for or binds Linear tickets before edits.",
28
29
  "Dashboard URL is optional for normal production use; pass `--dashboard-url` only for staging/custom dashboards or forced re-pairing.",
29
- "Manual collector path: `install`, `login`, `start [--ticket <id>]`, `sync`, `status`.",
30
+ "Manual collector path: `install`, `login`, `start [--ticket <id>]`, `sync`, `status`, `agent-rules`.",
30
31
  ].join("\n");
31
32
  }
32
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {