@wirecat/cli-core 0.422.0 → 0.424.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 (45) hide show
  1. package/CHANGELOG.md +29 -0
  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/config-command.js +33 -25
  8. package/dist/cli/config-issues.d.ts +52 -0
  9. package/dist/cli/config-issues.js +187 -0
  10. package/dist/cli/config-keys.js +4 -28
  11. package/dist/cli/context.d.ts +1 -1
  12. package/dist/cli/context.js +3 -1
  13. package/dist/cli/embedding-choice.d.ts +1 -2
  14. package/dist/cli/embedding-choice.js +2 -4
  15. package/dist/cli/index.d.ts +2 -1
  16. package/dist/cli/index.js +1 -1
  17. package/dist/cli/messenger/context.js +1 -1
  18. package/dist/cli/messenger/conversations-command.js +5 -9
  19. package/dist/cli/messenger/doctor-command.js +38 -4
  20. package/dist/cli/messenger/inbox.js +2 -6
  21. package/dist/cli/messenger/private-people-command.js +1 -2
  22. package/dist/cli/messenger/review.js +2 -5
  23. package/dist/cli/permission-keys.d.ts +9 -2
  24. package/dist/cli/permission-keys.js +21 -6
  25. package/dist/cli/preview.d.ts +3 -2
  26. package/dist/cli/preview.js +4 -3
  27. package/dist/cli/program.d.ts +1 -1
  28. package/dist/cli/program.js +14 -9
  29. package/dist/cli/runs/search.d.ts +1 -1
  30. package/dist/cli/settings.d.ts +34 -117
  31. package/dist/cli/settings.js +130 -118
  32. package/dist/cli/shared-command-exports.d.ts +4 -232
  33. package/dist/entities/index.d.ts +3 -3
  34. package/dist/entities/recorded-run.d.ts +3 -3
  35. package/dist/entities/recorded-run.js +1 -1
  36. package/dist/mcp/tool.js +3 -1
  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/runs/recording.d.ts +2 -1
  41. package/dist/runs/recording.js +2 -2
  42. package/dist/runs/run.d.ts +3 -2
  43. package/dist/runs/run.js +1 -1
  44. package/dist/search/account-list.js +8 -7
  45. 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");
@@ -52,6 +52,7 @@ export type { CommandAvailability, DiscoveredCommand } from "./command-availabil
52
52
  export { commandContract, type JsonSchema, SCHEMA_DIALECT } from "./command-contract.js";
53
53
  export { CONTRACT } from "./commands-command.js";
54
54
  export { refuseUnknownKey } from "./config-command.js";
55
+ export type { ConfigIssue, ConfigIssueKind } from "./config-issues.js";
55
56
  export { loadSnakeCaseConfig } from "./config-keys.js";
56
57
  export { type AgentRun, type BaseContext, type BaseEnvironment, baseContext, environmentOf, outputFor, provide, secretInputOf, } from "./context.js";
57
58
  export { addContractInfo, type ContractEvidenceOptions, contractEvidence } from "./contract-evidence.js";
