@wirecat/cli-core 0.423.0 → 0.425.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 (64) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/cli/author.d.ts +5 -6
  3. package/dist/cli/author.js +9 -16
  4. package/dist/cli/config-issues.d.ts +20 -2
  5. package/dist/cli/config-issues.js +111 -17
  6. package/dist/cli/context.d.ts +3 -1
  7. package/dist/cli/context.js +20 -7
  8. package/dist/cli/dry-run-guard.d.ts +2 -0
  9. package/dist/cli/dry-run-guard.js +68 -0
  10. package/dist/cli/entry.js +14 -9
  11. package/dist/cli/index.d.ts +4 -4
  12. package/dist/cli/index.js +4 -3
  13. package/dist/cli/list-query.d.ts +2 -0
  14. package/dist/cli/list-query.js +5 -2
  15. package/dist/cli/messenger/store-maintenance-command.js +15 -5
  16. package/dist/cli/preview.d.ts +44 -5
  17. package/dist/cli/preview.js +26 -8
  18. package/dist/cli/program.d.ts +1 -1
  19. package/dist/cli/program.js +36 -23
  20. package/dist/cli/runs/search.d.ts +1 -1
  21. package/dist/cli/runs/search.js +2 -0
  22. package/dist/cli/settings.d.ts +13 -0
  23. package/dist/cli/settings.js +36 -16
  24. package/dist/cli/shared-command-exports.js +3 -1
  25. package/dist/entities/index.d.ts +151 -3
  26. package/dist/entities/recorded-run.d.ts +87 -3
  27. package/dist/entities/recorded-run.js +16 -1
  28. package/dist/entities/store-maintenance.d.ts +64 -0
  29. package/dist/entities/store-maintenance.js +24 -1
  30. package/dist/mcp/agent.js +3 -4
  31. package/dist/mcp/bot/server.js +1 -1
  32. package/dist/mcp/meeting-preparation.js +2 -2
  33. package/dist/mcp/meeting-project-candidates.js +2 -2
  34. package/dist/mcp/meeting-series-inventory.js +2 -2
  35. package/dist/mcp/server.js +1 -1
  36. package/dist/mcp/surface.d.ts +3 -1
  37. package/dist/mcp/surface.js +20 -9
  38. package/dist/mcp/text.d.ts +1 -1
  39. package/dist/mcp/text.js +1 -1
  40. package/dist/mcp/tool.d.ts +1 -1
  41. package/dist/mcp/tool.js +6 -2
  42. package/dist/meetings/mcp/agent-text.d.ts +2 -0
  43. package/dist/meetings/mcp/agent-text.js +2 -0
  44. package/dist/meetings/mcp/index.d.ts +1 -1
  45. package/dist/meetings/mcp/index.js +3 -3
  46. package/dist/meetings/mcp/passage.js +2 -2
  47. package/dist/meetings/mcp/project-evidence.js +2 -2
  48. package/dist/meetings/mcp/search.js +2 -2
  49. package/dist/runs/error-detail.d.ts +42 -0
  50. package/dist/runs/error-detail.js +131 -0
  51. package/dist/runs/events.d.ts +4 -0
  52. package/dist/runs/events.js +2 -0
  53. package/dist/runs/log-attributes.js +2 -0
  54. package/dist/runs/recording.d.ts +7 -3
  55. package/dist/runs/recording.js +22 -21
  56. package/dist/runs/run-context.d.ts +37 -0
  57. package/dist/runs/run-context.js +148 -0
  58. package/dist/runs/run.d.ts +22 -5
  59. package/dist/runs/run.js +2 -1
  60. package/dist/store/exports.d.ts +2 -0
  61. package/dist/store/exports.js +2 -0
  62. package/dist/store/sqlite/store-contents.d.ts +47 -0
  63. package/dist/store/sqlite/store-contents.js +104 -0
  64. package/package.json +1 -1
