@wirecat/cli-core 0.421.0 → 0.423.0

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/dist/analysis/settings.d.ts +0 -6
  3. package/dist/analysis/settings.js +2 -5
  4. package/dist/cli/analysis-choice.d.ts +0 -1
  5. package/dist/cli/analysis-choice.js +9 -22
  6. package/dist/cli/bot/context.js +1 -1
  7. package/dist/cli/commands-command.js +1 -1
  8. package/dist/cli/config-command.d.ts +0 -4
  9. package/dist/cli/config-command.js +36 -26
  10. package/dist/cli/config-issues.d.ts +52 -0
  11. package/dist/cli/config-issues.js +187 -0
  12. package/dist/cli/config-keys.js +4 -28
  13. package/dist/cli/embedding-choice.d.ts +1 -2
  14. package/dist/cli/embedding-choice.js +2 -4
  15. package/dist/cli/first-run.d.ts +0 -16
  16. package/dist/cli/first-run.js +1 -6
  17. package/dist/cli/index.d.ts +3 -1
  18. package/dist/cli/index.js +2 -1
  19. package/dist/cli/messenger/context.js +1 -1
  20. package/dist/cli/messenger/conversations-command.js +5 -9
  21. package/dist/cli/messenger/doctor-command.js +38 -4
  22. package/dist/cli/messenger/inbox.js +2 -6
  23. package/dist/cli/messenger/private-people-command.js +1 -2
  24. package/dist/cli/messenger/review.js +2 -5
  25. package/dist/cli/permission-keys.d.ts +9 -2
  26. package/dist/cli/permission-keys.js +21 -6
  27. package/dist/cli/preview.d.ts +3 -2
  28. package/dist/cli/preview.js +4 -3
  29. package/dist/cli/profile.d.ts +2 -0
  30. package/dist/cli/profile.js +10 -0
  31. package/dist/cli/program.d.ts +1 -1
  32. package/dist/cli/program.js +18 -10
  33. package/dist/cli/settings.d.ts +21 -117
  34. package/dist/cli/settings.js +107 -114
  35. package/dist/cli/shared-command-exports.d.ts +4 -232
  36. package/dist/mcp/surface.js +9 -3
  37. package/dist/meetings/mcp/index.d.ts +15 -15
  38. package/dist/models/settings.d.ts +1 -7
  39. package/dist/models/settings.js +1 -18
  40. package/dist/ports/app.d.ts +6 -0
  41. package/dist/search/account-list.js +8 -7
  42. package/dist/search/dictionary.d.ts +3 -6
  43. package/dist/search/dictionary.js +2 -1
  44. package/dist/search/languages.d.ts +28 -0
  45. package/dist/search/languages.js +7 -0
  46. package/dist/search/stem.d.ts +5 -4
  47. package/dist/search/stem.js +3 -2
  48. package/package.json +1 -1
@@ -6,10 +6,8 @@ import { isLocal, remoteModel } from "../embeddings/remote.js";
6
6
  import { readOnlyLockOf, readOnlyRefusal } from "../ports/app.js";
7
7
  import { typedCommand } from "../runs/scope.js";
8
8
  /** A local model id, or a remote model with its key from `models keys set`. */