@@ -102,7 +103,7 @@ export { knownBeside, knownPermissionKeys, type PermissionKeyOf, unknownPermissi
102
103
  export { asFirstWord, commandWords, liftProfile, refuseCommandName, rootOf, usableProfileName } from "./profile.js";
103
104
  export { createProgram, type ProgramDefinition, type ProgramOptions, type RunOptions, run } from "./program.js";
104
105
  export { fieldsOf, projectFields } from "./result-fields.js";
105
- export { type Config, type Configuration, count, first, flag, fromFile, type GlobalFlags, NONE_NAMED, type ProfileKind, plain, type ResolveOptions, type Settings, type SettingsExtension, type SettingsExtensionContext, type Source, settingsFor, } from "./settings.js";
106
+ export { type Config, type Configuration, count, first, flag, fromFile, type GlobalFlags, NONE_NAMED, namedProfile, type ProfileKind, plain, type ResolveOptions, type Settings, type SettingsExtension, type SettingsExtensionContext, type Source, settingsFor, } from "./settings.js";
106
107
  export { type PermissionAnswer, readSetupAnswers, type SetupAnswers, setupPermissionCeiling } from "./setup-answers.js";
107
108
  export { AGENTS, type Agent, agentFor, screenFor } from "./setup-screen.js";
108
109
  export { accountCommand, addMeetingBatchCommand, addMeetingPreparationCommand, addMeetingProjectCandidatesCommand, addMeetingSeriesInventoryCommand, attachmentsCommand, botAdminsCommand, botCommand, botMcpCommand, botMembersCommand, botMessagesCommand, callsCommand, chatsCommand, commandsCommand, completeCommand, configCommand, contactsCommand, conversationsCommand, deleteCommand, doctorCommand, editCommand, floodCommand, forwardCommand, generatedApiCommand, guideCommand, imagesCommand, inboxCommand, markReadCommand, mcpCommand, mentionsCommand, messagesCommand, metadataCommand, modelCatalogueCommand, modelsCommand, pinCommand, pollsCommand, reactionsCommand, recipientsCommand, repliesCommand, reviewCommand, runsCommand, scheduleCommand, searchCommand, searchesCommand, sendCommand, sendsCommand, serveCommand, serverCommand, skillCommand, starCommands, statsCommand, stickersCommand, storeCommand, tagsCommand, tasksCommand, tasksStatsCommand, topicsCommand, unpinCommand, upgradeCommand, watchCommand, } from "./shared-command-exports.js";
package/dist/cli/index.js CHANGED
@@ -93,7 +93,7 @@ export { knownBeside, knownPermissionKeys, unknownPermissionKeys } from "./permi
93
93
  export { asFirstWord, commandWords, liftProfile, refuseCommandName, rootOf, usableProfileName } from "./profile.js";
94
94
  export { createProgram, run } from "./program.js";
95
95
  export { fieldsOf, projectFields } from "./result-fields.js";
96
- export { count, first, flag, fromFile, NONE_NAMED, plain, settingsFor, } from "./settings.js";
96
+ export { count, first, flag, fromFile, NONE_NAMED, namedProfile, plain, settingsFor, } from "./settings.js";
97
97
  export { readSetupAnswers, setupPermissionCeiling } from "./setup-answers.js";
98
98
  export { AGENTS, agentFor, screenFor } from "./setup-screen.js";
99
99
  export { accountCommand, addMeetingBatchCommand, addMeetingPreparationCommand, addMeetingProjectCandidatesCommand, addMeetingSeriesInventoryCommand, attachmentsCommand, botAdminsCommand, botCommand, botMcpCommand, botMembersCommand, botMessagesCommand, callsCommand, chatsCommand, commandsCommand, completeCommand, configCommand, contactsCommand, conversationsCommand, deleteCommand, doctorCommand, editCommand, floodCommand, forwardCommand, generatedApiCommand, guideCommand, imagesCommand, inboxCommand, markReadCommand, mcpCommand, mentionsCommand, messagesCommand, metadataCommand, modelCatalogueCommand, modelsCommand, pinCommand, pollsCommand, reactionsCommand, recipientsCommand, repliesCommand, reviewCommand, runsCommand, scheduleCommand, searchCommand, searchesCommand, sendCommand, sendsCommand, serveCommand, serverCommand, skillCommand, starCommands, statsCommand, stickersCommand, storeCommand, tagsCommand, tasksCommand, tasksStatsCommand, topicsCommand, unpinCommand, upgradeCommand, watchCommand, } from "./shared-command-exports.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)) {
@@ -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" | "sources">>;
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,9 +26,9 @@ 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, sampleCommand, 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
- import { settingsFor } from "./settings.js";
31
+ import { NoProfileHere, namedProfile, settingsFor, } from "./settings.js";
32
32
  import { assertSharedContracts } from "./shared-contracts.js";
33
33
  import { commandAt, lineOf, withLines } from "./steps.js";
34
34
  /**
@@ -379,9 +379,13 @@ const runOnce = async (argv, definition, options) => {
379
379
  // No permission governs housekeeping like `skill install`, so no profile's settings change its preview.
380
380
  needsProfile: keyForCommand(path) !== null,
381
381
  });
382
- const permissions = "permissions" in resolved ? resolved.permissions : {};
383
- const unset = `${command} ${asFirstWord(resolved.profile)}config unset${path[0] === "bot" ? " --bot" : ""}`;
384
- 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);
385
389
  renderAnswer(action, outputFor(action), itemOf(snakeKeys(previewed)));
386
390
  throw new PreviewComplete();
387
391
  }
@@ -576,7 +580,7 @@ const keepFailure = async (failure, { definition, program, rest, profile, option
576
580
  await recorded({
577
581
  app: definition.app,
578
582
  command: `${options.agent ? "mcp " : ""}${path || definition.app.command}`,
579
- profile: settings?.profile ?? profile ?? "default",
583
+ profile: settings ? namedProfile(settings) : profile,
580
584
  record: false,
581
585
  keepFailed: settings?.keepFailedRuns ?? !rest.includes("--no-record"),
582
586
  trace: false,
@@ -605,10 +609,11 @@ const commandPath = (program, rest) => {
605
609
  return words.join(" ");
606
610
  };
607
611
  const failed = ({ app }, program, rest, error, settled, debug) => {
608
- const failure = settled.settlementFailed && isCliFailure(error)
609
- ? new CliError(error.code, error.message, { ...error.details, settlement_failed: true })
610
- : error;
611
612
  const path = commandPath(program, program.args.length > 0 ? program.args : rest);
613
+ const told = error instanceof NoProfileHere && !commandAt(program, "setup") ? error.withoutSetup(path) : error;
614
+ const failure = settled.settlementFailed && isCliFailure(told)
615
+ ? new CliError(told.code, told.message, { ...told.details, settlement_failed: true })
616
+ : told;
612
617
  const effects = judgedEffects(path ? metaOf(commandAt(program, path) ?? program).effects : undefined);
613
618
  const object = errorObjectOf(failure, {
614
619
  ...failureContext(app.errors),
@@ -14,7 +14,7 @@ export declare const searchRuns: (dir: string, { query, terms, status, errorCode
14
14
  items: {
15
15
  runId: string;
16
16
  command: string;
17
- profile: string;
17
+ profile: string | undefined;
18
18
  status: "running" | import("../../runs/run.js").RunStatus;
19
19
  startedAt: string;
20
20
  completedAt: string | undefined;