@lordierclaw/bluenote-term 0.4.1
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/bin/bluenote-term.js +7 -0
- package/bin/bluenote-term.ts +7 -0
- package/bin/bn.ts +7 -0
- package/dist/app-t3b9bp7k.js +33 -0
- package/dist/command-0v6na3yp.js +35 -0
- package/dist/command-efmr44ky.js +69823 -0
- package/dist/command.js +201 -0
- package/dist/entry-9xj9qt6a.js +1060 -0
- package/dist/highlights-25px198m.js +7 -0
- package/dist/highlights-3v8s2sge.js +7 -0
- package/dist/highlights-9ftarw6a.js +7 -0
- package/dist/highlights-a9pkzpkp.js +7 -0
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-njnvvqgn.js +7 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/injections-6zvhaks7.js +7 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/tree-sitter-javascript-jcz708z6.js +7 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown-7a146rpb.js +7 -0
- package/dist/tree-sitter-markdown_inline-8ph78a4b.js +7 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-3zsd3cky.js +7 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/dist/tree-sitter-zig-ea20rsfb.js +7 -0
- package/package.json +38 -0
- package/src/command.d.ts +22 -0
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppError,
|
|
3
|
+
CodexAuthClientError,
|
|
4
|
+
CodexProviderSetupRequiredError,
|
|
5
|
+
CodexTextGenerationClientError,
|
|
6
|
+
EditorLaunchError,
|
|
7
|
+
UsageError,
|
|
8
|
+
archiveNote,
|
|
9
|
+
clipboardy_default,
|
|
10
|
+
createAiConfigRepository,
|
|
11
|
+
createAiTextGenerationClient,
|
|
12
|
+
createCodexAuthClient,
|
|
13
|
+
createCodexAuthRepository,
|
|
14
|
+
createNote,
|
|
15
|
+
createNoteDescription,
|
|
16
|
+
createNoteRepository,
|
|
17
|
+
deleteNote,
|
|
18
|
+
dropDescribeNoteJobIfNoteMissing,
|
|
19
|
+
enqueueDescribeNoteIfAiEnabled,
|
|
20
|
+
ensureManagedRoot,
|
|
21
|
+
formatCodexAuthStatus,
|
|
22
|
+
generateNoteDescription,
|
|
23
|
+
initRoot,
|
|
24
|
+
isValidationOrDataError,
|
|
25
|
+
listNotes,
|
|
26
|
+
listPendingAiJobs,
|
|
27
|
+
listRetryableAiJobs,
|
|
28
|
+
markDescribeNoteJobFailedIfContentHashMatches,
|
|
29
|
+
maskApiKey,
|
|
30
|
+
parsePlainNote,
|
|
31
|
+
rebuildIndexes,
|
|
32
|
+
renameNote,
|
|
33
|
+
resolveBlueNoteRoot,
|
|
34
|
+
runTuiCli,
|
|
35
|
+
sanitizeAiErrorMessage,
|
|
36
|
+
sanitizeCodexAuthErrorMessage,
|
|
37
|
+
searchNotes,
|
|
38
|
+
selectNote,
|
|
39
|
+
showNote,
|
|
40
|
+
systemClock
|
|
41
|
+
} from "./command-efmr44ky.js";
|
|
42
|
+
import"./command-0v6na3yp.js";
|
|
43
|
+
|
|
44
|
+
// packages/term/src/core/edit-note.ts
|
|
45
|
+
import path from "node:path";
|
|
46
|
+
|
|
47
|
+
// packages/term/src/platform/editor.ts
|
|
48
|
+
import { spawnSync } from "node:child_process";
|
|
49
|
+
function resolveEditorCommand(env = process.env) {
|
|
50
|
+
const editor = env.EDITOR?.trim();
|
|
51
|
+
if (!editor) {
|
|
52
|
+
throw new EditorLaunchError("EDITOR is not set.", {
|
|
53
|
+
hint: "Set EDITOR to a command like 'vim' or 'nano' and retry."
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return editor;
|
|
57
|
+
}
|
|
58
|
+
function parseEditorCommand(editor) {
|
|
59
|
+
const parts = editor.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
60
|
+
return parts.map((part) => {
|
|
61
|
+
if (part.startsWith('"') && part.endsWith('"') || part.startsWith("'") && part.endsWith("'")) {
|
|
62
|
+
return part.slice(1, -1);
|
|
63
|
+
}
|
|
64
|
+
return part;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function defaultLauncher(command) {
|
|
68
|
+
const result = spawnSync(command[0], command.slice(1), { stdio: "inherit" });
|
|
69
|
+
if (result.error) {
|
|
70
|
+
throw new EditorLaunchError(`Could not launch editor '${command[0]}'.`, {
|
|
71
|
+
hint: "Ensure EDITOR points to an installed executable.",
|
|
72
|
+
cause: result.error
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
exitCode: result.status ?? 1
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function launchEditor(notePath, options = {}) {
|
|
80
|
+
const editor = resolveEditorCommand(options.env);
|
|
81
|
+
const launcher = options.launcher ?? defaultLauncher;
|
|
82
|
+
const command = [...parseEditorCommand(editor), notePath];
|
|
83
|
+
const result = launcher(command);
|
|
84
|
+
if (result.exitCode !== 0) {
|
|
85
|
+
throw new EditorLaunchError(`Editor '${editor}' exited with code ${result.exitCode}.`, {
|
|
86
|
+
hint: "Fix the editor command or exit the editor successfully, then retry."
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// packages/term/src/core/edit-note.ts
|
|
92
|
+
function extractEditedTitle(body, fallbackTitle) {
|
|
93
|
+
const firstMeaningfulLine = body.split(/\r?\n/u).map((line) => line.trim()).find((line) => line.length > 0);
|
|
94
|
+
if (firstMeaningfulLine && /^#\s+.+$/u.test(firstMeaningfulLine)) {
|
|
95
|
+
return firstMeaningfulLine.replace(/^#\s+/u, "").trim();
|
|
96
|
+
}
|
|
97
|
+
return fallbackTitle;
|
|
98
|
+
}
|
|
99
|
+
function enqueueAiDescriptionAfterEdit(rootPath, input) {
|
|
100
|
+
enqueueDescribeNoteIfAiEnabled(rootPath, {
|
|
101
|
+
key: input.key,
|
|
102
|
+
relativePath: input.relativePath,
|
|
103
|
+
title: input.title,
|
|
104
|
+
body: input.body,
|
|
105
|
+
currentDescription: input.description,
|
|
106
|
+
replaceKey: input.replaceKey
|
|
107
|
+
}, { clock: input.clock, warn: (message) => console.warn(message) });
|
|
108
|
+
}
|
|
109
|
+
function editNote(options) {
|
|
110
|
+
const rootPath = resolveBlueNoteRoot(options);
|
|
111
|
+
const repository = createNoteRepository(rootPath);
|
|
112
|
+
const selected = selectNote({ repository, selector: options.selector, visibility: options.visibility });
|
|
113
|
+
const notePath = path.join(rootPath, selected.sourcePath);
|
|
114
|
+
const clock = options.clock ?? systemClock;
|
|
115
|
+
launchEditor(notePath, options);
|
|
116
|
+
const editedRaw = repository.readRaw(notePath);
|
|
117
|
+
const edited = parsePlainNote(editedRaw, selected.sourcePath);
|
|
118
|
+
const title = extractEditedTitle(edited.body, selected.frontmatter.title);
|
|
119
|
+
const updatedAt = clock.now().toISOString();
|
|
120
|
+
const titleChanged = title !== selected.frontmatter.title;
|
|
121
|
+
const bodyChanged = edited.body !== selected.body;
|
|
122
|
+
if (titleChanged) {
|
|
123
|
+
const renamed = renameNote({
|
|
124
|
+
override: rootPath,
|
|
125
|
+
selector: options.selector,
|
|
126
|
+
title,
|
|
127
|
+
body: edited.body,
|
|
128
|
+
updatedAt,
|
|
129
|
+
visibility: options.visibility,
|
|
130
|
+
randomSource: options.randomSource
|
|
131
|
+
});
|
|
132
|
+
rebuildIndexes({ override: rootPath });
|
|
133
|
+
enqueueAiDescriptionAfterEdit(rootPath, {
|
|
134
|
+
key: renamed.key,
|
|
135
|
+
title,
|
|
136
|
+
body: edited.body,
|
|
137
|
+
description: createNoteDescription(edited.body),
|
|
138
|
+
relativePath: renamed.relativePath,
|
|
139
|
+
clock,
|
|
140
|
+
replaceKey: renamed.previousKey
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
rootPath,
|
|
144
|
+
notePath: renamed.notePath,
|
|
145
|
+
relativePath: renamed.relativePath,
|
|
146
|
+
previousKey: renamed.previousKey,
|
|
147
|
+
key: renamed.key
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const synced = repository.syncEditedNote(notePath, {
|
|
151
|
+
title,
|
|
152
|
+
body: edited.body,
|
|
153
|
+
updatedAt
|
|
154
|
+
});
|
|
155
|
+
rebuildIndexes({ override: rootPath });
|
|
156
|
+
if (bodyChanged) {
|
|
157
|
+
enqueueAiDescriptionAfterEdit(rootPath, {
|
|
158
|
+
key: selected.frontmatter.id,
|
|
159
|
+
title,
|
|
160
|
+
body: edited.body,
|
|
161
|
+
description: createNoteDescription(edited.body),
|
|
162
|
+
relativePath: synced.relativePath,
|
|
163
|
+
clock
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
rootPath,
|
|
168
|
+
notePath: synced.notePath,
|
|
169
|
+
relativePath: synced.relativePath,
|
|
170
|
+
previousKey: selected.frontmatter.id,
|
|
171
|
+
key: selected.frontmatter.id
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// packages/term/src/platform/clipboard.ts
|
|
176
|
+
var desktopClipboard = {
|
|
177
|
+
readText() {
|
|
178
|
+
return clipboardy_default.readSync();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// packages/term/src/cli/ai.ts
|
|
183
|
+
var PLAINTEXT_WARNING = [
|
|
184
|
+
"Warning: API key is stored in plaintext under .data/ai/config.json.",
|
|
185
|
+
"Do not commit or share your BlueNote managed root if it contains secrets."
|
|
186
|
+
].join(`
|
|
187
|
+
`);
|
|
188
|
+
function formatAiHelp() {
|
|
189
|
+
return [
|
|
190
|
+
"Opt-in AI description generation for BlueNote notes.",
|
|
191
|
+
"",
|
|
192
|
+
"Usage:",
|
|
193
|
+
" bn ai <command> [options]",
|
|
194
|
+
"",
|
|
195
|
+
"Commands:",
|
|
196
|
+
" config set [--provider openai-compatible] --base-url <url> --api-key <key> --model <model> [--max-attempts <n>] [--output-language <text>] Configure OpenAI-compatible AI",
|
|
197
|
+
" config set --provider codex --model <model> [--max-attempts <n>] [--output-language <text>] Configure Codex AI model selection",
|
|
198
|
+
" config show Show configured provider settings with the API key masked",
|
|
199
|
+
" codex auth login Authenticate Codex with device-code OAuth",
|
|
200
|
+
" codex auth status Show Codex auth status without secrets",
|
|
201
|
+
" codex auth logout Remove stored Codex auth while keeping AI config",
|
|
202
|
+
" describe <key|path> Generate and automatically apply a note description",
|
|
203
|
+
" queue Show pending AI description jobs",
|
|
204
|
+
" process-queue [--limit <n>] Process queued description refreshes",
|
|
205
|
+
"",
|
|
206
|
+
"AI is disabled until configured. Core BlueNote commands work offline; AI provider calls require network access."
|
|
207
|
+
].join(`
|
|
208
|
+
`) + `
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
function readFlagValue(args, flagName) {
|
|
212
|
+
const flagIndex = args.indexOf(flagName);
|
|
213
|
+
if (flagIndex === -1) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const value = args[flagIndex + 1];
|
|
217
|
+
if (value === undefined || value.startsWith("--")) {
|
|
218
|
+
throw new UsageError(`Missing value for ${flagName}.`, {
|
|
219
|
+
hint: `Pass ${flagName} "...".`
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
function requireFlag(args, flagName, hint) {
|
|
225
|
+
const value = readFlagValue(args, flagName);
|
|
226
|
+
if (!value) {
|
|
227
|
+
throw new UsageError(`Missing required ${flagName} for AI config.`, { hint });
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
function parseLimit(args) {
|
|
232
|
+
const raw = readFlagValue(args, "--limit");
|
|
233
|
+
if (raw === undefined) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const limit = Number(raw);
|
|
237
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
238
|
+
throw new UsageError("Invalid --limit for AI queue processing.", {
|
|
239
|
+
hint: "Run bn ai process-queue --limit <positive-integer>."
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return limit;
|
|
243
|
+
}
|
|
244
|
+
function parsePositiveIntegerFlag(args, flagName) {
|
|
245
|
+
const raw = readFlagValue(args, flagName);
|
|
246
|
+
if (raw === undefined) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const value = Number(raw);
|
|
250
|
+
if (!Number.isInteger(value) || value < 1 || value > 10) {
|
|
251
|
+
throw new UsageError(`Invalid ${flagName} for AI config.`, {
|
|
252
|
+
hint: `Run bn ai config set ${flagName} <integer-from-1-to-10>.`
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
function readOptionalOutputLanguage(args) {
|
|
258
|
+
const value = readFlagValue(args, "--output-language");
|
|
259
|
+
if (value === undefined)
|
|
260
|
+
return;
|
|
261
|
+
if (value.trim() === "") {
|
|
262
|
+
throw new UsageError("Invalid --output-language for AI config.", {
|
|
263
|
+
hint: "Pass a non-empty language preference string."
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
var DEFAULT_AI_LOGGING = {
|
|
269
|
+
usage: true,
|
|
270
|
+
conversations: false,
|
|
271
|
+
results: true
|
|
272
|
+
};
|
|
273
|
+
function createDefaultConfig(input) {
|
|
274
|
+
const existingOpenAiConfig = input.existing?.provider === "openai-compatible" ? input.existing : null;
|
|
275
|
+
return {
|
|
276
|
+
version: 1,
|
|
277
|
+
enabled: existingOpenAiConfig?.enabled ?? true,
|
|
278
|
+
provider: "openai-compatible",
|
|
279
|
+
baseUrl: input.baseUrl,
|
|
280
|
+
apiKey: input.apiKey,
|
|
281
|
+
model: input.model,
|
|
282
|
+
logging: existingOpenAiConfig?.logging ?? DEFAULT_AI_LOGGING,
|
|
283
|
+
maxAttempts: input.maxAttempts ?? input.existing?.maxAttempts ?? 3,
|
|
284
|
+
outputLanguage: input.outputLanguage ?? input.existing?.outputLanguage ?? "English"
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function createCodexConfig(input) {
|
|
288
|
+
const existingCodexConfig = input.existing?.provider === "codex" ? input.existing : null;
|
|
289
|
+
return {
|
|
290
|
+
version: 1,
|
|
291
|
+
enabled: existingCodexConfig?.enabled ?? true,
|
|
292
|
+
provider: "codex",
|
|
293
|
+
model: input.model,
|
|
294
|
+
logging: existingCodexConfig?.logging ?? DEFAULT_AI_LOGGING,
|
|
295
|
+
maxAttempts: input.maxAttempts ?? input.existing?.maxAttempts ?? 3,
|
|
296
|
+
outputLanguage: input.outputLanguage ?? input.existing?.outputLanguage ?? "English"
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function getConfiguredRootPath() {
|
|
300
|
+
return ensureManagedRoot(resolveBlueNoteRoot());
|
|
301
|
+
}
|
|
302
|
+
function requireAiConfig(rootPath) {
|
|
303
|
+
if (!createAiConfigRepository(rootPath).exists()) {
|
|
304
|
+
throw new UsageError("AI is not configured.", {
|
|
305
|
+
hint: "Run bn ai config set --base-url <url> --api-key <key> --model <model>. For Codex, run bn ai config set --provider codex --model <model>."
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function requireCodexConfig(rootPath) {
|
|
310
|
+
requireAiConfig(rootPath);
|
|
311
|
+
const config = createAiConfigRepository(rootPath).read();
|
|
312
|
+
if (config.provider !== "codex") {
|
|
313
|
+
throw new UsageError("Codex is not the configured AI provider.", {
|
|
314
|
+
hint: "Run bn ai config set --provider codex --model <model> before Codex auth commands."
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return config;
|
|
318
|
+
}
|
|
319
|
+
function getAiClient(config, runtime) {
|
|
320
|
+
if (runtime.aiClient) {
|
|
321
|
+
return runtime.aiClient;
|
|
322
|
+
}
|
|
323
|
+
if (config.provider === "codex") {
|
|
324
|
+
const rootPath = getConfiguredRootPath();
|
|
325
|
+
const repository = createCodexAuthRepository(rootPath, runtime.codexAuth);
|
|
326
|
+
const authClient = createCodexAuthClient({
|
|
327
|
+
...runtime.codexAuth,
|
|
328
|
+
fetch: runtime.fetch ?? fetch,
|
|
329
|
+
repository
|
|
330
|
+
});
|
|
331
|
+
return createAiTextGenerationClient(config, {
|
|
332
|
+
fetch: runtime.fetch ?? fetch,
|
|
333
|
+
codexAuth: {
|
|
334
|
+
hasAuth: () => repository.exists(),
|
|
335
|
+
async getAuth() {
|
|
336
|
+
return repository.exists() ? repository.read() : null;
|
|
337
|
+
},
|
|
338
|
+
async refreshAuth(auth) {
|
|
339
|
+
const refreshed = await authClient.refreshAuth(auth);
|
|
340
|
+
repository.write(refreshed);
|
|
341
|
+
return refreshed;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return createAiTextGenerationClient(config, { fetch: runtime.fetch ?? fetch });
|
|
347
|
+
}
|
|
348
|
+
function formatConfig(config) {
|
|
349
|
+
return [
|
|
350
|
+
"AI config:",
|
|
351
|
+
` enabled: ${config.enabled}`,
|
|
352
|
+
` provider: ${config.provider}`,
|
|
353
|
+
` model: ${config.model}`,
|
|
354
|
+
...config.provider === "openai-compatible" ? [
|
|
355
|
+
` baseUrl: ${config.baseUrl}`,
|
|
356
|
+
` apiKey: ${maskApiKey(config.apiKey)}`
|
|
357
|
+
] : [],
|
|
358
|
+
` logging.usage: ${config.logging.usage}`,
|
|
359
|
+
` logging.conversations: ${config.logging.conversations}`,
|
|
360
|
+
` logging.results: ${config.logging.results}`,
|
|
361
|
+
` maxAttempts: ${config.maxAttempts ?? 3}`,
|
|
362
|
+
` outputLanguage: ${config.outputLanguage ?? "English"}`
|
|
363
|
+
].join(`
|
|
364
|
+
`) + `
|
|
365
|
+
`;
|
|
366
|
+
}
|
|
367
|
+
function formatPendingJobs(jobs) {
|
|
368
|
+
if (jobs.length === 0) {
|
|
369
|
+
return `Pending AI jobs: 0
|
|
370
|
+
`;
|
|
371
|
+
}
|
|
372
|
+
return [
|
|
373
|
+
`Pending AI jobs: ${jobs.length}`,
|
|
374
|
+
...jobs.map((job) => `${job.kind} ${job.key} ${job.relativePath} attempts=${job.attempts}`)
|
|
375
|
+
].join(`
|
|
376
|
+
`) + `
|
|
377
|
+
`;
|
|
378
|
+
}
|
|
379
|
+
function markJobFailed(rootPath, job, error, secrets = []) {
|
|
380
|
+
const message = sanitizeAiErrorMessage(error, secrets);
|
|
381
|
+
return markDescribeNoteJobFailedIfContentHashMatches({
|
|
382
|
+
rootPath,
|
|
383
|
+
key: job.key,
|
|
384
|
+
contentHash: job.contentHash,
|
|
385
|
+
lastError: message
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
function describeOutput(result) {
|
|
389
|
+
if (result.status === "applied" && result.description) {
|
|
390
|
+
return {
|
|
391
|
+
exitCode: 0,
|
|
392
|
+
stdout: `Updated AI description for ${result.key}
|
|
393
|
+
Description: ${result.description}
|
|
394
|
+
`,
|
|
395
|
+
stderr: ""
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
if (result.status === "stale") {
|
|
399
|
+
throw new UsageError(`AI description result was stale: ${result.error ?? "note changed while AI description was generating"}.`, {
|
|
400
|
+
hint: "The existing note description was left unchanged. Run bn ai describe again to refresh it."
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
throw new UsageError(result.error ?? "Provider returned an invalid description.", {
|
|
404
|
+
hint: "The existing note description was left unchanged."
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
function providerFailureError(error, secrets = []) {
|
|
408
|
+
return new UsageError(`AI provider request failed: ${sanitizeAiErrorMessage(error, secrets)}`, {
|
|
409
|
+
hint: "The existing note description was left unchanged."
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function isCodexProviderSetupBlocked(error) {
|
|
413
|
+
if (error instanceof CodexProviderSetupRequiredError) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
if (!(error instanceof CodexTextGenerationClientError)) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
const message = error.message.toLowerCase();
|
|
420
|
+
return message.includes("codex auth setup is required") || message.includes("codex auth refresh failed") || message.includes("codex auth is expired") || message.includes("run bn ai codex auth login");
|
|
421
|
+
}
|
|
422
|
+
async function runConfigCommand(args) {
|
|
423
|
+
const [subcommand, ...subcommandArgs] = args;
|
|
424
|
+
const rootPath = getConfiguredRootPath();
|
|
425
|
+
const repository = createAiConfigRepository(rootPath);
|
|
426
|
+
if (subcommand === "set") {
|
|
427
|
+
const existingConfig = repository.exists() ? repository.read() : null;
|
|
428
|
+
const provider = readFlagValue(subcommandArgs, "--provider") ?? existingConfig?.provider ?? "openai-compatible";
|
|
429
|
+
if (provider !== "openai-compatible" && provider !== "codex") {
|
|
430
|
+
throw new UsageError("Invalid AI provider.", {
|
|
431
|
+
hint: "Use --provider openai-compatible or --provider codex."
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
const maxAttempts = parsePositiveIntegerFlag(subcommandArgs, "--max-attempts");
|
|
435
|
+
const outputLanguage = readOptionalOutputLanguage(subcommandArgs);
|
|
436
|
+
const config = provider === "codex" ? createCodexConfig({
|
|
437
|
+
model: readFlagValue(subcommandArgs, "--model") ?? (existingConfig?.provider === "codex" ? existingConfig.model : undefined) ?? requireFlag(subcommandArgs, "--model", "Run bn ai config set --provider codex --model <model>."),
|
|
438
|
+
maxAttempts,
|
|
439
|
+
outputLanguage,
|
|
440
|
+
existing: existingConfig
|
|
441
|
+
}) : createDefaultConfig({
|
|
442
|
+
baseUrl: readFlagValue(subcommandArgs, "--base-url") ?? (existingConfig?.provider === "openai-compatible" ? existingConfig.baseUrl : undefined) ?? requireFlag(subcommandArgs, "--base-url", "Run bn ai config set --base-url <url> --api-key <key> --model <model>."),
|
|
443
|
+
apiKey: readFlagValue(subcommandArgs, "--api-key") ?? (existingConfig?.provider === "openai-compatible" ? existingConfig.apiKey : undefined) ?? requireFlag(subcommandArgs, "--api-key", "Run bn ai config set --base-url <url> --api-key <key> --model <model>."),
|
|
444
|
+
model: readFlagValue(subcommandArgs, "--model") ?? (existingConfig?.provider === "openai-compatible" ? existingConfig.model : undefined) ?? requireFlag(subcommandArgs, "--model", "Run bn ai config set --base-url <url> --api-key <key> --model <model>."),
|
|
445
|
+
maxAttempts,
|
|
446
|
+
outputLanguage,
|
|
447
|
+
existing: existingConfig
|
|
448
|
+
});
|
|
449
|
+
repository.write(config);
|
|
450
|
+
return {
|
|
451
|
+
exitCode: 0,
|
|
452
|
+
stdout: config.provider === "codex" ? `AI Codex config saved. Run bn ai codex auth login before Codex generation.
|
|
453
|
+
` : `AI config saved.
|
|
454
|
+
${PLAINTEXT_WARNING}
|
|
455
|
+
`,
|
|
456
|
+
stderr: ""
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
if (subcommand === "show") {
|
|
460
|
+
requireAiConfig(rootPath);
|
|
461
|
+
return { exitCode: 0, stdout: formatConfig(repository.read()), stderr: "" };
|
|
462
|
+
}
|
|
463
|
+
throw new UsageError(`Unknown AI config command: ${subcommand ?? ""}`.trim(), {
|
|
464
|
+
hint: "Run bn ai config set ... or bn ai config show."
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
function assertNoExtraArgs(args, command) {
|
|
468
|
+
if (args.length > 0) {
|
|
469
|
+
throw new UsageError(`Unexpected arguments for ${command}.`, {
|
|
470
|
+
hint: `Run ${command}.`
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function runCodexAuthCommand(args, runtime) {
|
|
475
|
+
const [subcommand, ...subcommandArgs] = args;
|
|
476
|
+
const rootPath = getConfiguredRootPath();
|
|
477
|
+
const config = requireCodexConfig(rootPath);
|
|
478
|
+
const repository = createCodexAuthRepository(rootPath, runtime.codexAuth);
|
|
479
|
+
if (subcommand === "status") {
|
|
480
|
+
assertNoExtraArgs(subcommandArgs, "bn ai codex auth status");
|
|
481
|
+
return {
|
|
482
|
+
exitCode: 0,
|
|
483
|
+
stdout: `${formatCodexAuthStatus(repository.getStatus({ provider: config.provider }))}
|
|
484
|
+
`,
|
|
485
|
+
stderr: ""
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (subcommand === "login") {
|
|
489
|
+
assertNoExtraArgs(subcommandArgs, "bn ai codex auth login");
|
|
490
|
+
const outputLines = [];
|
|
491
|
+
const shouldStream = runtime.writeStdout !== undefined || runtime.fetch === undefined && runtime.codexAuth === undefined && runtime.aiClient === undefined;
|
|
492
|
+
const writeInteractive = (line) => {
|
|
493
|
+
if (shouldStream) {
|
|
494
|
+
(runtime.writeStdout ?? process.stdout.write.bind(process.stdout))(`${line}
|
|
495
|
+
`);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
outputLines.push(line);
|
|
499
|
+
};
|
|
500
|
+
const client = createCodexAuthClient({
|
|
501
|
+
...runtime.codexAuth,
|
|
502
|
+
fetch: runtime.fetch ?? fetch,
|
|
503
|
+
repository
|
|
504
|
+
});
|
|
505
|
+
try {
|
|
506
|
+
await client.login({
|
|
507
|
+
onDeviceFlow(flow) {
|
|
508
|
+
writeInteractive(`Open ${flow.verificationUrl} and enter code ${flow.userCode}.`);
|
|
509
|
+
writeInteractive("Waiting for Codex authentication to complete...");
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
} catch (error) {
|
|
513
|
+
const message = error instanceof CodexAuthClientError ? sanitizeCodexAuthErrorMessage(error) : sanitizeCodexAuthErrorMessage(error);
|
|
514
|
+
throw new UsageError(`Codex auth login failed: ${message}`, {
|
|
515
|
+
hint: "Check network access and retry bn ai codex auth login."
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
const completeLine = "Codex auth login complete.";
|
|
519
|
+
outputLines.push(completeLine);
|
|
520
|
+
return { exitCode: 0, stdout: `${outputLines.join(`
|
|
521
|
+
`)}
|
|
522
|
+
`, stderr: "" };
|
|
523
|
+
}
|
|
524
|
+
if (subcommand === "logout") {
|
|
525
|
+
assertNoExtraArgs(subcommandArgs, "bn ai codex auth logout");
|
|
526
|
+
repository.delete();
|
|
527
|
+
return { exitCode: 0, stdout: `Codex auth removed. Codex AI config was kept.
|
|
528
|
+
`, stderr: "" };
|
|
529
|
+
}
|
|
530
|
+
throw new UsageError("Unknown AI Codex auth command.", {
|
|
531
|
+
hint: "Run bn ai codex auth login, bn ai codex auth status, or bn ai codex auth logout."
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
async function runAiCli(args, runtime = {}) {
|
|
535
|
+
const [subcommand, ...subcommandArgs] = args;
|
|
536
|
+
if (!subcommand || subcommand === "--help" || subcommand === "help") {
|
|
537
|
+
return { exitCode: 0, stdout: formatAiHelp(), stderr: "" };
|
|
538
|
+
}
|
|
539
|
+
if (subcommand === "config") {
|
|
540
|
+
return runConfigCommand(subcommandArgs);
|
|
541
|
+
}
|
|
542
|
+
if (subcommand === "codex") {
|
|
543
|
+
if (subcommandArgs[0] === "auth") {
|
|
544
|
+
return runCodexAuthCommand(subcommandArgs.slice(1), runtime);
|
|
545
|
+
}
|
|
546
|
+
throw new UsageError("Unknown AI Codex command.", {
|
|
547
|
+
hint: "Run bn ai codex auth login, bn ai codex auth status, or bn ai codex auth logout."
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
if (subcommand === "queue") {
|
|
551
|
+
const rootPath = getConfiguredRootPath();
|
|
552
|
+
return { exitCode: 0, stdout: formatPendingJobs(listPendingAiJobs(rootPath)), stderr: "" };
|
|
553
|
+
}
|
|
554
|
+
if (subcommand === "describe") {
|
|
555
|
+
const selector = subcommandArgs[0];
|
|
556
|
+
if (!selector) {
|
|
557
|
+
throw new UsageError("Missing required selector for AI describe.", {
|
|
558
|
+
hint: "Run bn ai describe <key|path>."
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
const rootPath = getConfiguredRootPath();
|
|
562
|
+
requireAiConfig(rootPath);
|
|
563
|
+
const config = createAiConfigRepository(rootPath).read();
|
|
564
|
+
if (!config.enabled) {
|
|
565
|
+
throw new UsageError("AI description generation is disabled.", {
|
|
566
|
+
hint: "Enable AI in .data/ai/config.json before generating note descriptions."
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
const secrets = config.provider === "openai-compatible" ? [config.apiKey] : [];
|
|
570
|
+
try {
|
|
571
|
+
return describeOutput(await generateNoteDescription({ rootPath, selector, client: getAiClient(config, runtime) }));
|
|
572
|
+
} catch (error) {
|
|
573
|
+
if (error instanceof UsageError) {
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
throw providerFailureError(error, secrets);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (subcommand === "process-queue") {
|
|
580
|
+
const rootPath = getConfiguredRootPath();
|
|
581
|
+
requireAiConfig(rootPath);
|
|
582
|
+
const config = createAiConfigRepository(rootPath).read();
|
|
583
|
+
const limit = parseLimit(subcommandArgs);
|
|
584
|
+
if (!config.enabled) {
|
|
585
|
+
const remaining2 = listPendingAiJobs(rootPath).length;
|
|
586
|
+
return {
|
|
587
|
+
exitCode: remaining2 > 0 ? 1 : 0,
|
|
588
|
+
stdout: `Processed AI queue: 0 applied, 0 failed, ${remaining2} remaining.
|
|
589
|
+
`,
|
|
590
|
+
stderr: ""
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const secrets = config.provider === "openai-compatible" ? [config.apiKey] : [];
|
|
594
|
+
const jobs = listRetryableAiJobs(rootPath, config.maxAttempts ?? 3);
|
|
595
|
+
const selectedJobs = jobs.slice(0, limit ?? jobs.length);
|
|
596
|
+
let applied = 0;
|
|
597
|
+
let failed = 0;
|
|
598
|
+
let setupBlocked = false;
|
|
599
|
+
for (const job of selectedJobs) {
|
|
600
|
+
try {
|
|
601
|
+
if (dropDescribeNoteJobIfNoteMissing(rootPath, job)) {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const result = await generateNoteDescription({ rootPath, selector: job.key, client: getAiClient(config, runtime) });
|
|
605
|
+
if (result.status === "applied") {
|
|
606
|
+
applied += 1;
|
|
607
|
+
} else if (result.status === "stale") {} else {
|
|
608
|
+
if (markJobFailed(rootPath, job, result.error ?? "invalid description", secrets)) {
|
|
609
|
+
failed += 1;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
} catch (error) {
|
|
613
|
+
if (isCodexProviderSetupBlocked(error)) {
|
|
614
|
+
setupBlocked = true;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (markJobFailed(rootPath, job, error, secrets)) {
|
|
618
|
+
failed += 1;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const remaining = listPendingAiJobs(rootPath).length;
|
|
623
|
+
return {
|
|
624
|
+
exitCode: failed > 0 || setupBlocked ? 1 : 0,
|
|
625
|
+
stdout: `Processed AI queue: ${applied} applied, ${failed} failed, ${remaining} remaining.
|
|
626
|
+
`,
|
|
627
|
+
stderr: ""
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
throw new UsageError(`Unknown AI command: ${subcommand ?? ""}`.trim(), {
|
|
631
|
+
hint: "Run bn ai config set, bn ai config show, bn ai describe, bn ai queue, or bn ai process-queue."
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// packages/term/src/cli/entry.ts
|
|
636
|
+
function formatCliError(error) {
|
|
637
|
+
const messageLines = [error.message];
|
|
638
|
+
if (error.hint) {
|
|
639
|
+
messageLines.push(`Hint: ${error.hint}`);
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
exitCode: isValidationOrDataError(error) ? 2 : 1,
|
|
643
|
+
stdout: "",
|
|
644
|
+
stderr: `${messageLines.join(`
|
|
645
|
+
`)}
|
|
646
|
+
`
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function parseVisibilityArgs(args) {
|
|
650
|
+
let visibility = "normal";
|
|
651
|
+
let index = 0;
|
|
652
|
+
for (;index < args.length; index += 1) {
|
|
653
|
+
const arg = args[index];
|
|
654
|
+
if (arg === "--drafts") {
|
|
655
|
+
if (visibility === "all") {
|
|
656
|
+
throw new UsageError("Choose either --drafts or --all, not both.", {
|
|
657
|
+
hint: "Use --drafts for normal + draft notes, or --all to include archived notes."
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
visibility = "drafts";
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (arg === "--all") {
|
|
664
|
+
if (visibility === "drafts") {
|
|
665
|
+
throw new UsageError("Choose either --drafts or --all, not both.", {
|
|
666
|
+
hint: "Use --drafts for normal + draft notes, or --all to include archived notes."
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
visibility = "all";
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
return { args: args.slice(index), visibility };
|
|
675
|
+
}
|
|
676
|
+
function parseSelectorArgs(command, args, options = {}) {
|
|
677
|
+
const selectors = [];
|
|
678
|
+
let force = false;
|
|
679
|
+
let visibility = "normal";
|
|
680
|
+
for (const arg of args) {
|
|
681
|
+
if (arg === "--drafts") {
|
|
682
|
+
if (visibility === "all") {
|
|
683
|
+
throw new UsageError("Choose either --drafts or --all, not both.", {
|
|
684
|
+
hint: "Use --drafts for normal + draft notes, or --all to include archived notes."
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
visibility = "drafts";
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (arg === "--all") {
|
|
691
|
+
if (visibility === "drafts") {
|
|
692
|
+
throw new UsageError("Choose either --drafts or --all, not both.", {
|
|
693
|
+
hint: "Use --drafts for normal + draft notes, or --all to include archived notes."
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
visibility = "all";
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
if (arg === "--force") {
|
|
700
|
+
if (!options.requireForce) {
|
|
701
|
+
throw new UsageError(`${command} does not accept --force.`, {
|
|
702
|
+
hint: `Run bn ${command} <key|path>.`
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
force = true;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (arg.startsWith("--")) {
|
|
709
|
+
throw new UsageError(`Unknown option for ${command}: ${arg}.`, {
|
|
710
|
+
hint: `Run bn ${command} <key|path>${options.requireForce ? " --force" : ""}.`
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
selectors.push(arg);
|
|
714
|
+
}
|
|
715
|
+
if (selectors.length === 0) {
|
|
716
|
+
throw new UsageError(`Missing required selector for ${command}.`, {
|
|
717
|
+
hint: `Run bn ${command} <key|path>${options.requireForce ? " --force" : ""}.`
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
if (selectors.length > 1) {
|
|
721
|
+
throw new UsageError(`Too many selectors for ${command}.`, {
|
|
722
|
+
hint: `Run bn ${command} <key|path>${options.requireForce ? " --force" : ""}.`
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
return { selector: selectors[0], force, visibility };
|
|
726
|
+
}
|
|
727
|
+
function parseNewArgs(args) {
|
|
728
|
+
const positional = [];
|
|
729
|
+
let title;
|
|
730
|
+
let destinationPath;
|
|
731
|
+
let useClipboard = false;
|
|
732
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
733
|
+
const arg = args[index];
|
|
734
|
+
if (arg === "--title" || arg === "-t") {
|
|
735
|
+
const value = args[index + 1];
|
|
736
|
+
if (value === undefined || value.startsWith("-")) {
|
|
737
|
+
throw new UsageError(`Missing value for ${arg}.`, { hint: 'Pass --title "..." or -t "...".' });
|
|
738
|
+
}
|
|
739
|
+
title = value;
|
|
740
|
+
index += 1;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (arg === "--path") {
|
|
744
|
+
const value = args[index + 1];
|
|
745
|
+
if (value === undefined || value.startsWith("--")) {
|
|
746
|
+
throw new UsageError("Missing value for --path.", { hint: "Pass --path note/<folder>." });
|
|
747
|
+
}
|
|
748
|
+
destinationPath = value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
749
|
+
index += 1;
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (arg === "--clipboard") {
|
|
753
|
+
useClipboard = true;
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (arg.startsWith("--")) {
|
|
757
|
+
throw new UsageError(`Unknown option for new note: ${arg}.`, {
|
|
758
|
+
hint: "Run bn new --help for available new-note options."
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
positional.push(arg);
|
|
762
|
+
}
|
|
763
|
+
if (positional.length > 1) {
|
|
764
|
+
throw new UsageError("Too many positional body arguments for new note.", {
|
|
765
|
+
hint: 'Quote the note body as one argument, e.g. bn new "Body text".'
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
return { title, path: destinationPath, useClipboard, body: positional[0] };
|
|
769
|
+
}
|
|
770
|
+
function readNewNoteBody(parsed, runtime) {
|
|
771
|
+
const hasPositionalBody = parsed.body !== undefined;
|
|
772
|
+
if (hasPositionalBody && parsed.useClipboard) {
|
|
773
|
+
throw new UsageError("Choose either positional body or --clipboard, not both.", {
|
|
774
|
+
hint: 'Run bn new "Body text" or bn new --clipboard.'
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
if (!hasPositionalBody && !parsed.useClipboard) {
|
|
778
|
+
throw new UsageError("Missing note body for new note.", {
|
|
779
|
+
hint: 'Pass a positional body or use --clipboard, e.g. bn new "Body text".'
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
if (hasPositionalBody) {
|
|
783
|
+
return parsed.body ?? "";
|
|
784
|
+
}
|
|
785
|
+
try {
|
|
786
|
+
const clipboardBody = (runtime.clipboard ?? desktopClipboard).readText();
|
|
787
|
+
if (clipboardBody.length === 0) {
|
|
788
|
+
throw new UsageError("Clipboard is empty or unavailable.", {
|
|
789
|
+
hint: 'Copy note text first, or pass a body directly with bn new "Body text".'
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
return clipboardBody;
|
|
793
|
+
} catch (error) {
|
|
794
|
+
if (error instanceof UsageError)
|
|
795
|
+
throw error;
|
|
796
|
+
throw new UsageError("Clipboard is empty or unavailable.", {
|
|
797
|
+
hint: 'Copy note text first, or pass a body directly with bn new "Body text".',
|
|
798
|
+
cause: error
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function assertNewNotePathIsAllowed(destinationPath, title) {
|
|
803
|
+
if (destinationPath === undefined) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (title === undefined || title.trim().length === 0) {
|
|
807
|
+
throw new UsageError("--path requires --title for normal note creation.", {
|
|
808
|
+
hint: 'Run bn new --path note/<folder> --title "Title" "Body text".'
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
if (destinationPath !== "note" && !destinationPath.startsWith("note/")) {
|
|
812
|
+
throw new UsageError("--path must point to an existing folder under note/.", {
|
|
813
|
+
hint: "Use --path note or an existing note/<folder> destination."
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function formatHelp(version) {
|
|
818
|
+
return [
|
|
819
|
+
`BlueNote v${version}`,
|
|
820
|
+
"Local-first terminal notes for plain-note storage and selector-friendly workflows",
|
|
821
|
+
"",
|
|
822
|
+
"Usage:",
|
|
823
|
+
" bn <command> [options]",
|
|
824
|
+
"",
|
|
825
|
+
"Commands:",
|
|
826
|
+
" --help Show this message",
|
|
827
|
+
" --version Print the current version",
|
|
828
|
+
" init Initialize the managed BlueNote root",
|
|
829
|
+
" new [--title <title>] [--path note/<folder>] [--clipboard] <body>",
|
|
830
|
+
" Create a draft from body text or clipboard; --path creates a normal note",
|
|
831
|
+
" list [--drafts|--all] List notes as title, key, description, and path",
|
|
832
|
+
" show [--drafts|--all] <key|path> Print a matching note summary and body",
|
|
833
|
+
" search [--drafts|--all] <query> Search indexed notes",
|
|
834
|
+
" edit [--drafts|--all] <key|path> Open a matching note in $EDITOR",
|
|
835
|
+
" archive [--drafts|--all] <key|path> Archive a matching normal note",
|
|
836
|
+
" delete [--drafts|--all] <key|path> --force Permanently remove a matching note and sidecar",
|
|
837
|
+
" rebuild Rebuild derived metadata and search indexes",
|
|
838
|
+
" tui Launch the terminal UI workspace",
|
|
839
|
+
" ai Configure and run opt-in AI description generation"
|
|
840
|
+
].join(`
|
|
841
|
+
`) + `
|
|
842
|
+
`;
|
|
843
|
+
}
|
|
844
|
+
function formatNewHelp() {
|
|
845
|
+
return [
|
|
846
|
+
"Usage:",
|
|
847
|
+
" bn new [--title <title>] [--path note/<folder>] [--clipboard] <body>",
|
|
848
|
+
"",
|
|
849
|
+
"Creates a new note from quoted body text or clipboard text.",
|
|
850
|
+
"Without --path, creates a draft under draft/.",
|
|
851
|
+
"With --path note/<folder> and --title, creates a normal note under an existing note folder.",
|
|
852
|
+
"",
|
|
853
|
+
"Options:",
|
|
854
|
+
" --title, -t <title> Set the note title",
|
|
855
|
+
" --path <folder> Existing note/<folder> destination for a normal note",
|
|
856
|
+
" --clipboard Read note body from the clipboard"
|
|
857
|
+
].join(`
|
|
858
|
+
`) + `
|
|
859
|
+
`;
|
|
860
|
+
}
|
|
861
|
+
async function runCliAsync(args, version, runtime = {}) {
|
|
862
|
+
try {
|
|
863
|
+
if (args[0] === "ai") {
|
|
864
|
+
return await runAiCli(args.slice(1), runtime.ai);
|
|
865
|
+
}
|
|
866
|
+
return runCli(args, version, runtime);
|
|
867
|
+
} catch (error) {
|
|
868
|
+
if (error instanceof AppError) {
|
|
869
|
+
return formatCliError(error);
|
|
870
|
+
}
|
|
871
|
+
throw error;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function formatSearchMatches(query, matches) {
|
|
875
|
+
if (matches.length === 0) {
|
|
876
|
+
return `No notes matched "${query}".
|
|
877
|
+
`;
|
|
878
|
+
}
|
|
879
|
+
return matches.map((match) => {
|
|
880
|
+
const lines = [
|
|
881
|
+
match.title,
|
|
882
|
+
` key: ${match.key}`,
|
|
883
|
+
` path: ${match.relativePath}`,
|
|
884
|
+
` match: ${match.match.label}`
|
|
885
|
+
];
|
|
886
|
+
if (match.match.excerpt) {
|
|
887
|
+
lines.push(" excerpt:");
|
|
888
|
+
lines.push(` ${match.match.excerpt}`);
|
|
889
|
+
}
|
|
890
|
+
return lines.join(`
|
|
891
|
+
`);
|
|
892
|
+
}).join(`
|
|
893
|
+
|
|
894
|
+
`) + `
|
|
895
|
+
`;
|
|
896
|
+
}
|
|
897
|
+
function runCli(args, version, runtime = {}) {
|
|
898
|
+
try {
|
|
899
|
+
const [command, ...commandArgs] = args;
|
|
900
|
+
if (!command || command === "--help" || command === "help") {
|
|
901
|
+
return { exitCode: 0, stdout: formatHelp(version), stderr: "" };
|
|
902
|
+
}
|
|
903
|
+
if (command === "--version" || command === "version") {
|
|
904
|
+
return { exitCode: 0, stdout: `${version}
|
|
905
|
+
`, stderr: "" };
|
|
906
|
+
}
|
|
907
|
+
if (command === "init") {
|
|
908
|
+
const summary = initRoot();
|
|
909
|
+
return {
|
|
910
|
+
exitCode: 0,
|
|
911
|
+
stdout: `Initialized BlueNote root: ${summary.rootPath}
|
|
912
|
+
`,
|
|
913
|
+
stderr: ""
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
if (command === "tui") {
|
|
917
|
+
return (runtime.tuiRunner ?? runTuiCli)();
|
|
918
|
+
}
|
|
919
|
+
if (command === "new") {
|
|
920
|
+
if (commandArgs.length === 1 && commandArgs[0] === "--help") {
|
|
921
|
+
return { exitCode: 0, stdout: formatNewHelp(), stderr: "" };
|
|
922
|
+
}
|
|
923
|
+
const parsed = parseNewArgs(commandArgs);
|
|
924
|
+
const body = readNewNoteBody(parsed, runtime);
|
|
925
|
+
assertNewNotePathIsAllowed(parsed.path, parsed.title);
|
|
926
|
+
const summary = createNote({
|
|
927
|
+
title: parsed.title,
|
|
928
|
+
body,
|
|
929
|
+
type: parsed.path === undefined ? "draft" : "normal",
|
|
930
|
+
...parsed.path === undefined ? {} : { destinationFolder: parsed.path },
|
|
931
|
+
...runtime.createNoteOptions
|
|
932
|
+
});
|
|
933
|
+
return {
|
|
934
|
+
exitCode: 0,
|
|
935
|
+
stdout: `Created note
|
|
936
|
+
Key: ${summary.key}
|
|
937
|
+
Path: ${summary.relativePath}
|
|
938
|
+
`,
|
|
939
|
+
stderr: ""
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
if (command === "edit") {
|
|
943
|
+
const { selector, visibility } = parseSelectorArgs("edit", commandArgs);
|
|
944
|
+
const summary = editNote({ selector, visibility });
|
|
945
|
+
const renameLine = summary.previousKey !== undefined && summary.key !== undefined && summary.previousKey !== summary.key ? `Renamed key: ${summary.previousKey} -> ${summary.key}
|
|
946
|
+
` : "";
|
|
947
|
+
return {
|
|
948
|
+
exitCode: 0,
|
|
949
|
+
stdout: `Edited note: ${summary.relativePath}
|
|
950
|
+
${renameLine}`,
|
|
951
|
+
stderr: ""
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
if (command === "archive") {
|
|
955
|
+
const { selector, visibility } = parseSelectorArgs("archive", commandArgs);
|
|
956
|
+
const summary = archiveNote({ selector, visibility });
|
|
957
|
+
return {
|
|
958
|
+
exitCode: 0,
|
|
959
|
+
stdout: `Archived note: ${summary.relativePath}
|
|
960
|
+
`,
|
|
961
|
+
stderr: ""
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
if (command === "delete") {
|
|
965
|
+
const { selector, force, visibility } = parseSelectorArgs("delete", commandArgs, { requireForce: true });
|
|
966
|
+
const summary = deleteNote({
|
|
967
|
+
selector,
|
|
968
|
+
force,
|
|
969
|
+
visibility
|
|
970
|
+
});
|
|
971
|
+
return {
|
|
972
|
+
exitCode: 0,
|
|
973
|
+
stdout: `Deleted note: ${summary.relativePath}
|
|
974
|
+
`,
|
|
975
|
+
stderr: ""
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
if (command === "list") {
|
|
979
|
+
const parsedVisibility = parseVisibilityArgs(commandArgs);
|
|
980
|
+
if (parsedVisibility.args.length > 0) {
|
|
981
|
+
throw new UsageError(`Unknown option for list: ${parsedVisibility.args[0]}.`, {
|
|
982
|
+
hint: "Run bn list [--drafts|--all]."
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
const summaries = listNotes({ visibility: parsedVisibility.visibility });
|
|
986
|
+
const stdout = summaries.map((summary) => `${summary.title} ${summary.key} ${summary.description} ${summary.relativePath}`).join(`
|
|
987
|
+
`);
|
|
988
|
+
return {
|
|
989
|
+
exitCode: 0,
|
|
990
|
+
stdout: stdout === "" ? "" : `${stdout}
|
|
991
|
+
`,
|
|
992
|
+
stderr: ""
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
if (command === "search") {
|
|
996
|
+
const parsedVisibility = parseVisibilityArgs(commandArgs);
|
|
997
|
+
const query = parsedVisibility.args.join(" ").trim();
|
|
998
|
+
if (query === "") {
|
|
999
|
+
throw new UsageError("Missing required query for search.", {
|
|
1000
|
+
hint: 'Run bn search [--drafts|--all] "keywords".'
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
const matches = searchNotes(query, { visibility: parsedVisibility.visibility });
|
|
1004
|
+
return {
|
|
1005
|
+
exitCode: 0,
|
|
1006
|
+
stdout: formatSearchMatches(query, matches),
|
|
1007
|
+
stderr: ""
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
if (command === "show") {
|
|
1011
|
+
const { selector, visibility } = parseSelectorArgs("show", commandArgs);
|
|
1012
|
+
const shown = showNote({ selector, visibility });
|
|
1013
|
+
return {
|
|
1014
|
+
exitCode: 0,
|
|
1015
|
+
stdout: `Title: ${shown.title}
|
|
1016
|
+
Key: ${shown.key}
|
|
1017
|
+
Path: ${shown.relativePath}
|
|
1018
|
+
Description: ${shown.description}
|
|
1019
|
+
|
|
1020
|
+
${shown.body}`,
|
|
1021
|
+
stderr: ""
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
if (command === "rebuild") {
|
|
1025
|
+
const summary = rebuildIndexes(runtime.rebuildIndexesOptions);
|
|
1026
|
+
if (summary.validationErrors.length > 0) {
|
|
1027
|
+
return {
|
|
1028
|
+
exitCode: 2,
|
|
1029
|
+
stdout: "",
|
|
1030
|
+
stderr: `Validation failed while rebuilding indexes.
|
|
1031
|
+
${summary.validationErrors.join(`
|
|
1032
|
+
`)}
|
|
1033
|
+
`
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
exitCode: 0,
|
|
1038
|
+
stdout: `Rebuilt indexes for ${summary.noteCount} note(s).
|
|
1039
|
+
`,
|
|
1040
|
+
stderr: ""
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
return formatCliError(new UsageError(`Unknown command: ${command}`, {
|
|
1044
|
+
hint: "Use --help to see available commands."
|
|
1045
|
+
}));
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
if (error instanceof AppError) {
|
|
1048
|
+
return formatCliError(error);
|
|
1049
|
+
}
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
export {
|
|
1054
|
+
runCliAsync,
|
|
1055
|
+
runCli,
|
|
1056
|
+
formatSearchMatches,
|
|
1057
|
+
formatNewHelp,
|
|
1058
|
+
formatHelp,
|
|
1059
|
+
formatCliError
|
|
1060
|
+
};
|