@bli-cockpit/cli 0.1.15 → 0.1.17
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 +16 -5
- package/dist/agent-rules.js +97 -0
- package/dist/autostart.js +2 -2
- package/dist/commands/local-args.js +111 -18
- package/dist/commands/local.js +76 -18
- package/dist/commands/public-root.js +3 -2
- package/dist/upload.js +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ npm install -g @bli-cockpit/cli@latest
|
|
|
19
19
|
cockpit onboard \
|
|
20
20
|
--email <APPROVED_EMAIL> \
|
|
21
21
|
--device-name "$COCKPIT_DEVICE_NAME" \
|
|
22
|
-
--
|
|
22
|
+
--workspace "$PWD"
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
If a work folder contains multiple repos, run the same command from the parent:
|
|
@@ -31,7 +31,7 @@ npm install -g @bli-cockpit/cli@latest
|
|
|
31
31
|
cockpit onboard \
|
|
32
32
|
--email <APPROVED_EMAIL> \
|
|
33
33
|
--device-name "$COCKPIT_DEVICE_NAME" \
|
|
34
|
-
--
|
|
34
|
+
--workspace "$PWD"
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
The CLI defaults to the production dashboard. Normal intern/operator setup,
|
|
@@ -39,7 +39,18 @@ updates, and syncs omit `--dashboard-url`. Pass `--dashboard-url` only for
|
|
|
39
39
|
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
|
-
`cockpit sync --
|
|
42
|
+
`cockpit sync --workspace "$PWD" --json`. `--repo <path>` remains supported
|
|
43
|
+
for older prompts and the Codex ticket-binding guardrail.
|
|
44
|
+
|
|
45
|
+
On machines where Codex agents will do ticketed work, install the user-scope
|
|
46
|
+
agent rule once:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cockpit agent-rules install
|
|
50
|
+
```
|
|
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.
|
|
43
54
|
|
|
44
55
|
Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
|
|
45
56
|
repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
|
|
@@ -87,7 +98,7 @@ cockpit start \
|
|
|
87
98
|
--repo "$PWD"
|
|
88
99
|
|
|
89
100
|
cockpit sync \
|
|
90
|
-
--
|
|
101
|
+
--workspace "$PWD" \
|
|
91
102
|
--json
|
|
92
103
|
```
|
|
93
104
|
|
|
@@ -139,7 +150,7 @@ repo, ticket, and raw JSONL object, see
|
|
|
139
150
|
Local attribution preview:
|
|
140
151
|
|
|
141
152
|
```bash
|
|
142
|
-
cockpit sessions --
|
|
153
|
+
cockpit sessions --workspace "$PWD" --json
|
|
143
154
|
```
|
|
144
155
|
|
|
145
156
|
Remote metadata path:
|
|
@@ -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
|
@@ -126,12 +126,12 @@ async function existingWatchPaths(homeDir) {
|
|
|
126
126
|
return candidates.filter((_, i) => present[i]);
|
|
127
127
|
}
|
|
128
128
|
function renderPlist(options) {
|
|
129
|
-
// The
|
|
129
|
+
// The work path is shell-quoted because it lands inside a `/bin/zsh -lc "…"`
|
|
130
130
|
// command string; the whole command is then XML-escaped for the <string>.
|
|
131
131
|
const dashboardArg = options.dashboardUrl === DEFAULT_DASHBOARD_URL
|
|
132
132
|
? ""
|
|
133
133
|
: ` --dashboard-url ${shellQuote(options.dashboardUrl)}`;
|
|
134
|
-
const command = `cockpit sync --
|
|
134
|
+
const command = `cockpit sync --workspace ${shellQuote(options.workDir)}${dashboardArg} --json`;
|
|
135
135
|
return [
|
|
136
136
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
137
137
|
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// Behavior-preserving extraction: functions moved verbatim, no logic change.
|
|
6
6
|
import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
|
|
7
7
|
import { DEFAULT_AUTOSTART_INTERVAL_SECONDS } from "../autostart.js";
|
|
8
|
+
const WORK_ROOT_FLAGS = ["--repo", "--workspace"];
|
|
8
9
|
export function parseLocalArgs(argv) {
|
|
9
10
|
const command = argv[0];
|
|
10
11
|
switch (command) {
|
|
@@ -29,6 +30,8 @@ export function parseLocalArgs(argv) {
|
|
|
29
30
|
return parseServeArgs(argv.slice(1));
|
|
30
31
|
case "autostart":
|
|
31
32
|
return parseAutostartArgs(argv.slice(1));
|
|
33
|
+
case "agent-rules":
|
|
34
|
+
return parseAgentRulesArgs(argv.slice(1));
|
|
32
35
|
default:
|
|
33
36
|
throw new Error(`Unknown local command: ${command ?? ""}`);
|
|
34
37
|
}
|
|
@@ -38,6 +41,7 @@ function parseOnboardArgs(args) {
|
|
|
38
41
|
allowedFlags: [
|
|
39
42
|
"--home",
|
|
40
43
|
"--repo",
|
|
44
|
+
"--workspace",
|
|
41
45
|
"--dashboard-url",
|
|
42
46
|
"--email",
|
|
43
47
|
"--device-name",
|
|
@@ -52,6 +56,7 @@ function parseOnboardArgs(args) {
|
|
|
52
56
|
valueFlags: [
|
|
53
57
|
"--home",
|
|
54
58
|
"--repo",
|
|
59
|
+
"--workspace",
|
|
55
60
|
"--dashboard-url",
|
|
56
61
|
"--email",
|
|
57
62
|
"--device-name",
|
|
@@ -67,7 +72,7 @@ function parseOnboardArgs(args) {
|
|
|
67
72
|
return {
|
|
68
73
|
kind: "onboard",
|
|
69
74
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
70
|
-
repoRoot: optionalNonEmpty(values
|
|
75
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
71
76
|
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
72
77
|
claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
|
|
73
78
|
deviceName: optionalNonEmpty(values.flags.get("--device-name")),
|
|
@@ -85,17 +90,24 @@ function parseInstallArgs(args) {
|
|
|
85
90
|
allowedFlags: [
|
|
86
91
|
"--home",
|
|
87
92
|
"--repo",
|
|
93
|
+
"--workspace",
|
|
88
94
|
"--dashboard-url",
|
|
89
95
|
"--supabase-url",
|
|
90
96
|
"--json",
|
|
91
97
|
],
|
|
92
|
-
valueFlags: [
|
|
98
|
+
valueFlags: [
|
|
99
|
+
"--home",
|
|
100
|
+
"--repo",
|
|
101
|
+
"--workspace",
|
|
102
|
+
"--dashboard-url",
|
|
103
|
+
"--supabase-url",
|
|
104
|
+
],
|
|
93
105
|
});
|
|
94
106
|
assertNoPositionals(values.positionals, "install");
|
|
95
107
|
return {
|
|
96
108
|
kind: "install",
|
|
97
109
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
98
|
-
repoRoot: optionalNonEmpty(values
|
|
110
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
99
111
|
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
100
112
|
supabaseUrl: optionalNonEmpty(values.flags.get("--supabase-url")),
|
|
101
113
|
json: values.booleans.has("--json"),
|
|
@@ -150,6 +162,7 @@ function parseStartArgs(args) {
|
|
|
150
162
|
allowedFlags: [
|
|
151
163
|
"--home",
|
|
152
164
|
"--repo",
|
|
165
|
+
"--workspace",
|
|
153
166
|
"--branch",
|
|
154
167
|
"--ticket",
|
|
155
168
|
"--operator-id",
|
|
@@ -161,6 +174,7 @@ function parseStartArgs(args) {
|
|
|
161
174
|
valueFlags: [
|
|
162
175
|
"--home",
|
|
163
176
|
"--repo",
|
|
177
|
+
"--workspace",
|
|
164
178
|
"--branch",
|
|
165
179
|
"--ticket",
|
|
166
180
|
"--operator-id",
|
|
@@ -173,7 +187,7 @@ function parseStartArgs(args) {
|
|
|
173
187
|
return {
|
|
174
188
|
kind: "start",
|
|
175
189
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
176
|
-
repoRoot: optionalNonEmpty(values
|
|
190
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
177
191
|
branch: optionalNonEmpty(values.flags.get("--branch")),
|
|
178
192
|
activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
|
|
179
193
|
operatorId: optionalNonEmpty(values.flags.get("--operator-id")),
|
|
@@ -185,14 +199,29 @@ function parseStartArgs(args) {
|
|
|
185
199
|
}
|
|
186
200
|
function parseSyncArgs(args) {
|
|
187
201
|
const values = parseNamedArgs(args, {
|
|
188
|
-
allowedFlags: [
|
|
189
|
-
|
|
202
|
+
allowedFlags: [
|
|
203
|
+
"--home",
|
|
204
|
+
"--repo",
|
|
205
|
+
"--workspace",
|
|
206
|
+
"--dashboard-url",
|
|
207
|
+
"--json",
|
|
208
|
+
"--max-depth",
|
|
209
|
+
"--max-repos",
|
|
210
|
+
],
|
|
211
|
+
valueFlags: [
|
|
212
|
+
"--home",
|
|
213
|
+
"--repo",
|
|
214
|
+
"--workspace",
|
|
215
|
+
"--dashboard-url",
|
|
216
|
+
"--max-depth",
|
|
217
|
+
"--max-repos",
|
|
218
|
+
],
|
|
190
219
|
});
|
|
191
220
|
assertNoPositionals(values.positionals, "sync");
|
|
192
221
|
return {
|
|
193
222
|
kind: "sync",
|
|
194
223
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
195
|
-
repoRoot: optionalNonEmpty(values
|
|
224
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
196
225
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
197
226
|
json: values.booleans.has("--json"),
|
|
198
227
|
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
@@ -201,14 +230,27 @@ function parseSyncArgs(args) {
|
|
|
201
230
|
}
|
|
202
231
|
function parseStatusArgs(args) {
|
|
203
232
|
const values = parseNamedArgs(args, {
|
|
204
|
-
allowedFlags: [
|
|
205
|
-
|
|
233
|
+
allowedFlags: [
|
|
234
|
+
"--home",
|
|
235
|
+
"--repo",
|
|
236
|
+
"--workspace",
|
|
237
|
+
"--json",
|
|
238
|
+
"--max-depth",
|
|
239
|
+
"--max-repos",
|
|
240
|
+
],
|
|
241
|
+
valueFlags: [
|
|
242
|
+
"--home",
|
|
243
|
+
"--repo",
|
|
244
|
+
"--workspace",
|
|
245
|
+
"--max-depth",
|
|
246
|
+
"--max-repos",
|
|
247
|
+
],
|
|
206
248
|
});
|
|
207
249
|
assertNoPositionals(values.positionals, "status");
|
|
208
250
|
return {
|
|
209
251
|
kind: "status",
|
|
210
252
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
211
|
-
repoRoot: optionalNonEmpty(values
|
|
253
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
212
254
|
json: values.booleans.has("--json"),
|
|
213
255
|
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
214
256
|
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
@@ -216,8 +258,23 @@ function parseStatusArgs(args) {
|
|
|
216
258
|
}
|
|
217
259
|
function parseSessionsArgs(args) {
|
|
218
260
|
const values = parseNamedArgs(args, {
|
|
219
|
-
allowedFlags: [
|
|
220
|
-
|
|
261
|
+
allowedFlags: [
|
|
262
|
+
"--home",
|
|
263
|
+
"--repo",
|
|
264
|
+
"--workspace",
|
|
265
|
+
"--source",
|
|
266
|
+
"--json",
|
|
267
|
+
"--max-depth",
|
|
268
|
+
"--max-repos",
|
|
269
|
+
],
|
|
270
|
+
valueFlags: [
|
|
271
|
+
"--home",
|
|
272
|
+
"--repo",
|
|
273
|
+
"--workspace",
|
|
274
|
+
"--source",
|
|
275
|
+
"--max-depth",
|
|
276
|
+
"--max-repos",
|
|
277
|
+
],
|
|
221
278
|
});
|
|
222
279
|
assertNoPositionals(values.positionals, "sessions");
|
|
223
280
|
const source = values.flags.get("--source");
|
|
@@ -227,7 +284,7 @@ function parseSessionsArgs(args) {
|
|
|
227
284
|
return {
|
|
228
285
|
kind: "sessions",
|
|
229
286
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
230
|
-
repoRoot: optionalNonEmpty(values
|
|
287
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
231
288
|
source,
|
|
232
289
|
json: values.booleans.has("--json"),
|
|
233
290
|
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
@@ -236,8 +293,8 @@ function parseSessionsArgs(args) {
|
|
|
236
293
|
}
|
|
237
294
|
function parseServeArgs(args) {
|
|
238
295
|
const values = parseNamedArgs(args, {
|
|
239
|
-
allowedFlags: ["--home", "--repo", "--port"],
|
|
240
|
-
valueFlags: ["--home", "--repo", "--port"],
|
|
296
|
+
allowedFlags: ["--home", "--repo", "--workspace", "--port"],
|
|
297
|
+
valueFlags: ["--home", "--repo", "--workspace", "--port"],
|
|
241
298
|
});
|
|
242
299
|
assertNoPositionals(values.positionals, "serve");
|
|
243
300
|
const port = Number(values.flags.get("--port") ?? "4174");
|
|
@@ -247,7 +304,7 @@ function parseServeArgs(args) {
|
|
|
247
304
|
return {
|
|
248
305
|
kind: "serve",
|
|
249
306
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
250
|
-
repoRoot: optionalNonEmpty(values
|
|
307
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
251
308
|
port,
|
|
252
309
|
};
|
|
253
310
|
}
|
|
@@ -256,11 +313,18 @@ function parseAutostartArgs(args) {
|
|
|
256
313
|
allowedFlags: [
|
|
257
314
|
"--home",
|
|
258
315
|
"--repo",
|
|
316
|
+
"--workspace",
|
|
259
317
|
"--dashboard-url",
|
|
260
318
|
"--interval-seconds",
|
|
261
319
|
"--json",
|
|
262
320
|
],
|
|
263
|
-
valueFlags: [
|
|
321
|
+
valueFlags: [
|
|
322
|
+
"--home",
|
|
323
|
+
"--repo",
|
|
324
|
+
"--workspace",
|
|
325
|
+
"--dashboard-url",
|
|
326
|
+
"--interval-seconds",
|
|
327
|
+
],
|
|
264
328
|
});
|
|
265
329
|
if (values.positionals.length > 1) {
|
|
266
330
|
throw new Error("autostart accepts at most one action (install|uninstall|status).");
|
|
@@ -273,12 +337,31 @@ function parseAutostartArgs(args) {
|
|
|
273
337
|
kind: "autostart",
|
|
274
338
|
action,
|
|
275
339
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
276
|
-
repoRoot: optionalNonEmpty(values
|
|
340
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
277
341
|
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
278
342
|
intervalSeconds: optionalPositiveInteger(values.flags.get("--interval-seconds"), "--interval-seconds") ?? DEFAULT_AUTOSTART_INTERVAL_SECONDS,
|
|
279
343
|
json: values.booleans.has("--json"),
|
|
280
344
|
};
|
|
281
345
|
}
|
|
346
|
+
function parseAgentRulesArgs(args) {
|
|
347
|
+
const values = parseNamedArgs(args, {
|
|
348
|
+
allowedFlags: ["--home", "--json"],
|
|
349
|
+
valueFlags: ["--home"],
|
|
350
|
+
});
|
|
351
|
+
if (values.positionals.length > 1) {
|
|
352
|
+
throw new Error("agent-rules accepts at most one action (install|uninstall|status).");
|
|
353
|
+
}
|
|
354
|
+
const action = values.positionals[0] ?? "install";
|
|
355
|
+
if (action !== "install" && action !== "uninstall" && action !== "status") {
|
|
356
|
+
throw new Error("agent-rules action must be install, uninstall, or status.");
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
kind: "agent-rules",
|
|
360
|
+
action,
|
|
361
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
362
|
+
json: values.booleans.has("--json"),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
282
365
|
function parseNamedArgs(args, options) {
|
|
283
366
|
const allowed = new Set(options.allowedFlags);
|
|
284
367
|
const valueFlags = new Set(options.valueFlags);
|
|
@@ -313,6 +396,16 @@ function parseNamedArgs(args, options) {
|
|
|
313
396
|
}
|
|
314
397
|
return { flags, booleans, positionals };
|
|
315
398
|
}
|
|
399
|
+
function workRootFlagValue(values) {
|
|
400
|
+
const provided = WORK_ROOT_FLAGS.filter((flag) => values.flags.has(flag));
|
|
401
|
+
if (provided.length === 0)
|
|
402
|
+
return undefined;
|
|
403
|
+
const uniqueValues = new Set(provided.map((flag) => values.flags.get(flag)).filter(Boolean));
|
|
404
|
+
if (uniqueValues.size > 1) {
|
|
405
|
+
throw new Error("--repo and --workspace must point to the same path.");
|
|
406
|
+
}
|
|
407
|
+
return values.flags.get("--workspace") ?? values.flags.get("--repo");
|
|
408
|
+
}
|
|
316
409
|
function assertNoPositionals(positionals, command) {
|
|
317
410
|
if (positionals.length > 0) {
|
|
318
411
|
throw new Error(`${command} does not accept positional arguments.`);
|
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) {
|
|
@@ -71,17 +75,18 @@ export function localCommandHelp(command) {
|
|
|
71
75
|
if (command)
|
|
72
76
|
return localSubcommandHelp(command);
|
|
73
77
|
return [
|
|
74
|
-
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--
|
|
75
|
-
" cockpit install [--dashboard-url <url>] [--
|
|
78
|
+
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
79
|
+
" cockpit install [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
76
80
|
" cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
77
81
|
" cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
78
82
|
" cockpit logout",
|
|
79
|
-
" cockpit start [--ticket <id>] [--
|
|
80
|
-
" cockpit sync [--
|
|
81
|
-
" cockpit status [--
|
|
82
|
-
" cockpit sessions [--source codex|claude] [--
|
|
83
|
-
" cockpit serve [--port <port>] [--
|
|
84
|
-
" cockpit autostart [install|uninstall|status] [--
|
|
83
|
+
" cockpit start [--ticket <id>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
84
|
+
" cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
85
|
+
" cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
86
|
+
" cockpit sessions [--source codex|claude] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
87
|
+
" cockpit serve [--port <port>] [--workspace <path>]",
|
|
88
|
+
" cockpit autostart [install|uninstall|status] [--workspace <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");
|
|
@@ -91,10 +96,11 @@ function localSubcommandHelp(command) {
|
|
|
91
96
|
[
|
|
92
97
|
"onboard",
|
|
93
98
|
[
|
|
94
|
-
"Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--
|
|
99
|
+
"Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--json]",
|
|
95
100
|
"",
|
|
96
101
|
"Installs, pairs, starts work context(s), syncs once, and prints readiness proof.",
|
|
97
|
-
"If --
|
|
102
|
+
"If --workspace is a parent folder, scans child git repos/worktrees and rolls them up by repo.",
|
|
103
|
+
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
98
104
|
`Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
|
|
99
105
|
"Pass --dashboard-url only for staging/custom dashboards or to force a different pairing.",
|
|
100
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).",
|
|
@@ -103,9 +109,10 @@ function localSubcommandHelp(command) {
|
|
|
103
109
|
[
|
|
104
110
|
"install",
|
|
105
111
|
[
|
|
106
|
-
"Usage: cockpit install [--dashboard-url <url>] [--
|
|
112
|
+
"Usage: cockpit install [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
107
113
|
"",
|
|
108
114
|
"Writes local collector config. Pair with `cockpit login`, then run `cockpit start` when work begins.",
|
|
115
|
+
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
109
116
|
"Omit --dashboard-url for the production dashboard; pass it only for staging/custom dashboards.",
|
|
110
117
|
],
|
|
111
118
|
],
|
|
@@ -130,19 +137,21 @@ function localSubcommandHelp(command) {
|
|
|
130
137
|
[
|
|
131
138
|
"start",
|
|
132
139
|
[
|
|
133
|
-
"Usage: cockpit start [--ticket <id>] [--
|
|
140
|
+
"Usage: cockpit start [--ticket <id>] [--workspace <path>] [--branch <name>] [--json]",
|
|
134
141
|
"",
|
|
135
142
|
"Starts local ambient capture. Parent folders start each child git worktree.",
|
|
136
143
|
"Add --ticket only when the work already has a visible ticket.",
|
|
144
|
+
"`--repo <path>` remains supported and is still the canonical agent-rule spelling.",
|
|
137
145
|
],
|
|
138
146
|
],
|
|
139
147
|
[
|
|
140
148
|
"sync",
|
|
141
149
|
[
|
|
142
|
-
"Usage: cockpit sync [--
|
|
150
|
+
"Usage: cockpit sync [--workspace <path>] [--dashboard-url <url>] [--json]",
|
|
143
151
|
"",
|
|
144
152
|
"Uploads latest local ambient envelope(s), or spools safe retries if blocked.",
|
|
145
153
|
"Omit --dashboard-url for normal production sync; pass it only for staging/custom dashboards or forced re-pairing.",
|
|
154
|
+
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
146
155
|
"Parent folders sync each child git worktree; Codex AND Claude Code JSONL",
|
|
147
156
|
"transcripts (and Claude subagent sidecars) are attributed to repos",
|
|
148
157
|
"deterministically and ambiguous transcripts are retained as unattributed",
|
|
@@ -156,42 +165,57 @@ function localSubcommandHelp(command) {
|
|
|
156
165
|
[
|
|
157
166
|
"status",
|
|
158
167
|
[
|
|
159
|
-
"Usage: cockpit status [--
|
|
168
|
+
"Usage: cockpit status [--workspace <path>] [--json]",
|
|
160
169
|
"",
|
|
161
170
|
"Prints install, pairing, active work, upload, and retry state.",
|
|
171
|
+
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
162
172
|
],
|
|
163
173
|
],
|
|
164
174
|
[
|
|
165
175
|
"sessions",
|
|
166
176
|
[
|
|
167
|
-
"Usage: cockpit sessions [--source codex|claude] [--
|
|
177
|
+
"Usage: cockpit sessions [--source codex|claude] [--workspace <path>] [--json]",
|
|
168
178
|
"",
|
|
169
179
|
"Read-only: re-runs Codex + Claude session attribution and prints each",
|
|
170
180
|
"session's id, source, state, reason, scores, signals, and per-sidecar",
|
|
171
181
|
"skip reasons. No upload, no cursor writes. Answers \"why is session X",
|
|
172
182
|
"missing?\" locally — counts and labels only, never paths or content.",
|
|
183
|
+
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
173
184
|
],
|
|
174
185
|
],
|
|
175
186
|
[
|
|
176
187
|
"serve",
|
|
177
188
|
[
|
|
178
|
-
"Usage: cockpit serve [--port <port>] [--
|
|
189
|
+
"Usage: cockpit serve [--port <port>] [--workspace <path>]",
|
|
179
190
|
"",
|
|
180
191
|
"Starts the local collector HTTP status server.",
|
|
192
|
+
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
181
193
|
],
|
|
182
194
|
],
|
|
183
195
|
[
|
|
184
196
|
"autostart",
|
|
185
197
|
[
|
|
186
|
-
"Usage: cockpit autostart [install|uninstall|status] [--
|
|
198
|
+
"Usage: cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
187
199
|
"",
|
|
188
200
|
"Installs a macOS launchd LaunchAgent that runs `cockpit sync` at login and",
|
|
189
201
|
"every 15 min (default), surviving reboots — so machines never drift to Stale.",
|
|
190
|
-
"Action defaults to `install`. `--
|
|
202
|
+
"Action defaults to `install`. `--workspace` is the parent work folder to sync.",
|
|
203
|
+
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
191
204
|
"Omit --dashboard-url for production; pass it only for staging/custom dashboards.",
|
|
192
205
|
"macOS-only for now; see docs/runbooks/cockpit-launchd-sync.md.",
|
|
193
206
|
],
|
|
194
207
|
],
|
|
208
|
+
[
|
|
209
|
+
"agent-rules",
|
|
210
|
+
[
|
|
211
|
+
"Usage: cockpit agent-rules [install|uninstall|status] [--json]",
|
|
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.",
|
|
216
|
+
"Action defaults to `install`.",
|
|
217
|
+
],
|
|
218
|
+
],
|
|
195
219
|
]);
|
|
196
220
|
return (helpByCommand.get(command) ?? [localCommandHelp()]).join("\n");
|
|
197
221
|
}
|
|
@@ -1007,6 +1031,40 @@ async function runAutostart(command, io) {
|
|
|
1007
1031
|
writeAutostartResult(io, result);
|
|
1008
1032
|
return result.status === "unsupported" ? 1 : 0;
|
|
1009
1033
|
}
|
|
1034
|
+
async function runAgentRules(command, io) {
|
|
1035
|
+
const result = command.action === "install"
|
|
1036
|
+
? await installCodexAgentRules({ homeDir: command.homeDir })
|
|
1037
|
+
: command.action === "uninstall"
|
|
1038
|
+
? await uninstallCodexAgentRules({ homeDir: command.homeDir })
|
|
1039
|
+
: await inspectCodexAgentRules({ homeDir: command.homeDir });
|
|
1040
|
+
if (command.json) {
|
|
1041
|
+
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
1042
|
+
return 0;
|
|
1043
|
+
}
|
|
1044
|
+
if ("installed" in result) {
|
|
1045
|
+
writeLine(io.stdout, result.installed
|
|
1046
|
+
? "Cockpit Codex agent rules are installed."
|
|
1047
|
+
: "Cockpit Codex agent rules are not installed.");
|
|
1048
|
+
}
|
|
1049
|
+
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
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
writeLine(io.stdout, `AGENTS.md: ${result.agents_file}`);
|
|
1066
|
+
return 0;
|
|
1067
|
+
}
|
|
1010
1068
|
function writeAutostartResult(io, result) {
|
|
1011
1069
|
switch (result.status) {
|
|
1012
1070
|
case "installed":
|
|
@@ -24,9 +24,10 @@ function cockpitHelp() {
|
|
|
24
24
|
"",
|
|
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
|
-
"Already onboarded: run `cockpit sync --
|
|
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
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/dist/upload.js
CHANGED
|
@@ -19,7 +19,7 @@ export async function buildLocalAmbientEnvelope(options = {}) {
|
|
|
19
19
|
const now = options.now ?? new Date();
|
|
20
20
|
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
21
21
|
const config = await readLocalCollectorConfig(paths).catch(() => {
|
|
22
|
-
throw new LocalUploadBlockedError("not_installed", "Local collector config missing. Install/update the CLI, then run `cockpit onboard` before `cockpit sync`.", "npm install -g @bli-cockpit/cli@latest && cockpit onboard --email <email> --
|
|
22
|
+
throw new LocalUploadBlockedError("not_installed", "Local collector config missing. Install/update the CLI, then run `cockpit onboard` before `cockpit sync`.", "npm install -g @bli-cockpit/cli@latest && cockpit onboard --email <email> --workspace \"$PWD\"");
|
|
23
23
|
});
|
|
24
24
|
const sessionFile = await readLocalCollectorSessionFile(paths).catch(() => {
|
|
25
25
|
throw new LocalUploadBlockedError("unpaired", "No paired collector session found. Run `cockpit login` or `cockpit pair` before `cockpit sync`.", "cockpit login");
|
|
@@ -32,7 +32,7 @@ export async function buildLocalAmbientEnvelope(options = {}) {
|
|
|
32
32
|
}
|
|
33
33
|
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
34
34
|
const activeContext = await readLocalWorkContextForRepo(paths, repoRoot).catch(() => {
|
|
35
|
-
throw new LocalUploadBlockedError("missing_context", "Active work context missing. Run `cockpit start --
|
|
35
|
+
throw new LocalUploadBlockedError("missing_context", "Active work context missing. Run `cockpit start --workspace \"$PWD\"` before `cockpit sync`.", "cockpit start --workspace \"$PWD\"");
|
|
36
36
|
});
|
|
37
37
|
const repoLabel = safeRepoLabel(activeContext.repo_label ?? repoRoot);
|
|
38
38
|
const uploadContext = makeUploadWorkContext({
|