@wirecat/cli-core 0.543.0 → 0.545.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.
- package/CHANGELOG.md +36 -0
- package/dist/cli/config-command.js +32 -2
- package/dist/cli/messenger/archive-commands.js +20 -2
- package/dist/cli/messenger/backfill-command.js +13 -2
- package/dist/cli/messenger/context.js +3 -0
- package/dist/cli/messenger/history-gaps.d.ts +12 -0
- package/dist/cli/messenger/history-gaps.js +41 -0
- package/dist/cli/messenger/messages-command.js +7 -3
- package/dist/cli/messenger/messages-search-command.js +5 -1
- package/dist/cli/messenger/search-answers.d.ts +3 -2
- package/dist/cli/messenger/search-answers.js +12 -5
- package/dist/cli/messenger/search-command.js +3 -1
- package/dist/cli/messenger/stored.d.ts +6 -1
- package/dist/cli/messenger/stored.js +38 -2
- package/dist/cli/program.js +22 -0
- package/dist/cli/settings.d.ts +9 -0
- package/dist/cli/settings.js +20 -0
- package/dist/cli/shared-command-exports.d.ts +24 -0
- package/dist/cli/skill-command.d.ts +2 -0
- package/dist/cli/skill-command.js +10 -0
- package/dist/cli/skill-refresh.d.ts +18 -0
- package/dist/cli/skill-refresh.js +82 -0
- package/dist/cli/steps.js +1 -0
- package/dist/entities/index.d.ts +1 -0
- package/dist/entities/message-transfer.d.ts +4 -0
- package/dist/entities/message-transfer.js +2 -0
- package/dist/services/history-limits.d.ts +45 -0
- package/dist/services/history-limits.js +50 -0
- package/dist/store/chats.d.ts +10 -0
- package/dist/store/chats.js +25 -0
- package/dist/store/sqlite/chats.js +2 -1
- package/dist/store/sqlite/history-skips.d.ts +24 -0
- package/dist/store/sqlite/history-skips.js +95 -0
- package/dist/store/store.d.ts +1 -0
- package/package.json +1 -1
- package/parity.json +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.545.0 — 02.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **History limits for pushed history, per profile.** A messenger whose history is pushed (`history: "store"`,
|
|
15
|
+
WhatsApp) saves one-to-one chats back `history_dialog_days` (1095 by default), groups of more than two people back
|
|
16
|
+
`history_group_days` (365) and nothing of a group above `history_group_members_max` people (50); at most 1095 days.
|
|
17
|
+
The chat row stays, so the chat is still listed. Live messages, `messages sync` and what the store already holds are
|
|
18
|
+
never dropped. `messages sync <chat> --full-history` fetches a chat and keeps its pushed history in full from then on
|
|
19
|
+
(`history_included` in the answer). `ChatsStore` gains `historySkips`, `historyShapes`, `recordHistorySkips` and
|
|
20
|
+
`setHistoryIncluded`; `Saving` takes `historyLimits`.
|
|
21
|
+
|
|
22
|
+
## 0.544.0 — 02.11.2026
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **The first run of a new version refreshes the agent instructions an older version installed**: each `SKILL.md`
|
|
27
|
+
copy whose `skill status` is `stale` (Claude Code's, `~/.agents/skills`, and Hermes's own copy) is rewritten once,
|
|
28
|
+
with one line on stderr — `Updated the tg instructions for Claude Code and other agents to 0.112.0`. A missing copy
|
|
29
|
+
stays missing, an `unversioned` one (no released `skill install` wrote it) and a symlink are left as they are, and a
|
|
30
|
+
failure is one note, never a failed command. The version tried is kept in `<state>/skill-refresh.json`. Off in a
|
|
31
|
+
moved run, under vitest, during `skill`, and with `<PREFIX>_NO_SKILL_REFRESH=1`; `--quiet` keeps the line out.
|
|
32
|
+
|
|
33
|
+
### Changed — may break callers
|
|
34
|
+
|
|
35
|
+
- **An agent's own shell may tighten a permission but never loosen it.** `config set|unset permissions…` run where
|
|
36
|
+
`AI_AGENT`, `CLAUDECODE`, `OPENCLAW_SHELL=exec` or another agent marker is set is now treated like an MCP agent's
|
|
37
|
+
call: `mcp.authority` and `bot.api` stay owner-only, and a level may only go down (`readonly` → `deny`, never
|
|
38
|
+
`readonly` → `allow`). Unsetting a permission, replacing `permissions` whole, or `--defaults`/`--personal`/`--bot`
|
|
39
|
+
accept only `deny`. Before, Hermes or OpenClaw could undo the read-only the desktop app set, from any chat message
|
|
40
|
+
that talked it into running `config set`. The owner changes them in a terminal outside the agent; the desktop app
|
|
41
|
+
already runs the tools without these variables. Not a sandbox: an agent with a general shell can still edit the
|
|
42
|
+
file itself, so keep its own command approvals on.
|
|
43
|
+
|
|
8
44
|
## 0.543.0 — 02.11.2026
|
|
9
45
|
|
|
10
46
|
### Added
|
|
@@ -2,7 +2,8 @@ import { CliError, pathsAreOverridden } from "@wirecat/cli-base";
|
|
|
2
2
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
4
|
import { envName } from "../ports/app.js";
|
|
5
|
-
import {
|
|
5
|
+
import { agentOf } from "../runs/run-context.js";
|
|
6
|
+
import { AUTHORITY_KEY, BOT_API_KEY, LEVELS, levelFor } from "../sends/permissions.js";
|
|
6
7
|
import { baseContext, environmentOf } from "./context.js";
|
|
7
8
|
import { grantsAuthority, LOCAL_READ, LOCAL_WRITE } from "./effects.js";
|
|
8
9
|
import { itemOf, renderAnswer } from "./envelope.js";
|
|
@@ -115,8 +116,13 @@ export const configCommand = (app, config, { permissionKey = config.permissions,
|
|
|
115
116
|
// The defaults are every other profile's settings too, and the top of the file is everyone's.
|
|
116
117
|
throw new CliError("permission_error", `this process is locked to profile ${settings.profile} (${lock}) — ${root ? setting : "--defaults"} changes other profiles`);
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
+
// An agent's own shell counts too: Hermes and OpenClaw run the tool there for anyone their chat accepts (SEC-116).
|
|
120
|
+
if (environmentOf(this).agent || agentOf(env)) {
|
|
119
121
|
refuseGuardedKey(setting, action === "set" ? String(given) : undefined, ownerOnly);
|
|
122
|
+
refuseLoosening(settings.permissions, setting, action === "set" ? String(given) : undefined, {
|
|
123
|
+
wide: Boolean(everyone || root || personal || bot),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
120
126
|
if (action === "set")
|
|
121
127
|
refuseUnknownKey(this, setting, String(given), permissionKey);
|
|
122
128
|
const saved = config.changeSetting(settings.configPath, {
|
|
@@ -185,6 +191,30 @@ const refuseGuardedKey = (setting, value, ownerOnly) => {
|
|
|
185
191
|
return;
|
|
186
192
|
throw new CliError("permission_error", `an agent cannot change permissions.${guarded} — the owner sets it in a terminal`, { permission: guarded });
|
|
187
193
|
};
|
|
194
|
+
/**
|
|
195
|
+
* An agent may tighten a permission, never loosen one: a message in a chat the agent reads must not be able to talk it
|
|
196
|
+
* into lifting the owner's `readonly`. Removing a key, replacing `permissions` whole, or changing what several profiles
|
|
197
|
+
* get can loosen in ways this profile's levels cannot show, so there an agent may only set `deny`.
|
|
198
|
+
*/
|
|
199
|
+
const refuseLoosening = (levels, setting, value, { wide }) => {
|
|
200
|
+
if (setting !== "permissions" && !setting.startsWith("permissions."))
|
|
201
|
+
return;
|
|
202
|
+
const key = setting.slice("permissions.".length);
|
|
203
|
+
const refuse = (why) => {
|
|
204
|
+
throw new CliError("permission_error", `an agent cannot loosen permissions: ${why} — the owner does it in a terminal`, {
|
|
205
|
+
permission: key || "permissions",
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
if (value === "deny" && setting !== "permissions")
|
|
209
|
+
return;
|
|
210
|
+
if (value === undefined)
|
|
211
|
+
refuse(`unsetting ${setting} can lift a limit`);
|
|
212
|
+
if (setting === "permissions" || wide)
|
|
213
|
+
refuse(`only deny may be set for ${setting} with --defaults, --personal, --bot or the whole object`);
|
|
214
|
+
const now = levelFor(levels, key).level;
|
|
215
|
+
if (LEVELS.indexOf(value) > LEVELS.indexOf(now))
|
|
216
|
+
refuse(`${setting} is ${now}, ${value} is looser`);
|
|
217
|
+
};
|
|
188
218
|
/** The keys of a `permissions` object given whole; anything else is left to the schema to refuse. */
|
|
189
219
|
const objectKeys = (value) => {
|
|
190
220
|
try {
|
|
@@ -12,11 +12,12 @@ import { kindsOf } from "../../services/inbox.js";
|
|
|
12
12
|
import { dateWindow } from "../../services/moment.js";
|
|
13
13
|
import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
|
|
14
14
|
import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
15
|
-
import { commandAt } from "../steps.js";
|
|
15
|
+
import { commandAt, lineOf } from "../steps.js";
|
|
16
16
|
import { jobsCommand } from "./backfill-command.js";
|
|
17
17
|
import { messengerContext } from "./context.js";
|
|
18
18
|
import { admitPassword, linesOf, openFolder, runFileFor, saveManifest, sealRun, writeChanges } from "./export-folder.js";
|
|
19
19
|
import { gapsCommand } from "./gaps-command.js";
|
|
20
|
+
import { historyLimitGaps } from "./history-gaps.js";
|
|
20
21
|
import { messageItems } from "./message-items.js";
|
|
21
22
|
import { ENCRYPT_OPTION, passwordOf } from "./password.js";
|
|
22
23
|
import { storeMaintenanceCommands } from "./store-maintenance-command.js";
|
|
@@ -71,9 +72,26 @@ const statusCommand = (messenger) => annotate(new Command("status"), { output: {
|
|
|
71
72
|
.action(async function (chat) {
|
|
72
73
|
const context = messengerContext(this, messenger);
|
|
73
74
|
const rows = await context.withServices((services) => services.archive.status(chat));
|
|
74
|
-
|
|
75
|
+
const skips = await context
|
|
76
|
+
.withStore((store, account) => store.chats.historySkips(account))
|
|
77
|
+
.then((all) => chat === undefined ? all : all.filter((skip) => rows.some((row) => row.chatId === skip.chatId)))
|
|
78
|
+
.catch(() => []);
|
|
79
|
+
const limited = historyLimitGaps(skips);
|
|
80
|
+
renderAnswer(this, context, listOf("store status", {
|
|
81
|
+
items: rows.map(snakeKeys),
|
|
82
|
+
limit: Math.max(1, rows.length),
|
|
83
|
+
has_more: false,
|
|
84
|
+
next: limited.next,
|
|
85
|
+
missing: limited.missing,
|
|
86
|
+
}));
|
|
75
87
|
if (rows.length === 0 && context.format === "pretty")
|
|
76
88
|
context.renderer.note("the store holds no messages for this profile yet");
|
|
89
|
+
if (skips.length > 0 && context.format === "pretty")
|
|
90
|
+
context.renderer.note(`history limits left out pushed history of ${skips.length} chats — \`${lineOf(this, "messages sync <chat> --full-history")}\` fetches one and keeps it in full:\n` +
|
|
91
|
+
skips
|
|
92
|
+
.slice(0, 20)
|
|
93
|
+
.map((skip) => ` ${skip.title ?? skip.chatId} ${skip.messages} messages ${skip.reason}`)
|
|
94
|
+
.join("\n"));
|
|
77
95
|
});
|
|
78
96
|
const LISTED = 20;
|
|
79
97
|
/** Titles are the owner's: these lines go to a person's terminal, never into an error message. */
|
|
@@ -58,14 +58,17 @@ export const syncCommand = (messenger) => {
|
|
|
58
58
|
.option("--catch-up-chunks <n>", "at most this many local vector chunks", wholeNumber)
|
|
59
59
|
.option("--catch-up-messages <n>", "skip a graph rebuild larger than this many messages", wholeNumber)
|
|
60
60
|
.option("--catch-up-time <duration>", "local preparation time budget, 30s by default")
|
|
61
|
+
.option("--full-history", "keep this chat's pushed history in full from now on, past the history limits (history_* settings)")
|
|
61
62
|
.option("--background", "run as a job that outlives this command; `store jobs show` follows it")
|
|
62
63
|
.option("--dry-run", "only estimate how many messages, requests and minutes a full fetch would still take — from the store, no request")
|
|
63
64
|
.action(async function (chat) {
|
|
64
|
-
const { all, pause, sinceTime: since, last, background, dryRun: estimate, catchUp, catchUpChunks, catchUpMessages, catchUpTime, ...sizes } = this.opts();
|
|
65
|
+
const { all, pause, sinceTime: since, last, background, dryRun: estimate, catchUp, catchUpChunks, catchUpMessages, catchUpTime, fullHistory: include, ...sizes } = this.opts();
|
|
65
66
|
if ((chat === undefined) === (all !== true))
|
|
66
67
|
throw new CliError("validation_error", "name a chat, or --all for every chat");
|
|
67
68
|
if (all && estimate)
|
|
68
69
|
throw new CliError("validation_error", "--dry-run prices one chat; not with --all");
|
|
70
|
+
if (include && chat === undefined)
|
|
71
|
+
throw new CliError("validation_error", "--full-history keeps one chat: name it");
|
|
69
72
|
if (all && messenger.history === "store") {
|
|
70
73
|
throw new CliError("validation_error", `--all walks the messenger's chat list, and ${messenger.name ?? messenger.app.command} pushes its chats ` +
|
|
71
74
|
`instead — keep \`${lineOf(this, "serve")}\` running${messenger.historyOnDemand ? ", or name one chat" : ""}`);
|
|
@@ -164,6 +167,10 @@ export const syncCommand = (messenger) => {
|
|
|
164
167
|
const result = await context.withServices((services) => chat === undefined ? services.archive.fetchAll(options) : services.archive.fetch(chat, options));
|
|
165
168
|
if (jobId)
|
|
166
169
|
finishJob(messenger.app, jobs, jobId, { finishedAt: new Date().toISOString(), result });
|
|
170
|
+
const synced = "chat" in result ? result.chat : null;
|
|
171
|
+
const included = include && synced
|
|
172
|
+
? await context.withStore((store, account) => store.chats.setHistoryIncluded(account, synced, true))
|
|
173
|
+
: undefined;
|
|
167
174
|
const tasks = await taskRulesAfterSync(context);
|
|
168
175
|
const prepared = "prepared" in result ? result.prepared : undefined;
|
|
169
176
|
const unprepared = prepared && !prepared.complete && "chat" in result && result.chat !== null;
|
|
@@ -171,7 +178,11 @@ export const syncCommand = (messenger) => {
|
|
|
171
178
|
const review = await context.withServices((services) => services.mentions.newCount(point.after));
|
|
172
179
|
const toReview = review.count;
|
|
173
180
|
const shown = `${toReview}${review.more ? "+" : ""}`;
|
|
174
|
-
renderAnswer(this, context, itemOf(snakeKeys(
|
|
181
|
+
renderAnswer(this, context, itemOf(snakeKeys({
|
|
182
|
+
...result,
|
|
183
|
+
...(tasks && tasks.added + tasks.closed + (tasks.held ?? 0) > 0 ? { tasks } : {}),
|
|
184
|
+
...(included === undefined ? {} : { historyIncluded: included }),
|
|
185
|
+
}), {
|
|
175
186
|
next: [
|
|
176
187
|
...(unprepared
|
|
177
188
|
? [
|
|
@@ -231,6 +231,9 @@ export const connected = (connection, { app, provider, name, deletedWithoutChat,
|
|
|
231
231
|
recipientIds: () => (new RecipientList(recipientsPathFor(app, settings.profile, env), app.command).read() ?? []).flatMap((recipient) => [recipient.id, ...(recipient.partnerId ? [recipient.partnerId] : [])]),
|
|
232
232
|
...(history === "store" ? { namesFromStore: savedChatId ? { savedChatId } : {} } : {}),
|
|
233
233
|
...(unreadCounts === "store" ? { countsUnread: true } : {}),
|
|
234
|
+
...(history === "store" && settings.historyLimits
|
|
235
|
+
? { historyLimits: settings.historyLimits, command: `${app.command} ${settings.profile}` }
|
|
236
|
+
: {}),
|
|
234
237
|
});
|
|
235
238
|
const passives = history === "store" && self !== null && !watching
|
|
236
239
|
? passivesOf(saving).map((passive) => listened(passive, events, renderer.warn, closeWaitMs))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Missing, NextStep } from "@wirecat/cli-base/standards";
|
|
2
|
+
import type { AccountKey, MessageStore, StoredHistorySkip } from "../../store/store.js";
|
|
3
|
+
/**
|
|
4
|
+
* What the history limits (`history_*` settings) left out of pushed history, as `missing` and `next`: an answer read
|
|
5
|
+
* from the store must not pass for every message the messenger has.
|
|
6
|
+
*/
|
|
7
|
+
export declare const historyLimitGaps: (skips: StoredHistorySkip[]) => {
|
|
8
|
+
next: NextStep[];
|
|
9
|
+
missing: Missing[];
|
|
10
|
+
};
|
|
11
|
+
/** What the limits left out in every account an answer read, most messages first. */
|
|
12
|
+
export declare const historySkipsOf: (store: Pick<MessageStore, "chats">, accounts: readonly AccountKey[]) => Promise<StoredHistorySkip[]>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { nextStep } from "../steps.js";
|
|
2
|
+
const SHOWN = 5;
|
|
3
|
+
const reasonOf = (skip) => skip.reason === "group_too_large"
|
|
4
|
+
? `a group of ${skip.members ?? "many"} people, not saved`
|
|
5
|
+
: `only from ${skip.keptFrom?.slice(0, 10) ?? "the window's start"}`;
|
|
6
|
+
const nameOf = (skip) => skip.title ?? skip.chatId;
|
|
7
|
+
/**
|
|
8
|
+
* What the history limits (`history_*` settings) left out of pushed history, as `missing` and `next`: an answer read
|
|
9
|
+
* from the store must not pass for every message the messenger has.
|
|
10
|
+
*/
|
|
11
|
+
export const historyLimitGaps = (skips) => {
|
|
12
|
+
const [first] = skips;
|
|
13
|
+
if (!first)
|
|
14
|
+
return { next: [], missing: [] };
|
|
15
|
+
const messages = skips.reduce((sum, skip) => sum + skip.messages, 0);
|
|
16
|
+
const listed = skips
|
|
17
|
+
.slice(0, SHOWN)
|
|
18
|
+
.map((skip) => `${nameOf(skip)} (${skip.messages}, ${reasonOf(skip)})`)
|
|
19
|
+
.join("; ");
|
|
20
|
+
const more = skips.length > SHOWN ? `; and ${skips.length - SHOWN} more — \`store status\` lists them` : "";
|
|
21
|
+
return {
|
|
22
|
+
next: [
|
|
23
|
+
{
|
|
24
|
+
...nextStep("messages.sync", { args: { chat: first.chatId, full_history: true } }),
|
|
25
|
+
why: "fetches this chat's history and keeps it in full from now on, past the history limits",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
missing: [
|
|
29
|
+
{
|
|
30
|
+
field: "history_limits",
|
|
31
|
+
why: `the history limits (history_* settings) left ${messages} pushed messages of ${skips.length} chats out of ` +
|
|
32
|
+
`the store: ${listed}${more}`,
|
|
33
|
+
step: "messages.sync",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/** What the limits left out in every account an answer read, most messages first. */
|
|
39
|
+
export const historySkipsOf = async (store, accounts) => (await Promise.all(accounts.map((account) => store.chats.historySkips(account))))
|
|
40
|
+
.flat()
|
|
41
|
+
.sort((a, b) => b.messages - a.messages);
|
|
@@ -18,6 +18,7 @@ import { messengerContext } from "./context.js";
|
|
|
18
18
|
import { linksCommand } from "./conversations-command.js";
|
|
19
19
|
import { downloadSubcommand } from "./download-command.js";
|
|
20
20
|
import { heardItems, hearForCommand, hearingMissing, MODEL_OPTION, spokenItems, TRANSCRIBE_OPTION, } from "./hearing-command.js";
|
|
21
|
+
import { historyLimitGaps } from "./history-gaps.js";
|
|
21
22
|
import { cursorOf, historyQuery, historyWindow } from "./history-query.js";
|
|
22
23
|
import { hideNever } from "./lacks.js";
|
|
23
24
|
import { threadAnswer } from "./message-answers.js";
|
|
@@ -74,9 +75,12 @@ export const messagesCommand = (messenger) => {
|
|
|
74
75
|
: undefined;
|
|
75
76
|
// Offline, the store is the whole answer: say how much of the chat it holds. Online the messenger is read.
|
|
76
77
|
const gaps = context.settings.offline
|
|
77
|
-
?
|
|
78
|
-
await storedChatId(messenger, chat, store, account, { includeLeft: true })
|
|
79
|
-
|
|
78
|
+
? await context.withStore(async (store, account) => {
|
|
79
|
+
const chatId = await storedChatId(messenger, chat, store, account, { includeLeft: true });
|
|
80
|
+
const held = coverageGaps(await chatCoverage(store, account, [chatId]));
|
|
81
|
+
const limited = historyLimitGaps((await store.chats.historySkips(account)).filter((skip) => skip.chatId === chatId));
|
|
82
|
+
return { next: [...limited.next, ...held.next], missing: [...limited.missing, ...held.missing] };
|
|
83
|
+
})
|
|
80
84
|
: { next: [], missing: [] };
|
|
81
85
|
renderAnswer(this, context, listOf("messages list", {
|
|
82
86
|
items: messageItems(heardItems(page.items, hearing), reader),
|
|
@@ -12,6 +12,7 @@ import { renderAnswer } from "../envelope.js";
|
|
|
12
12
|
import { positiveCount } from "../paging.js";
|
|
13
13
|
import { archiveSummary } from "./archive-summary.js";
|
|
14
14
|
import { messengerContext } from "./context.js";
|
|
15
|
+
import { historySkipsOf } from "./history-gaps.js";
|
|
15
16
|
import { meaningNote, messagesAnswer, offsetOf, shownOf } from "./search-answers.js";
|
|
16
17
|
import { backendOptions, backendRequest, noteServer } from "./search-backend-options.js";
|
|
17
18
|
import { syncOptions, syncRequest, syncsFirst } from "./search-sync-options.js";
|
|
@@ -207,7 +208,10 @@ const searchAction = (messenger) => async function (given) {
|
|
|
207
208
|
...(newest ? { newest: true } : {}),
|
|
208
209
|
...(timezone === undefined ? {} : { timezone }),
|
|
209
210
|
};
|
|
210
|
-
|
|
211
|
+
const skips = await context
|
|
212
|
+
.withStore((store) => historySkipsOf(store, found.coverage?.accounts ?? found.accounts ?? []))
|
|
213
|
+
.catch(() => []);
|
|
214
|
+
renderAnswer(this, context, messagesAnswer("search messages", found, { limit, page: page ?? 1, again }, given.join(" "), skips), view);
|
|
211
215
|
};
|
|
212
216
|
const withOffset = (page, limit) => {
|
|
213
217
|
const offset = offsetOf(page, limit);
|
|
@@ -8,6 +8,7 @@ import type { MeaningSkipped } from "../../services/messages-discovery.js";
|
|
|
8
8
|
import type { FoundNote, NotesFoundOf } from "../../services/notes-search.js";
|
|
9
9
|
import type { SearchAllItem } from "../../services/search-all.js";
|
|
10
10
|
import type { MeetingSearchItem, SearchAllIncludingMeetingsFound } from "../../services/search-all-meetings.js";
|
|
11
|
+
import type { StoredHistorySkip } from "../../store/store.js";
|
|
11
12
|
import { type ListBody } from "../envelope.js";
|
|
12
13
|
/** What a source tool's `search all` reads: never mail, which is memo's (docs/dev/TOOL-BOUNDARIES.md). */
|
|
13
14
|
export declare const SOURCE_SEARCHED: readonly ["messages", "notes", "events", "meetings"];
|
|
@@ -42,10 +43,10 @@ export interface Paging {
|
|
|
42
43
|
/** "showing 20 of 144", for the human view under a page of hits; `+` when the total is a lower bound. */
|
|
43
44
|
export declare const shownOf: (shown: number, offset: number, total: SearchTotal | undefined) => string | undefined;
|
|
44
45
|
/** `search messages` and `search mail`, CLI and MCP alike. */
|
|
45
|
-
export declare const messagesAnswer: (command: string, found: SearchFound, { limit, page, again }: Paging, query?: string) => ListBody<MessageHitItem>;
|
|
46
|
+
export declare const messagesAnswer: (command: string, found: SearchFound, { limit, page, again }: Paging, query?: string, skips?: StoredHistorySkip[]) => ListBody<MessageHitItem>;
|
|
46
47
|
export declare const noteHitItem: (hit: FoundNote) => NoteHitItem;
|
|
47
48
|
export declare const allHitItem: (item: SearchAllItem | MeetingSearchItem) => HitItem;
|
|
48
49
|
/** `search notes`, CLI and MCP alike. */
|
|
49
50
|
export declare const notesAnswer: (command: string, found: NotesFoundOf, { limit, page, again }: Paging, exact?: boolean) => ListBody<NoteHitItem>;
|
|
50
51
|
/** `search all`, CLI and MCP alike. A bounded meeting scan that could not tell whether more match says so. */
|
|
51
|
-
export declare const allAnswer: (command: string, found: SearchAllIncludingMeetingsFound, { limit, page, again }: Paging, exact?: boolean) => ListBody<HitItem>;
|
|
52
|
+
export declare const allAnswer: (command: string, found: SearchAllIncludingMeetingsFound, { limit, page, again }: Paging, exact?: boolean, skips?: StoredHistorySkip[]) => ListBody<HitItem>;
|
|
@@ -7,6 +7,7 @@ import { MAX_SEARCH_OFFSET } from "../../services/messages-archive-search.js";
|
|
|
7
7
|
import { listOf } from "../envelope.js";
|
|
8
8
|
import { nextStep } from "../steps.js";
|
|
9
9
|
import { archiveSummary } from "./archive-summary.js";
|
|
10
|
+
import { historyLimitGaps } from "./history-gaps.js";
|
|
10
11
|
/** What a source tool's `search all` reads: never mail, which is memo's (docs/dev/TOOL-BOUNDARIES.md). */
|
|
11
12
|
export const SOURCE_SEARCHED = ["messages", "notes", "events", "meetings"];
|
|
12
13
|
const nonEmpty = (list) => (list.length > 0 ? list : undefined);
|
|
@@ -249,8 +250,10 @@ export const shownOf = (shown, offset, total) => total === undefined || shown ==
|
|
|
249
250
|
? undefined
|
|
250
251
|
: `showing ${offset + 1}–${offset + shown} of ${total.value}${total.relation === "gte" ? "+" : ""}`;
|
|
251
252
|
/** `search messages` and `search mail`, CLI and MCP alike. */
|
|
252
|
-
export const messagesAnswer = (command, found, { limit, page, again }, query = "") => {
|
|
253
|
+
export const messagesAnswer = (command, found, { limit, page, again }, query = "", skips = []) => {
|
|
253
254
|
const accounts = found.coverage?.accounts ?? found.accounts ?? [];
|
|
255
|
+
const gaps = gapsOf(found);
|
|
256
|
+
const limited = historyLimitGaps(skips);
|
|
254
257
|
return listOf(command, {
|
|
255
258
|
items: [...found.items.map((hit) => messageHitItem(hit, query)), ...pictureItems(found)],
|
|
256
259
|
limit,
|
|
@@ -258,8 +261,9 @@ export const messagesAnswer = (command, found, { limit, page, again }, query = "
|
|
|
258
261
|
page: page ?? 1,
|
|
259
262
|
...(found.total ? { total: found.total } : {}),
|
|
260
263
|
...(again ? { args: again } : {}),
|
|
261
|
-
...
|
|
262
|
-
|
|
264
|
+
next: [...gaps.next, ...limited.next],
|
|
265
|
+
missing: [...gaps.missing, ...limited.missing],
|
|
266
|
+
meta: metaOf(accounts, found.skipped, coverageGaps(found).missing.length > 0 || skips.length > 0),
|
|
263
267
|
});
|
|
264
268
|
};
|
|
265
269
|
/**
|
|
@@ -327,7 +331,8 @@ export const notesAnswer = (command, found, { limit, page, again }, exact = fals
|
|
|
327
331
|
meta: metaOf(found.accounts, found.skipped),
|
|
328
332
|
});
|
|
329
333
|
/** `search all`, CLI and MCP alike. A bounded meeting scan that could not tell whether more match says so. */
|
|
330
|
-
export const allAnswer = (command, found, { limit, page, again }, exact = false) => {
|
|
334
|
+
export const allAnswer = (command, found, { limit, page, again }, exact = false, skips = []) => {
|
|
335
|
+
const limited = historyLimitGaps(skips);
|
|
331
336
|
const scan = found.meetings?.complete === false ? found.meetings : undefined;
|
|
332
337
|
const notes = meaningGap(found.notes?.meaningSkipped, exact);
|
|
333
338
|
const messages = messageMeaningGap(found.messages?.meaningSkipped, "found_by");
|
|
@@ -351,6 +356,7 @@ export const allAnswer = (command, found, { limit, page, again }, exact = false)
|
|
|
351
356
|
...notes.next,
|
|
352
357
|
...messages.next.filter((step) => !notes.next.some((asked) => JSON.stringify(asked) === JSON.stringify(step))),
|
|
353
358
|
...archive.next,
|
|
359
|
+
...limited.next,
|
|
354
360
|
],
|
|
355
361
|
missing: [
|
|
356
362
|
...(scan
|
|
@@ -365,7 +371,8 @@ export const allAnswer = (command, found, { limit, page, again }, exact = false)
|
|
|
365
371
|
...notes.missing,
|
|
366
372
|
...messages.missing,
|
|
367
373
|
...archive.missing,
|
|
374
|
+
...limited.missing,
|
|
368
375
|
],
|
|
369
|
-
meta: metaOf(found.accounts, found.skipped, archive.missing.length > 0),
|
|
376
|
+
meta: metaOf(found.accounts, found.skipped, archive.missing.length > 0 || skips.length > 0),
|
|
370
377
|
});
|
|
371
378
|
};
|
|
@@ -10,6 +10,7 @@ import { runsSearchCommand } from "../runs/search-command.js";
|
|
|
10
10
|
import { commandAt } from "../steps.js";
|
|
11
11
|
import { messengerContext } from "./context.js";
|
|
12
12
|
import { conversationsSearchCommand } from "./conversations-command.js";
|
|
13
|
+
import { historySkipsOf } from "./history-gaps.js";
|
|
13
14
|
import { messagesSearchCommand } from "./messages-search-command.js";
|
|
14
15
|
import { allAnswer, meaningNote, notesAnswer, offsetOf, SOURCE_SEARCHED, shownOf, } from "./search-answers.js";
|
|
15
16
|
import { backendOptions, backendRequest, noteServer } from "./search-backend-options.js";
|
|
@@ -92,7 +93,8 @@ const allCommand = (messenger) => {
|
|
|
92
93
|
...(exact ? { exact: true } : {}),
|
|
93
94
|
...(timezone === undefined ? {} : { timezone }),
|
|
94
95
|
};
|
|
95
|
-
|
|
96
|
+
const skips = await context.withStore((store) => historySkipsOf(store, found.accounts)).catch(() => []);
|
|
97
|
+
renderAnswer(this, context, allAnswer("search all", found, { limit, page: page ?? 1, again }, false, skips), () => {
|
|
96
98
|
if (found.items.length === 0)
|
|
97
99
|
context.renderer.note(found.server && !found.server.skipped
|
|
98
100
|
? "nothing found in the local store or on the messenger's server"
|
|
@@ -2,6 +2,7 @@ import type { Id } from "../../domain/models.js";
|
|
|
2
2
|
import { type MessengerAdapter } from "../../ports/messenger.js";
|
|
3
3
|
import type { MessengerTraits } from "../../ports/traits.js";
|
|
4
4
|
import type { EventSink, Warn } from "../../runs/events.js";
|
|
5
|
+
import { type HistoryLimits } from "../../services/history-limits.js";
|
|
5
6
|
import type { AccountKey, DeletionScope, MessageStore } from "../../store/store.js";
|
|
6
7
|
export interface Saving {
|
|
7
8
|
account: AccountKey;
|
|
@@ -41,6 +42,10 @@ export interface Saving {
|
|
|
41
42
|
namesFromStore?: Pick<MessengerTraits, "savedChatId">;
|
|
42
43
|
/** `MessengerTraits.unreadCounts: "store"`: a message `watch` or `listen` saves raises its chat's unread count. */
|
|
43
44
|
countsUnread?: boolean;
|
|
45
|
+
/** Pushed history (`feed`, `listenHistory`) outside these is not saved, and each chat records what it left out. */
|
|
46
|
+
historyLimits?: HistoryLimits;
|
|
47
|
+
/** The app command and profile, for the warning that names the command listing what the limits left out. */
|
|
48
|
+
command?: string;
|
|
44
49
|
}
|
|
45
50
|
/**
|
|
46
51
|
* The adapter, with what each read answers written to the store. **A failed write never fails the
|
|
@@ -50,4 +55,4 @@ export interface Saving {
|
|
|
50
55
|
* would erase what `chats list` stored. Nor are `chat` and `contact`: a card's member list is not
|
|
51
56
|
* known to be whole, and `saveMembers` replaces the list.
|
|
52
57
|
*/
|
|
53
|
-
export declare const stored: (messenger: MessengerAdapter, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing, closing, namesFromStore, recipientIds, countsUnread, }: Saving) => MessengerAdapter;
|
|
58
|
+
export declare const stored: (messenger: MessengerAdapter, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing, closing, namesFromStore, recipientIds, countsUnread, historyLimits, command, }: Saving) => MessengerAdapter;
|
|
@@ -2,6 +2,7 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
2
2
|
import { capability, throughWrapper, } from "../../ports/messenger.js";
|
|
3
3
|
import { foldName, isId, pickPerson } from "../../resolve.js";
|
|
4
4
|
import { codeOf } from "../../sends/guarded.js";
|
|
5
|
+
import { limitBatch } from "../../services/history-limits.js";
|
|
5
6
|
import { chatIdIn } from "../../services/message-resolution.js";
|
|
6
7
|
const REKEY_PACING = { lockMs: 50, pauseMs: 25 };
|
|
7
8
|
/**
|
|
@@ -12,8 +13,37 @@ const REKEY_PACING = { lockMs: 50, pauseMs: 25 };
|
|
|
12
13
|
* would erase what `chats list` stored. Nor are `chat` and `contact`: a card's member list is not
|
|
13
14
|
* known to be whole, and `saveMembers` replaces the list.
|
|
14
15
|
*/
|
|
15
|
-
export const stored = (messenger, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing = REKEY_PACING, closing, namesFromStore, recipientIds, countsUnread = false, }) => {
|
|
16
|
+
export const stored = (messenger, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing = REKEY_PACING, closing, namesFromStore, recipientIds, countsUnread = false, historyLimits, command = "cli", }) => {
|
|
16
17
|
let warned = false;
|
|
18
|
+
let limitedNoted = false;
|
|
19
|
+
const limited = async (opened, batch) => {
|
|
20
|
+
if (!historyLimits || !batch.messages?.length)
|
|
21
|
+
return { batch, skipped: new Map() };
|
|
22
|
+
const shapes = await opened.chats.historyShapes(account, [...new Set(batch.messages.map((one) => one.chatId))]);
|
|
23
|
+
return limitBatch(batch, (chatId) => shapes.get(chatId), (chatId) => shapes.get(chatId)?.included === true, historyLimits);
|
|
24
|
+
};
|
|
25
|
+
const noteLimited = (skipped) => {
|
|
26
|
+
if (skipped.size === 0)
|
|
27
|
+
return;
|
|
28
|
+
const counts = {
|
|
29
|
+
chats: skipped.size,
|
|
30
|
+
messages: 0,
|
|
31
|
+
group_too_large: 0,
|
|
32
|
+
older_than_window: 0,
|
|
33
|
+
};
|
|
34
|
+
for (const skip of skipped.values()) {
|
|
35
|
+
counts.messages = (counts.messages ?? 0) + skip.messages;
|
|
36
|
+
counts[skip.reason] = (counts[skip.reason] ?? 0) + 1;
|
|
37
|
+
}
|
|
38
|
+
const warning = { event: "warning", code: "history_limited", operation: "history.save", counts };
|
|
39
|
+
events(warning);
|
|
40
|
+
if (limitedNoted)
|
|
41
|
+
return;
|
|
42
|
+
limitedNoted = true;
|
|
43
|
+
warn(`history limits left out ${counts.messages} pushed messages of ${skipped.size} chats (big groups, older ` +
|
|
44
|
+
`than the window) — \`${command} store status\` lists them; \`${command} messages sync <chat> --full-history\` ` +
|
|
45
|
+
"fetches one and keeps it in full", warning);
|
|
46
|
+
};
|
|
17
47
|
let recorded = profile === undefined;
|
|
18
48
|
const notWritten = (operation, error) => {
|
|
19
49
|
const warning = { event: "warning", code: "store_not_written", operation };
|
|
@@ -155,7 +185,13 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
|
|
|
155
185
|
// A refused re-key leaves its chat split, never the others or the batch's history unsaved.
|
|
156
186
|
const saving = previous.then(async () => {
|
|
157
187
|
await saveRekeys(rekeys, round);
|
|
158
|
-
return attempt(operation, (opened) =>
|
|
188
|
+
return attempt(operation, async (opened) => {
|
|
189
|
+
const { batch: kept, skipped } = await limited(opened, batch);
|
|
190
|
+
await fill(opened, account, kept);
|
|
191
|
+
if (skipped.size > 0)
|
|
192
|
+
await opened.chats.recordHistorySkips(account, skipped);
|
|
193
|
+
noteLimited(skipped);
|
|
194
|
+
});
|
|
159
195
|
});
|
|
160
196
|
previous = saving;
|
|
161
197
|
pending?.add(saving);
|
package/dist/cli/program.js
CHANGED
|
@@ -31,6 +31,8 @@ import { commandWords, isProfileName, liftProfile, sampleCommand, takesProfile,
|
|
|
31
31
|
import { absolutePaths, relayTarget } from "./relay-route.js";
|
|
32
32
|
import { NoProfileHere, namedProfile, settingsFor, } from "./settings.js";
|
|
33
33
|
import { assertSharedContracts } from "./shared-contracts.js";
|
|
34
|
+
import { skillSourceOf } from "./skill-command.js";
|
|
35
|
+
import { refreshSkills, skipsSkillRefresh } from "./skill-refresh.js";
|
|
34
36
|
import { commandAt, failureSteps, lineOf, withLines } from "./steps.js";
|
|
35
37
|
// An MCP server builds the tree on every tool call; the contracts of one definition's tree do not change between builds.
|
|
36
38
|
const contractsChecked = new WeakSet();
|
|
@@ -470,6 +472,9 @@ const runOnce = async (argv, definition, options) => {
|
|
|
470
472
|
try {
|
|
471
473
|
await control.race(() => program.parseAsync(rest, { from: "user" }));
|
|
472
474
|
const code = exit.code ?? (process.exitCode === undefined ? 0 : Number(process.exitCode));
|
|
475
|
+
const refreshed = code === 0 ? refreshFor(definition, program, rest, options) : undefined;
|
|
476
|
+
if (refreshed && !rest.includes("--quiet"))
|
|
477
|
+
streams.diagnostic(refreshed);
|
|
473
478
|
const hint = code === 0 && rest[0] !== "commands" && rest[0] !== "help" && !rest.includes("--quiet")
|
|
474
479
|
? hintFor(definition, rest, options)
|
|
475
480
|
: undefined;
|
|
@@ -555,6 +560,23 @@ const failingNoticeFor = (app, env, program) => {
|
|
|
555
560
|
return undefined;
|
|
556
561
|
}
|
|
557
562
|
};
|
|
563
|
+
const NO_REFRESH_COMMANDS = ["skill", "complete", "commands", "help"];
|
|
564
|
+
/** Not while `skill` runs: right after an update, `skill status` must report the copies as they are. */
|
|
565
|
+
const refreshFor = ({ app }, program, argv, options) => {
|
|
566
|
+
const env = options.env ?? process.env;
|
|
567
|
+
try {
|
|
568
|
+
if (NO_REFRESH_COMMANDS.includes(argv[0] ?? "") || skipsSkillRefresh(app, env))
|
|
569
|
+
return undefined;
|
|
570
|
+
const skill = skillSourceOf(program, app.appName);
|
|
571
|
+
if (!skill)
|
|
572
|
+
return undefined;
|
|
573
|
+
const state = resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).state;
|
|
574
|
+
return refreshSkills({ app, skill, env, markerPath: join(state, "skill-refresh.json") });
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
558
580
|
/** The update notice's state file, so both daily lines share one file per CLI. */
|
|
559
581
|
const hintFor = ({ app, configuration }, argv, options) => {
|
|
560
582
|
const env = options.env ?? process.env;
|
package/dist/cli/settings.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type AISettings } from "../analysis/settings.js";
|
|
|
4
4
|
import { type AppIdentity } from "../ports/app.js";
|
|
5
5
|
import type { RecordInput } from "../runs/run-context.js";
|
|
6
6
|
import { type Level, type PermissionKey } from "../sends/permissions.js";
|
|
7
|
+
import { type HistoryLimits } from "../services/history-limits.js";
|
|
7
8
|
import { type ConfigIssue } from "./config-issues.js";
|
|
8
9
|
import type { CommandPermissions } from "./permission-keys.js";
|
|
9
10
|
export declare const plain: (rule: string) => (issue: v.BaseIssue<unknown>) => string;
|
|
@@ -94,6 +95,8 @@ export interface Settings extends AISettings {
|
|
|
94
95
|
color: boolean | undefined;
|
|
95
96
|
senderColors: boolean;
|
|
96
97
|
searchCatchUp?: boolean;
|
|
98
|
+
/** How much pushed history the store keeps, by chat kind; a messenger that answers history on request ignores it. */
|
|
99
|
+
historyLimits?: HistoryLimits;
|
|
97
100
|
/** Search and `images embed` read the pictures themselves, with a model on this machine. Off until set. */
|
|
98
101
|
pictureSearch?: boolean;
|
|
99
102
|
/** The reranker that orders `search messages --discover`'s first hits once it is downloaded, or `off`. */
|
|
@@ -300,6 +303,9 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
|
|
|
300
303
|
transcribe_with: v.OptionalSchema<v.PicklistSchema<["auto", "messenger", "local"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
301
304
|
speech_model: v.OptionalSchema<v.StringSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
302
305
|
search_catch_up: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
306
|
+
history_dialog_days: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>, v.MaxValueAction<number, 1095, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
307
|
+
history_group_days: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>, v.MaxValueAction<number, 1095, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
308
|
+
history_group_members_max: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
303
309
|
picture_search: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
304
310
|
search_reranker: v.OptionalSchema<v.PicklistSchema<[...string[], "off"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
305
311
|
update_check: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
@@ -376,6 +382,9 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
|
|
|
376
382
|
transcribe_with: v.OptionalSchema<v.PicklistSchema<["auto", "messenger", "local"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
377
383
|
speech_model: v.OptionalSchema<v.StringSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
378
384
|
search_catch_up: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
385
|
+
history_dialog_days: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>, v.MaxValueAction<number, 1095, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
386
|
+
history_group_days: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>, v.MaxValueAction<number, 1095, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
387
|
+
history_group_members_max: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
379
388
|
picture_search: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
380
389
|
search_reranker: v.OptionalSchema<v.PicklistSchema<[...string[], "off"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
381
390
|
}, (issue: v.StrictObjectIssue) => string>, "has to be an object of profiles, by name">, {}>;
|