@bli-cockpit/cli 0.2.99 → 0.2.101
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/dist/agent-rules.js +2 -1
- package/dist/backfill-lock.js +1 -1
- package/dist/commands/backfill-checkpoint.js +3 -1
- package/dist/commands/backfill-issues.js +8 -55
- package/dist/commands/backfill-report.js +22 -6
- package/dist/commands/backfill-scan.js +2 -1
- package/dist/commands/backfill-skip-policy.js +134 -0
- package/dist/commands/careers.js +16 -0
- package/dist/commands/doctor-access.js +34 -10
- package/dist/commands/doctor-lock-wait.js +46 -0
- package/dist/commands/doctor-pipeline-verdicts.js +238 -0
- package/dist/commands/doctor-pipeline.js +49 -111
- package/dist/commands/doctor-registration.js +23 -2
- package/dist/commands/doctor-report.js +48 -9
- package/dist/commands/doctor-update.js +16 -5
- package/dist/commands/doctor.js +98 -58
- package/dist/commands/local-args-collector-setup.js +6 -0
- package/dist/commands/local-args-tower-careers.js +20 -0
- package/dist/commands/local-args-tower-pages.js +17 -2
- package/dist/commands/local-args-tower-usage.js +2 -2
- package/dist/commands/local-args-tower.js +2 -1
- package/dist/commands/local-args.js +3 -1
- package/dist/commands/local-help-commands-tower.js +4 -2
- package/dist/commands/local-help-commands.js +28 -12
- package/dist/commands/local-help.js +4 -2
- package/dist/commands/local.js +4 -0
- package/dist/commands/notes-file.js +8 -1
- package/dist/commands/notes-folders.js +35 -0
- package/dist/commands/notes-writes.js +37 -4
- package/dist/commands/notes.js +6 -0
- package/dist/commands/public-root.js +4 -4
- package/dist/commands/usage-format.js +18 -0
- package/dist/commands/usage.js +13 -3
- package/dist/cursors/backfill-completion-marker.js +135 -0
- package/dist/cursors/backfill-cursor.js +18 -99
- package/dist/scheduled-self-update.js +1 -1
- package/dist/sync-lock.js +15 -1
- package/package.json +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSemverBelow } from "../scheduled-self-update.js";
|
|
1
2
|
import { DEFAULT_DASHBOARD_URL, LOCAL_COLLECTOR_VERSION } from "../local-state.js";
|
|
2
3
|
import { createInteractiveExecRunner } from "../process-runner.js";
|
|
3
4
|
import { asRecord, fail, needsFix, ok } from "./doctor-report.js";
|
|
@@ -11,7 +12,7 @@ export async function checkCliLatest(context) {
|
|
|
11
12
|
if (!latest) {
|
|
12
13
|
return needsFix("cli-latest", "latest_version_unknown", `could not confirm npm latest; will run npm install for ${LOCAL_COLLECTOR_VERSION}`);
|
|
13
14
|
}
|
|
14
|
-
if (latest === LOCAL_COLLECTOR_VERSION) {
|
|
15
|
+
if (latest === LOCAL_COLLECTOR_VERSION || (!context.command.updateTag && /^\d+\.\d+\.\d+$/u.test(latest) && !isSemverBelow(LOCAL_COLLECTOR_VERSION, latest))) {
|
|
15
16
|
return ok("cli-latest", "already_latest", `current ${LOCAL_COLLECTOR_VERSION}`);
|
|
16
17
|
}
|
|
17
18
|
return needsFix("cli-latest", "stale_cli", `current ${LOCAL_COLLECTOR_VERSION}; npm latest ${latest}`);
|
|
@@ -28,7 +29,7 @@ export async function fixCliLatest(context, state) {
|
|
|
28
29
|
}
|
|
29
30
|
if (context.io.env["COCKPIT_DOCTOR_REEXEC"] === "1") {
|
|
30
31
|
if (state.code === "stale_cli") {
|
|
31
|
-
return fail("cli-latest", "stale_after_self_update", "self-update ran but this process still reports the old CLI version; rerun `cockpit
|
|
32
|
+
return fail("cli-latest", "stale_after_self_update", "self-update ran but this process still reports the old CLI version; rerun `cockpit doctor`.");
|
|
32
33
|
}
|
|
33
34
|
return ok("cli-latest", "updated_reexec_guarded", "self-update ran; re-exec guard already set, continuing.");
|
|
34
35
|
}
|
|
@@ -67,9 +68,19 @@ function parseNpmVersion(stdout) {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
export function reexecDoctor(command, io) {
|
|
70
|
-
const args = ["
|
|
71
|
-
if (command.
|
|
72
|
-
args.push("--
|
|
71
|
+
const args = ["doctor"];
|
|
72
|
+
if (command.homeDir)
|
|
73
|
+
args.push("--home", command.homeDir);
|
|
74
|
+
if (command.lockWaitSeconds)
|
|
75
|
+
args.push("--lock-wait", String(command.lockWaitSeconds));
|
|
76
|
+
if (command.allowHomeRoot)
|
|
77
|
+
args.push("--allow-home-root");
|
|
78
|
+
if (command.maxDepth)
|
|
79
|
+
args.push("--max-depth", String(command.maxDepth));
|
|
80
|
+
if (command.maxRepos)
|
|
81
|
+
args.push("--max-repos", String(command.maxRepos));
|
|
82
|
+
for (const root of command.collectionRoots ?? (command.repoRoot ? [command.repoRoot] : []))
|
|
83
|
+
args.push("--workspace", root);
|
|
73
84
|
if (command.dashboardUrl !== DEFAULT_DASHBOARD_URL) {
|
|
74
85
|
args.push("--dashboard-url", command.dashboardUrl);
|
|
75
86
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { LOCAL_COLLECTOR_VERSION } from "../local-state.js";
|
|
2
|
+
import { isSemverBelow } from "../scheduled-self-update.js";
|
|
3
|
+
import { describeError } from "../health-detail.js";
|
|
1
4
|
import { withStage } from "../crash-guard.js";
|
|
2
5
|
import { checkSingleInstallState, fixAuthState, fixRootState, readAuthState, readRootState, } from "./doctor-access.js";
|
|
3
|
-
import { backfillCompletionStepState, checkBackfillState, checkDiskState, checkGcState, checkSyncState, fixBackfillState, fixDiskState, fixGcState, fixSyncState, syncBacklogDrainingVerdict, } from "./doctor-pipeline.js";
|
|
6
|
+
import { backfillCompletionStepState, backfillFixVerdict, checkBackfillState, checkDiskState, checkGcState, checkSyncState, fixBackfillState, fixDiskState, fixGcState, fixSyncState, syncBacklogDrainingVerdict, syncStandAsideVerdict, } from "./doctor-pipeline.js";
|
|
4
7
|
import { checkMcpAnswersState } from "./doctor-mcp.js";
|
|
5
8
|
import { checkMemoryDaemonState } from "./doctor-memory-daemon.js";
|
|
6
|
-
import { checkAutostartState, checkMemoryState, fixAutostartState, fixMemoryState, } from "./doctor-registration.js";
|
|
7
|
-
import { dryRunPreview, isInteractiveDoctorFix, maybeReportDoctorEvents, writeDoctorOutput, } from "./doctor-report.js";
|
|
8
|
-
import { refreshSetupReceipt } from "./setup-receipt.js";
|
|
9
|
+
import { checkDoctorAgentRules, fixDoctorAgentRules, checkAutostartState, checkMemoryState, fixAutostartState, fixMemoryState, } from "./doctor-registration.js";
|
|
10
|
+
import { dryRunPreview, isInteractiveDoctorFix, maybeReportDoctorEvents, writeDoctorOutput, doctorNeedsPerson, } from "./doctor-report.js";
|
|
11
|
+
import { buildSetupReceipt, refreshSetupReceipt } from "./setup-receipt.js";
|
|
9
12
|
import { checkCliLatest, fixCliLatest, latestCliVersionFromNpm, reexecDoctor, } from "./doctor-update.js";
|
|
10
13
|
export async function runDoctor(command, io, hooks, overrides = {}) {
|
|
11
14
|
const deps = { ...defaultDoctorDeps(hooks), ...overrides };
|
|
@@ -14,58 +17,93 @@ export async function runDoctor(command, io, hooks, overrides = {}) {
|
|
|
14
17
|
export async function runDoctorWithDeps(command, io, deps) {
|
|
15
18
|
const context = { command, io, deps };
|
|
16
19
|
const rows = [];
|
|
20
|
+
const repairs = [];
|
|
21
|
+
const diagnoseOnly = command.dryRun || command.checkOnly;
|
|
22
|
+
const diagnosed = new Map();
|
|
17
23
|
for (const invariant of doctorInvariants()) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
rows.push(checked);
|
|
21
|
-
continue;
|
|
24
|
+
try {
|
|
25
|
+
diagnosed.set(invariant.id, await withStage(`doctor:check:${invariant.id}`, () => invariant.check(context)));
|
|
22
26
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error(`[doctor] ${invariant.id} diagnosis_failed`, JSON.stringify({ reason: "check_threw", ...describeError(error) }));
|
|
29
|
+
diagnosed.set(invariant.id, { id: invariant.id, status: "fail", code: "check_threw", message: "Run `cockpit doctor` to retry this check." });
|
|
26
30
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
}
|
|
32
|
+
for (const invariant of doctorInvariants()) {
|
|
33
|
+
let row;
|
|
34
|
+
try {
|
|
35
|
+
row = diagnosed.get(invariant.id);
|
|
36
|
+
const needsCollection = ["backfill-complete", "sync-fresh"].includes(invariant.id);
|
|
37
|
+
const prerequisitesReady = !needsCollection || rows.filter((prior) => prior.id === "authed" || prior.id === "roots-ok").every((prior) => prior.status === "ok");
|
|
38
|
+
const canFix = !diagnoseOnly && prerequisitesReady && invariant.fix &&
|
|
39
|
+
(!invariant.requiresInteractiveFix || isInteractiveDoctorFix(context) || Boolean(command.collectionRoots?.length));
|
|
40
|
+
if (row.status !== "ok" && row.status !== "skipped" && canFix && invariant.fix) {
|
|
41
|
+
row = await withStage(`doctor:fix:${invariant.id}`, () => invariant.fix(context, row));
|
|
42
|
+
repairs.push({ step: invariant.id, outcome: row.status === "ok" ? "repaired" : "needs_person", reason: row.code });
|
|
43
|
+
// The replacement process owns the one final receipt, including JSON.
|
|
44
|
+
if (row.reexecExitCode !== undefined)
|
|
45
|
+
return row.reexecExitCode;
|
|
46
|
+
row = { ...row, fixed: row.status === "ok" };
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
repairs.push({ step: invariant.id, outcome: diagnoseOnly ? "check_only" : row.status === "ok" ? "already_ok" : row.status === "skipped" ? "not_needed" : "needs_person", reason: row.code });
|
|
50
|
+
if (command.dryRun && invariant.fix && row.status !== "ok" && row.status !== "skipped")
|
|
51
|
+
row = dryRunPreview(row);
|
|
52
|
+
}
|
|
34
53
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
rows.push({ ...fixed, fixed: fixed.status !== "fail" });
|
|
40
|
-
if (fixed.hardStop)
|
|
41
|
-
break;
|
|
42
|
-
if (fixed.reexecExitCode !== undefined) {
|
|
43
|
-
await maybeReportDoctorEvents(context, rows);
|
|
44
|
-
writeDoctorOutput(command, io, rows, await deps.readSetupReceipt(context));
|
|
45
|
-
return fixed.reexecExitCode;
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(`[doctor] ${invariant.id} failed`, JSON.stringify({ reason: "step_threw", ...describeError(error) }));
|
|
56
|
+
row = { id: invariant.id, status: "fail", code: "step_threw", message: "The step could not finish. Run `cockpit doctor`." };
|
|
57
|
+
repairs.push({ step: invariant.id, outcome: "needs_person", reason: row.code });
|
|
46
58
|
}
|
|
59
|
+
console.error(`[doctor] ${invariant.id} ${row.status}`, JSON.stringify({ reason: row.code, repaired: row.fixed ?? false }));
|
|
60
|
+
rows.push(row);
|
|
47
61
|
}
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
// Re-read host state after repairs. Sync is proven by its upload receipts;
|
|
63
|
+
// re-running its diagnostic would always request a new upload by design.
|
|
64
|
+
if (!diagnoseOnly) {
|
|
65
|
+
for (const invariant of doctorInvariants()) {
|
|
66
|
+
if (["cli-latest", "authed", "roots-ok", "backfill-complete", "sync-fresh", "gc-checked", "disk-bounded"].includes(invariant.id))
|
|
67
|
+
continue;
|
|
68
|
+
const index = rows.findIndex((row) => row.id === invariant.id);
|
|
69
|
+
if (rows[index]?.status === "fail")
|
|
70
|
+
continue;
|
|
71
|
+
try {
|
|
72
|
+
const checked = await invariant.check(context);
|
|
73
|
+
rows[index] = { ...checked, fixed: rows[index]?.fixed && checked.status === "ok" };
|
|
74
|
+
console.error(`[doctor] ${invariant.id} verified`, JSON.stringify({ reason: checked.code, status: checked.status }));
|
|
75
|
+
if (checked.status === "needs_fix" || checked.status === "fail") {
|
|
76
|
+
const repair = repairs.find((entry) => entry.step === invariant.id);
|
|
77
|
+
if (repair) {
|
|
78
|
+
repair.outcome = "needs_person";
|
|
79
|
+
repair.reason = checked.code;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(`[doctor] ${invariant.id} recheck_failed`, JSON.stringify({ reason: "recheck_threw", ...describeError(error) }));
|
|
85
|
+
rows[index] = { id: invariant.id, status: "fail", code: "recheck_threw", message: "Run `cockpit doctor` to retry the host check." };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const floor = await maybeReportDoctorEvents(context, rows);
|
|
90
|
+
if (floor && isSemverBelow(LOCAL_COLLECTOR_VERSION, floor)) {
|
|
91
|
+
const index = rows.findIndex((row) => row.id === "cli-latest");
|
|
92
|
+
rows[index] = { id: "cli-latest", status: "needs_fix", code: "below_fleet_floor", message: `This process is below the fleet floor ${floor}. Run \`cockpit doctor\` to update.`, nextAction: "cockpit doctor" };
|
|
93
|
+
console.error("[doctor] cli-latest below_fleet_floor", JSON.stringify({ reason: "below_fleet_floor", current_version: LOCAL_COLLECTOR_VERSION, minimum_version: floor }));
|
|
94
|
+
}
|
|
95
|
+
let receipt = null;
|
|
96
|
+
if (!command.dryRun) {
|
|
97
|
+
try {
|
|
98
|
+
receipt = await deps.readSetupReceipt(context);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
console.error("[doctor] setup_receipt failed", JSON.stringify({ reason: "receipt_read_threw", ...describeError(error) }));
|
|
102
|
+
rows.push({ id: "memory-registered", status: "needs_fix", code: "receipt_read_threw", message: "Run `cockpit doctor` to read the final setup receipt." });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
writeDoctorOutput(command, io, rows, receipt, repairs);
|
|
106
|
+
return doctorNeedsPerson(rows, receipt).length === 0 ? 0 : 1;
|
|
69
107
|
}
|
|
70
108
|
function doctorInvariants() {
|
|
71
109
|
return [
|
|
@@ -74,7 +112,6 @@ function doctorInvariants() {
|
|
|
74
112
|
id: "authed",
|
|
75
113
|
check: (context) => context.deps.readAuth(context),
|
|
76
114
|
fix: fixAuthState,
|
|
77
|
-
requiresInteractiveFix: true,
|
|
78
115
|
},
|
|
79
116
|
{
|
|
80
117
|
id: "roots-ok",
|
|
@@ -99,6 +136,7 @@ function doctorInvariants() {
|
|
|
99
136
|
check: (context) => context.deps.checkMemory(context),
|
|
100
137
|
fix: (context, _state) => context.deps.fixMemory(context),
|
|
101
138
|
},
|
|
139
|
+
{ id: "agent-rules", check: (context) => context.deps.checkAgentRules(context), fix: (context) => context.deps.fixAgentRules(context) },
|
|
102
140
|
// BLI-3804. Right after the registration row, because it asks the second
|
|
103
141
|
// half of the same question: `memory-registered` proves the entry exists
|
|
104
142
|
// and names a bin, this one proves the server behind it starts, speaks the
|
|
@@ -122,6 +160,11 @@ function doctorInvariants() {
|
|
|
122
160
|
check: (context) => context.deps.checkBackfill(context),
|
|
123
161
|
fix: (context, _state) => context.deps.fixBackfill(context),
|
|
124
162
|
},
|
|
163
|
+
{
|
|
164
|
+
id: "sync-fresh",
|
|
165
|
+
check: (context) => context.deps.checkSync(context),
|
|
166
|
+
fix: (context, _state) => context.deps.fixSync(context),
|
|
167
|
+
},
|
|
125
168
|
{
|
|
126
169
|
id: "gc-checked",
|
|
127
170
|
check: (context) => context.deps.checkGc(context),
|
|
@@ -136,15 +179,12 @@ function doctorInvariants() {
|
|
|
136
179
|
check: (context) => context.deps.checkDisk(context),
|
|
137
180
|
fix: (context, _state) => context.deps.fixDisk(context),
|
|
138
181
|
},
|
|
139
|
-
{
|
|
140
|
-
id: "sync-fresh",
|
|
141
|
-
check: (context) => context.deps.checkSync(context),
|
|
142
|
-
fix: (context, _state) => context.deps.fixSync(context),
|
|
143
|
-
},
|
|
144
182
|
];
|
|
145
183
|
}
|
|
146
184
|
function defaultDoctorDeps(hooks) {
|
|
147
185
|
return {
|
|
186
|
+
checkAgentRules: checkDoctorAgentRules,
|
|
187
|
+
fixAgentRules: fixDoctorAgentRules,
|
|
148
188
|
latestCliVersion: latestCliVersionFromNpm,
|
|
149
189
|
selfUpdate: hooks.selfUpdate,
|
|
150
190
|
reexecDoctor: reexecDoctor,
|
|
@@ -168,7 +208,7 @@ function defaultDoctorDeps(hooks) {
|
|
|
168
208
|
fixDisk: fixDiskState,
|
|
169
209
|
checkSync: checkSyncState,
|
|
170
210
|
fixSync: fixSyncState,
|
|
171
|
-
readSetupReceipt: (context) => refreshSetupReceipt(context.io, {
|
|
211
|
+
readSetupReceipt: (context) => (context.command.checkOnly ? buildSetupReceipt : refreshSetupReceipt)(context.io, {
|
|
172
212
|
...(context.command.homeDir ? { homeDir: context.command.homeDir } : {}),
|
|
173
213
|
...(context.command.dashboardUrl
|
|
174
214
|
? { dashboardUrl: context.command.dashboardUrl }
|
|
@@ -178,4 +218,4 @@ function defaultDoctorDeps(hooks) {
|
|
|
178
218
|
}
|
|
179
219
|
// Re-exported so every consumer keeps importing from `./doctor.js` regardless
|
|
180
220
|
// of which sibling a symbol now lives in.
|
|
181
|
-
export { backfillCompletionStepState, syncBacklogDrainingVerdict };
|
|
221
|
+
export { backfillCompletionStepState, backfillFixVerdict, syncBacklogDrainingVerdict, syncStandAsideVerdict, };
|
|
@@ -98,6 +98,9 @@ export function parseDoctorArgs(alias, args) {
|
|
|
98
98
|
"--dashboard-url",
|
|
99
99
|
"--update-tag",
|
|
100
100
|
"--dry-run",
|
|
101
|
+
"--check",
|
|
102
|
+
"--no-repair",
|
|
103
|
+
"--lock-wait",
|
|
101
104
|
"--json",
|
|
102
105
|
"--allow-home-root",
|
|
103
106
|
"--max-depth",
|
|
@@ -113,6 +116,7 @@ export function parseDoctorArgs(alias, args) {
|
|
|
113
116
|
"--timeout-ms",
|
|
114
117
|
],
|
|
115
118
|
valueFlags: [
|
|
119
|
+
"--lock-wait",
|
|
116
120
|
"--home",
|
|
117
121
|
"--repo",
|
|
118
122
|
"--workspace",
|
|
@@ -135,6 +139,8 @@ export function parseDoctorArgs(alias, args) {
|
|
|
135
139
|
}
|
|
136
140
|
return {
|
|
137
141
|
kind: "doctor",
|
|
142
|
+
checkOnly: values.booleans.has("--check") || values.booleans.has("--no-repair"),
|
|
143
|
+
lockWaitSeconds: optionalPositiveInteger(values.flags.get("--lock-wait"), "--lock-wait") ?? 600,
|
|
138
144
|
alias,
|
|
139
145
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
140
146
|
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from './local-arg-values.js';
|
|
2
|
+
export function parseCareersArgs(args) {
|
|
3
|
+
const values = parseNamedArgs(args, { allowedFlags: ['--role', '--min-score', '--since', '--home', '--dashboard-url', '--json'], valueFlags: ['--role', '--min-score', '--since', '--home', '--dashboard-url'] });
|
|
4
|
+
const action = values.positionals[0] ?? 'list';
|
|
5
|
+
if (action !== 'list' && action !== 'show' && action !== 'rescreen')
|
|
6
|
+
throw new Error('careers takes list, show, or rescreen.');
|
|
7
|
+
const id = values.positionals[1];
|
|
8
|
+
if (action !== 'list' && !id)
|
|
9
|
+
throw new Error(`careers ${action} needs an application id.`);
|
|
10
|
+
if (values.positionals.length > (action === 'list' ? 1 : 2))
|
|
11
|
+
throw new Error('Unexpected careers argument.');
|
|
12
|
+
const rawScore = values.flags.get('--min-score');
|
|
13
|
+
const minScore = rawScore === undefined ? undefined : Number(rawScore);
|
|
14
|
+
if (minScore !== undefined && (!Number.isFinite(minScore) || minScore < 0 || minScore > 100))
|
|
15
|
+
throw new Error('--min-score must be 0 to 100.');
|
|
16
|
+
const since = optionalNonEmpty(values.flags.get('--since'));
|
|
17
|
+
if (since && !Number.isFinite(Date.parse(since)))
|
|
18
|
+
throw new Error('--since must be a date.');
|
|
19
|
+
return { kind: 'careers', action, id, role: optionalNonEmpty(values.flags.get('--role')), minScore, since: since ? new Date(since).toISOString() : undefined, homeDir: optionalNonEmpty(values.flags.get('--home')), dashboardUrl: optionalUrl(values.flags.get('--dashboard-url')), json: values.booleans.has('--json') };
|
|
20
|
+
}
|
|
@@ -153,6 +153,7 @@ export function parseBriefArgs(args) {
|
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
155
|
const NOTES_ACTIONS = new Set([
|
|
156
|
+
"folders", "mkdir", "rmdir", "rename",
|
|
156
157
|
"list",
|
|
157
158
|
"show",
|
|
158
159
|
"shelf",
|
|
@@ -175,12 +176,14 @@ const NOTES_ACTIONS_NEEDING_A_NOTE = new Set([
|
|
|
175
176
|
export function parseNotesArgs(args) {
|
|
176
177
|
const values = parseNamedArgs(args, {
|
|
177
178
|
allowedFlags: [
|
|
179
|
+
"--wait",
|
|
178
180
|
"--home",
|
|
179
181
|
"--dashboard-url",
|
|
180
182
|
"--file",
|
|
181
183
|
"--name",
|
|
182
184
|
"--exclude",
|
|
183
185
|
"--to",
|
|
186
|
+
"--folder",
|
|
184
187
|
"--clear-shelf",
|
|
185
188
|
"--apply",
|
|
186
189
|
"--series",
|
|
@@ -190,6 +193,7 @@ export function parseNotesArgs(args) {
|
|
|
190
193
|
"--limit",
|
|
191
194
|
"--yes",
|
|
192
195
|
"--json",
|
|
196
|
+
"--tree",
|
|
193
197
|
],
|
|
194
198
|
valueFlags: [
|
|
195
199
|
"--home",
|
|
@@ -198,6 +202,7 @@ export function parseNotesArgs(args) {
|
|
|
198
202
|
"--name",
|
|
199
203
|
"--exclude",
|
|
200
204
|
"--to",
|
|
205
|
+
"--folder",
|
|
201
206
|
"--series",
|
|
202
207
|
"--kind",
|
|
203
208
|
"--since",
|
|
@@ -236,23 +241,33 @@ export function parseNotesArgs(args) {
|
|
|
236
241
|
if (paths.length === 0)
|
|
237
242
|
throw new Error("notes upload needs at least one file path.");
|
|
238
243
|
}
|
|
244
|
+
else if (action === "mkdir" || action === "rmdir" || action === "rename") {
|
|
245
|
+
if (rest.length !== 1 || !rest[0]?.trim())
|
|
246
|
+
throw new Error(`notes ${action} needs one folder path.`);
|
|
247
|
+
}
|
|
239
248
|
else if (action !== "paste" && !NOTES_ACTIONS_NEEDING_A_NOTE.has(action) && rest.length > 0) {
|
|
240
249
|
throw new Error(`notes ${action} does not take "${rest[0]}".`);
|
|
241
250
|
}
|
|
251
|
+
if (action === "rename" && !optionalNonEmpty(values.flags.get("--name")))
|
|
252
|
+
throw new Error("notes rename needs --name <name>.");
|
|
242
253
|
const to = optionalNonEmpty(values.flags.get("--to"));
|
|
243
254
|
const clearShelf = values.booleans.has("--clear-shelf");
|
|
244
255
|
if (action === "move") {
|
|
245
256
|
if (to && clearShelf) {
|
|
246
257
|
throw new Error("notes move accepts either --to or --clear-shelf, not both.");
|
|
247
258
|
}
|
|
248
|
-
if (!to && !clearShelf) {
|
|
249
|
-
throw new Error('notes move needs --to "<shelf>"
|
|
259
|
+
if (!to && !clearShelf && !values.flags.get("--folder")) {
|
|
260
|
+
throw new Error('notes move needs --to "<shelf>", --clear-shelf, or --folder "<path>".');
|
|
250
261
|
}
|
|
251
262
|
}
|
|
252
263
|
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
253
264
|
return {
|
|
254
265
|
kind: "notes",
|
|
266
|
+
...(values.booleans.has("--wait") ? { wait: true } : {}),
|
|
255
267
|
action,
|
|
268
|
+
...(values.flags.has("--folder") ? { folder: values.flags.get("--folder") } : {}),
|
|
269
|
+
...(action === "mkdir" || action === "rmdir" || action === "rename" ? { folder: rest[0] } : {}),
|
|
270
|
+
...(values.booleans.has("--tree") ? { tree: true } : {}),
|
|
256
271
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
257
272
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
258
273
|
...(noteId ? { noteId } : {}),
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
2
2
|
export function parseUsageArgs(args) {
|
|
3
|
-
const values = parseNamedArgs(args, { allowedFlags: ["--since", "--until", "--include-automated", "--home", "--dashboard-url", "--json"], valueFlags: ["--since", "--until", "--home", "--dashboard-url"] });
|
|
3
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--since", "--until", "--include-automated", "--detail", "--home", "--dashboard-url", "--json"], valueFlags: ["--since", "--until", "--home", "--dashboard-url"] });
|
|
4
4
|
const action = values.positionals[0] ?? "people";
|
|
5
5
|
if (action !== "people" || values.positionals.length > 1)
|
|
6
6
|
throw new Error("usage takes one verb: people.");
|
|
7
|
-
return { kind: "usage", action: "people", since: optionalNonEmpty(values.flags.get("--since")) ?? "30d", until: optionalNonEmpty(values.flags.get("--until")), includeAutomated: values.booleans.has("--include-automated"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
|
|
7
|
+
return { kind: "usage", action: "people", since: optionalNonEmpty(values.flags.get("--since")) ?? "30d", until: optionalNonEmpty(values.flags.get("--until")), detail: values.booleans.has("--detail"), includeAutomated: values.booleans.has("--include-automated"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
|
|
8
8
|
}
|
|
@@ -36,4 +36,5 @@ export { SEARCH_KINDS, parseSearchArgs } from "./local-args-tower-search.js";
|
|
|
36
36
|
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
|
-
export { parseUsageArgs } from "./local-args-tower-usage.js";
|
|
39
|
+
export { parseUsageArgs } from "./local-args-tower-usage.js";
|
|
40
|
+
export { parseCareersArgs } from './local-args-tower-careers.js';
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* verbatim, no logic change.
|
|
17
17
|
*/
|
|
18
18
|
import { parseAgentRulesArgs, parseAnalyzeArgs, parseAutostartArgs, parseBackfillArgs, parseCleanArgs, parseDoctorArgs, parseInstallArgs, parseLoginArgs, parseMemoryArgs, parseLogoutArgs, parseOnboardArgs, parseReleaseArgs, parseServeArgs, parseSessionsArgs, parseStartArgs, parseStatusArgs, parseSyncArgs, parseUpdateArgs, } from "./local-args-collector.js";
|
|
19
|
-
import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, } from "./local-args-tower.js";
|
|
19
|
+
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";
|
|
20
20
|
// `normalizeUrl` has always been part of this module's surface — `local.ts` and
|
|
21
21
|
// `local-auth.ts` import it from here — so it stays exported from this address
|
|
22
22
|
// even though it now lives next door. The same goes for the four names the
|
|
@@ -115,6 +115,8 @@ export function parseLocalArgs(argv) {
|
|
|
115
115
|
return parseSearchArgs(argv.slice(1));
|
|
116
116
|
case "release":
|
|
117
117
|
return parseReleaseArgs(argv.slice(1));
|
|
118
|
+
case "careers":
|
|
119
|
+
return parseCareersArgs(argv.slice(1));
|
|
118
120
|
case "usage":
|
|
119
121
|
return parseUsageArgs(argv.slice(1));
|
|
120
122
|
default:
|
|
@@ -22,6 +22,7 @@ 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
|
+
["careers", ["Usage: cockpit careers [list|show <id>|rescreen <id>] [--role <slug>] [--min-score <n>] [--since <date>] [--json]", "Super-admin application review. Lists at most 100 matches with total and has_more; use filters to narrow."]],
|
|
25
26
|
[
|
|
26
27
|
"docs",
|
|
27
28
|
[
|
|
@@ -142,12 +143,13 @@ export const TOWER_COMMAND_HELP = [
|
|
|
142
143
|
[
|
|
143
144
|
"usage",
|
|
144
145
|
[
|
|
145
|
-
"Usage: cockpit usage people [--since
|
|
146
|
+
"Usage: cockpit usage people [--since <n>d|<n>h|<iso>] [--detail] [--until <iso>] [--include-automated] [--json]",
|
|
146
147
|
"",
|
|
147
148
|
"Claude Code and Codex usage per person: sessions observed and extracted, tokens (total, output,",
|
|
148
149
|
"and the input / cache split when the row carries it), and an API list-price equivalent that is",
|
|
149
150
|
"labelled as such and is never actual spend. A super_admin sees everyone; a member sees their own row.",
|
|
150
|
-
"--since takes
|
|
151
|
+
"--since takes <n>d (1 to 365), <n>h (1 to 8760), or an ISO timestamp; --until an ISO timestamp (default now).",
|
|
152
|
+
"--detail adds Output, Input, Cache read and Cache creation. Counts use K/M/B; dollars are rounded.",
|
|
151
153
|
"--include-automated adds harness, subagent and scheduled sessions, which are excluded by default.",
|
|
152
154
|
"It presses the same door as the /usage page and the usage_people MCP tool (GET /api/usage/people).",
|
|
153
155
|
"--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
|
|
@@ -71,30 +71,40 @@ export function localSubcommandHelp(command) {
|
|
|
71
71
|
[
|
|
72
72
|
"do-everything",
|
|
73
73
|
[
|
|
74
|
-
"Usage: cockpit do-everything [--workspace <path>] [--
|
|
74
|
+
"Usage: cockpit do-everything [--workspace <path>] [--check | --no-repair] [--lock-wait <seconds>] [--json]",
|
|
75
75
|
"",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"--
|
|
76
|
+
"Diagnose, repair, and verify this machine. doctor, do-everything, and fix run the same job.",
|
|
77
|
+
"Repairs CLI, background sync, sign-in, roots, memory and hooks, agent rules, catch-up, uploads, and cleanup.",
|
|
78
|
+
"--check or --no-repair diagnoses without repairs. --dry-run previews repairs.",
|
|
79
|
+
"--lock-wait defaults to 600 seconds; progress prints every 30 seconds.",
|
|
80
|
+
"--json includes steps, repairs, and needs_person. Exit 0 means nothing needs you.",
|
|
81
|
+
"Maintainers: --update-tag next keeps self-update on the prerelease candidate.",
|
|
80
82
|
],
|
|
81
83
|
],
|
|
82
84
|
[
|
|
83
85
|
"fix",
|
|
84
86
|
[
|
|
85
|
-
"Usage: cockpit fix [
|
|
87
|
+
"Usage: cockpit fix [--workspace <path>] [--check | --no-repair] [--lock-wait <seconds>] [--json]",
|
|
86
88
|
"",
|
|
87
|
-
"
|
|
89
|
+
"Diagnose, repair, and verify this machine. doctor, do-everything, and fix run the same job.",
|
|
90
|
+
"Repairs CLI, background sync, sign-in, roots, memory and hooks, agent rules, catch-up, uploads, and cleanup.",
|
|
91
|
+
"--check or --no-repair diagnoses without repairs. --dry-run previews repairs.",
|
|
92
|
+
"--lock-wait defaults to 600 seconds; progress prints every 30 seconds.",
|
|
93
|
+
"--json includes steps, repairs, and needs_person. Exit 0 means nothing needs you.",
|
|
94
|
+
"Maintainers: --update-tag next keeps self-update on the prerelease candidate.",
|
|
88
95
|
],
|
|
89
96
|
],
|
|
90
97
|
[
|
|
91
98
|
"doctor",
|
|
92
99
|
[
|
|
93
|
-
"Usage: cockpit doctor [
|
|
100
|
+
"Usage: cockpit doctor [--workspace <path>] [--check | --no-repair] [--lock-wait <seconds>] [--json]",
|
|
94
101
|
"",
|
|
95
|
-
"
|
|
96
|
-
"CLI
|
|
97
|
-
"
|
|
102
|
+
"Diagnose, repair, and verify this machine. doctor, do-everything, and fix run the same job.",
|
|
103
|
+
"Repairs CLI, background sync, sign-in, roots, memory and hooks, agent rules, catch-up, uploads, and cleanup.",
|
|
104
|
+
"--check or --no-repair diagnoses without repairs. --dry-run previews repairs.",
|
|
105
|
+
"--lock-wait defaults to 600 seconds; progress prints every 30 seconds.",
|
|
106
|
+
"--json includes steps, repairs, and needs_person. Exit 0 means nothing needs you.",
|
|
107
|
+
"Maintainers: --update-tag next keeps self-update on the prerelease candidate.",
|
|
98
108
|
],
|
|
99
109
|
],
|
|
100
110
|
[
|
|
@@ -450,8 +460,14 @@ export function localSubcommandHelp(command) {
|
|
|
450
460
|
[
|
|
451
461
|
"notes",
|
|
452
462
|
[
|
|
453
|
-
"Usage: cockpit notes [list|show <id>|shelf|shelves|upload <paths
|
|
463
|
+
"Usage: cockpit notes [list|show <id>|shelf|shelves|folders|mkdir <path>|rmdir <path>|rename <path>|upload <paths...> [--wait]|paste|share <id>|unshare <id>|move <id>] [flags]",
|
|
464
|
+
"Audio uploads: MP3, M4A, WAV, WebM and OGG, up to 4 MB. --wait polls transcription for at most 330 seconds.",
|
|
454
465
|
"",
|
|
466
|
+
"folders [--tree] [--json]: list folders and visible note counts.",
|
|
467
|
+
"mkdir <path> [--json]: create a folder and any missing ancestors.",
|
|
468
|
+
"rename <path> --name <name> [--json]: rename a folder and its descendant paths.",
|
|
469
|
+
"rmdir <path> [--json]: delete an empty folder; refuses notes or subfolders.",
|
|
470
|
+
"upload, paste and move accept --folder <path>; use / for Root. Shelf placement stays independent.",
|
|
455
471
|
"The /meeting-notes surface, typed. Bare `cockpit notes` lists the library.",
|
|
456
472
|
"list [--series <shelf>] [--kind <kind>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] — every note you may open, grouped by shelf, newest meeting first. Never a word of a note.",
|
|
457
473
|
"show <id> — one note with its text, its shelf, and why you are allowed to see it.",
|
|
@@ -52,6 +52,7 @@ export const rootCommandNames = new Set([
|
|
|
52
52
|
"project",
|
|
53
53
|
"search",
|
|
54
54
|
"release",
|
|
55
|
+
"careers",
|
|
55
56
|
"usage",
|
|
56
57
|
]);
|
|
57
58
|
export function localCommandHelp(command) {
|
|
@@ -63,7 +64,7 @@ export function localCommandHelp(command) {
|
|
|
63
64
|
" cockpit upgrade [same flags as update]",
|
|
64
65
|
" cockpit do-everything [--workspace <path>] [--dashboard-url <url>] [--update-tag <tag>] [--dry-run] [--json]",
|
|
65
66
|
" cockpit fix [same flags as do-everything]",
|
|
66
|
-
" cockpit doctor [
|
|
67
|
+
" cockpit doctor [--check | --no-repair] [--lock-wait <seconds>] [--json]",
|
|
67
68
|
" cockpit install [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--json]",
|
|
68
69
|
" cockpit login [--pair <code>] [--no-browser] [--legacy-pair] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
69
70
|
" cockpit pair [--pair <code>] [--no-browser] [--legacy-pair] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
@@ -83,7 +84,7 @@ export function localCommandHelp(command) {
|
|
|
83
84
|
" cockpit brief [edit|rewrite|history] [--for <person>] [--date <YYYY-MM-DD>] [--delta [--against <YYYY-MM-DD>]] [--version <pageId>] [--tldr|--full] [--versions] [--claims] [--days <n>] [--reason \"<why>\"] [--wait|--no-wait] [--dashboard-url <url>] [--json]",
|
|
84
85
|
" cockpit brief status [--who <person>] [--render] [--dashboard-url <url>] [--json]",
|
|
85
86
|
" cockpit correct --claim <claimId> --text \"<what is wrong>\" [--for <person>] [--version <pageId>] [--supersedes <id>] [--dashboard-url <url>] [--json]",
|
|
86
|
-
" cockpit notes [list|show <id>|shelf|shelves|upload <paths
|
|
87
|
+
" cockpit notes [list|show <id>|shelf|shelves|folders|mkdir <path>|rmdir <path>|rename <path>|upload <paths...> [--wait]|paste|share <id>|unshare <id>|move <id>|place <id>] [--folder <path>] [--series <shelf>] [--kind <kind>] [--since <YYYY-MM-DD>] [--until <YYYY-MM-DD>] [--limit <n>] [--file <path>] [--name <n>] [--exclude \"<sentence>\"] [--to \"<shelf>\"|--clear-shelf] [--apply] [--yes] [--dashboard-url <url>] [--json]",
|
|
87
88
|
" cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--max-depth <n>] [--max-repos <n>] [--yes] [--workspace <path>] [--json]",
|
|
88
89
|
" cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
89
90
|
" cockpit sessions [--source codex|claude] [--since-days <n>|--all] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
@@ -100,6 +101,7 @@ export function localCommandHelp(command) {
|
|
|
100
101
|
" cockpit project [list] [--archived] [--dashboard-url <url>] [--json]",
|
|
101
102
|
` cockpit search "<words>" [--kind ${SEARCH_KINDS.join(",")}] [--limit <n>] [--dashboard-url <url>] [--json]`,
|
|
102
103
|
" cockpit release [--dry-run] [--skip-checks] [--no-floor] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
|
|
104
|
+
" cockpit careers [list|show <id>|rescreen <id>] [--role <slug>] [--min-score <n>] [--since <date>] [--json]",
|
|
103
105
|
" cockpit usage people [--since 30d|<iso>] [--until <iso>] [--include-automated] [--dashboard-url <url>] [--json]",
|
|
104
106
|
"",
|
|
105
107
|
`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.`,
|
package/dist/commands/local.js
CHANGED
|
@@ -43,6 +43,7 @@ import { runCal } from "./cal.js";
|
|
|
43
43
|
import { runMail } from "./mail.js";
|
|
44
44
|
import { runProject } from "./project.js";
|
|
45
45
|
import { runSearch } from "./search.js";
|
|
46
|
+
import { runCareers } from "./careers.js";
|
|
46
47
|
import { runUsage } from "./usage.js";
|
|
47
48
|
import { parseLocalArgs } from "./local-args.js";
|
|
48
49
|
// `./local.js` is the published entry point for this command surface: the
|
|
@@ -180,6 +181,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
180
181
|
return await runSearch(command, io);
|
|
181
182
|
case "release":
|
|
182
183
|
return await runRelease(command, io);
|
|
184
|
+
case "careers":
|
|
185
|
+
return await runCareers(command, io);
|
|
183
186
|
case "usage":
|
|
184
187
|
return await runUsage(command, io);
|
|
185
188
|
}
|
|
@@ -206,6 +209,7 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
206
209
|
async function runDoctorLogin(command, io) {
|
|
207
210
|
return runLogin({
|
|
208
211
|
kind: "login",
|
|
212
|
+
homeDir: command.homeDir,
|
|
209
213
|
dashboardUrl: command.dashboardUrl,
|
|
210
214
|
json: command.json,
|
|
211
215
|
noAuth: false,
|
|
@@ -34,6 +34,10 @@ import { errorMessage } from "./cli-io.js";
|
|
|
34
34
|
* courtesy check to avoid uploading something the server is certain to refuse,
|
|
35
35
|
* not a second source of truth.
|
|
36
36
|
*/
|
|
37
|
+
export function noteAudioMime(name) {
|
|
38
|
+
const mime = { mp3: "audio/mpeg", m4a: "audio/mp4", wav: "audio/wav", webm: "audio/webm", ogg: "audio/ogg" };
|
|
39
|
+
return mime[name.split(".").at(-1)?.toLowerCase() ?? ""];
|
|
40
|
+
}
|
|
37
41
|
export const NOTE_FILE_MAX_BYTES = 20 * 1024 * 1024;
|
|
38
42
|
/**
|
|
39
43
|
* Above this, the terminal says it is working before it starts. The route's own
|
|
@@ -52,7 +56,7 @@ export function noteFileRefusalSentence(refusal, filePath) {
|
|
|
52
56
|
case "file_empty":
|
|
53
57
|
return `There is nothing in that file: ${filePath}`;
|
|
54
58
|
case "file_too_big":
|
|
55
|
-
return "That file is too big
|
|
59
|
+
return "That file is too big. Audio is limited to 4 MB; other notes to 20 MB.";
|
|
56
60
|
case "looks_like_a_key_file":
|
|
57
61
|
// The same rule the server's gate applies, said the same way: the name is
|
|
58
62
|
// all it takes to decide, and looking inside to be sure would already be
|
|
@@ -88,6 +92,9 @@ export async function readNoteFile(filePath) {
|
|
|
88
92
|
}
|
|
89
93
|
if (size === 0)
|
|
90
94
|
return { ok: false, refusal: "file_empty", detail: "byte_size_0" };
|
|
95
|
+
if (noteAudioMime(fileName) && size > 4_000_000) {
|
|
96
|
+
return { ok: false, refusal: "file_too_big", detail: "file_too_large: audio limit is 4 MB (4,000,000 bytes)" };
|
|
97
|
+
}
|
|
91
98
|
if (size > NOTE_FILE_MAX_BYTES) {
|
|
92
99
|
return { ok: false, refusal: "file_too_big", detail: `byte_size_${size}` };
|
|
93
100
|
}
|