@bli-cockpit/cli 0.1.16 → 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 +13 -9
- package/dist/agent-rules.js +80 -21
- package/dist/autostart.js +2 -2
- package/dist/commands/local-args.js +99 -20
- package/dist/commands/local.js +62 -44
- package/dist/commands/public-root.js +1 -1
- 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,17 +39,21 @@ 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 agent ticket-binding guardrail.
|
|
43
44
|
|
|
44
|
-
On machines where Codex agents will do ticketed work, install the
|
|
45
|
-
agent rule once:
|
|
45
|
+
On machines where Codex or Claude agents will do ticketed work, install the
|
|
46
|
+
user-scope agent rule once:
|
|
46
47
|
|
|
47
48
|
```bash
|
|
48
49
|
cockpit agent-rules install
|
|
49
50
|
```
|
|
50
51
|
|
|
51
|
-
That updates `~/.codex/AGENTS.md` with the Cockpit
|
|
52
|
-
tickets before edits, or ask once when the ticket ID
|
|
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.
|
|
53
57
|
|
|
54
58
|
Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
|
|
55
59
|
repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
|
|
@@ -97,7 +101,7 @@ cockpit start \
|
|
|
97
101
|
--repo "$PWD"
|
|
98
102
|
|
|
99
103
|
cockpit sync \
|
|
100
|
-
--
|
|
104
|
+
--workspace "$PWD" \
|
|
101
105
|
--json
|
|
102
106
|
```
|
|
103
107
|
|
|
@@ -149,7 +153,7 @@ repo, ticket, and raw JSONL object, see
|
|
|
149
153
|
Local attribution preview:
|
|
150
154
|
|
|
151
155
|
```bash
|
|
152
|
-
cockpit sessions --
|
|
156
|
+
cockpit sessions --workspace "$PWD" --json
|
|
153
157
|
```
|
|
154
158
|
|
|
155
159
|
Remote metadata path:
|
package/dist/agent-rules.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
29
|
+
return agentRulesResult(host, rulesFile, "unchanged", block);
|
|
20
30
|
}
|
|
21
|
-
await mkdir(path.dirname(
|
|
22
|
-
await writeFile(
|
|
23
|
-
return
|
|
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
|
-
|
|
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(
|
|
50
|
+
existing = await readFile(rulesFile, "utf8");
|
|
31
51
|
}
|
|
32
52
|
catch {
|
|
33
|
-
return
|
|
53
|
+
return agentRulesResult(host, rulesFile, "missing", block);
|
|
34
54
|
}
|
|
35
55
|
const next = removeManagedBlock(existing);
|
|
36
56
|
if (next === existing) {
|
|
37
|
-
return
|
|
57
|
+
return agentRulesResult(host, rulesFile, "missing", block);
|
|
38
58
|
}
|
|
39
|
-
await writeFile(
|
|
40
|
-
return
|
|
59
|
+
await writeFile(rulesFile, next, "utf8");
|
|
60
|
+
return agentRulesResult(host, rulesFile, "updated", block);
|
|
41
61
|
}
|
|
42
62
|
export async function inspectCodexAgentRules(options = {}) {
|
|
43
|
-
|
|
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(
|
|
80
|
+
existing = await readFile(rulesFile, "utf8");
|
|
48
81
|
}
|
|
49
82
|
catch {
|
|
50
|
-
return {
|
|
83
|
+
return { ...agentRulesResult(host, rulesFile, "missing", block), installed: false };
|
|
51
84
|
}
|
|
85
|
+
const installed = hasManagedBlock(existing);
|
|
52
86
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
}
|
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) {
|
|
@@ -40,6 +41,7 @@ function parseOnboardArgs(args) {
|
|
|
40
41
|
allowedFlags: [
|
|
41
42
|
"--home",
|
|
42
43
|
"--repo",
|
|
44
|
+
"--workspace",
|
|
43
45
|
"--dashboard-url",
|
|
44
46
|
"--email",
|
|
45
47
|
"--device-name",
|
|
@@ -54,6 +56,7 @@ function parseOnboardArgs(args) {
|
|
|
54
56
|
valueFlags: [
|
|
55
57
|
"--home",
|
|
56
58
|
"--repo",
|
|
59
|
+
"--workspace",
|
|
57
60
|
"--dashboard-url",
|
|
58
61
|
"--email",
|
|
59
62
|
"--device-name",
|
|
@@ -69,7 +72,7 @@ function parseOnboardArgs(args) {
|
|
|
69
72
|
return {
|
|
70
73
|
kind: "onboard",
|
|
71
74
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
72
|
-
repoRoot: optionalNonEmpty(values
|
|
75
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
73
76
|
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
74
77
|
claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
|
|
75
78
|
deviceName: optionalNonEmpty(values.flags.get("--device-name")),
|
|
@@ -87,17 +90,24 @@ function parseInstallArgs(args) {
|
|
|
87
90
|
allowedFlags: [
|
|
88
91
|
"--home",
|
|
89
92
|
"--repo",
|
|
93
|
+
"--workspace",
|
|
90
94
|
"--dashboard-url",
|
|
91
95
|
"--supabase-url",
|
|
92
96
|
"--json",
|
|
93
97
|
],
|
|
94
|
-
valueFlags: [
|
|
98
|
+
valueFlags: [
|
|
99
|
+
"--home",
|
|
100
|
+
"--repo",
|
|
101
|
+
"--workspace",
|
|
102
|
+
"--dashboard-url",
|
|
103
|
+
"--supabase-url",
|
|
104
|
+
],
|
|
95
105
|
});
|
|
96
106
|
assertNoPositionals(values.positionals, "install");
|
|
97
107
|
return {
|
|
98
108
|
kind: "install",
|
|
99
109
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
100
|
-
repoRoot: optionalNonEmpty(values
|
|
110
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
101
111
|
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
102
112
|
supabaseUrl: optionalNonEmpty(values.flags.get("--supabase-url")),
|
|
103
113
|
json: values.booleans.has("--json"),
|
|
@@ -152,6 +162,7 @@ function parseStartArgs(args) {
|
|
|
152
162
|
allowedFlags: [
|
|
153
163
|
"--home",
|
|
154
164
|
"--repo",
|
|
165
|
+
"--workspace",
|
|
155
166
|
"--branch",
|
|
156
167
|
"--ticket",
|
|
157
168
|
"--operator-id",
|
|
@@ -163,6 +174,7 @@ function parseStartArgs(args) {
|
|
|
163
174
|
valueFlags: [
|
|
164
175
|
"--home",
|
|
165
176
|
"--repo",
|
|
177
|
+
"--workspace",
|
|
166
178
|
"--branch",
|
|
167
179
|
"--ticket",
|
|
168
180
|
"--operator-id",
|
|
@@ -175,7 +187,7 @@ function parseStartArgs(args) {
|
|
|
175
187
|
return {
|
|
176
188
|
kind: "start",
|
|
177
189
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
178
|
-
repoRoot: optionalNonEmpty(values
|
|
190
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
179
191
|
branch: optionalNonEmpty(values.flags.get("--branch")),
|
|
180
192
|
activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
|
|
181
193
|
operatorId: optionalNonEmpty(values.flags.get("--operator-id")),
|
|
@@ -187,14 +199,29 @@ function parseStartArgs(args) {
|
|
|
187
199
|
}
|
|
188
200
|
function parseSyncArgs(args) {
|
|
189
201
|
const values = parseNamedArgs(args, {
|
|
190
|
-
allowedFlags: [
|
|
191
|
-
|
|
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
|
+
],
|
|
192
219
|
});
|
|
193
220
|
assertNoPositionals(values.positionals, "sync");
|
|
194
221
|
return {
|
|
195
222
|
kind: "sync",
|
|
196
223
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
197
|
-
repoRoot: optionalNonEmpty(values
|
|
224
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
198
225
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
199
226
|
json: values.booleans.has("--json"),
|
|
200
227
|
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
@@ -203,14 +230,27 @@ function parseSyncArgs(args) {
|
|
|
203
230
|
}
|
|
204
231
|
function parseStatusArgs(args) {
|
|
205
232
|
const values = parseNamedArgs(args, {
|
|
206
|
-
allowedFlags: [
|
|
207
|
-
|
|
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
|
+
],
|
|
208
248
|
});
|
|
209
249
|
assertNoPositionals(values.positionals, "status");
|
|
210
250
|
return {
|
|
211
251
|
kind: "status",
|
|
212
252
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
213
|
-
repoRoot: optionalNonEmpty(values
|
|
253
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
214
254
|
json: values.booleans.has("--json"),
|
|
215
255
|
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
216
256
|
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
@@ -218,8 +258,23 @@ function parseStatusArgs(args) {
|
|
|
218
258
|
}
|
|
219
259
|
function parseSessionsArgs(args) {
|
|
220
260
|
const values = parseNamedArgs(args, {
|
|
221
|
-
allowedFlags: [
|
|
222
|
-
|
|
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
|
+
],
|
|
223
278
|
});
|
|
224
279
|
assertNoPositionals(values.positionals, "sessions");
|
|
225
280
|
const source = values.flags.get("--source");
|
|
@@ -229,7 +284,7 @@ function parseSessionsArgs(args) {
|
|
|
229
284
|
return {
|
|
230
285
|
kind: "sessions",
|
|
231
286
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
232
|
-
repoRoot: optionalNonEmpty(values
|
|
287
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
233
288
|
source,
|
|
234
289
|
json: values.booleans.has("--json"),
|
|
235
290
|
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
@@ -238,8 +293,8 @@ function parseSessionsArgs(args) {
|
|
|
238
293
|
}
|
|
239
294
|
function parseServeArgs(args) {
|
|
240
295
|
const values = parseNamedArgs(args, {
|
|
241
|
-
allowedFlags: ["--home", "--repo", "--port"],
|
|
242
|
-
valueFlags: ["--home", "--repo", "--port"],
|
|
296
|
+
allowedFlags: ["--home", "--repo", "--workspace", "--port"],
|
|
297
|
+
valueFlags: ["--home", "--repo", "--workspace", "--port"],
|
|
243
298
|
});
|
|
244
299
|
assertNoPositionals(values.positionals, "serve");
|
|
245
300
|
const port = Number(values.flags.get("--port") ?? "4174");
|
|
@@ -249,7 +304,7 @@ function parseServeArgs(args) {
|
|
|
249
304
|
return {
|
|
250
305
|
kind: "serve",
|
|
251
306
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
252
|
-
repoRoot: optionalNonEmpty(values
|
|
307
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
253
308
|
port,
|
|
254
309
|
};
|
|
255
310
|
}
|
|
@@ -258,11 +313,18 @@ function parseAutostartArgs(args) {
|
|
|
258
313
|
allowedFlags: [
|
|
259
314
|
"--home",
|
|
260
315
|
"--repo",
|
|
316
|
+
"--workspace",
|
|
261
317
|
"--dashboard-url",
|
|
262
318
|
"--interval-seconds",
|
|
263
319
|
"--json",
|
|
264
320
|
],
|
|
265
|
-
valueFlags: [
|
|
321
|
+
valueFlags: [
|
|
322
|
+
"--home",
|
|
323
|
+
"--repo",
|
|
324
|
+
"--workspace",
|
|
325
|
+
"--dashboard-url",
|
|
326
|
+
"--interval-seconds",
|
|
327
|
+
],
|
|
266
328
|
});
|
|
267
329
|
if (values.positionals.length > 1) {
|
|
268
330
|
throw new Error("autostart accepts at most one action (install|uninstall|status).");
|
|
@@ -275,7 +337,7 @@ function parseAutostartArgs(args) {
|
|
|
275
337
|
kind: "autostart",
|
|
276
338
|
action,
|
|
277
339
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
278
|
-
repoRoot: optionalNonEmpty(values
|
|
340
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
279
341
|
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
280
342
|
intervalSeconds: optionalPositiveInteger(values.flags.get("--interval-seconds"), "--interval-seconds") ?? DEFAULT_AUTOSTART_INTERVAL_SECONDS,
|
|
281
343
|
json: values.booleans.has("--json"),
|
|
@@ -283,8 +345,8 @@ function parseAutostartArgs(args) {
|
|
|
283
345
|
}
|
|
284
346
|
function parseAgentRulesArgs(args) {
|
|
285
347
|
const values = parseNamedArgs(args, {
|
|
286
|
-
allowedFlags: ["--home", "--json"],
|
|
287
|
-
valueFlags: ["--home"],
|
|
348
|
+
allowedFlags: ["--home", "--host", "--json"],
|
|
349
|
+
valueFlags: ["--home", "--host"],
|
|
288
350
|
});
|
|
289
351
|
if (values.positionals.length > 1) {
|
|
290
352
|
throw new Error("agent-rules accepts at most one action (install|uninstall|status).");
|
|
@@ -296,10 +358,17 @@ function parseAgentRulesArgs(args) {
|
|
|
296
358
|
return {
|
|
297
359
|
kind: "agent-rules",
|
|
298
360
|
action,
|
|
361
|
+
host: parseAgentRulesHost(values.flags.get("--host")),
|
|
299
362
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
300
363
|
json: values.booleans.has("--json"),
|
|
301
364
|
};
|
|
302
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
|
+
}
|
|
303
372
|
function parseNamedArgs(args, options) {
|
|
304
373
|
const allowed = new Set(options.allowedFlags);
|
|
305
374
|
const valueFlags = new Set(options.valueFlags);
|
|
@@ -334,6 +403,16 @@ function parseNamedArgs(args, options) {
|
|
|
334
403
|
}
|
|
335
404
|
return { flags, booleans, positionals };
|
|
336
405
|
}
|
|
406
|
+
function workRootFlagValue(values) {
|
|
407
|
+
const provided = WORK_ROOT_FLAGS.filter((flag) => values.flags.has(flag));
|
|
408
|
+
if (provided.length === 0)
|
|
409
|
+
return undefined;
|
|
410
|
+
const uniqueValues = new Set(provided.map((flag) => values.flags.get(flag)).filter(Boolean));
|
|
411
|
+
if (uniqueValues.size > 1) {
|
|
412
|
+
throw new Error("--repo and --workspace must point to the same path.");
|
|
413
|
+
}
|
|
414
|
+
return values.flags.get("--workspace") ?? values.flags.get("--repo");
|
|
415
|
+
}
|
|
337
416
|
function assertNoPositionals(positionals, command) {
|
|
338
417
|
if (positionals.length > 0) {
|
|
339
418
|
throw new Error(`${command} does not accept positional arguments.`);
|
package/dist/commands/local.js
CHANGED
|
@@ -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 {
|
|
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";
|
|
@@ -75,18 +75,18 @@ export function localCommandHelp(command) {
|
|
|
75
75
|
if (command)
|
|
76
76
|
return localSubcommandHelp(command);
|
|
77
77
|
return [
|
|
78
|
-
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--
|
|
79
|
-
" 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]",
|
|
80
80
|
" cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
81
81
|
" cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
82
82
|
" cockpit logout",
|
|
83
|
-
" cockpit start [--ticket <id>] [--
|
|
84
|
-
" cockpit sync [--
|
|
85
|
-
" cockpit status [--
|
|
86
|
-
" cockpit sessions [--source codex|claude] [--
|
|
87
|
-
" cockpit serve [--port <port>] [--
|
|
88
|
-
" cockpit autostart [install|uninstall|status] [--
|
|
89
|
-
" cockpit agent-rules [install|uninstall|status] [--json]",
|
|
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] [--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");
|
|
@@ -96,10 +96,11 @@ function localSubcommandHelp(command) {
|
|
|
96
96
|
[
|
|
97
97
|
"onboard",
|
|
98
98
|
[
|
|
99
|
-
"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]",
|
|
100
100
|
"",
|
|
101
101
|
"Installs, pairs, starts work context(s), syncs once, and prints readiness proof.",
|
|
102
|
-
"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.",
|
|
103
104
|
`Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
|
|
104
105
|
"Pass --dashboard-url only for staging/custom dashboards or to force a different pairing.",
|
|
105
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).",
|
|
@@ -108,9 +109,10 @@ function localSubcommandHelp(command) {
|
|
|
108
109
|
[
|
|
109
110
|
"install",
|
|
110
111
|
[
|
|
111
|
-
"Usage: cockpit install [--dashboard-url <url>] [--
|
|
112
|
+
"Usage: cockpit install [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
112
113
|
"",
|
|
113
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.",
|
|
114
116
|
"Omit --dashboard-url for the production dashboard; pass it only for staging/custom dashboards.",
|
|
115
117
|
],
|
|
116
118
|
],
|
|
@@ -135,19 +137,21 @@ function localSubcommandHelp(command) {
|
|
|
135
137
|
[
|
|
136
138
|
"start",
|
|
137
139
|
[
|
|
138
|
-
"Usage: cockpit start [--ticket <id>] [--
|
|
140
|
+
"Usage: cockpit start [--ticket <id>] [--workspace <path>] [--branch <name>] [--json]",
|
|
139
141
|
"",
|
|
140
142
|
"Starts local ambient capture. Parent folders start each child git worktree.",
|
|
141
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.",
|
|
142
145
|
],
|
|
143
146
|
],
|
|
144
147
|
[
|
|
145
148
|
"sync",
|
|
146
149
|
[
|
|
147
|
-
"Usage: cockpit sync [--
|
|
150
|
+
"Usage: cockpit sync [--workspace <path>] [--dashboard-url <url>] [--json]",
|
|
148
151
|
"",
|
|
149
152
|
"Uploads latest local ambient envelope(s), or spools safe retries if blocked.",
|
|
150
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.",
|
|
151
155
|
"Parent folders sync each child git worktree; Codex AND Claude Code JSONL",
|
|
152
156
|
"transcripts (and Claude subagent sidecars) are attributed to repos",
|
|
153
157
|
"deterministically and ambiguous transcripts are retained as unattributed",
|
|
@@ -161,38 +165,42 @@ function localSubcommandHelp(command) {
|
|
|
161
165
|
[
|
|
162
166
|
"status",
|
|
163
167
|
[
|
|
164
|
-
"Usage: cockpit status [--
|
|
168
|
+
"Usage: cockpit status [--workspace <path>] [--json]",
|
|
165
169
|
"",
|
|
166
170
|
"Prints install, pairing, active work, upload, and retry state.",
|
|
171
|
+
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
167
172
|
],
|
|
168
173
|
],
|
|
169
174
|
[
|
|
170
175
|
"sessions",
|
|
171
176
|
[
|
|
172
|
-
"Usage: cockpit sessions [--source codex|claude] [--
|
|
177
|
+
"Usage: cockpit sessions [--source codex|claude] [--workspace <path>] [--json]",
|
|
173
178
|
"",
|
|
174
179
|
"Read-only: re-runs Codex + Claude session attribution and prints each",
|
|
175
180
|
"session's id, source, state, reason, scores, signals, and per-sidecar",
|
|
176
181
|
"skip reasons. No upload, no cursor writes. Answers \"why is session X",
|
|
177
182
|
"missing?\" locally — counts and labels only, never paths or content.",
|
|
183
|
+
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
178
184
|
],
|
|
179
185
|
],
|
|
180
186
|
[
|
|
181
187
|
"serve",
|
|
182
188
|
[
|
|
183
|
-
"Usage: cockpit serve [--port <port>] [--
|
|
189
|
+
"Usage: cockpit serve [--port <port>] [--workspace <path>]",
|
|
184
190
|
"",
|
|
185
191
|
"Starts the local collector HTTP status server.",
|
|
192
|
+
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
186
193
|
],
|
|
187
194
|
],
|
|
188
195
|
[
|
|
189
196
|
"autostart",
|
|
190
197
|
[
|
|
191
|
-
"Usage: cockpit autostart [install|uninstall|status] [--
|
|
198
|
+
"Usage: cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
192
199
|
"",
|
|
193
200
|
"Installs a macOS launchd LaunchAgent that runs `cockpit sync` at login and",
|
|
194
201
|
"every 15 min (default), surviving reboots — so machines never drift to Stale.",
|
|
195
|
-
"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.",
|
|
196
204
|
"Omit --dashboard-url for production; pass it only for staging/custom dashboards.",
|
|
197
205
|
"macOS-only for now; see docs/runbooks/cockpit-launchd-sync.md.",
|
|
198
206
|
],
|
|
@@ -200,11 +208,13 @@ function localSubcommandHelp(command) {
|
|
|
200
208
|
[
|
|
201
209
|
"agent-rules",
|
|
202
210
|
[
|
|
203
|
-
"Usage: cockpit agent-rules [install|uninstall|status] [--json]",
|
|
211
|
+
"Usage: cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--json]",
|
|
204
212
|
"",
|
|
205
|
-
"Installs a managed Cockpit Ticket Binding block into ~/.codex/AGENTS.md
|
|
206
|
-
"
|
|
207
|
-
"
|
|
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.",
|
|
208
218
|
"Action defaults to `install`.",
|
|
209
219
|
],
|
|
210
220
|
],
|
|
@@ -1024,39 +1034,47 @@ async function runAutostart(command, io) {
|
|
|
1024
1034
|
return result.status === "unsupported" ? 1 : 0;
|
|
1025
1035
|
}
|
|
1026
1036
|
async function runAgentRules(command, io) {
|
|
1037
|
+
const hosts = agentRuleHosts(command.host);
|
|
1027
1038
|
const result = command.action === "install"
|
|
1028
|
-
? await
|
|
1039
|
+
? await installAgentRules({ homeDir: command.homeDir, hosts })
|
|
1029
1040
|
: command.action === "uninstall"
|
|
1030
|
-
? await
|
|
1031
|
-
: await
|
|
1041
|
+
? await uninstallAgentRules({ homeDir: command.homeDir, hosts })
|
|
1042
|
+
: await inspectAgentRules({ homeDir: command.homeDir, hosts });
|
|
1032
1043
|
if (command.json) {
|
|
1033
1044
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
1034
1045
|
return 0;
|
|
1035
1046
|
}
|
|
1036
1047
|
if ("installed" in result) {
|
|
1037
1048
|
writeLine(io.stdout, result.installed
|
|
1038
|
-
? "Cockpit
|
|
1039
|
-
: "Cockpit
|
|
1049
|
+
? "Cockpit agent rules are installed."
|
|
1050
|
+
: "Cockpit agent rules are not installed.");
|
|
1040
1051
|
}
|
|
1041
1052
|
else {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
}
|
|
1053
|
+
writeLine(io.stdout, agentRulesStatusLine(result));
|
|
1054
|
+
}
|
|
1055
|
+
for (const target of result.targets) {
|
|
1056
|
+
writeLine(io.stdout, `${agentRuleHostLabel(target.host)}: ${target.rules_file}`);
|
|
1056
1057
|
}
|
|
1057
|
-
writeLine(io.stdout, `AGENTS.md: ${result.agents_file}`);
|
|
1058
1058
|
return 0;
|
|
1059
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
|
+
}
|
|
1060
1078
|
function writeAutostartResult(io, result) {
|
|
1061
1079
|
switch (result.status) {
|
|
1062
1080
|
case "installed":
|
|
@@ -24,7 +24,7 @@ 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
28
|
"Agent setup: run `cockpit agent-rules install` so Codex asks for or binds Linear tickets before edits.",
|
|
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`.",
|
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({
|