@wirecat/cli-core 0.561.0 → 0.563.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 +78 -0
- package/dist/cli/app-doctor-command.js +3 -1
- package/dist/cli/config-command.js +18 -1
- package/dist/cli/envelope.js +13 -1
- package/dist/cli/execution.js +1 -0
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/messenger/reply-stats-command.d.ts +4 -0
- package/dist/cli/messenger/reply-stats-command.js +57 -0
- package/dist/cli/messenger/search-command.js +8 -0
- package/dist/cli/messenger/stats-answers.d.ts +3 -0
- package/dist/cli/messenger/stats-answers.js +18 -0
- package/dist/cli/messenger/stats-command.js +5 -0
- package/dist/cli/output-budget.d.ts +26 -0
- package/dist/cli/output-budget.js +73 -0
- package/dist/cli/program.js +14 -1
- package/dist/cli/setting-defaults.d.ts +27 -0
- package/dist/cli/setting-defaults.js +36 -0
- package/dist/cli/settings.d.ts +5 -0
- package/dist/cli/settings.js +88 -5
- package/dist/cli/shared-command-exports.d.ts +8 -0
- package/dist/domain/admin-statistics.d.ts +5 -0
- package/dist/domain/admin-statistics.js +4 -3
- package/dist/entities/index.d.ts +31 -1
- package/dist/entities/index.js +4 -1
- package/dist/entities/reply-stats.d.ts +33 -0
- package/dist/entities/reply-stats.js +19 -0
- package/dist/errors/reasons.d.ts +1 -1
- package/dist/errors/reasons.js +2 -3
- package/dist/mail/attachments.d.ts +15 -2
- package/dist/mail/attachments.js +19 -3
- package/dist/mail/cli/index.d.ts +17 -1
- package/dist/mail/cli/index.js +1 -1
- package/dist/mail/cli/review.d.ts +5 -2
- package/dist/mail/cli/review.js +47 -39
- package/dist/mail/index.d.ts +2 -0
- package/dist/mail/index.js +2 -0
- package/dist/mail/partial.d.ts +3 -1
- package/dist/mail/partial.js +2 -2
- package/dist/mail/retry.d.ts +3 -1
- package/dist/mail/retry.js +3 -3
- package/dist/mail/sync-types.d.ts +3 -0
- package/dist/mail/sync.js +5 -3
- package/dist/mail/whole.d.ts +28 -0
- package/dist/mail/whole.js +69 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +2 -0
- package/dist/services/proposal-tasks.js +3 -12
- package/dist/services/proposals/kinds/action.js +3 -2
- package/dist/services/proposals/registry.js +8 -4
- package/dist/services/proposals/types.d.ts +2 -0
- package/dist/services/reply-stats.d.ts +48 -0
- package/dist/services/reply-stats.js +137 -0
- package/dist/store/decisions.d.ts +14 -11
- package/dist/store/decisions.js +9 -2
- package/dist/store/index.d.ts +3 -3
- package/dist/store/index.js +2 -2
- package/dist/store/queries.d.ts +4 -0
- package/dist/store/queries.js +3 -0
- package/dist/store/sqlite/email-locations.d.ts +4 -0
- package/dist/store/sqlite/email-locations.js +26 -11
- package/dist/store/sqlite/lucene-fields.d.ts +2 -0
- package/dist/store/sqlite/lucene-fields.js +3 -1
- package/dist/store/sqlite/mail-findings.js +1 -1
- package/dist/store/sqlite/meeting-findings.js +1 -1
- package/dist/store/sqlite/proposed-actions.d.ts +2 -0
- package/dist/store/sqlite/proposed-actions.js +24 -17
- package/dist/store/sqlite/reply-times.d.ts +57 -0
- package/dist/store/sqlite/reply-times.js +113 -0
- package/dist/store/sqlite/work-tasks.d.ts +12 -2
- package/dist/store/sqlite/work-tasks.js +35 -26
- package/dist/store/store.d.ts +1 -0
- package/package.json +1 -1
- package/parity.json +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,84 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.563.0 — 03.11.2026
|
|
11
|
+
|
|
12
|
+
### Changed — may break callers
|
|
13
|
+
|
|
14
|
+
- **Mail sync reads attachments up to 10 MB a file by default, and can skip them.** Until now every attachment was
|
|
15
|
+
read, whatever its size; at the next bump a file over 10 MB is no longer read into the index unless the caller
|
|
16
|
+
passes a policy. `MailSyncOptions.attachments` takes a `MailAttachmentPolicy` (`read`, `maxBytes`); left out it is
|
|
17
|
+
`DEFAULT_ATTACHMENT_POLICY`. A file the policy leaves is stored with its name, type and size but no text, with
|
|
18
|
+
`extractionError` `not-read` or `too-large` (`POLICY_SKIPS`), and the run's `attachments` log says the same.
|
|
19
|
+
Partial completion and `retryAttachments` follow it too.
|
|
20
|
+
- **Moving a task to another project moves its subtasks with it.** `tasks.edit(id, { project })` on a task with
|
|
21
|
+
subtasks no longer refuses with `task.has-subtasks`; that reason is gone. Every subtask, all the way down, takes the
|
|
22
|
+
next key in the new project and keeps its parent, and its old key still finds it. The answer names them as
|
|
23
|
+
`movedSubtasks: [{ from, to }]`, old key and new; the field is left out when nothing moved with the task. A subtask
|
|
24
|
+
moved on its own still needs `parent: null` in the same edit (`task.is-subtask`).
|
|
25
|
+
|
|
26
|
+
- **Owner-only proposals refuse an agent on every decide path.** `mayDecide` and `assertMayDecide` now read
|
|
27
|
+
the row's stored `kind`: a `mail_findings` or `meeting_findings` row is the owner's alone, whatever
|
|
28
|
+
`agent_decisions` says. The registry's `get` and `list` rows carry `ownerOnly` from their kind, and the generic
|
|
29
|
+
action kind marks a stored findings row too, so a host checking `assertMayDecide(await registry.get(id), …)` is
|
|
30
|
+
refused. `approveAsTask` and `proposedActions.approve` / `reject` refuse a mail findings batch as they refuse
|
|
31
|
+
meeting and evidence findings (`mail-findings.dedicated-review`), and the generic action kind no longer lists any
|
|
32
|
+
findings batch. Meeting findings were open to another agent with a reason before; now only the owner reviews them.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **An answer over the output cap says what to run instead (WC-OUT-22).** The `output.too-large` failure now names up
|
|
37
|
+
to two reads, both safe to run unasked: the same command line with `--compact`, when the command declares a row and
|
|
38
|
+
was not compact already, and the same line with a `--limit` scaled down to fit. The lines are rebuilt from the
|
|
39
|
+
words the caller typed, never from the answer. The failure also carries `needed_bytes`.
|
|
40
|
+
- **A JSON answer over 256 KiB adds one step to the compact form.** The answer is still printed whole. Nothing is
|
|
41
|
+
added when the answer already names two steps, over MCP, or for a command with no declared row.
|
|
42
|
+
- **`search messages --chat` and `search all --chat` say how to search one chat:** `chat:<chat>` in the query.
|
|
43
|
+
|
|
44
|
+
- **`readEmailWhole` reads one stored email again from its mail server, every attachment of it with no limit.** It
|
|
45
|
+
saves the new text on the email as a sync does, keeping its mailboxes, flags and rule, and answers each attachment's
|
|
46
|
+
bytes for the caller to save. Gmail's email is found by id, another server's at its stored place.
|
|
47
|
+
`store.mail.emailPlace(accountId, externalId)` gives an email's thread and stored places.
|
|
48
|
+
- **`stats messages responses`**: how fast replies come in one-to-one chats, the owner's (`--who me`) or the other
|
|
49
|
+
side's (`--who others`): count, median, p90, mean and four time buckets, in total or `--by day` in `--timezone`.
|
|
50
|
+
Several messages in a row followed by one answer are one reply, timed from the first. Reads by index, so all time
|
|
51
|
+
works on a large store; the query after `--` takes `date:`, `chat:` and `kind:` only.
|
|
52
|
+
- **`stats messages waiting`**: how many chats wait on the owner — a one-to-one chat whose newest message is a
|
|
53
|
+
question, or a group whose newest message mentions or replies to the owner — with the messages since the owner
|
|
54
|
+
last wrote, the oldest of them, and the split by kind. Muted or archived one-to-one chats are left out, as in
|
|
55
|
+
`inbox`.
|
|
56
|
+
- **tg, max, wa, mail, gcal and drive's config file holds every shared setting with its default.** A first run now
|
|
57
|
+
writes all of them into `defaults`, not five; `config set` and `config unset` add the missing ones (but not the one
|
|
58
|
+
just unset); and the new `<cli> config migrate` fills an existing file. Your values, their order and unknown keys
|
|
59
|
+
stay; a file without `defaults` gets that section, and no other section is created. `config show` gives each of
|
|
60
|
+
these settings its `default` and `description`, and no longer creates the file when there is none. `settingsFor`
|
|
61
|
+
returns `declaredSettings` and `migrate(path)`; `extension.settings` declares a CLI's own (cli-base WC-CMD-30).
|
|
62
|
+
Settings with no single default (`timeout_ms`, `color`, `requests_per_minute`, the embedding model) and objects
|
|
63
|
+
(`permissions`, `models`) are left out.
|
|
64
|
+
- **`MailReads.mount`: the steps a mail review hands on name where the host mounted it.** A host that mounts `review`
|
|
65
|
+
and `messages acknowledge` elsewhere passes their paths and the option naming the mailbox — memo passes
|
|
66
|
+
`review: "mail review"`, `conversation: null` (it mounts no thread read) and `mailbox: { option: "source", value }`
|
|
67
|
+
— and `next` names those paths and `--source email:<account>`. `mailReviewSteps(mount)` gives the definitions for
|
|
68
|
+
a host's step checks; a mail CLI leaves it out, and its step lines carry the profile as before.
|
|
69
|
+
|
|
70
|
+
## 0.562.0 — 03.11.2026
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **A CLI can write each of its settings into its config file with the default, so the owner sees every setting
|
|
75
|
+
there.** A CLI declares its scalar settings as `DeclaredSetting` (`key`, `default`, `description`).
|
|
76
|
+
`withSettingDefaults(raw, declared)` returns the file with each missing setting added at its default. It keeps the
|
|
77
|
+
owner's values, their order and unknown keys. It does not create a section that is not there.
|
|
78
|
+
`settingRows(raw, declared)` gives `config show` each setting with its value, default, description and `from`
|
|
79
|
+
(`config file` or `default`). memo is the first user (cli-base WC-CMD-30).
|
|
80
|
+
- **`KEEP_SUCCESSFUL_RUNS_FOR_DAYS` and `RUNS_SIZE_LIMIT_MB` are exported** beside `KEEP_RUNS_FOR_DAYS`. A host can
|
|
81
|
+
then name the run-record defaults it writes into its config file.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- **The shared `doctor` (`appDoctorCommand`) answers with the clock:** `meta.now`, `meta.now_local` and
|
|
86
|
+
`meta.timezone`, as a setup check should (WC-TIME-7). A CLI that kept its own doctor for this can use the shared one.
|
|
87
|
+
|
|
10
88
|
## 0.561.0 — 03.11.2026
|
|
11
89
|
|
|
12
90
|
### Changed — may break callers
|
|
@@ -2,6 +2,8 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { CliError, configFilePath, resolvePaths } from "@wirecat/cli-base";
|
|
4
4
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
5
|
+
import { timezoneOf } from "@wirecat/cli-base/query";
|
|
6
|
+
import { clockMeta } from "@wirecat/cli-base/standards";
|
|
5
7
|
import { isNewer, latestVersion } from "@wirecat/cli-base/update";
|
|
6
8
|
import { Command } from "commander";
|
|
7
9
|
import { envName, readOnlyLockOf } from "../ports/app.js";
|
|
@@ -149,7 +151,7 @@ export const appDoctorCommand = (app, configuration, options = {}) => profileFre
|
|
|
149
151
|
? await onlineCheck(app, env, environment.fetch ?? fetch, options)
|
|
150
152
|
: undefined;
|
|
151
153
|
const output = outputFor(this);
|
|
152
|
-
renderAnswer(this, output, itemOf({ ...report, ...local.fields, ...(online ? { online } : {}) }));
|
|
154
|
+
renderAnswer(this, output, itemOf({ ...report, ...local.fields, ...(online ? { online } : {}) }, { meta: clockMeta(Date.now(), timezoneOf()) }));
|
|
153
155
|
for (const fix of fixes)
|
|
154
156
|
output.renderer.note(`to make it private: ${fix}`);
|
|
155
157
|
for (const note of local.notes)
|
|
@@ -30,6 +30,7 @@ export const configCommand = (app, config, { permissionKey = config.permissions,
|
|
|
30
30
|
const bot = this.opts().bot === true;
|
|
31
31
|
const context = baseContext(this, (flags, options) => config.resolveSettings(flags, {
|
|
32
32
|
...options,
|
|
33
|
+
readOnly: true,
|
|
33
34
|
needsProfile: false,
|
|
34
35
|
withIssues: true,
|
|
35
36
|
...(bot ? { kind: "bot" } : {}),
|
|
@@ -48,7 +49,13 @@ export const configCommand = (app, config, { permissionKey = config.permissions,
|
|
|
48
49
|
paths_overridden: overridden,
|
|
49
50
|
settings: [...config.allSettings, "command_timeout_ms"]
|
|
50
51
|
.filter((setting) => bot || setting !== "read_other_bots")
|
|
51
|
-
.map((setting) =>
|
|
52
|
+
.map((setting) => {
|
|
53
|
+
const declared = config.declaredSettings.find(({ key }) => key === `defaults.${setting}`);
|
|
54
|
+
return {
|
|
55
|
+
...sourced(settings, setting),
|
|
56
|
+
...(declared ? { default: declared.default, description: declared.description } : {}),
|
|
57
|
+
};
|
|
58
|
+
}),
|
|
52
59
|
store_settings: await storeSettings(env),
|
|
53
60
|
}));
|
|
54
61
|
for (const issue of [
|
|
@@ -144,9 +151,19 @@ export const configCommand = (app, config, { permissionKey = config.permissions,
|
|
|
144
151
|
}));
|
|
145
152
|
});
|
|
146
153
|
}
|
|
154
|
+
command.addCommand(migrateCommand(app, config));
|
|
147
155
|
command.addCommand(repairCommand(app, config));
|
|
148
156
|
return command;
|
|
149
157
|
};
|
|
158
|
+
const migrateCommand = (app, config) => profileFree(grantsAuthority(annotate(new Command("migrate"), { effects: LOCAL_WRITE, examples: [`${app.command} config migrate`] })))
|
|
159
|
+
.description("write each setting the file lacks at its default, keeping yours")
|
|
160
|
+
.action(function () {
|
|
161
|
+
const context = baseContext(this, (flags, options) => config.resolveSettings(flags, { ...options, needsProfile: false, withIssues: true }));
|
|
162
|
+
const file = context.settings.configPath;
|
|
163
|
+
const added = config.migrate(file);
|
|
164
|
+
renderAnswer(this, context, itemOf({ config_file: file, added }));
|
|
165
|
+
context.renderer.note(added.length > 0 ? `added ${added.join(", ")} to ${file}` : `${file} already has every setting`);
|
|
166
|
+
});
|
|
150
167
|
/**
|
|
151
168
|
* Mends JSON syntax with one reading and keeps every value (NEED-2178): no network, no AI, since the file may hold
|
|
152
169
|
* keys. Anything else is refused with nothing changed.
|
package/dist/cli/envelope.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as v from "valibot";
|
|
|
5
5
|
import { currentRun } from "../runs/scope.js";
|
|
6
6
|
import { compactText, entityRows, rowShapeOf, rowsOfEntity, TEXT_CHARS } from "./compact.js";
|
|
7
7
|
import { environmentOf, setExitCode } from "./context.js";
|
|
8
|
+
import { largeAnswerSteps, SOFT_OUTPUT_BYTES } from "./output-budget.js";
|
|
8
9
|
import { withoutProviderRefs } from "./provider-refs.js";
|
|
9
10
|
import { nextStep, rootOf, runnable, stepLine, withLines, withSafety } from "./steps.js";
|
|
10
11
|
/** Every answer: a list or one thing, with items of `item` — any value where the command declares no entity. */
|
|
@@ -131,6 +132,16 @@ export const sealed = (value) => {
|
|
|
131
132
|
};
|
|
132
133
|
/** The steps a compact answer prints, each rerunning in the compact form the caller chose. */
|
|
133
134
|
const compactSteps = (root, steps) => steps.map((step) => step.kind === "command" ? { ...step, line: `${step.line ?? stepLine(root, step)} ${COMPACT_FLAG}` } : step);
|
|
135
|
+
/** A JSON answer past the soft budget, printed whole, with one more step: the same read in the compact form. */
|
|
136
|
+
const withLargeAnswerStep = (command, body) => {
|
|
137
|
+
if ((body.next?.length ?? 0) >= MAX_NEXT_STEPS)
|
|
138
|
+
return body;
|
|
139
|
+
const added = largeAnswerSteps(command);
|
|
140
|
+
if (added.length === 0 || Buffer.byteLength(JSON.stringify(body)) <= SOFT_OUTPUT_BYTES)
|
|
141
|
+
return body;
|
|
142
|
+
const root = rootOf(command);
|
|
143
|
+
return { ...body, next: [...(body.next ?? []), ...withLines(root, runnable(root, added))] };
|
|
144
|
+
};
|
|
134
145
|
/** An answer as JSON lines: one line per item, then the end line. One thing is a list of one that has no more. */
|
|
135
146
|
const linesOf = (body) => jsonlOf("items" in body ? body : { items: [body.item], page: { mode: "page", limit: 1, has_more: false }, meta: body.meta });
|
|
136
147
|
/** An answer as JSON-lines records, for a writer that streams objects: one per item, then the end record. */
|
|
@@ -151,9 +162,10 @@ export const renderAnswer = (command, { renderer, format, streams, compact, full
|
|
|
151
162
|
const kept = withoutProviderRefs({ ...body, ...(next.length > 0 ? { next } : {}) });
|
|
152
163
|
if (next.length === 0)
|
|
153
164
|
delete kept.next;
|
|
154
|
-
const
|
|
165
|
+
const sealedBody = sealed(kept);
|
|
155
166
|
const environment = environmentOf(command);
|
|
156
167
|
const drawn = compact && !environment.agent;
|
|
168
|
+
const whole = format === "json" && !drawn && !environment.agent ? withLargeAnswerStep(command, sealedBody) : sealedBody;
|
|
157
169
|
if (drawn) {
|
|
158
170
|
const lined = whole.next ? { ...whole, next: compactSteps(root, whole.next) } : whole;
|
|
159
171
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
package/dist/cli/execution.js
CHANGED
|
@@ -116,6 +116,7 @@ export const execution = (streams, { timeoutMs, fields, maxOutputBytes = DEFAULT
|
|
|
116
116
|
reason: "output.too-large",
|
|
117
117
|
max_bytes: maxOutputBytes,
|
|
118
118
|
emitted_bytes: bytes,
|
|
119
|
+
needed_bytes: bytes + size,
|
|
119
120
|
partial_output: bytes > 0,
|
|
120
121
|
});
|
|
121
122
|
bytes += size;
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export { type CacheEvent, type DiagnosticEvent, type EventSink, providerErrorKey
|
|
|
26
26
|
export { CORE_LOG_EVENTS } from "../runs/log-events.js";
|
|
27
27
|
export { openProviderLog, PROVIDER_LOG_BYTES, type ProviderWarnings, providerLogPathFor, providerWarnings, writeProviderWarning, } from "../runs/provider-log.js";
|
|
28
28
|
export { crashOf, type Recording, type RecordingOptions, recorded, startRecording, wasSettled, } from "../runs/recording.js";
|
|
29
|
-
export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, type RunContext, type RunMetadata, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
|
|
29
|
+
export { findRun, KEEP_RUNS_FOR_DAYS, KEEP_SUCCESSFUL_RUNS_FOR_DAYS, listRuns, pruneRuns, RUNS_SIZE_LIMIT_MB, type RunContext, type RunMetadata, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
|
|
30
30
|
export { runContextOf, secretInput } from "../runs/run-context.js";
|
|
31
31
|
export { type Attempt, currentAttempt, currentRun, inRun, type RunScope, runEvent, useAsProcessRun, withAttempt, } from "../runs/scope.js";
|
|
32
32
|
export { LANGUAGES } from "../search/languages.js";
|
|
@@ -119,6 +119,7 @@ export { type ProjectStatusHints, type ProjectStatusItem, projectStatusAnswer, p
|
|
|
119
119
|
export { STATUS_ROWS, STATUS_SIGNAL_ROWS } from "./project-status-rows.js";
|
|
120
120
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
|
121
121
|
export type { ScheduleHost } from "./schedule/schedule-command.js";
|
|
122
|
+
export { type DeclaredSetting, type SettingRow, settingRows, withSettingDefaults } from "./setting-defaults.js";
|
|
122
123
|
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";
|
|
123
124
|
export { type PermissionAnswer, readSetupAnswers, type SetupAnswers, setupPermissionCeiling } from "./setup-answers.js";
|
|
124
125
|
export { AGENTS, type Agent, agentFor, screenFor } from "./setup-screen.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -22,7 +22,7 @@ export { providerErrorKey, renderEvent, } from "../runs/events.js";
|
|
|
22
22
|
export { CORE_LOG_EVENTS } from "../runs/log-events.js";
|
|
23
23
|
export { openProviderLog, PROVIDER_LOG_BYTES, providerLogPathFor, providerWarnings, writeProviderWarning, } from "../runs/provider-log.js";
|
|
24
24
|
export { crashOf, recorded, startRecording, wasSettled, } from "../runs/recording.js";
|
|
25
|
-
export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
|
|
25
|
+
export { findRun, KEEP_RUNS_FOR_DAYS, KEEP_SUCCESSFUL_RUNS_FOR_DAYS, listRuns, pruneRuns, RUNS_SIZE_LIMIT_MB, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
|
|
26
26
|
export { runContextOf, secretInput } from "../runs/run-context.js";
|
|
27
27
|
export { currentAttempt, currentRun, inRun, runEvent, useAsProcessRun, withAttempt, } from "../runs/scope.js";
|
|
28
28
|
export { LANGUAGES } from "../search/languages.js";
|
|
@@ -108,6 +108,7 @@ export { projectAnswerParts, projectCutoffTime, projectEvidenceText, projectWind
|
|
|
108
108
|
export { projectStatusAnswer, projectStatusRow, projectStatusSchema, projectStatusText, } from "./project-status.js";
|
|
109
109
|
export { STATUS_ROWS, STATUS_SIGNAL_ROWS } from "./project-status-rows.js";
|
|
110
110
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
|
111
|
+
export { settingRows, withSettingDefaults } from "./setting-defaults.js";
|
|
111
112
|
export { count, first, flag, fromFile, NONE_NAMED, namedProfile, plain, settingsFor, } from "./settings.js";
|
|
112
113
|
export { readSetupAnswers, setupPermissionCeiling } from "./setup-answers.js";
|
|
113
114
|
export { AGENTS, agentFor, screenFor } from "./setup-screen.js";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { annotate } from "@wirecat/cli-base/commands";
|
|
2
|
+
import { Command, Option } from "commander";
|
|
3
|
+
import { LOCAL_READ } from "../effects.js";
|
|
4
|
+
import { renderAnswer } from "../envelope.js";
|
|
5
|
+
import { messengerContext } from "./context.js";
|
|
6
|
+
import { refuseJsonl, responsesAnswer, waitingAnswer } from "./stats-answers.js";
|
|
7
|
+
const QUERY = "date:, chat: and kind: terms, AND-ed — date>=30d, date:[60d TO 30d]; none reads all time";
|
|
8
|
+
const GUIDE = "Guide: https://github.com/WireCatLabs/cli-core/blob/main/docs/rankings.md#reply-times-and-chats-waiting-on-you";
|
|
9
|
+
const minutes = (ms) => (ms === null ? "—" : `${Math.round(ms / 60_000)} min`);
|
|
10
|
+
export const replyResponsesCommand = (messenger) => annotate(new Command("responses"), { effects: LOCAL_READ, output: { entity: "response_times" } })
|
|
11
|
+
.summary("how fast replies come in one-to-one chats: median, p90, mean and buckets")
|
|
12
|
+
.description("how long the owner took to answer (--who me), or the other side took to answer the owner (--who others), in " +
|
|
13
|
+
"one-to-one chats — the local store only. Several messages in a row followed by one answer count as one " +
|
|
14
|
+
"reply, timed from the first unanswered message; the day of a row is the day of the reply")
|
|
15
|
+
.argument("[query...]", QUERY)
|
|
16
|
+
.addOption(new Option("--who <side>", "whose replies: the owner's or the other side's").choices(["me", "others"]))
|
|
17
|
+
.addOption(new Option("--by <group>", "one row per calendar day of the reply, or none").choices(["day", "total"]))
|
|
18
|
+
.option("--timezone <zone>", "the IANA timezone for calendar days and date: bounds")
|
|
19
|
+
.option("--source <accounts>", "every account of this messenger held in the store; personal, bots or all")
|
|
20
|
+
.addHelpText("after", GUIDE)
|
|
21
|
+
.action(async function (words) {
|
|
22
|
+
const context = messengerContext(this, messenger);
|
|
23
|
+
refuseJsonl(context.format);
|
|
24
|
+
const { who, by, timezone, source } = this.opts();
|
|
25
|
+
const found = await context.withServices((services) => services.replyStats.responses({
|
|
26
|
+
who: who ?? "me",
|
|
27
|
+
by: by ?? "total",
|
|
28
|
+
...(words.length ? { text: words.join(" ") } : {}),
|
|
29
|
+
...(timezone === undefined ? {} : { timezone }),
|
|
30
|
+
...(source === undefined ? {} : { source }),
|
|
31
|
+
}));
|
|
32
|
+
renderAnswer(this, context, responsesAnswer(found), () => [
|
|
33
|
+
`${found.replies} replies · median ${minutes(found.medianMs)} · p90 ${minutes(found.p90Ms)} · mean ${minutes(found.meanMs)}`,
|
|
34
|
+
...found.rows.map((row) => `${row.key} ${row.replies} replies · median ${minutes(row.medianMs)}`),
|
|
35
|
+
"",
|
|
36
|
+
].join("\n"));
|
|
37
|
+
});
|
|
38
|
+
export const replyWaitingCommand = (messenger) => annotate(new Command("waiting"), { effects: LOCAL_READ, output: { entity: "waiting_chats" } })
|
|
39
|
+
.summary("chats whose newest message asks the owner something")
|
|
40
|
+
.description("how many chats wait on the owner — the local store only: a one-to-one chat whose newest message is a " +
|
|
41
|
+
"question, or a group whose newest message mentions the owner or replies to them. Muted or archived " +
|
|
42
|
+
"one-to-one chats are left out, as inbox leaves them out; date: bounds the newest message's time")
|
|
43
|
+
.argument("[query...]", QUERY)
|
|
44
|
+
.option("--timezone <zone>", "the IANA timezone for date: bounds")
|
|
45
|
+
.option("--source <accounts>", "every account of this messenger held in the store; personal, bots or all")
|
|
46
|
+
.addHelpText("after", GUIDE)
|
|
47
|
+
.action(async function (words) {
|
|
48
|
+
const context = messengerContext(this, messenger);
|
|
49
|
+
refuseJsonl(context.format);
|
|
50
|
+
const { timezone, source } = this.opts();
|
|
51
|
+
const found = await context.withServices((services) => services.replyStats.waiting({
|
|
52
|
+
...(words.length ? { text: words.join(" ") } : {}),
|
|
53
|
+
...(timezone === undefined ? {} : { timezone }),
|
|
54
|
+
...(source === undefined ? {} : { source }),
|
|
55
|
+
}));
|
|
56
|
+
renderAnswer(this, context, waitingAnswer(found), () => `${found.chats} chats wait on you (${found.byKind.private} one-to-one, ${found.byKind.group} groups) · ${found.messages} messages${found.oldestAt ? ` · oldest ${found.oldestAt}` : ""}\n`);
|
|
57
|
+
});
|
|
@@ -5,6 +5,7 @@ import { SAVE_HELP } from "../../meetings/index.js";
|
|
|
5
5
|
import { searchNotesOf } from "../../services/notes-search.js";
|
|
6
6
|
import { LOCAL_READ, REMOTE_READ } from "../effects.js";
|
|
7
7
|
import { renderAnswer } from "../envelope.js";
|
|
8
|
+
import { withOptionHints } from "../option-hints.js";
|
|
8
9
|
import { positiveCount } from "../paging.js";
|
|
9
10
|
import { runsSearchCommand } from "../runs/search-command.js";
|
|
10
11
|
import { commandAt } from "../steps.js";
|
|
@@ -193,5 +194,12 @@ export const searchCommand = (messenger, { topics = false } = {}) => {
|
|
|
193
194
|
if (command)
|
|
194
195
|
annotate(command, { examples: [`${messenger.app.command} personal search ${path} ${args}`] });
|
|
195
196
|
}
|
|
197
|
+
for (const path of ["all", "messages"]) {
|
|
198
|
+
const command = commandAt(search, path);
|
|
199
|
+
if (command)
|
|
200
|
+
withOptionHints(command, {
|
|
201
|
+
"--chat": `search in one chat with chat:<chat> in the query: search ${path} 'chat:<chat> words'`,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
196
204
|
return search;
|
|
197
205
|
};
|
|
@@ -8,6 +8,7 @@ import type { AdminEvidenceFound, AdminFound } from "../../services/admin-statis
|
|
|
8
8
|
import type { ChatStats } from "../../services/chat-stats.js";
|
|
9
9
|
import type { MessageStats } from "../../services/messages-search.js";
|
|
10
10
|
import type { RankingFound } from "../../services/rankings.js";
|
|
11
|
+
import type { ReplyTimes, Waiting } from "../../services/reply-stats.js";
|
|
11
12
|
import type { RetentionService } from "../../services/retention.js";
|
|
12
13
|
import type { AccountKey, RankedEvidence } from "../../store/store.js";
|
|
13
14
|
import type { GroupStats } from "../../tasks/index.js";
|
|
@@ -72,4 +73,6 @@ export declare const chatStatsAnswer: (stats: ChatStats, account: AccountKey, me
|
|
|
72
73
|
export declare const MEMBERS_UNRECORDED = "joins and leaves are not recorded for this chat yet, so they are not counted";
|
|
73
74
|
/** `stats tasks show`, CLI and MCP alike. */
|
|
74
75
|
export declare const taskStatsAnswer: (rows: GroupStats[], account: AccountKey) => ItemBody<Of<"task_stats">>;
|
|
76
|
+
export declare const responsesAnswer: (found: ReplyTimes) => ItemBody<Of<"response_times">>;
|
|
77
|
+
export declare const waitingAnswer: (found: Waiting) => ItemBody<Of<"waiting_chats">>;
|
|
75
78
|
export {};
|
|
@@ -420,3 +420,21 @@ export const taskStatsAnswer = (rows, account) => itemOf({
|
|
|
420
420
|
median_close_ms: row.medianCloseMs ?? null,
|
|
421
421
|
})),
|
|
422
422
|
});
|
|
423
|
+
const replyFigures = (one) => ({
|
|
424
|
+
replies: one.replies,
|
|
425
|
+
median_ms: one.medianMs,
|
|
426
|
+
p90_ms: one.p90Ms,
|
|
427
|
+
mean_ms: one.meanMs,
|
|
428
|
+
});
|
|
429
|
+
export const responsesAnswer = (found) => itemOf({
|
|
430
|
+
who: found.who,
|
|
431
|
+
...replyFigures(found),
|
|
432
|
+
buckets: {
|
|
433
|
+
under_5m: found.buckets.under5m,
|
|
434
|
+
under_1h: found.buckets.under1h,
|
|
435
|
+
under_1d: found.buckets.under1d,
|
|
436
|
+
over_1d: found.buckets.over1d,
|
|
437
|
+
},
|
|
438
|
+
rows: found.rows.map((row) => ({ key: row.key, ...replyFigures(row) })),
|
|
439
|
+
});
|
|
440
|
+
export const waitingAnswer = (found) => itemOf({ chats: found.chats, messages: found.messages, oldest_at: found.oldestAt, by_kind: found.byKind });
|
|
@@ -20,6 +20,7 @@ import { countersCommand } from "./counters-command.js";
|
|
|
20
20
|
import { messagesStatsCommand } from "./messages-stats-command.js";
|
|
21
21
|
import { officialStatsCommand } from "./official-stats-command.js";
|
|
22
22
|
import { rankingEvidenceCommand, rankingsTopCommand } from "./rankings-command.js";
|
|
23
|
+
import { replyResponsesCommand, replyWaitingCommand } from "./reply-stats-command.js";
|
|
23
24
|
import { retentionCommand } from "./retention-command.js";
|
|
24
25
|
import { chartAnswer, refuseJsonl } from "./stats-answers.js";
|
|
25
26
|
import { tasksStatsCommand } from "./tasks-command.js";
|
|
@@ -31,6 +32,8 @@ export const statsCommand = (messenger, loadRenderer = chartRenderer) => {
|
|
|
31
32
|
.addCommand(countersCommand(messenger))
|
|
32
33
|
.addCommand(adminStatisticsCommand(messenger, "unanswered"))
|
|
33
34
|
.addCommand(adminStatisticsCommand(messenger, "discussion"))
|
|
35
|
+
.addCommand(replyResponsesCommand(messenger))
|
|
36
|
+
.addCommand(replyWaitingCommand(messenger))
|
|
34
37
|
.addCommand(rankingsTopCommand(messenger, "messages"))
|
|
35
38
|
.addCommand(rankingEvidenceCommand(messenger, "messages")));
|
|
36
39
|
stats.addCommand(new Command("contacts")
|
|
@@ -96,10 +99,12 @@ export const statsCommand = (messenger, loadRenderer = chartRenderer) => {
|
|
|
96
99
|
"messages counters sync": '--chat "Book club" --dry-run',
|
|
97
100
|
"messages counters show": '--chat "Book club" --limit 10',
|
|
98
101
|
"messages discussion": 'chat:"Book club"',
|
|
102
|
+
"messages responses": "--who me --by day --timezone Europe/Madrid -- date>=30d",
|
|
99
103
|
"messages evidence": '"$message" --selection "$selection" --component reactions',
|
|
100
104
|
"messages show": '--chat "Book club" --by day',
|
|
101
105
|
"messages top": 'chat:"Book club" --limit 10',
|
|
102
106
|
"messages unanswered": 'chat:"Project room"',
|
|
107
|
+
"messages waiting": "kind:private",
|
|
103
108
|
"tasks show": '--chat "Project room"',
|
|
104
109
|
};
|
|
105
110
|
for (const [path, args] of Object.entries(examples)) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NextStep } from "@wirecat/cli-base/standards";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
/** About a third of a 200k-token context at ~3 bytes per token (inferred, not measured). */
|
|
4
|
+
export declare const SOFT_OUTPUT_BYTES: number;
|
|
5
|
+
/** The same read in a form that fits the cap: one line per item, then a `--limit` scaled down to fit (WC-OUT-22). */
|
|
6
|
+
export declare const tooLargeSteps: (command: Command, { maxBytes, neededBytes }: {
|
|
7
|
+
maxBytes: number;
|
|
8
|
+
neededBytes: number;
|
|
9
|
+
}) => ({
|
|
10
|
+
kind: "command";
|
|
11
|
+
step: string;
|
|
12
|
+
command: string;
|
|
13
|
+
args: {
|
|
14
|
+
[x: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
line?: string | undefined;
|
|
17
|
+
why: string;
|
|
18
|
+
safe_to_auto_execute: boolean;
|
|
19
|
+
} | {
|
|
20
|
+
kind: "user_action";
|
|
21
|
+
step: string;
|
|
22
|
+
why: string;
|
|
23
|
+
safe_to_auto_execute: false;
|
|
24
|
+
})[];
|
|
25
|
+
/** The step a large JSON answer adds: the same read, one line per item. */
|
|
26
|
+
export declare const largeAnswerSteps: (command: Command) => NextStep[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { entityRows, GENERIC_ROW, rowShapeOf } from "./compact.js";
|
|
2
|
+
import { environmentOf } from "./context.js";
|
|
3
|
+
import { rootOf } from "./steps.js";
|
|
4
|
+
/** About a third of a 200k-token context at ~3 bytes per token (inferred, not measured). */
|
|
5
|
+
export const SOFT_OUTPUT_BYTES = 256 * 1024;
|
|
6
|
+
const CARRIED = ["json", "compact", "offline", "timeout", "full", "brief", "maxOutputBytes"];
|
|
7
|
+
const NOT_WITH_COMPACT = new Set(["json", "brief"]);
|
|
8
|
+
const snake = (name) => name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
9
|
+
const typed = (command, key) => command.getOptionValueSource(key) === "cli";
|
|
10
|
+
/**
|
|
11
|
+
* The words the caller typed, as step arguments: never a default, never text an answer carried (WC-OUT-9). A
|
|
12
|
+
* `--no-x` flag and `--json=<fields>` have no form a step can carry, so a line holding one gets no step.
|
|
13
|
+
*/
|
|
14
|
+
const typedArgs = (command) => {
|
|
15
|
+
const root = rootOf(command);
|
|
16
|
+
if (root.getOptionValue("fields") !== undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (command.options.some((option) => option.negate && typed(command, option.attributeName())))
|
|
19
|
+
return undefined;
|
|
20
|
+
const positional = command.registeredArguments.flatMap((argument, index) => {
|
|
21
|
+
const value = command.processedArgs[index];
|
|
22
|
+
const word = Array.isArray(value) ? value.join(" ") : value;
|
|
23
|
+
return word === undefined || word === "" ? [] : [[argument.name(), word]];
|
|
24
|
+
});
|
|
25
|
+
const own = command.options
|
|
26
|
+
.map((option) => option.attributeName())
|
|
27
|
+
.filter((key) => typed(command, key))
|
|
28
|
+
.map((key) => [snake(key), command.getOptionValue(key)]);
|
|
29
|
+
const global = CARRIED.filter((key) => typed(root, key)).map((key) => [snake(key), root.getOptionValue(key)]);
|
|
30
|
+
return Object.fromEntries([...positional, ...own, ...global]);
|
|
31
|
+
};
|
|
32
|
+
const declaresRow = (command) => {
|
|
33
|
+
const { rows } = environmentOf(command);
|
|
34
|
+
return rowShapeOf(command, rows && entityRows(rows)) !== GENERIC_ROW;
|
|
35
|
+
};
|
|
36
|
+
const pathOf = (command) => {
|
|
37
|
+
const words = [];
|
|
38
|
+
for (let at = command; at.parent; at = at.parent)
|
|
39
|
+
words.unshift(at.name());
|
|
40
|
+
return words.join(" ");
|
|
41
|
+
};
|
|
42
|
+
const step = (command, id, args, why) => ({
|
|
43
|
+
kind: "command",
|
|
44
|
+
step: id,
|
|
45
|
+
command: pathOf(command),
|
|
46
|
+
args,
|
|
47
|
+
why,
|
|
48
|
+
safe_to_auto_execute: true,
|
|
49
|
+
});
|
|
50
|
+
const compactStep = (command, args, why) => rootOf(command).getOptionValue("compact") === true || !declaresRow(command)
|
|
51
|
+
? []
|
|
52
|
+
: [
|
|
53
|
+
step(command, "output.compact", { ...Object.fromEntries(Object.entries(args).filter(([key]) => !NOT_WITH_COMPACT.has(key))), compact: true }, why),
|
|
54
|
+
];
|
|
55
|
+
/** The same read in a form that fits the cap: one line per item, then a `--limit` scaled down to fit (WC-OUT-22). */
|
|
56
|
+
export const tooLargeSteps = (command, { maxBytes, neededBytes }) => {
|
|
57
|
+
const args = typedArgs(command);
|
|
58
|
+
if (!args)
|
|
59
|
+
return [];
|
|
60
|
+
const limit = Number(command.getOptionValue("limit"));
|
|
61
|
+
const fits = Math.floor(((limit * maxBytes) / neededBytes) * 0.9);
|
|
62
|
+
return [
|
|
63
|
+
...compactStep(command, args, "the answer is over the output cap as JSON; one line per item keeps every ref"),
|
|
64
|
+
...(Number.isSafeInteger(limit) && fits >= 1 && fits < limit
|
|
65
|
+
? [step(command, "output.limit", { ...args, limit: fits }, `about ${fits} items fit under the output cap`)]
|
|
66
|
+
: []),
|
|
67
|
+
];
|
|
68
|
+
};
|
|
69
|
+
/** The step a large JSON answer adds: the same read, one line per item. */
|
|
70
|
+
export const largeAnswerSteps = (command) => {
|
|
71
|
+
const args = typedArgs(command);
|
|
72
|
+
return args ? compactStep(command, args, "the answer is large as JSON; one line per item keeps every ref") : [];
|
|
73
|
+
};
|
package/dist/cli/program.js
CHANGED
|
@@ -29,6 +29,7 @@ import { installHelp } from "./help/first-screen.js";
|
|
|
29
29
|
import { showInputFields } from "./input-fields.js";
|
|
30
30
|
import { MAX_BUFFERED_INPUT, provideInputPolicy } from "./input-policy.js";
|
|
31
31
|
import { checkJson, liftJson } from "./json-fields.js";
|
|
32
|
+
import { tooLargeSteps } from "./output-budget.js";
|
|
32
33
|
import { PreviewComplete, preview } from "./preview.js";
|
|
33
34
|
import { commandWords, isProfileName, liftProfile, sampleCommand, takesProfile, valuedOptions } from "./profile.js";
|
|
34
35
|
import { absolutePaths, relayTarget } from "./relay-route.js";
|
|
@@ -705,6 +706,15 @@ const commandPath = (program, rest) => {
|
|
|
705
706
|
}
|
|
706
707
|
return words.join(" ");
|
|
707
708
|
};
|
|
709
|
+
const budgetSteps = (program, path, failure) => {
|
|
710
|
+
const command = path ? commandAt(program, path) : undefined;
|
|
711
|
+
if (!command || !(failure instanceof CliError) || failure.details?.reason !== "output.too-large")
|
|
712
|
+
return [];
|
|
713
|
+
const { max_bytes: maxBytes, needed_bytes: neededBytes } = failure.details;
|
|
714
|
+
return typeof maxBytes === "number" && typeof neededBytes === "number"
|
|
715
|
+
? tooLargeSteps(command, { maxBytes, neededBytes })
|
|
716
|
+
: [];
|
|
717
|
+
};
|
|
708
718
|
const failed = ({ app }, program, rest, error, settled, debug) => {
|
|
709
719
|
const path = commandPath(program, program.args.length > 0 ? program.args : rest);
|
|
710
720
|
const told = error instanceof NoProfileHere && !commandAt(program, "setup") ? error.withoutSetup(path) : error;
|
|
@@ -718,7 +728,10 @@ const failed = ({ app }, program, rest, error, settled, debug) => {
|
|
|
718
728
|
...(effects ? { effects } : {}),
|
|
719
729
|
});
|
|
720
730
|
const { next: _named, ...said } = built.error;
|
|
721
|
-
const steps = failureSteps(program,
|
|
731
|
+
const steps = failureSteps(program, {
|
|
732
|
+
...built.error,
|
|
733
|
+
next: [...(built.error.next ?? []), ...budgetSteps(program, path, failure)],
|
|
734
|
+
});
|
|
722
735
|
const object = { ...built, error: { ...said, ...(steps.length > 0 ? { next: steps } : {}) } };
|
|
723
736
|
showInputFields(object, (path && commandAt(program, path)) || program);
|
|
724
737
|
const command = path || app.command;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scalar setting a CLI writes into its config file with its default, so the owner finds and edits it there without
|
|
3
|
+
* reading the docs (WC-CMD-30). Never declare a secret: `config show` prints every declared value.
|
|
4
|
+
*/
|
|
5
|
+
export interface DeclaredSetting {
|
|
6
|
+
/** The dotted path in the file. A nested setting is written only where its section already exists. */
|
|
7
|
+
key: string;
|
|
8
|
+
default: string | number | boolean;
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SettingRow {
|
|
12
|
+
setting: string;
|
|
13
|
+
value: unknown;
|
|
14
|
+
default: string | number | boolean;
|
|
15
|
+
from: "config file" | "default";
|
|
16
|
+
description: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The file with every declared setting it lacks, at its default. What the owner wrote stays as written, in its order,
|
|
20
|
+
* unknown keys included; a section that is not there is not created, since it may need fields no default can give.
|
|
21
|
+
*/
|
|
22
|
+
export declare const withSettingDefaults: (raw: unknown, declared: readonly DeclaredSetting[]) => {
|
|
23
|
+
config: unknown;
|
|
24
|
+
added: string[];
|
|
25
|
+
};
|
|
26
|
+
/** Every declared setting with what holds and whether the file says it, for `config show`. */
|
|
27
|
+
export declare const settingRows: (raw: unknown, declared: readonly DeclaredSetting[]) => SettingRow[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const isNode = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2
|
+
const parentOf = (config, key) => {
|
|
3
|
+
const path = key.split(".");
|
|
4
|
+
const leaf = path.pop();
|
|
5
|
+
return { node: path.reduce((node, part) => (isNode(node) ? node[part] : undefined), config), leaf };
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The file with every declared setting it lacks, at its default. What the owner wrote stays as written, in its order,
|
|
9
|
+
* unknown keys included; a section that is not there is not created, since it may need fields no default can give.
|
|
10
|
+
*/
|
|
11
|
+
export const withSettingDefaults = (raw, declared) => {
|
|
12
|
+
if (raw !== undefined && !isNode(raw))
|
|
13
|
+
return { config: raw, added: [] };
|
|
14
|
+
const config = raw === undefined ? {} : structuredClone(raw);
|
|
15
|
+
const added = [];
|
|
16
|
+
for (const setting of declared) {
|
|
17
|
+
const { node, leaf } = parentOf(config, setting.key);
|
|
18
|
+
if (!isNode(node) || Object.hasOwn(node, leaf))
|
|
19
|
+
continue;
|
|
20
|
+
node[leaf] = setting.default;
|
|
21
|
+
added.push(setting.key);
|
|
22
|
+
}
|
|
23
|
+
return { config, added };
|
|
24
|
+
};
|
|
25
|
+
/** Every declared setting with what holds and whether the file says it, for `config show`. */
|
|
26
|
+
export const settingRows = (raw, declared) => declared.map((setting) => {
|
|
27
|
+
const { node, leaf } = isNode(raw) ? parentOf(raw, setting.key) : { node: undefined, leaf: "" };
|
|
28
|
+
const written = isNode(node) && Object.hasOwn(node, leaf);
|
|
29
|
+
return {
|
|
30
|
+
setting: setting.key,
|
|
31
|
+
value: written ? node[leaf] : setting.default,
|
|
32
|
+
default: setting.default,
|
|
33
|
+
from: written ? "config file" : "default",
|
|
34
|
+
description: setting.description,
|
|
35
|
+
};
|
|
36
|
+
});
|
package/dist/cli/settings.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type Level, type PermissionKey } from "../sends/permissions.js";
|
|
|
7
7
|
import { type HistoryLimits } from "../services/history-limits.js";
|
|
8
8
|
import { type ConfigIssue } from "./config-issues.js";
|
|
9
9
|
import type { CommandPermissions } from "./permission-keys.js";
|
|
10
|
+
import { type DeclaredSetting } from "./setting-defaults.js";
|
|
10
11
|
export declare const plain: (rule: string) => (issue: v.BaseIssue<unknown>) => string;
|
|
11
12
|
export declare const count: 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>]>;
|
|
12
13
|
export declare const flag: v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>;
|
|
@@ -32,6 +33,8 @@ export interface SettingsExtension<T extends object = Record<never, never>> {
|
|
|
32
33
|
profile?: v.ObjectEntries;
|
|
33
34
|
/** For the whole program only, never per profile. */
|
|
34
35
|
defaults?: v.ObjectEntries;
|
|
36
|
+
/** Its own scalar settings, written into the file's `defaults` beside the shared ones (WC-CMD-30). */
|
|
37
|
+
settings?: readonly DeclaredSetting[];
|
|
35
38
|
/** Profiles this messenger keeps in files of its own, beyond the config file and the remembered accounts. */
|
|
36
39
|
profiles?: (env: NodeJS.ProcessEnv) => string[];
|
|
37
40
|
/**
|
|
@@ -229,6 +232,8 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
|
|
|
229
232
|
kind?: ProfileKind | undefined;
|
|
230
233
|
root?: boolean;
|
|
231
234
|
}) => unknown;
|
|
235
|
+
migrate: (path: string) => string[];
|
|
236
|
+
declaredSettings: DeclaredSetting[];
|
|
232
237
|
allSettings: string[];
|
|
233
238
|
defaultsOnlyKeys: string[];
|
|
234
239
|
schema: v.StrictObjectSchema<{
|