@@ -11,7 +11,7 @@ import { MIGRATIONS, migrate } from "../../store/migrations.js";
11
11
  import { openCache } from "../../store/open.js";
12
12
  import { storePath } from "../../store/path.js";
13
13
  import { deleteCopy, repairStore } from "../../store/repair.js";
14
- import { drainNoteIndex, fillSearchIndex, fillStems, involvementStoreOver, noteIndexState, openStore, orphanPointers, pendingNormalization, resetAttachmentWords, resetNoteIndex, resetSearchIndex, resetStems, searchIndexState, stemmerCache, stemsState, } from "../../store/store.js";
14
+ import { drainNoteIndex, fillSearchIndex, fillStems, involvementStoreOver, noteIndexState, openSqlite, openStore, orphanPointers, pendingNormalization, resetAttachmentWords, resetNoteIndex, resetSearchIndex, resetStems, rowCounts, searchIndexState, stemmerCache, stemsState, storeContents, } from "../../store/store.js";
15
15
  import { environmentOf, outputFor } from "../context.js";
16
16
  import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
17
17
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
@@ -49,6 +49,16 @@ const reading = async (path, read) => {
49
49
  database.close();
50
50
  }
51
51
  };
52
+ /** For a command that only reads: the connection refuses writes, and opening sets no journal mode. */
53
+ const readingOnly = async (path, read) => {
54
+ const { database } = await openSqlite(path, { readOnly: true });
55
+ try {
56
+ return read(database);
57
+ }
58
+ finally {
59
+ database.close();
60
+ }
61
+ };
52
62
  /** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
53
63
  export const storeSummary = async (env) => {
54
64
  const path = storePath(env);
@@ -154,25 +164,25 @@ const copiesCommand = () => new Command("copies").description("the tables `store
154
164
  }));
155
165
  const infoCommand = () => annotate(new Command("info"), { output: { entity: "store_info" }, effects: LOCAL_READ })
156
166
  .summary("the local archive's format, size and contents")
157
- .description("the store file: where it is, its size, its schema and how many rows it holds; changes nothing")
167
+ .description("the store file: where it is, its size, its schema and how many rows it holds, by provider, chat kind, account, year (UTC) and attachment kind, over every account; changes nothing")
158
168
  .action(async function () {
159
169
  const path = storePath(environmentOf(this).env ?? process.env);
160
170
  if (!existsSync(path)) {
161
171
  result(this, { path, exists: false });
162
172
  return;
163
173
  }
164
- const read = reading(path, (database) => {
174
+ const read = readingOnly(path, (database) => {
165
175
  const { version, minCompatible, writable } = schemaOf(database);
166
- const tables = ["accounts", "chats", "messages", "attachments", "identities"];
167
176
  return {
168
177
  path,
169
178
  exists: true,
170
179
  bytes: { file: bytesOf(path), wal: bytesOf(`${path}-wal`) },
171
180
  schema: { version, minCompatible, speaks: SPEAKS, writable },
172
- rows: version > 0 ? Object.fromEntries(tables.map((table) => [table, count(database, table)])) : {},
181
+ rows: version > 0 ? rowCounts(database) : {},
173
182
  pendingNormalization: pendingIfKnown(database),
174
183
  wordIndex: searchIndexState(database) ?? null,
175
184
  stemIndex: stemsState(database) ?? null,
185
+ ...(version > 0 ? storeContents(database) : {}),
176
186
  };
177
187
  });
178
188
  result(this, await read.catch((error) => ({ path, exists: true, opens: false, error: messageOf(error) })));
@@ -3,13 +3,51 @@ import { type PermissionKey } from "../sends/permissions.js";
3
3
  import type { Settings } from "./settings.js";
4
4
  export declare class PreviewComplete extends Error {
5
5
  }
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
+ export declare const preview: (command: Command, settings: Pick<Settings, "permissions" | "permissionSources" | "configPath" | "savedPermissions" | "profile">, tool: string, keyOf?: (path: readonly string[]) => PermissionKey | null | undefined, messenger?: boolean) => {
7
7
  preview: boolean;
8
8
  path: string[];
9
- permission: {
10
- key: string | null;
11
- level: "allow" | "ask" | "readonly";
9
+ permission?: {
10
+ key: PermissionKey | null;
11
+ level: string;
12
+ } | undefined;
13
+ validation: {
14
+ arguments: string;
15
+ permissions: string;
16
+ targets: string;
17
+ businessRules: string;
18
+ };
19
+ effects: {
20
+ actionInvoked: boolean;
21
+ writeReserved: boolean;
22
+ messengerContacted?: boolean | undefined;
23
+ serviceContacted: boolean;
24
+ configurationRead: boolean;
12
25
  };
26
+ arguments: ({
27
+ name: string;
28
+ reference: string;
29
+ } | {
30
+ name: string;
31
+ supplied: boolean;
32
+ bytes: number | undefined;
33
+ })[];
34
+ options: string[];
35
+ trust: string;
36
+ };
37
+ /**
38
+ * `messenger_contacted` stays for the messengers, whose callers may read it; `service_contacted` says the same
39
+ * for every tool. Without `permission` nothing was checked: a host that is not `run` has no settings to check.
40
+ */
41
+ export declare const previewBody: (command: Command, permission: {
42
+ key: PermissionKey | null;
43
+ level: string;
44
+ } | undefined, messenger: boolean) => {
45
+ preview: boolean;
46
+ path: string[];
47
+ permission?: {
48
+ key: PermissionKey | null;
49
+ level: string;
50
+ } | undefined;
13
51
  validation: {
14
52
  arguments: string;
15
53
  permissions: string;
@@ -19,7 +57,8 @@ export declare const preview: (command: Command, settings: Pick<Settings, "permi
19
57
  effects: {
20
58
  actionInvoked: boolean;
21
59
  writeReserved: boolean;
22
- messengerContacted: boolean;
60
+ messengerContacted?: boolean | undefined;
61
+ serviceContacted: boolean;
23
62
  configurationRead: boolean;
24
63
  };
25
64
  arguments: ({
@@ -6,11 +6,11 @@ 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, settings, tool, keyOf) => {
9
+ export const preview = (command, settings, tool, keyOf, messenger = true) => {
10
10
  const path = commandPathOf(command);
11
11
  const { permissions } = settings;
12
12
  refuseUnknownPermissionKeys(command, settings, tool, keyOf && ((one) => keyOf(commandPathOf(one))));
13
- const key = keyForCommand(path);
13
+ const key = keyOf?.(path) ?? keyForCommand(path);
14
14
  const level = key ? levelFor(permissions, key).level : "allow";
15
15
  for (const source of readKeysForCommand(path)) {
16
16
  if (levelFor(permissions, source).level === "deny")
@@ -21,18 +21,36 @@ export const preview = (command, settings, tool, keyOf) => {
21
21
  (key !== null && key !== undefined && WRITE_KEYS.includes(key));
22
22
  if (level === "deny" || (writes && level === "readonly"))
23
23
  throw new CliError("permission_error", `this profile does not permit ${path.join(" ")}`, { permission: key });
24
- const targets = new Set(["chat", "message", "person", "profile"]);
24
+ return previewBody(command, { key: key ?? null, level }, messenger);
25
+ };
26
+ const TARGETS = new Set(["chat", "message", "person", "profile"]);
27
+ /**
28
+ * `messenger_contacted` stays for the messengers, whose callers may read it; `service_contacted` says the same
29
+ * for every tool. Without `permission` nothing was checked: a host that is not `run` has no settings to check.
30
+ */
31
+ export const previewBody = (command, permission, messenger) => {
25
32
  return {
26
33
  preview: true,
27
- path,
28
- permission: { key: key ?? null, level },
29
- validation: { arguments: "parsed", permissions: "checked", targets: "unresolved", businessRules: "not-executed" },
30
- effects: { actionInvoked: false, writeReserved: false, messengerContacted: false, configurationRead: true },
34
+ path: commandPathOf(command),
35
+ ...(permission ? { permission } : {}),
36
+ validation: {
37
+ arguments: "parsed",
38
+ permissions: permission ? "checked" : "not-checked",
39
+ targets: "unresolved",
40
+ businessRules: "not-executed",
41
+ },
42
+ effects: {
43
+ actionInvoked: false,
44
+ writeReserved: false,
45
+ ...(messenger ? { messengerContacted: false } : {}),
46
+ serviceContacted: false,
47
+ configurationRead: permission !== undefined,
48
+ },
31
49
  arguments: command.registeredArguments.map((argument, at) => {
32
50
  const value = command.processedArgs[at];
33
51
  return {
34
52
  name: argument.name(),
35
- ...(targets.has(argument.name()) && typeof value === "string"
53
+ ...(TARGETS.has(argument.name()) && typeof value === "string"
36
54
  ? { reference: value }
37
55
  : { supplied: value !== undefined, bytes: typeof value === "string" ? Buffer.byteLength(value) : undefined }),
38
56
  };
@@ -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" | "permissionSources" | "configPath" | "savedPermissions">>;
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. */
@@ -10,6 +10,7 @@ import { isCliFailure, isCommanderFailure } from "../errors/failures.js";
10
10
  import { failureContext } from "../errors/reasons.js";
11
11
  import { envName, readOnlyLockOf, readOnlyRefusal } from "../ports/app.js";
12
12
  import { recorded, wasSettled } from "../runs/recording.js";
13
+ import { runContextOf } from "../runs/run-context.js";
13
14
  import { processRun, useAsProcessRun } from "../runs/scope.js";
14
15
  import { AUTHORITY_KEY, BOT_API_KEY, isBotApi, keyForCommand } from "../sends/permissions.js";
15
16
  import { logParsedArgv, logVersion } from "./argv-log.js";
@@ -28,7 +29,7 @@ import { checkJson, liftJson } from "./json-fields.js";
28
29
  import { PreviewComplete, preview } from "./preview.js";
29
30
  import { commandWords, liftProfile, PROFILE_FREE, sampleCommand, valuedOptions } from "./profile.js";
30
31
  import { absolutePaths, relayTarget } from "./relay-route.js";
31
- import { settingsFor } from "./settings.js";
32
+ import { NoProfileHere, namedProfile, settingsFor, } from "./settings.js";
32
33
  import { assertSharedContracts } from "./shared-contracts.js";
33
34
  import { commandAt, lineOf, withLines } from "./steps.js";
34
35
  /**
@@ -191,6 +192,7 @@ const runOnce = async (argv, definition, options) => {
191
192
  const exit = {};
192
193
  const environment = {
193
194
  ...options,
195
+ argv,
194
196
  streams: control.streams,
195
197
  signal: options.signal ? AbortSignal.any([control.signal, options.signal]) : control.signal,
196
198
  commandSignal: control.signal,
@@ -267,16 +269,18 @@ const runOnce = async (argv, definition, options) => {
267
269
  const path = commandPathOf(action);
268
270
  const lock = readOnlyLockOf(definition.app, commandEnv);
269
271
  const input = options.stdin ?? process.stdin;
270
- await setupAtStart({
271
- command,
272
- env: commandEnv,
273
- contentLanguages: definition.app.contentLanguages,
274
- path,
275
- headless,
276
- readOnlyLock: lock,
277
- input,
278
- terminal: !options.streams && input.isTTY === true && process.stderr.isTTY === true,
279
- });
272
+ // The first-run questions write the shared config; a dry run writes nothing.
273
+ if (!flags.dryRun)
274
+ await setupAtStart({
275
+ command,
276
+ env: commandEnv,
277
+ contentLanguages: definition.app.contentLanguages,
278
+ path,
279
+ headless,
280
+ readOnlyLock: lock,
281
+ input,
282
+ terminal: !options.streams && input.isTTY === true && process.stderr.isTTY === true,
283
+ });
280
284
  if (lock && isLogin(action) && !flags.dryRun)
281
285
  throw readOnlyRefusal(lock, "log in or out");
282
286
  if (lock && isStoreChange(action) && !flags.dryRun)
@@ -385,7 +389,7 @@ const runOnce = async (argv, definition, options) => {
385
389
  configPath: resolved.configPath ?? "",
386
390
  profile: resolved.profile,
387
391
  ...(resolved.savedPermissions ? { savedPermissions: resolved.savedPermissions } : {}),
388
- }, command, definition.configuration?.permissionKey);
392
+ }, command, definition.configuration?.permissionKey, definition.app.messenger !== false);
389
393
  renderAnswer(action, outputFor(action), itemOf(snakeKeys(previewed)));
390
394
  throw new PreviewComplete();
391
395
  }
@@ -412,7 +416,7 @@ const runOnce = async (argv, definition, options) => {
412
416
  refuseJoinedPath(program, words.filter((word) => !word.startsWith("-")), profile !== undefined && !/\s/.test(profile) ? [profile] : []);
413
417
  }
414
418
  catch (failure) {
415
- const settled = await settleFailure(failure, { definition, program, rest: lifted, profile, options });
419
+ const settled = await settleFailure(failure, { definition, program, rest: lifted, argv, profile, options });
416
420
  return report(streams, reporting, failed(definition, program, lifted, failure, settled, debug));
417
421
  }
418
422
  if (profile !== undefined)
@@ -427,7 +431,7 @@ const runOnce = async (argv, definition, options) => {
427
431
  const message = `"${profile}" is not a command, so it was read as a profile name — and no command followed it. ` +
428
432
  `Run \`${command} --help\` for the commands, or \`${command} ${profile} account show\` if "${profile}" is your profile.`;
429
433
  const failure = new CliError("validation_error", message, { param: "profile" });
430
- const settled = await settleFailure(failure, { definition, program, rest, profile, options });
434
+ const settled = await settleFailure(failure, { definition, program, rest, argv, profile, options });
431
435
  return report(streams, reporting, failed(definition, program, rest, failure, settled, debug));
432
436
  }
433
437
  try {
@@ -472,10 +476,10 @@ const runOnce = async (argv, definition, options) => {
472
476
  message += ` — "${profile}" is not a command, so it was read as a profile name — which left "${rest[0]}" to be one.`;
473
477
  }
474
478
  const failure = new CliError("validation_error", message);
475
- const settled = await settleFailure(failure, { definition, program, rest, profile, options });
479
+ const settled = await settleFailure(failure, { definition, program, rest, argv, profile, options });
476
480
  return report(streams, reporting, failed(definition, program, rest, failure, settled, debug));
477
481
  }
478
- const settled = await settleFailure(error, { definition, program, rest, profile, options });
482
+ const settled = await settleFailure(error, { definition, program, rest, argv, profile, options });
479
483
  const exit = report(streams, reporting, failed(definition, program, rest, error, settled, debug));
480
484
  return signalExit ?? exit;
481
485
  }
@@ -562,9 +566,9 @@ const settleFailure = async (failure, state) => {
562
566
  /**
563
567
  * **Every failure is kept as a run** — a usage error, a configuration that will not load, a command
564
568
  * that never opens a run — unless recording was turned off. One its own run already kept is skipped.
565
- * Only the command's words are named, never its arguments: those can be a message.
569
+ * Its arguments are kept masked (`maskArgv`): a value that can be a message never is.
566
570
  */
567
- const keepFailure = async (failure, { definition, program, rest, profile, options }) => {
571
+ const keepFailure = async (failure, { definition, program, rest, argv, profile, options }) => {
568
572
  if (wasSettled(failure))
569
573
  return;
570
574
  const env = options.env ?? process.env;
@@ -577,10 +581,18 @@ const keepFailure = async (failure, { definition, program, rest, profile, option
577
581
  catch {
578
582
  // A configuration that will not load is a failure worth keeping too; the flags are all there is to go on.
579
583
  }
584
+ const command = `${options.agent ? "mcp " : ""}${path || definition.app.command}`;
580
585
  await recorded({
581
586
  app: definition.app,
582
- command: `${options.agent ? "mcp " : ""}${path || definition.app.command}`,
583
- profile: settings?.profile ?? profile ?? "default",
587
+ command,
588
+ context: runContextOf({
589
+ bin: definition.app.command,
590
+ command: path ? command : options.agent ? "mcp" : "",
591
+ program,
592
+ argv,
593
+ env,
594
+ }),
595
+ profile: settings ? namedProfile(settings) : profile,
584
596
  record: false,
585
597
  keepFailed: settings?.keepFailedRuns ?? !rest.includes("--no-record"),
586
598
  trace: false,
@@ -609,10 +621,11 @@ const commandPath = (program, rest) => {
609
621
  return words.join(" ");
610
622
  };
611
623
  const failed = ({ app }, program, rest, error, settled, debug) => {
612
- const failure = settled.settlementFailed && isCliFailure(error)
613
- ? new CliError(error.code, error.message, { ...error.details, settlement_failed: true })
614
- : error;
615
624
  const path = commandPath(program, program.args.length > 0 ? program.args : rest);
625
+ const told = error instanceof NoProfileHere && !commandAt(program, "setup") ? error.withoutSetup(path) : error;
626
+ const failure = settled.settlementFailed && isCliFailure(told)
627
+ ? new CliError(told.code, told.message, { ...told.details, settlement_failed: true })
628
+ : told;
616
629
  const effects = judgedEffects(path ? metaOf(commandAt(program, path) ?? program).effects : undefined);
617
630
  const object = errorObjectOf(failure, {
618
631
  ...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;
@@ -8,6 +8,8 @@ const FIELDS = [
8
8
  "opcode",
9
9
  "seq",
10
10
  "status",
11
+ "method",
12
+ "endpoint_template",
11
13
  "bytes",
12
14
  "ids",
13
15
  "counts",
@@ -1,3 +1,4 @@
1
+ import { CliError } from "@wirecat/cli-base";
1
2
  import * as v from "valibot";
2
3
  import { type AISettings } from "../analysis/settings.js";
3
4
  import { type AppIdentity } from "../ports/app.js";
@@ -161,6 +162,18 @@ export interface ResolveOptions {
161
162
  }
162
163
  /** Where `profile` came from when none was named and the command needs none. */
163
164
  export declare const NONE_NAMED = "none named";
165
+ /** The profile a run is for: none when nothing named one, even though `Settings.profile` holds a placeholder. */
166
+ export declare const namedProfile: (settings: Pick<Settings, "profile"> & Partial<Pick<Settings, "sources">>) => string | undefined;
167
+ /**
168
+ * No profile named and none on this computer. The settings cannot see the program, so the program swaps the
169
+ * `setup` this suggests for `withoutSetup` when it has no such command.
170
+ */
171
+ export declare class NoProfileHere extends CliError {
172
+ private readonly app;
173
+ constructor(app: AppIdentity);
174
+ /** `words`: the command that was typed, without its arguments, so the suggestion runs as written. */
175
+ withoutSetup(words: string): CliError;
176
+ }
164
177
  /** The first candidate that is set, and which layer it came from. */
165
178
  export declare const first: <T>(candidates: [Source, T | undefined][], fallback: T) => {
166
179
  value: T;
@@ -7,7 +7,7 @@ import { envName, readOnlyLockOf } from "../ports/app.js";
7
7
  import { assertOverridesTighten, CEILING_KEY, LEVELS, layerPermissions, permissionOverrides, RESOURCES, } from "../sends/permissions.js";
8
8
  import { storePath } from "../store/path.js";
9
9
  import { sharedConfigPath } from "../store/shared-config.js";
10
- import { issueMessage, parseConfigText, publicIssue, savedPermissionKeys, schemaIssues, withoutIssues, } from "./config-issues.js";
10
+ import { checkConfigText, issueMessage, parseConfigText, publicIssue, savedPermissionKeys, schemaIssues, } from "./config-issues.js";
11
11
  import { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
12
12
  import { profilesHere } from "./messenger/accounts.js";
13
13
  import { usableProfileName } from "./profile.js";
@@ -81,6 +81,26 @@ const SHARED_DEFAULTS_ONLY = {
81
81
  export const NONE_NAMED = "none named";
82
82
  /** A name for a command that speaks for no account; never chosen for one that does. */
83
83
  const NO_PROFILE = "default";
84
+ /** The profile a run is for: none when nothing named one, even though `Settings.profile` holds a placeholder. */
85
+ export const namedProfile = (settings) => settings.sources?.profile === NONE_NAMED ? undefined : settings.profile;
86
+ /**
87
+ * No profile named and none on this computer. The settings cannot see the program, so the program swaps the
88
+ * `setup` this suggests for `withoutSetup` when it has no such command.
89
+ */
90
+ export class NoProfileHere extends CliError {
91
+ app;
92
+ constructor(app) {
93
+ super("validation_error", `name a profile: there is none on this computer yet — create one with \`${app.command} personal setup\` ` +
94
+ "(any name in place of personal)", { profiles: [] });
95
+ this.app = app;
96
+ }
97
+ /** `words`: the command that was typed, without its arguments, so the suggestion runs as written. */
98
+ withoutSetup(words) {
99
+ const command = words ? `${this.app.command} personal ${words}` : `${this.app.command} personal …`;
100
+ return new CliError("validation_error", `name a profile: there is none on this computer yet — any name works, such as \`${command}\` ` +
101
+ `(or set ${envName(this.app, "PROFILE")})`, { profiles: [] });
102
+ }
103
+ }
84
104
  /** The first candidate that is set, and which layer it came from. */
85
105
  export const first = (candidates, fallback) => {
86
106
  for (const [from, value] of candidates)
@@ -137,14 +157,15 @@ export const settingsFor = (app, extension = {}) => {
137
157
  };
138
158
  /** The file with every problem found; what loads is the file without the settings they name. */
139
159
  const loadConfig = (path) => {
140
- const read = readRaw(path);
141
- if ("issue" in read)
142
- return { config: { profiles: {} }, issues: [read.issue], raw: undefined };
143
- if (read.raw === undefined)
144
- return { config: { profiles: {} }, issues: [], raw: undefined };
145
- const issues = schemaIssues(read.raw, readSchema, context(path));
146
- const parsed = v.safeParse(readSchema, withoutIssues(read.raw, issues));
147
- return { config: (parsed.success ? parsed.output : { profiles: {} }), issues, raw: read.raw };
160
+ let text;
161
+ try {
162
+ text = readFileSync(path, "utf8");
163
+ }
164
+ catch {
165
+ return { config: { profiles: {} }, issues: [] };
166
+ }
167
+ const { config, issues } = checkConfigText(text, { file: path, tool: app.command, schema: readSchema });
168
+ return { config: (config ?? { profiles: {} }), issues };
148
169
  };
149
170
  /**
150
171
  * A lock pins a process to one profile: the owner sets it where an agent runs, and a first word
@@ -170,10 +191,9 @@ export const settingsFor = (app, extension = {}) => {
170
191
  if (!needed)
171
192
  return { value: NO_PROFILE, from: NONE_NAMED };
172
193
  const here = profilesOf(config, env);
173
- throw new CliError("validation_error", here.length === 0
174
- ? `name a profile: there is none on this computer yet — create one with \`${app.command} personal setup\` ` +
175
- "(any name in place of personal)"
176
- : `name a profile: \`${app.command} <profile> …\` — profiles here: ${here.join(", ")} (or set ${PROFILE})`, { profiles: here });
194
+ if (here.length === 0)
195
+ throw new NoProfileHere(app);
196
+ throw new CliError("validation_error", `name a profile: \`${app.command} <profile> …\` — profiles here: ${here.join(", ")} (or set ${PROFILE})`, { profiles: here });
177
197
  };
178
198
  const profilesOf = (config, env) => profilesHere(app, [...namedProfiles(config), ...(extension.profiles?.(env) ?? [])], env);
179
199
  const resolveSettings = (flags = {}, options = {}) => {
@@ -188,7 +208,7 @@ export const settingsFor = (app, extension = {}) => {
188
208
  }
189
209
  const loaded = loadConfig(configPath);
190
210
  if (loaded.issues.length > 0 && !options.withIssues)
191
- throw new CliError("configuration_error", issueMessage(loaded.issues.map(publicIssue)));
211
+ throw new CliError("configuration_error", issueMessage(loaded.issues));
192
212
  const { config } = loaded;
193
213
  const lock = given(env[PROFILE_LOCK]);
194
214
  const named = first([
@@ -311,7 +331,7 @@ export const settingsFor = (app, extension = {}) => {
311
331
  },
312
332
  configured,
313
333
  shared,
314
- configIssues: loaded.issues.map(publicIssue),
334
+ configIssues: loaded.issues,
315
335
  savedPermissions: savedPermissionKeys(config),
316
336
  };
317
337
  // The file was checked by the schema; a flag was not, and `--limit abc` is `NaN` by the time it
@@ -332,7 +352,7 @@ export const settingsFor = (app, extension = {}) => {
332
352
  const configuredProfiles = (options = {}) => {
333
353
  const { config, issues } = loadConfig(configPathFor(options));
334
354
  if (issues.length > 0 && !options.withIssues)
335
- throw new CliError("configuration_error", issueMessage(issues.map(publicIssue)));
355
+ throw new CliError("configuration_error", issueMessage(issues));
336
356
  return namedProfiles(config);
337
357
  };
338
358
  /**
@@ -5,6 +5,7 @@ import { botMcpCommand as botMcpCommandDefinition } from "./bot/mcp.js";
5
5
  import { botMessagesCommand as botMessagesCommandDefinition } from "./bot/messages.js";
6
6
  import { commandsCommand as commandsCommandDefinition } from "./commands-command.js";
7
7
  import { configCommand as configCommandDefinition } from "./config-command.js";
8
+ import { honoursDryRun } from "./dry-run-guard.js";
8
9
  import { guideCommand as guideCommandDefinition } from "./guide.js";
9
10
  import { addMeetingBatchCommand as addMeetingBatchCommandDefinition } from "./meeting-batch.js";
10
11
  import { addMeetingPreparationCommand as addMeetingPreparationCommandDefinition } from "./meeting-preparation-command.js";
@@ -48,9 +49,10 @@ import { tasksCommand as tasksCommandDefinition, tasksStatsCommand as tasksStats
48
49
  import { topicsCommand as topicsCommandDefinition } from "./messenger/topics-command.js";
49
50
  import { watchCommand as watchCommandDefinition } from "./messenger/watch-command.js";
50
51
  import { runsCommand as runsCommandDefinition } from "./runs/command.js";
51
- import { sharedCommand } from "./shared-contracts.js";
52
+ import { sharedCommand as protectedCommand } from "./shared-contracts.js";
52
53
  import { skillCommand as skillCommandDefinition } from "./skill-command.js";
53
54
  import { upgradeCommand as upgradeCommandDefinition } from "./upgrade-command.js";
55
+ const sharedCommand = (factory) => honoursDryRun(protectedCommand(factory));
54
56
  export const botAdminsCommand = sharedCommand(botAdminsCommandDefinition);
55
57
  export const botMembersCommand = sharedCommand(botMembersCommandDefinition);
56
58
  export const generatedApiCommand = sharedCommand(generatedApiCommandDefinition);