@bli-cockpit/cli 0.2.160 → 0.2.162

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.
@@ -55,6 +55,8 @@ async function planCareersRequest(command) {
55
55
  // BLI-12183: `{}` sends the second-chance email now; `--record-sent-at`
56
56
  // records one sent outside Tower and sends nothing.
57
57
  case 'followup': return { path: `${application}/followup`, method: 'POST', body: command.recordSentAt ? { record_sent_at: command.recordSentAt } : {} };
58
+ // BLI-12198: `--at` books or reschedules an interview Edward booked himself; `--clear` undoes it.
59
+ case 'interview': return { path: `${application}/interview`, method: 'POST', body: command.clear ? { clear: true } : { at: command.interviewAt } };
58
60
  case 'grade': return { path: `${application}/grade`, method: 'POST', body: { now: command.now === true } };
59
61
  case 'decide': return { path: `${application}/decide`, method: 'POST', body: { decision: command.decision } };
60
62
  // `resume` never arrives here: `runCareers` hands it to `careers-resume.ts`
@@ -1,14 +1,14 @@
1
1
  import { optionalNonEmpty, optionalUrl, parseNamedArgs } from './local-arg-values.js';
2
- const ACTIONS = ['list', 'show', 'rescreen', 'invite', 'decide', 'followup', 'grade', 'resume', 'takehome-show', 'takehome-set', 'settings-show', 'settings-set'];
2
+ const ACTIONS = ['list', 'show', 'rescreen', 'invite', 'decide', 'followup', 'interview', 'grade', 'resume', 'takehome-show', 'takehome-set', 'settings-show', 'settings-set'];
3
3
  /** Typed word → the one action it means. The left side is what a person reads on the button. */
4
4
  const ALSO_SPELLED = { 'send-takehome': 'invite' };
5
5
  const DECISIONS = ['passed_on', 'archived', 'reopen'];
6
6
  /** Verbs that name an application id, a role slug, or nothing at all. */
7
- const NEEDS_ID = ['show', 'rescreen', 'invite', 'decide', 'followup', 'grade', 'resume'];
7
+ const NEEDS_ID = ['show', 'rescreen', 'invite', 'decide', 'followup', 'interview', 'grade', 'resume'];
8
8
  const NEEDS_ROLE = ['takehome-show', 'takehome-set'];
9
9
  export function parseCareersArgs(args) {
10
- const valueFlags = ['--role', '--min-score', '--since', '--home', '--dashboard-url', '--decision', '--stage', '--subject', '--body-file', '--link', '--pass-score', '--account', '--auto-invite-min-score', '--attention-min-score', '--enabled', '--booking-link', '--grader-model', '--out', '--record-sent-at', '--followup-enabled', '--followup-after-days', '--takehome-window-hours'];
11
- const values = parseNamedArgs(args, { allowedFlags: [...valueFlags, '--json', '--now', '--force'], valueFlags });
10
+ const valueFlags = ['--role', '--min-score', '--since', '--home', '--dashboard-url', '--decision', '--stage', '--subject', '--body-file', '--link', '--pass-score', '--account', '--auto-invite-min-score', '--attention-min-score', '--enabled', '--booking-link', '--grader-model', '--out', '--record-sent-at', '--at', '--followup-enabled', '--followup-after-days', '--takehome-window-hours'];
11
+ const values = parseNamedArgs(args, { allowedFlags: [...valueFlags, '--json', '--now', '--force', '--clear'], valueFlags });
12
12
  const positionals = [...values.positionals];
13
13
  // `takehome show` and `settings set` are two words a person types and one
14
14
  // action everything downstream reads.
@@ -56,6 +56,21 @@ export function parseCareersArgs(args) {
56
56
  throw new Error('--record-sent-at belongs to `cockpit careers followup <id>`.');
57
57
  if (recordSentAt !== undefined && !Number.isFinite(Date.parse(recordSentAt)))
58
58
  throw new Error('--record-sent-at must be an ISO date-time, e.g. 2026-10-03T08:00:00Z.');
59
+ // BLI-12198: an interview Edward booked himself. Exactly one of `--at <iso>`
60
+ // (book or reschedule) or `--clear`, refused here as well as on the door.
61
+ const interviewAt = optionalNonEmpty(values.flags.get('--at'));
62
+ const clear = values.booleans.has('--clear');
63
+ if (interviewAt !== undefined && verb !== 'interview')
64
+ throw new Error('--at belongs to `cockpit careers interview <id>`.');
65
+ if (clear && verb !== 'interview')
66
+ throw new Error('--clear belongs to `cockpit careers interview <id>`.');
67
+ if (verb === 'interview') {
68
+ if ((interviewAt === undefined) === !clear)
69
+ throw new Error('careers interview needs exactly one of --at <iso with offset> or --clear.');
70
+ if (interviewAt !== undefined && (!/(Z|[+-]\d{2}:\d{2})$/i.test(interviewAt) || !Number.isFinite(Date.parse(interviewAt)))) {
71
+ throw new Error('--at must be an ISO date-time with an offset, e.g. 2026-10-05T10:00:00-07:00.');
72
+ }
73
+ }
59
74
  const followupEnabledRaw = optionalNonEmpty(values.flags.get('--followup-enabled'));
60
75
  if (followupEnabledRaw !== undefined && followupEnabledRaw !== 'true' && followupEnabledRaw !== 'false')
61
76
  throw new Error('--followup-enabled must be true or false.');
@@ -84,6 +99,7 @@ export function parseCareersArgs(args) {
84
99
  graderModel: optionalNonEmpty(values.flags.get('--grader-model')),
85
100
  now: values.booleans.has('--now'),
86
101
  outPath, force,
102
+ interviewAt: interviewAt ? new Date(interviewAt).toISOString() : undefined, clear: clear || undefined,
87
103
  recordSentAt: recordSentAt ? new Date(recordSentAt).toISOString() : undefined,
88
104
  followupEnabled: followupEnabledRaw === undefined ? undefined : followupEnabledRaw === 'true',
89
105
  followupAfterDays, takehomeWindowHours,
@@ -0,0 +1,48 @@
1
+ import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
2
+ export const RENAME_SCOPES = ["page", "everywhere", "undo"];
3
+ export function parseRenameArgs(args) {
4
+ const values = parseNamedArgs(args, {
5
+ allowedFlags: ["--home", "--dashboard-url", "--scope", "--wrong", "--right", "--json"],
6
+ valueFlags: ["--home", "--dashboard-url", "--scope", "--wrong", "--right"],
7
+ });
8
+ const positionals = [...values.positionals];
9
+ const typed = positionals.shift() ?? "list";
10
+ if (typed !== "list" && typed !== "resolve") {
11
+ throw new Error(`Unknown rename command: ${typed}. Try list, or resolve <id> --scope page|everywhere|undo.`);
12
+ }
13
+ const id = positionals.shift();
14
+ // An OLD rule has no open choice and so no id: `--wrong X --right Y --scope
15
+ // undo` names it by its pair and removes the caller's own row.
16
+ const wrong = optionalNonEmpty(values.flags.get("--wrong"));
17
+ const right = optionalNonEmpty(values.flags.get("--right"));
18
+ const byPair = typed === "resolve" && !id && wrong !== undefined && right !== undefined;
19
+ if (typed === "resolve" && !id && !byPair) {
20
+ throw new Error("rename resolve needs the id from `cockpit rename list`, or --wrong and --right with --scope undo.");
21
+ }
22
+ if ((wrong !== undefined || right !== undefined) && (typed !== "resolve" || id || wrong === undefined || right === undefined)) {
23
+ throw new Error("--wrong and --right go together, on `cockpit rename resolve --scope undo`, instead of an id.");
24
+ }
25
+ if (positionals.length > 0 || (typed === "list" && id)) {
26
+ throw new Error(`rename ${typed} does not take "${typed === "list" ? id : positionals[0]}".`);
27
+ }
28
+ const scope = optionalNonEmpty(values.flags.get("--scope"));
29
+ if (typed === "resolve" && (!scope || !RENAME_SCOPES.includes(scope))) {
30
+ throw new Error(`rename resolve needs --scope ${RENAME_SCOPES.join("|")}.`);
31
+ }
32
+ if (byPair && scope !== "undo") {
33
+ throw new Error("--wrong and --right name a rule that already exists, so they take --scope undo only.");
34
+ }
35
+ if (typed === "list" && scope) {
36
+ throw new Error("--scope belongs to `cockpit rename resolve`.");
37
+ }
38
+ return {
39
+ kind: "rename",
40
+ action: typed,
41
+ ...(id ? { id } : {}),
42
+ ...(scope ? { scope: scope } : {}),
43
+ ...(byPair ? { wrongForm: wrong, canonicalForm: right } : {}),
44
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
45
+ dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
46
+ json: values.booleans.has("--json"),
47
+ };
48
+ }
@@ -37,4 +37,5 @@ export { parseModelsArgs } from "./local-args-tower-models.js";
37
37
  export { parseMailArgs } from "./local-args-tower-mail.js";
38
38
  export { parseCalArgs } from "./local-args-tower-cal.js";
39
39
  export { parseUsageArgs } from "./local-args-tower-usage.js";
40
- export { parseCareersArgs } from './local-args-tower-careers.js';
40
+ export { parseCareersArgs } from './local-args-tower-careers.js';
41
+ export { parseRenameArgs } from './local-args-tower-rename.js';
@@ -18,7 +18,7 @@
18
18
  import { parseRootsArgs } from "./roots.js";
19
19
  import { SURFACE_ALIASES } from "./local-help.js";
20
20
  import { parseAgentRulesArgs, parseAnalyzeArgs, parseAutostartArgs, parseBackfillArgs, parseCleanArgs, parseDoctorArgs, parseInstallArgs, parseLoginArgs, parseMemoryArgs, parseLogoutArgs, parseOnboardArgs, parseReleaseArgs, parseServeArgs, parseSessionsArgs, parseSourcesArgs, parseStartArgs, parseStatusArgs, parseSyncArgs, parseUpdateArgs, } from "./local-args-collector.js";
21
- import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, parseCareersArgs, } from "./local-args-tower.js";
21
+ import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, parseCareersArgs, parseRenameArgs, } from "./local-args-tower.js";
22
22
  // `normalizeUrl` has always been part of this module's surface — `local.ts` and
23
23
  // `local-auth.ts` import it from here — so it stays exported from this address
24
24
  // even though it now lives next door. The same goes for the four names the
@@ -127,6 +127,8 @@ export function parseLocalArgs(argv) {
127
127
  return parseReleaseArgs(argv.slice(1));
128
128
  case "careers":
129
129
  return parseCareersArgs(argv.slice(1));
130
+ case "rename":
131
+ return parseRenameArgs(argv.slice(1));
130
132
  case "usage":
131
133
  return parseUsageArgs(argv.slice(1));
132
134
  default:
@@ -22,16 +22,28 @@ const SEARCH_KIND_LIST = SEARCH_KINDS.join(",");
22
22
  const SEARCH_CORPORA_COUNT = SEARCH_KINDS.length;
23
23
  /** One entry per Tower noun, in the order `cockpit --help` lists them. */
24
24
  export const TOWER_COMMAND_HELP = [
25
+ [
26
+ "rename",
27
+ [
28
+ "Usage: cockpit rename [list|resolve <id> --scope <page|everywhere|undo>] [--dashboard-url <url>] [--json]",
29
+ "",
30
+ "A correction JARVIS cannot classify is fixed on ONE page and waits for your call: just this page, or everywhere from now on. A bare `cockpit rename` lists what is waiting.",
31
+ "resolve <id> --scope page — keep it a one-page fix. everywhere — write the rule and fix every other page. undo — take back a rule JARVIS applied on its own (yours only, unless you are a super admin).",
32
+ "An OLD rule has no open choice: `cockpit rename resolve --wrong <form> --right <form> --scope undo` removes your own row for that pair (a super admin may remove anyone's).",
33
+ "A fact (a status, a date, a number) is never made a rule, whatever is asked. The same door answers the panel's two buttons, a spoken answer, `cockpit jarvis` and the rename_* MCP tools.",
34
+ ],
35
+ ],
25
36
  [
26
37
  "careers",
27
38
  [
28
- "Usage: cockpit careers [list|show <id>|rescreen <id>|send-takehome <id>|decide <id> --decision <d>|followup <id>|grade <id>|resume <id>|takehome show <role>|takehome set <role>|settings show|settings set] [flags]",
39
+ "Usage: cockpit careers [list|show <id>|rescreen <id>|send-takehome <id>|decide <id> --decision <d>|followup <id>|interview <id>|grade <id>|resume <id>|takehome show <role>|takehome set <role>|settings show|settings set] [flags]",
29
40
  "",
30
41
  "Super-admin application review and pipeline. Lists at most 100 matches with total and has_more; use filters to narrow.",
31
- "list [--role <slug>] [--min-score <n>] [--since <date>] [--stage <screened|invited|submitted|graded|booking_sent|passed_on|archived>] — the board, newest first, with the two thresholds in force.",
42
+ "list [--role <slug>] [--min-score <n>] [--since <date>] [--stage <screened|invited|submitted|graded|booking_sent|interview|passed_on|archived>] — the board, newest first, with the two thresholds in force.",
32
43
  "send-takehome <id> (also `invite <id>`): sends that role's take-home from the configured mailbox, whatever the screening scored. The same act as Tower's Send take-home button, and only a row still at `screened`.",
33
44
  "decide <id> --decision <passed_on|archived|reopen> — your own call on a row; `reopen` puts it back to `screened`.",
34
45
  "followup <id> [--record-sent-at <iso>]: sends the one second-chance email now, in the take-home thread (the Send follow-up button), or with --record-sent-at records one you sent by hand and sends nothing (Already followed up). A second follow-up is refused. list and show carry each row's deadline label: overdue, second_chance, no_submission or incomplete.",
46
+ "interview <id> --at <iso with offset> [--clear]: records an interview you booked yourself (the Interview booked control). The row moves to the interview stage and its chip reads Interview plus the time; booking again reschedules; --clear (instead of --at) returns the row to where it stood. An archived row is refused. list --stage interview lists them.",
35
47
  "settings set also takes --followup-enabled <true|false> (default false), --followup-after-days <1-60> (default 7) and --takehome-window-hours <1-720> (default 72).",
36
48
  "grade <id> [--now]: puts one submission back in the take-home grader's queue. The grader runs on Railway every 5 minutes; --now says so rather than pretending the run happened here.",
37
49
  "resume <id> [--out <path>] [--force]: the candidate's resume, the same file the drawer's Open resume button shows. Bare, it prints a signed link that dies in 60 seconds; --out downloads the file to that path and refuses to overwrite one already there unless you pass --force.",
@@ -75,6 +75,7 @@ export const rootCommandNames = new Set([
75
75
  "search",
76
76
  "release",
77
77
  "careers",
78
+ "rename",
78
79
  "usage",
79
80
  ...SURFACE_ALIASES.keys(),
80
81
  ]);
@@ -127,7 +128,8 @@ export function localCommandHelp(command) {
127
128
  " cockpit project [list] [--archived] [--dashboard-url <url>] [--json]",
128
129
  ` cockpit search "<words>" [--kind ${SEARCH_KINDS.join(",")}] [--limit <n>] [--dashboard-url <url>] [--json]`,
129
130
  " cockpit release [--dry-run] [--skip-checks] [--no-floor] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
130
- " cockpit careers [list|show <id>|rescreen <id>|send-takehome <id>|decide <id> --decision <passed_on|archived|reopen>|followup <id> [--record-sent-at <iso>]|grade <id> [--now]|resume <id> [--out <path>]|takehome show <role>|takehome set <role> --body-file <path>|settings show|settings set --enabled <true|false> --followup-enabled <true|false> --followup-after-days <n> --takehome-window-hours <n>] [--role <slug>] [--min-score <n>] [--since <date>] [--stage <s>] [--force] [--json]",
131
+ " cockpit careers [list|show <id>|rescreen <id>|send-takehome <id>|decide <id> --decision <passed_on|archived|reopen>|followup <id> [--record-sent-at <iso>]|interview <id> <--at <iso>|--clear>|grade <id> [--now]|resume <id> [--out <path>]|takehome show <role>|takehome set <role> --body-file <path>|settings show|settings set --enabled <true|false> --followup-enabled <true|false> --followup-after-days <n> --takehome-window-hours <n>] [--role <slug>] [--min-score <n>] [--since <date>] [--stage <s>] [--force] [--json]",
132
+ " cockpit rename [list|resolve <id> --scope <page|everywhere|undo>] [--dashboard-url <url>] [--json]",
131
133
  " cockpit usage sessions [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--since <window>] [--json]",
132
134
  " cockpit usage sessions --session <id> [--from-window <n>] [--json]",
133
135
  " --by-topic groups work types; --by-subject groups open subjects from session summaries.",
@@ -46,6 +46,7 @@ import { runMail } from "./mail.js";
46
46
  import { runProject } from "./project.js";
47
47
  import { runSearch } from "./search.js";
48
48
  import { runCareers } from "./careers.js";
49
+ import { runRename } from "./rename-scope.js";
49
50
  import { runUsage } from "./usage.js";
50
51
  import { parseLocalArgs } from "./local-args.js";
51
52
  // `./local.js` is the published entry point for this command surface: the
@@ -234,6 +235,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
234
235
  return await runRelease(command, io);
235
236
  case "careers":
236
237
  return await runCareers(command, io);
238
+ case "rename":
239
+ return await runRename(command, io);
237
240
  case "usage":
238
241
  return await runUsage(command, io);
239
242
  }
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
21
21
  }
22
22
 
23
23
  if (command === "--version" || command === "-V" || command === "version") {
24
- writeLine(io?.stdout ?? process.stdout, "0.2.160");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.162");
25
25
  return 0;
26
26
  }
27
27
 
@@ -0,0 +1,57 @@
1
+ /**
2
+ * `cockpit rename list` / `cockpit rename resolve <id> --scope ...` (BLI-12378).
3
+ *
4
+ * The terminal twin of the panel's two buttons, over the same door
5
+ * (`/api/jarvis/rename-scope`) with the same device token. The door's reply
6
+ * sentence is printed as it is: it is the one JARVIS says everywhere.
7
+ *
8
+ * Metadata only in the logs: the ids and counts, never the words being renamed.
9
+ */
10
+ import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
11
+ import { writeLine } from "./cli-io.js";
12
+ const TAG = "[rename cli]";
13
+ const DEADLINE_MS = 120_000;
14
+ export async function runRename(command, io) {
15
+ const door = await openAgentDoor("rename", command, io);
16
+ const listing = command.action === "list";
17
+ const answer = await askAgentDoor(door, {
18
+ path: "/api/jarvis/rename-scope",
19
+ method: listing ? "GET" : "POST",
20
+ label: `rename ${command.action}`,
21
+ timeoutMs: DEADLINE_MS,
22
+ ...(listing
23
+ ? {}
24
+ : {
25
+ body: command.id
26
+ ? { pendingId: command.id, choice: command.scope }
27
+ : { wrongForm: command.wrongForm, canonicalForm: command.canonicalForm, choice: command.scope },
28
+ }),
29
+ });
30
+ if (!answer.ok)
31
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
32
+ const body = answer.body;
33
+ // An HTTP 200 with no recognisable shape is a dashboard that has not shipped
34
+ // this door, not an empty list.
35
+ if (listing ? !Array.isArray(body.pending) : typeof body.reply !== "string") {
36
+ return failAgentDoor(door, TAG, "rename_answer_unreadable", "Tower answered 200 with no rename receipt in it. The usual cause is a dashboard that has not shipped /api/jarvis/rename-scope yet; nothing was changed.");
37
+ }
38
+ writeLine(door.io.stderr, `${TAG} ${command.action} ${JSON.stringify({ open: body.pending?.length ?? null, ok: body.ok ?? null, scope: command.scope ?? null })}`);
39
+ if (door.json)
40
+ return emitAgentDoor(door, answer.body);
41
+ if (listing) {
42
+ const open = body.pending ?? [];
43
+ if (open.length === 0) {
44
+ writeLine(door.io.stdout, "No correction is waiting on you. Nothing was made a rule without a yes.");
45
+ return 0;
46
+ }
47
+ for (const row of open) {
48
+ const what = row.kind === "undo" ? "applied everywhere, can be undone" : "this page only, waiting for your call";
49
+ writeLine(door.io.stdout, `${row.id} “${row.wrongForm}” -> ${row.canonicalForm} (${what})`);
50
+ }
51
+ writeLine(door.io.stdout, "");
52
+ writeLine(door.io.stdout, "Answer one with: cockpit rename resolve <id> --scope page|everywhere|undo");
53
+ return 0;
54
+ }
55
+ writeLine(door.io.stdout, body.reply);
56
+ return body.ok === false ? 1 : 0;
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.160",
3
+ "version": "0.2.162",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,8 +27,13 @@
27
27
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-surface-doors.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-cli-doctor-json.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
28
28
  },
29
29
  "dependencies": {
30
- "@bli-cockpit/memory-mcp": "0.1.44",
31
- "@bli-cockpit/mcp": "0.1.78",
32
- "@bli-cockpit/telemetry-core": "0.1.56"
30
+ "@bli-cockpit/memory-mcp": "0.1.45",
31
+ "@bli-cockpit/mcp": "0.1.80",
32
+ "@bli-cockpit/telemetry-core": "0.1.57"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/BLI-INC/bli-cockpit.git",
37
+ "directory": "packages/cockpit-cli"
33
38
  }
34
39
  }