@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 +10 -0
- package/dist/agent-rules.js +97 -0
- package/dist/autostart.js +2 -2
- package/dist/commands/local-args.js +23 -1
- package/dist/commands/local.js +52 -2
- package/dist/commands/public-root.js +2 -1
- package/package.json +1 -1
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
|
|
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 ??
|
|
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") ??
|
|
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
|
}
|
package/dist/commands/local.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|