9
- export const embeddingChoice = ({ model, provider, embeddingProvider, baseUrl, dims, concurrency }, app, settings, env, { needKey = true, keyring } = {}) => {
10
- if (embeddingProvider !== undefined && provider !== undefined)
11
- throw new CliError("validation_error", "give --embedding-provider or its legacy --provider spelling, not both");
12
- provider = embeddingProvider ?? provider;
9
+ export const embeddingChoice = ({ model, embeddingProvider, baseUrl, dims, concurrency }, app, settings, env, { needKey = true, keyring } = {}) => {
10
+ let provider = embeddingProvider;
13
11
  const flagsChooseProvider = (provider !== undefined && provider !== (settings.embeddingProvider ?? "local")) ||
14
12
  (baseUrl !== undefined && baseUrl.replace(/\/+$/, "") !== settings.embeddingBaseUrl?.replace(/\/+$/, ""));
15
13
  provider ??= flagsChooseProvider ? "openai" : (settings.embeddingProvider ?? "local");
@@ -1,22 +1,6 @@
1
1
  import { type Installer } from "@wirecat/cli-base/update";
2
2
  import { type Question } from "./setup-answers.js";
3
3
  /** The languages cli-core stems and has a dictionary for; each is one stemmer of its alphabet. */
4
- export declare const LANGUAGES: readonly [{
5
- readonly name: "russian";
6
- readonly code: "ru";
7
- readonly label: "Russian";
8
- readonly script: "cyrillic";
9
- }, {
10
- readonly name: "english";
11
- readonly code: "en";
12
- readonly label: "English";
13
- readonly script: "latin";
14
- }, {
15
- readonly name: "spanish";
16
- readonly code: "es";
17
- readonly label: "Spanish";
18
- readonly script: "latin";
19
- }];
20
4
  export interface FirstRunIo {
21
5
  ask: (prompt: string) => Promise<string>;
22
6
  say: (text: string) => void;
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { accessSync, constants, realpathSync } from "node:fs";
3
3
  import { delimiter, join } from "node:path";
4
4
  import { installerOf, updateCommand } from "@wirecat/cli-base/update";
5
+ import { LANGUAGES } from "../search/languages.js";
5
6
  import { DEFAULT_STEMMERS } from "../search/stem.js";
6
7
  import { openCache } from "../store/open.js";
7
8
  import { storePath } from "../store/path.js";
@@ -11,12 +12,6 @@ import { readSecret } from "../terminal/prompt.js";
11
12
  import { WIRECAT_TOOLS } from "./help/first-screen.js";
12
13
  import { buildStems } from "./messenger/store-maintenance-command.js";
13
14
  import { QUESTIONS, readSetupAnswers, unanswered, writeSetupAnswers, } from "./setup-answers.js";
14
- /** The languages cli-core stems and has a dictionary for; each is one stemmer of its alphabet. */
15
- export const LANGUAGES = [
16
- { name: "russian", code: "ru", label: "Russian", script: "cyrillic" },
17
- { name: "english", code: "en", label: "English", script: "latin" },
18
- { name: "spanish", code: "es", label: "Spanish", script: "latin" },
19
- ];
20
15
  /** Commands that run without first-run questions: they serve agents, help, or open the store themselves. */
21
16
  const SKIPPED = new Set(["help", "guide", "completion", "mcp", "serve", "server", "update", "store"]);
22
17
  /**
@@ -26,6 +26,7 @@ export { CORE_LOG_EVENTS } from "../runs/log-events.js";
26
26
  export { crashOf, type Recording, type RecordingOptions, recorded, startRecording, wasSettled, } from "../runs/recording.js";
27
27
  export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, type RunMetadata, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
28
28
  export { currentRun, inRun, type RunScope, useAsProcessRun } from "../runs/scope.js";
29
+ export { LANGUAGES } from "../search/languages.js";
29
30
  export { guardedApp, guardedPress, guardedStart, type Pressed } from "../sends/buttons.js";
30
31
  export { guardedClose, guardedCreatePoll, guardedVote } from "../sends/polls.js";
31
32
  export type { InboxReader } from "../services/inbox.js";
@@ -51,6 +52,7 @@ export type { CommandAvailability, DiscoveredCommand } from "./command-availabil
51
52
  export { commandContract, type JsonSchema, SCHEMA_DIALECT } from "./command-contract.js";
52
53
  export { CONTRACT } from "./commands-command.js";
53
54
  export { refuseUnknownKey } from "./config-command.js";
55
+ export type { ConfigIssue, ConfigIssueKind } from "./config-issues.js";
54
56
  export { loadSnakeCaseConfig } from "./config-keys.js";
55
57
  export { type AgentRun, type BaseContext, type BaseEnvironment, baseContext, environmentOf, outputFor, provide, secretInputOf, } from "./context.js";
56
58
  export { addContractInfo, type ContractEvidenceOptions, contractEvidence } from "./contract-evidence.js";
@@ -62,7 +64,7 @@ export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNoth
62
64
  export { type CliEntry, type EntryContext, type EntryIo, startCli } from "./entry.js";
63
65
  export { type AnswerOutput, answerSchema, type Body, type ItemBody, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, type ListBody, type ListInput, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
64
66
  export { withAbort } from "./execution.js";
65
- export { askSetupQuestions, asksAtStart, type FirstRun, type FirstRunIo, LANGUAGES, setupAtStart, systemLanguage, } from "./first-run.js";
67
+ export { askSetupQuestions, asksAtStart, type FirstRun, type FirstRunIo, setupAtStart, systemLanguage, } from "./first-run.js";
66
68
  export { checkGuide, checkStep, type GuideOptions, type GuideReference, type GuideStep, loadScenarios, MEMO_SCENARIOS, type Scenario, shellProblems, } from "./guide.js";
67
69
  export { type CommandHelp, commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, type NextChoice, nextUsually, } from "./help/command-help.js";
68
70
  export { commandsText } from "./help/commands-text.js";
package/dist/cli/index.js CHANGED
@@ -22,6 +22,7 @@ export { CORE_LOG_EVENTS } from "../runs/log-events.js";
22
22
  export { crashOf, recorded, startRecording, wasSettled, } from "../runs/recording.js";
23
23
  export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
24
24
  export { currentRun, inRun, useAsProcessRun } from "../runs/scope.js";
25
+ export { LANGUAGES } from "../search/languages.js";
25
26
  export { guardedApp, guardedPress, guardedStart } from "../sends/buttons.js";
26
27
  export { guardedClose, guardedCreatePoll, guardedVote } from "../sends/polls.js";
27
28
  export { INBOX_CHATS, newIn, REVIEW_CHATS, reviewIn, unreadIn } from "../services/inbox.js";
@@ -55,7 +56,7 @@ export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNoth
55
56
  export { startCli } from "./entry.js";
56
57
  export { answerSchema, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
57
58
  export { withAbort } from "./execution.js";
58
- export { askSetupQuestions, asksAtStart, LANGUAGES, setupAtStart, systemLanguage, } from "./first-run.js";
59
+ export { askSetupQuestions, asksAtStart, setupAtStart, systemLanguage, } from "./first-run.js";
59
60
  export { checkGuide, checkStep, loadScenarios, MEMO_SCENARIOS, shellProblems, } from "./guide.js";
60
61
  export { commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, nextUsually, } from "./help/command-help.js";
61
62
  export { commandsText } from "./help/commands-text.js";
@@ -355,7 +355,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
355
355
  const { profile } = base.settings;
356
356
  refuseDenied(command, base.settings);
357
357
  refuseLacking(messenger, pathOf(command));
358
- refuseUnknownPermissionKeys(command, base.settings.permissions, `${app.command} ${asFirstWord(profile)}config unset`, messenger.permissionKey);
358
+ refuseUnknownPermissionKeys(command, base.settings, app.command, messenger.permissionKey);
359
359
  // Read when warning, not now: serve swaps the renderer's methods for log records after this.
360
360
  const warn = (text, warning) => base.renderer.warn(text, warning);
361
361
  const accountFor = () => {
@@ -37,8 +37,8 @@ const conversationBuildCommand = (messenger) => boundOptions(reachesOtherService
37
37
  .option("--chat <chat>", messenger.chatArgument)
38
38
  .option("--analyze", "link batches using the configured analysis provider; requires --chat and remembers consent for this chat/provider")
39
39
  .addOption(new Option("--analysis-provider <provider>", "analysis: agent, openai or anthropic"))
40
- .option("--model <model>", "analysis model; overrides analysis_model")
41
- .option("--base-url <url>", "analysis API endpoint; overrides analysis_base_url")
40
+ .option("--model <model>", "analysis model; overrides models.analysis.model")
41
+ .option("--base-url <url>", "analysis API endpoint; overrides models.analysis.base_url")
42
42
  .option("--size <n>", "analysis answer messages per batch, 10–200; default 50", positiveCount("--size"))
43
43
  .option("--max-tokens <n>", "analysis input/output reservation cap per run; default 100000", positiveCount("--max-tokens")), { chunks: false }).action(async function () {
44
44
  const options = this.optsWithGlobals();
@@ -93,7 +93,6 @@ const conversationBuildCommand = (messenger) => boundOptions(reachesOtherService
93
93
  return;
94
94
  }
95
95
  if (options.analysisProvider !== undefined ||
96
- options.provider !== undefined ||
97
96
  options.model !== undefined ||
98
97
  options.baseUrl !== undefined ||
99
98
  options.size !== undefined ||
@@ -133,10 +132,7 @@ const embeddingClearCommand = (messenger) => withModelOptions(destroysNothing(an
133
132
  const chat = chatOf(options);
134
133
  const context = messengerContext(this, messenger);
135
134
  refuseLocalWrite(context, messenger.app.command, EMBED_KEY);
136
- const given = options.model !== undefined ||
137
- options.embeddingProvider !== undefined ||
138
- options.provider !== undefined ||
139
- options.baseUrl !== undefined;
135
+ const given = options.model !== undefined || options.embeddingProvider !== undefined || options.baseUrl !== undefined;
140
136
  const model = given ? choiceOf(options, messenger, context, { needKey: false }) : undefined;
141
137
  const cleared = await context.withServices((services) => services.embeddings.clear(chat, model));
142
138
  renderAnswer(this, context, itemOf(cleared), () => `${cleared.cleared} vectors dropped\n`);
@@ -379,14 +375,14 @@ export const conversationsCommand = (messenger) => {
379
375
  .option("--chat <chat>", "revoke only this chat's consents; defaults to every chat")
380
376
  .addOption(new Option("--consent-provider <identity>", "exact provider identity from consents list; defaults to every provider"))
381
377
  .action(async function () {
382
- const { chat, provider, consentProvider } = this.opts();
378
+ const { chat, consentProvider } = this.opts();
383
379
  const context = messengerContext(this, messenger);
384
380
  refuseLocalWrite(context, messenger.app.command, LINKS_KEY);
385
381
  await context.withStore(async (store, account) => {
386
382
  const chatId = chat === undefined
387
383
  ? undefined
388
384
  : (await servicesFor(storedDeps(messenger, store, account, context.guard)).conversations.batchStatus(chat, BATCH_SIZE.default)).chat;
389
- await analysisConsents(store, account).revoke(chatId, consentProvider ?? provider);
385
+ await analysisConsents(store, account).revoke(chatId, consentProvider);
390
386
  });
391
387
  renderAnswer(this, context, itemOf({ revoked: true }));
392
388
  });
@@ -1,5 +1,6 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { dirname, resolve } from "node:path";
2
- import { writeSecurely } from "@wirecat/cli-base";
3
+ import { configFilePath, resolvePaths, writeSecurely } from "@wirecat/cli-base";
3
4
  import { annotate } from "@wirecat/cli-base/commands";
4
5
  import { Command } from "commander";
5
6
  import { engineStates } from "../../attachments/extract.js";
@@ -11,6 +12,8 @@ import { storePath } from "../../store/path.js";
11
12
  import { baseContext, environmentOf, outputFor } from "../context.js";
12
13
  import { destroysNothing, LOCAL_WRITE, NO_EFFECTS, REMOTE_READ } from "../effects.js";
13
14
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
15
+ import { unknownPermissionIssues } from "../permission-keys.js";
16
+ import { NONE_NAMED } from "../settings.js";
14
17
  import { lineOf, nextStep } from "../steps.js";
15
18
  import { recalledAccount } from "./accounts.js";
16
19
  import { messengerContext } from "./context.js";
@@ -126,14 +129,45 @@ const diagnose = async (command, messenger) => {
126
129
  platform: process.platform,
127
130
  arch: process.arch,
128
131
  };
132
+ // Every problem in the file is reported, not refused: doctor is what a person runs when nothing else will.
129
133
  let context;
130
134
  try {
131
- context = baseContext(command, messenger.resolveSettings);
135
+ context = baseContext(command, (flags, options) => messenger.resolveSettings(flags, { ...options, needsProfile: false, readOnly: true, withIssues: true }));
132
136
  }
133
137
  catch (error) {
134
- return { report: { cli, config: { error: messageOf(error) } } };
138
+ // Not the file: a profile lock, a flag or an environment variable the settings refuse.
139
+ const path = configFilePath(resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).config);
140
+ return {
141
+ report: {
142
+ cli,
143
+ config: { path, found: existsSync(path), issues: [] },
144
+ settings: { error: messageOf(error) },
145
+ store: snakeKeys(await storeSummary(env)),
146
+ runs: { directory: runsDirFor(app, env), kept: listRuns(runsDirFor(app, env)).length },
147
+ },
148
+ };
135
149
  }
136
150
  const { settings, renderer } = context;
151
+ const config = {
152
+ path: settings.configPath,
153
+ found: settings.configFound,
154
+ issues: [
155
+ ...(settings.configIssues ?? []),
156
+ ...unknownPermissionIssues(command, settings, app.command, messenger.permissionKey),
157
+ ],
158
+ };
159
+ if (settings.sources.profile === NONE_NAMED)
160
+ return {
161
+ renderer,
162
+ report: {
163
+ cli,
164
+ profile: null,
165
+ config,
166
+ store: snakeKeys(await storeSummary(env)),
167
+ readers: engineStates(),
168
+ runs: { directory: runsDirFor(app, env), kept: listRuns(runsDirFor(app, env)).length },
169
+ },
170
+ };
137
171
  const { profile } = settings;
138
172
  const account = recalledAccount(app, provider, profile, env);
139
173
  const store = storePath(env);
@@ -151,7 +185,7 @@ const diagnose = async (command, messenger) => {
151
185
  report: {
152
186
  cli,
153
187
  profile,
154
- config: { path: settings.configPath, found: settings.configFound },
188
+ config,
155
189
  account: { remembered: account?.account ?? null },
156
190
  // A session file on disk says nothing about whether the messenger still accepts it.
157
191
  login: { state: "not checked" },
@@ -2,7 +2,6 @@ import { CliError } from "@wirecat/cli-base";
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
3
  import { Command } from "commander";
4
4
  import { renderMessages } from "../../render/messages.js";
5
- import { kindsOf } from "../../services/inbox.js";
6
5
  import { modelWith } from "../../speech/hearing.js";
7
6
  import { REMOTE_READ } from "../effects.js";
8
7
  import { renderAnswer } from "../envelope.js";
@@ -44,15 +43,12 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
44
43
  .option(...MODEL_OPTION)
45
44
  .addHelpText("after", "To mark a chat read, use `chats mark-read <chat>`. The other side sees it.")
46
45
  .action(async function (query) {
47
- const { new: fresh, all, kind, transcribe, model, } = this.opts();
46
+ const { new: fresh, all, transcribe, model, } = this.opts();
48
47
  const hearWith = modelWith(transcribe, model);
49
48
  const context = messengerContext(this, messenger);
50
49
  const { app } = messenger;
51
50
  const { settings, renderer, format, env } = context;
52
- const { since, meta, kinds: queriedKinds } = catchUpQuery(query, INBOX_QUERY, "inbox");
53
- if (kind !== undefined && queriedKinds !== undefined)
54
- throw new CliError("validation_error", "give kind: or its legacy --kind spelling, not both");
55
- const kinds = queriedKinds ?? (kind === undefined ? undefined : kindsOf(kind));
51
+ const { since, meta, kinds } = catchUpQuery(query, INBOX_QUERY, "inbox");
56
52
  if (fresh && since !== undefined)
57
53
  throw new CliError("validation_error", "--new keeps its own point — not with a date in the query");
58
54
  const saved = fresh && since === undefined
@@ -65,8 +65,7 @@ export const privatePeopleCommands = (messenger) => {
65
65
  command.action(async function (person, id) {
66
66
  const context = messengerContext(this, messenger);
67
67
  refuseLocalWrite(context, messenger.app.command, `contacts.notes.${name}`);
68
- const { textFile, file: legacyFile, revision, } = this.opts();
69
- const file = textFile ?? legacyFile;
68
+ const { textFile: file, revision } = this.opts();
70
69
  const text = file === undefined || file === "-" ? await readAll(context.stdin) : await readFile(file, "utf8");
71
70
  const version = Number(revision);
72
71
  if (name === "edit" && (!Number.isSafeInteger(version) || version < 1))
@@ -3,7 +3,7 @@ import { annotate } from "@wirecat/cli-base/commands";
3
3
  import { Command } from "commander";
4
4
  import { parseDuration } from "../../domain/duration.js";
5
5
  import { renderMessages } from "../../render/messages.js";
6
- import { kindsOf, REVIEW_DAYS, reviewStart, UNANSWERED_HOURS } from "../../services/inbox.js";
6
+ import { REVIEW_DAYS, reviewStart, UNANSWERED_HOURS } from "../../services/inbox.js";
7
7
  import { modelWith } from "../../speech/hearing.js";
8
8
  import { REMOTE_READ } from "../effects.js";
9
9
  import { renderAnswer } from "../envelope.js";
@@ -45,9 +45,7 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
45
45
  const hearWith = modelWith(options.transcribe, options.model);
46
46
  const context = messengerContext(this, messenger);
47
47
  const { settings, renderer, format, env } = context;
48
- const { since: from, chat, meta, kinds: queriedKinds } = catchUpQuery(query, REVIEW_QUERY, "review");
49
- if (options.kind !== undefined && queriedKinds !== undefined)
50
- throw new CliError("validation_error", "give kind: or its legacy --kind spelling, not both");
48
+ const { since: from, chat, meta, kinds } = catchUpQuery(query, REVIEW_QUERY, "review");
51
49
  if (options.new && (from !== undefined || options.unanswered !== undefined)) {
52
50
  // An open question stays open: moving past it would drop it from the next review.
53
51
  throw new CliError("validation_error", "--new keeps its own point — not with a date or --unanswered");
@@ -62,7 +60,6 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
62
60
  : undefined;
63
61
  const since = from ?? saved?.first ?? reviewStart();
64
62
  const hours = options.unanswered === undefined ? undefined : unansweredHours(options.unanswered);
65
- const kinds = queriedKinds ?? (options.kind === undefined ? undefined : kindsOf(options.kind));
66
63
  let hearing;
67
64
  const transcribe = options.transcribe === true;
68
65
  const { found, reader } = await context.withServices(async (services, connect) => {
@@ -1,5 +1,7 @@
1
1
  import type { Command } from "commander";
2
2
  import { type PermissionKey } from "../sends/permissions.js";
3
+ import { type ConfigIssue } from "./config-issues.js";
4
+ import type { Settings } from "./settings.js";
3
5
  export type PermissionKeyOf = (command: Command) => PermissionKey | null | undefined;
4
6
  /**
5
7
  * The keys `permissions` may name: every command of the program, every write key, and each of their
@@ -9,5 +11,10 @@ export declare const knownPermissionKeys: (command: Command, keyOf?: PermissionK
9
11
  /** The known keys one level under the unknown key's parent, to name in the refusal. */
10
12
  export declare const knownBeside: (key: PermissionKey, known: ReadonlySet<PermissionKey>) => PermissionKey[];
11
13
  export declare const unknownPermissionKeys: (levels: Readonly<Record<PermissionKey, unknown>>, known: ReadonlySet<PermissionKey>) => PermissionKey[];
12
- /** A saved key no command checks would do nothing, so the command refuses to run; `unset` names the fix. */
13
- export declare const refuseUnknownPermissionKeys: (command: Command, levels: Readonly<Record<PermissionKey, unknown>>, unset: string, keyOf?: PermissionKeyOf) => void;
14
+ /** Every unknown permission key saved in the file, in every section and profile, with the unset that removes it. */
15
+ export declare const unknownPermissionIssues: (command: Command, settings: Pick<Settings, "configPath" | "savedPermissions" | "permissions" | "permissionSources" | "profile">, tool: string, keyOf?: PermissionKeyOf) => ConfigIssue[];
16
+ /**
17
+ * A saved key no command checks would do nothing, so the command refuses to run and names the unset that removes
18
+ * it. A `--permission` key is checked too: it is no file's, so the flag is the fix.
19
+ */
20
+ export declare const refuseUnknownPermissionKeys: (command: Command, settings: Pick<Settings, "configPath" | "savedPermissions" | "permissions" | "permissionSources" | "profile">, tool: string, keyOf?: PermissionKeyOf) => void;
@@ -1,5 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { CEILING_KEY, keyForCommand, WRITE_KEYS } from "../sends/permissions.js";
3
+ import { issueMessage, permissionIssues, publicIssue } from "./config-issues.js";
3
4
  const pathOf = (command) => {
4
5
  const words = [];
5
6
  for (let at = command; at?.parent; at = at.parent)
@@ -42,10 +43,24 @@ export const knownBeside = (key, known) => {
42
43
  .sort();
43
44
  };
44
45
  export const unknownPermissionKeys = (levels, known) => Object.keys(levels).filter((key) => key !== CEILING_KEY && !known.has(key));
45
- /** A saved key no command checks would do nothing, so the command refuses to run; `unset` names the fix. */
46
- export const refuseUnknownPermissionKeys = (command, levels, unset, keyOf) => {
47
- const [key] = unknownPermissionKeys(levels, knownPermissionKeys(command, keyOf));
48
- if (key === undefined)
49
- return;
50
- throw new CliError("configuration_error", `permissions.${key} names no command — remove it with \`${unset} permissions.${key}\``, { permission: key });
46
+ /** Every unknown permission key saved in the file, in every section and profile, with the unset that removes it. */
47
+ export const unknownPermissionIssues = (command, settings, tool, keyOf) => {
48
+ const saved = settings.savedPermissions ??
49
+ Object.keys(settings.permissions)
50
+ .filter((key) => key !== CEILING_KEY && settings.permissionSources[key] !== "flag")
51
+ .map((key) => ({ key, at: ["profiles", settings.profile, "permissions", key] }));
52
+ return permissionIssues(saved, knownPermissionKeys(command, keyOf), { file: settings.configPath, tool }).map(publicIssue);
53
+ };
54
+ /**
55
+ * A saved key no command checks would do nothing, so the command refuses to run and names the unset that removes
56
+ * it. A `--permission` key is checked too: it is no file's, so the flag is the fix.
57
+ */
58
+ export const refuseUnknownPermissionKeys = (command, settings, tool, keyOf) => {
59
+ const issues = unknownPermissionIssues(command, settings, tool, keyOf);
60
+ if (issues.length > 0)
61
+ throw new CliError("configuration_error", issueMessage(issues), { permission: issues[0]?.key });
62
+ const flagged = Object.keys(settings.permissions).filter((key) => settings.permissionSources[key] === "flag");
63
+ const [key] = unknownPermissionKeys(Object.fromEntries(flagged.map((one) => [one, true])), knownPermissionKeys(command, keyOf));
64
+ if (key !== undefined)
65
+ throw new CliError("validation_error", `--permission ${key} names no command`, { permission: key });
51
66
  };
@@ -1,8 +1,9 @@
1
1
  import type { Command } from "commander";
2
- import { type Level, type PermissionKey } from "../sends/permissions.js";
2
+ import { type PermissionKey } from "../sends/permissions.js";
3
+ import type { Settings } from "./settings.js";
3
4
  export declare class PreviewComplete extends Error {
4
5
  }
5
- export declare const preview: (command: Command, permissions: Readonly<Record<string, Level>>, unset: string, keyOf?: (path: readonly string[]) => PermissionKey | null | undefined) => {
6
+ export declare const preview: (command: Command, settings: Pick<Settings, "permissions" | "permissionSources" | "configPath" | "savedPermissions" | "profile">, tool: string, keyOf?: (path: readonly string[]) => PermissionKey | null | undefined) => {
6
7
  preview: boolean;
7
8
  path: string[];
8
9
  permission: {
@@ -1,14 +1,15 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { metaOf } from "@wirecat/cli-base/commands";
3
3
  import { writes as writesAnything } from "@wirecat/cli-base/standards";
4
- import { keyForCommand, levelFor, readKeysForCommand, WRITE_KEYS, } from "../sends/permissions.js";
4
+ import { keyForCommand, levelFor, readKeysForCommand, WRITE_KEYS } from "../sends/permissions.js";
5
5
  import { commandPathOf } from "./command-contract.js";
6
6
  import { refuseUnknownPermissionKeys } from "./permission-keys.js";
7
7
  export class PreviewComplete extends Error {
8
8
  }
9
- export const preview = (command, permissions, unset, keyOf) => {
9
+ export const preview = (command, settings, tool, keyOf) => {
10
10
  const path = commandPathOf(command);
11
- refuseUnknownPermissionKeys(command, permissions, unset, keyOf && ((one) => keyOf(commandPathOf(one))));
11
+ const { permissions } = settings;
12
+ refuseUnknownPermissionKeys(command, settings, tool, keyOf && ((one) => keyOf(commandPathOf(one))));
12
13
  const key = keyForCommand(path);
13
14
  const level = key ? levelFor(permissions, key).level : "allow";
14
15
  for (const source of readKeysForCommand(path)) {
@@ -28,3 +28,5 @@ export declare const valuedOptions: (program: Command) => Set<string>;
28
28
  export declare const refuseCommandName: (profile: string, words: Set<string>, command: string) => void;
29
29
  /** The root of the command tree, from anywhere inside it. */
30
30
  export declare const rootOf: (command: Command) => Command;
31
+ /** A command this CLI really has, for an example: the first that takes a profile, down to its first leaf. */
32
+ export declare const sampleCommand: (commands: readonly Command[]) => string | undefined;
@@ -59,3 +59,13 @@ export const rootOf = (command) => {
59
59
  root = root.parent;
60
60
  return root;
61
61
  };
62
+ /** A command this CLI really has, for an example: the first that takes a profile, down to its first leaf. */
63
+ export const sampleCommand = (commands) => {
64
+ const words = [];
65
+ let command = commands.find((one) => !PROFILE_FREE.has(one.name()));
66
+ while (command) {
67
+ words.push(command.name());
68
+ command = command.commands.find((one) => one.name() !== "help");
69
+ }
70
+ return words.length ? words.join(" ") : undefined;
71
+ };
@@ -10,7 +10,7 @@ export interface ProgramDefinition {
10
10
  /** The CLI's own settings, for keeping a failure that happened before its command could; plain ones without. */
11
11
  configuration?: {
12
12
  permissionKey?: (path: readonly string[]) => string | null | undefined;
13
- resolveSettings: (flags?: GlobalFlags, options?: ResolveOptions) => Pick<Settings, "profile" | "keepFailedRuns" | "keepRunsForDays" | "skillHint"> & Partial<Pick<Settings, "permissions">>;
13
+ resolveSettings: (flags?: GlobalFlags, options?: ResolveOptions) => Pick<Settings, "profile" | "keepFailedRuns" | "keepRunsForDays" | "skillHint"> & Partial<Pick<Settings, "permissions" | "permissionSources" | "configPath" | "savedPermissions">>;
14
14
  };
15
15
  configure?: (program: Command) => void;
16
16
  /** The tool's pitch and "what you can do" lines on `--help` and a bare run. */
@@ -26,7 +26,7 @@ import { showInputFields } from "./input-fields.js";
26
26
  import { MAX_BUFFERED_INPUT, provideInputPolicy } from "./input-policy.js";
27
27
  import { checkJson, liftJson } from "./json-fields.js";
28
28
  import { PreviewComplete, preview } from "./preview.js";
29
- import { asFirstWord, commandWords, liftProfile, PROFILE_FREE, valuedOptions } from "./profile.js";
29
+ import { commandWords, liftProfile, PROFILE_FREE, sampleCommand, valuedOptions } from "./profile.js";
30
30
  import { absolutePaths, relayTarget } from "./relay-route.js";
31
31
  import { settingsFor } from "./settings.js";
32
32
  import { assertSharedContracts } from "./shared-contracts.js";
@@ -37,11 +37,15 @@ import { commandAt, lineOf, withLines } from "./steps.js";
37
37
  */
38
38
  export const createProgram = ({ app, commands, configure, firstScreen }, { out, err } = {}) => {
39
39
  const program = new Command();
40
+ const mounted = commands();
41
+ const messenger = app.messenger !== false;
42
+ const found = messenger ? "found messages" : "found items";
43
+ const content = messenger ? "message content" : "content";
40
44
  program
41
45
  .name(app.command)
42
46
  .usage("[profile] [options] <command>")
43
47
  .description(`${app.description}\n\n` +
44
- `The first word is the profile whenever it is not a command — \`${app.command} personal chats list\`.\n` +
48
+ `The first word is the profile whenever it is not a command — \`${app.command} personal ${messenger ? "chats list" : (sampleCommand(mounted) ?? "<command>")}\`.\n` +
45
49
  `\`${app.envPrefix}_PROFILE\` says the same thing for a whole shell session.\n` +
46
50
  "No profile is assumed: without one, a command that uses an account stops and lists the profiles here.")
47
51
  .version(app.version, "-V, --version")
@@ -49,21 +53,21 @@ export const createProgram = ({ app, commands, configure, firstScreen }, { out,
49
53
  .option("--json", "machine-readable output: one JSON value on stdout; --json=id,text keeps only those fields")
50
54
  .option("--jsonl", "machine-readable output: one JSON object per line, for streaming and jq")
51
55
  .option("--quiet", "diagnostics off; a failure is still said")
52
- .option("--trace", "the connection's own log lines on stderr — never message content")
56
+ .option("--trace", `the connection's own log lines on stderr — never ${content}`)
53
57
  .option("--debug", "on a failure, its whole record — cause and stack — in place of the short line")
54
58
  .option("--timeout <duration>", "give up on the whole command after this — 30s, 2m, 500ms")
55
59
  .option("--offline", "answer from what was recorded and never connect; fails if nothing was")
56
60
  .option("--no-input", "never prompt or open interactive login; piped input remains available")
57
61
  .option("--max-input-bytes <bytes>", "maximum buffered input bytes (default: 16777216)")
58
62
  .option("--max-output-bytes <bytes>", "maximum machine output bytes (default: 4194304; 0 disables)")
59
- .option("--brief", "found messages as reference, title, time, sender and excerpt only")
60
- .option("--full", "found messages with their whole text and every provider field")
63
+ .option("--brief", `${found} as reference, title, time, ${messenger ? "sender" : "author"} and excerpt only`)
64
+ .option("--full", `${found} with their whole text and every provider field`)
61
65
  .option("--dry-run", "preview parsed arguments and permissions before running the action")
62
66
  .option("--yes", "go ahead without the question an ask level puts before a write")
63
- .option("--record", "keep this run — ids and timings, never message content")
67
+ .option("--record", `keep this run — ids and timings, never ${content}`)
64
68
  .option("--no-record", "do not keep it, whatever the configuration says")
65
69
  .showHelpAfterError();
66
- for (const command of commands())
70
+ for (const command of mounted)
67
71
  program.addCommand(command);
68
72
  configure?.(program);
69
73
  assertSharedContracts(program);
@@ -375,9 +379,13 @@ const runOnce = async (argv, definition, options) => {
375
379
  // No permission governs housekeeping like `skill install`, so no profile's settings change its preview.
376
380
  needsProfile: keyForCommand(path) !== null,
377
381
  });
378
- const permissions = "permissions" in resolved ? resolved.permissions : {};
379
- const unset = `${command} ${asFirstWord(resolved.profile)}config unset${path[0] === "bot" ? " --bot" : ""}`;
380
- const previewed = preview(action, permissions, unset, definition.configuration?.permissionKey);
382
+ const previewed = preview(action, {
383
+ permissions: resolved.permissions ?? {},
384
+ permissionSources: resolved.permissionSources ?? {},
385
+ configPath: resolved.configPath ?? "",
386
+ profile: resolved.profile,
387
+ ...(resolved.savedPermissions ? { savedPermissions: resolved.savedPermissions } : {}),
388
+ }, command, definition.configuration?.permissionKey);
381
389
  renderAnswer(action, outputFor(action), itemOf(snakeKeys(previewed)));
382
390
  throw new PreviewComplete();
383
391
  }