@wirecat/cli-core 0.562.0 → 0.564.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 +139 -0
- package/dist/calendar/mcp/index.d.ts +12 -12
- 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/messenger/archive-commands.js +17 -0
- package/dist/cli/messenger/catch-up-answers.d.ts +5 -0
- package/dist/cli/messenger/catch-up-answers.js +3 -0
- 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-answers.d.ts +1 -0
- package/dist/cli/messenger/search-answers.js +1 -1
- package/dist/cli/messenger/search-command.js +8 -0
- package/dist/cli/messenger/stats-answers.d.ts +6 -3
- package/dist/cli/messenger/stats-answers.js +39 -9
- package/dist/cli/messenger/stats-command.js +5 -0
- package/dist/cli/messenger/tasks-command.js +35 -14
- 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/settings.d.ts +5 -0
- package/dist/cli/settings.js +88 -5
- package/dist/cli/shared-command-exports.d.ts +8 -0
- package/dist/conversations/mail-text.d.ts +6 -0
- package/dist/conversations/mail-text.js +73 -0
- package/dist/domain/admin-statistics.d.ts +5 -0
- package/dist/domain/admin-statistics.js +4 -3
- package/dist/domain/calendar.d.ts +2 -0
- package/dist/domain/calendar.js +6 -0
- package/dist/entities/chat-stats.d.ts +16 -2
- package/dist/entities/chat-stats.js +18 -3
- package/dist/entities/index.d.ts +80 -802
- package/dist/entities/index.js +26 -16
- package/dist/entities/mail-counts.d.ts +15 -0
- package/dist/entities/mail-counts.js +16 -0
- package/dist/entities/meeting.d.ts +1 -1
- package/dist/entities/meeting.js +1 -1
- package/dist/entities/reply-stats.d.ts +33 -0
- package/dist/entities/reply-stats.js +19 -0
- package/dist/entities/store-records.d.ts +8 -0
- package/dist/entities/store-records.js +8 -0
- package/dist/errors/reasons.d.ts +1 -1
- package/dist/errors/reasons.js +7 -3
- package/dist/mail/attachments.d.ts +15 -2
- package/dist/mail/attachments.js +19 -3
- package/dist/mail/cli/index.d.ts +22 -1
- package/dist/mail/cli/index.js +28 -4
- package/dist/mail/cli/review.d.ts +5 -2
- package/dist/mail/cli/review.js +58 -43
- package/dist/mail/index.d.ts +4 -0
- package/dist/mail/index.js +4 -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/stats.d.ts +50 -0
- package/dist/mail/stats.js +60 -0
- 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 +70 -0
- package/dist/meetings/index.d.ts +2 -2
- package/dist/meetings/index.js +2 -2
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/meetings/read-answers.d.ts +22 -1
- package/dist/meetings/read-answers.js +38 -0
- package/dist/meetings/stats.d.ts +44 -0
- package/dist/meetings/stats.js +77 -7
- package/dist/search/lucene/registry.js +2 -1
- package/dist/search/question-plan.d.ts +16 -0
- package/dist/search/question-plan.js +34 -0
- package/dist/services/events-search.d.ts +5 -1
- package/dist/services/events-search.js +10 -4
- package/dist/services/index.d.ts +3 -1
- package/dist/services/index.js +3 -1
- package/dist/services/meeting-stats.d.ts +12 -1
- package/dist/services/meeting-stats.js +12 -0
- package/dist/services/messages-discovery.d.ts +1 -5
- package/dist/services/messages-discovery.js +5 -18
- package/dist/services/messages-meaning.js +25 -3
- package/dist/services/messages.js +4 -3
- package/dist/services/notes-search.d.ts +5 -1
- package/dist/services/notes-search.js +16 -5
- package/dist/services/proposal-tasks.js +3 -12
- package/dist/services/proposals/ids.d.ts +2 -2
- package/dist/services/proposals/ids.js +5 -3
- package/dist/services/proposals/kinds/action.js +3 -2
- package/dist/services/proposals/kinds/findings.d.ts +1 -1
- package/dist/services/proposals/kinds/findings.js +1 -1
- package/dist/services/proposals/kinds/meeting-findings.d.ts +7 -0
- package/dist/services/proposals/kinds/meeting-findings.js +73 -0
- package/dist/services/proposals/kinds/task-close.d.ts +7 -0
- package/dist/services/proposals/kinds/task-close.js +76 -0
- package/dist/services/proposals/registry.js +12 -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/services/search-all.d.ts +5 -0
- package/dist/services/search-all.js +47 -14
- package/dist/services/task-stats.d.ts +41 -0
- package/dist/services/task-stats.js +126 -0
- package/dist/services/tasks.d.ts +17 -4
- package/dist/services/tasks.js +31 -14
- package/dist/store/chats.d.ts +3 -1
- package/dist/store/chats.js +6 -0
- package/dist/store/decisions.d.ts +16 -11
- package/dist/store/decisions.js +11 -2
- package/dist/store/exports.d.ts +1 -1
- package/dist/store/exports.js +1 -1
- package/dist/store/index.d.ts +5 -4
- package/dist/store/index.js +3 -3
- package/dist/store/queries.d.ts +4 -0
- package/dist/store/queries.js +3 -0
- package/dist/store/sqlite/completeness.d.ts +17 -0
- package/dist/store/sqlite/completeness.js +49 -13
- 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 +5 -1
- package/dist/store/sqlite/lucene-groups.d.ts +15 -0
- package/dist/store/sqlite/lucene-groups.js +21 -0
- package/dist/store/sqlite/lucene.d.ts +2 -9
- package/dist/store/sqlite/lucene.js +1 -12
- package/dist/store/sqlite/mail-findings.js +1 -1
- package/dist/store/sqlite/meeting-findings.js +1 -1
- package/dist/store/sqlite/meeting-stats.d.ts +2 -0
- package/dist/store/sqlite/meeting-stats.js +27 -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/retention.js +2 -7
- package/dist/store/sqlite/task-board.js +2 -0
- package/dist/store/sqlite/task-stats.d.ts +32 -0
- package/dist/store/sqlite/task-stats.js +45 -0
- package/dist/store/sqlite/tasks.d.ts +2 -1
- package/dist/store/sqlite/tasks.js +2 -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 +37 -1
|
@@ -410,13 +410,43 @@ export const chatStatsAnswer = (stats, account, meta) => {
|
|
|
410
410
|
};
|
|
411
411
|
/** Said where the store has kept no join, leave or other chat event for the chat: a 0 would read as nobody joined. */
|
|
412
412
|
export const MEMBERS_UNRECORDED = "joins and leaves are not recorded for this chat yet, so they are not counted";
|
|
413
|
-
/** `stats tasks show`, CLI and MCP alike. */
|
|
414
|
-
export const taskStatsAnswer = (
|
|
415
|
-
open:
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
413
|
+
/** `stats tasks show`, CLI and MCP alike. A row of `--by chat` also names its chat as `chat`, as it always has. */
|
|
414
|
+
export const taskStatsAnswer = (stats, account) => itemOf({
|
|
415
|
+
open: stats.open,
|
|
416
|
+
created: stats.created,
|
|
417
|
+
closed: stats.closed,
|
|
418
|
+
median_close_ms: stats.medianCloseMs ?? null,
|
|
419
|
+
mean_open_age_ms: stats.meanOpenAgeMs ?? null,
|
|
420
|
+
rows: stats.rows.map((row) => {
|
|
421
|
+
const chat = stats.by === "chat" ? chatRef(account, row.group) : undefined;
|
|
422
|
+
return {
|
|
423
|
+
...(chat === undefined ? {} : { chat }),
|
|
424
|
+
key: chat ?? (stats.by === "project" ? `project:${row.group}` : row.group),
|
|
425
|
+
name: row.name,
|
|
426
|
+
open: row.open,
|
|
427
|
+
created: row.created,
|
|
428
|
+
closed: row.closed,
|
|
429
|
+
oldest_open_at: row.oldestOpenAt?.toISOString() ?? null,
|
|
430
|
+
median_close_ms: row.medianCloseMs ?? null,
|
|
431
|
+
mean_open_age_ms: row.meanOpenAgeMs ?? null,
|
|
432
|
+
};
|
|
433
|
+
}),
|
|
434
|
+
});
|
|
435
|
+
const replyFigures = (one) => ({
|
|
436
|
+
replies: one.replies,
|
|
437
|
+
median_ms: one.medianMs,
|
|
438
|
+
p90_ms: one.p90Ms,
|
|
439
|
+
mean_ms: one.meanMs,
|
|
440
|
+
});
|
|
441
|
+
export const responsesAnswer = (found) => itemOf({
|
|
442
|
+
who: found.who,
|
|
443
|
+
...replyFigures(found),
|
|
444
|
+
buckets: {
|
|
445
|
+
under_5m: found.buckets.under5m,
|
|
446
|
+
under_1h: found.buckets.under1h,
|
|
447
|
+
under_1d: found.buckets.under1d,
|
|
448
|
+
over_1d: found.buckets.over1d,
|
|
449
|
+
},
|
|
450
|
+
rows: found.rows.map((row) => ({ key: row.key, ...replyFigures(row) })),
|
|
422
451
|
});
|
|
452
|
+
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)) {
|
|
@@ -8,7 +8,8 @@ import { aheadOf, momentOf } from "../../services/moment.js";
|
|
|
8
8
|
import { refuseAlreadyRead } from "../../services/task-analysis.js";
|
|
9
9
|
import { unlistedNote } from "../../services/task-categories.js";
|
|
10
10
|
import { ARRIVAL_WINDOW, applyTaskRulesToChats } from "../../services/task-rules.js";
|
|
11
|
-
import {
|
|
11
|
+
import { TASK_STATS_BY } from "../../services/task-stats.js";
|
|
12
|
+
import { CLAIM_LEASE_MS, closedStateOf, TASK_PAIR_QUERY, TASK_QUERY, TASK_STATS_QUERY, taskLinkKindOf, taskTypeOf, } from "../../services/tasks.js";
|
|
12
13
|
import { PRIORITY_LABELS, priorityOption } from "../../tasks/index.js";
|
|
13
14
|
import { actorOf, holderOf as holderIn } from "../author.js";
|
|
14
15
|
import { environmentOf } from "../context.js";
|
|
@@ -539,32 +540,52 @@ export const tasksCommand = (messenger) => {
|
|
|
539
540
|
});
|
|
540
541
|
return tasks;
|
|
541
542
|
};
|
|
543
|
+
const statsByOf = (value) => {
|
|
544
|
+
if (!TASK_STATS_BY.includes(value))
|
|
545
|
+
throw new CliError("validation_error", `--by takes ${TASK_STATS_BY.join(", ")}`);
|
|
546
|
+
return value;
|
|
547
|
+
};
|
|
548
|
+
const minutes = (ms) => (ms === undefined ? "" : `${Math.round(ms / 60_000)} min`);
|
|
542
549
|
export const tasksStatsCommand = (messenger) => {
|
|
543
|
-
return annotate(new Command("show"), {
|
|
544
|
-
|
|
550
|
+
return withQuery(annotate(new Command("show"), {
|
|
551
|
+
effects: LOCAL_READ,
|
|
552
|
+
output: { entity: "task_stats" },
|
|
553
|
+
examples: [`${messenger.app.command} stats tasks show --by meeting -- created>=30d`],
|
|
554
|
+
}), TASK_STATS_QUERY)
|
|
555
|
+
.summary("count tasks by chat, meeting or project, open and closed")
|
|
556
|
+
.description("tasks per chat, meeting or project: open now, and opened and closed in the period (created: in the query), " +
|
|
557
|
+
"with the median time to close and the mean age of the open ones")
|
|
558
|
+
.option("--by <group>", "chat (default) or meeting: this account's tasks; project: the owner's project tasks, every project", statsByOf)
|
|
545
559
|
.option("--chat <chat>", `only this chat; ${messenger.chatArgument}`)
|
|
546
560
|
.option("--type <type>", "only this type: question, request, mention or promise", taskTypeOf)
|
|
547
|
-
.
|
|
561
|
+
.option("--timezone <zone>", "the IANA timezone for calendar days in the period, such as created>=today")
|
|
562
|
+
.action(async function (words) {
|
|
548
563
|
const context = messengerContext(this, messenger);
|
|
549
|
-
const { chat, type } = this.opts();
|
|
564
|
+
const { by, chat, type, timezone } = this.opts();
|
|
550
565
|
refuseJsonl(context.format);
|
|
551
|
-
const
|
|
552
|
-
|
|
566
|
+
const query = listQuery(typedQuery(words), TASK_STATS_QUERY, "stats tasks show");
|
|
567
|
+
const { stats, account } = await context.withServices(async (services) => ({
|
|
568
|
+
stats: await services.tasks.stats({
|
|
569
|
+
...(by === undefined ? {} : { by }),
|
|
553
570
|
...(chat === undefined ? {} : { chat }),
|
|
554
|
-
...(type === undefined ? {} : { type }),
|
|
571
|
+
...(type === undefined ? {} : { kind: type }),
|
|
572
|
+
...(query === undefined ? {} : { query }),
|
|
573
|
+
...(timezone === undefined ? {} : { timezone }),
|
|
555
574
|
}),
|
|
556
575
|
account: await services.messages.reader(),
|
|
557
576
|
}));
|
|
558
|
-
if (rows.length === 0)
|
|
577
|
+
if (stats.rows.length === 0)
|
|
559
578
|
context.renderer.note("no tasks yet");
|
|
560
|
-
renderAnswer(this, context, taskStatsAnswer(
|
|
579
|
+
renderAnswer(this, context, taskStatsAnswer(stats, account), () => stats.rows.length === 0
|
|
561
580
|
? ""
|
|
562
|
-
: `${rows
|
|
563
|
-
.map(({ group, open, oldestOpenAt, medianCloseMs }) => [
|
|
564
|
-
group,
|
|
581
|
+
: `${stats.rows
|
|
582
|
+
.map(({ group, name, open, created, closed, oldestOpenAt, medianCloseMs }) => [
|
|
583
|
+
name ? `${name} (${group})` : group,
|
|
565
584
|
`${open} open`,
|
|
585
|
+
`${created} created`,
|
|
586
|
+
`${closed} closed`,
|
|
566
587
|
oldestOpenAt ? `oldest ${oldestOpenAt.toISOString().slice(0, 16).replace("T", " ")}` : "",
|
|
567
|
-
medianCloseMs === undefined ? "" : `median ${
|
|
588
|
+
medianCloseMs === undefined ? "" : `median ${minutes(medianCloseMs)} to close`,
|
|
568
589
|
]
|
|
569
590
|
.filter(Boolean)
|
|
570
591
|
.join(" "))
|
|
@@ -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;
|
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<{
|
package/dist/cli/settings.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { CliError, configFilePath, resolvePaths, saveConfigFile } from "@wirecat/cli-base";
|
|
3
3
|
import * as v from "valibot";
|
|
4
|
-
import { AI_ENTRIES, resolveAISettings } from "../analysis/settings.js";
|
|
4
|
+
import { AI_DEFAULTS, AI_ENTRIES, resolveAISettings } from "../analysis/settings.js";
|
|
5
5
|
import { parseDuration } from "../domain/duration.js";
|
|
6
6
|
import { DEFAULT_RERANK_MODEL, RERANK_MODELS } from "../embeddings/models.js";
|
|
7
7
|
import { envName, readOnlyLockOf } from "../ports/app.js";
|
|
@@ -14,6 +14,7 @@ import { checkConfigText, issueMessage, readConfigFile, readConfigText, savedPer
|
|
|
14
14
|
import { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
15
15
|
import { profilesHere } from "./messenger/accounts.js";
|
|
16
16
|
import { usableProfileName } from "./profile.js";
|
|
17
|
+
import { withSettingDefaults } from "./setting-defaults.js";
|
|
17
18
|
import { permissionCeiling } from "./setup-answers.js";
|
|
18
19
|
const DEFAULT_LIMIT = DEFAULT_CONFIG.defaults.limit;
|
|
19
20
|
const DEFAULT_KEEP_RUNS_FOR_DAYS = DEFAULT_CONFIG.defaults.keep_runs_for_days;
|
|
@@ -107,6 +108,70 @@ const SHARED_DEFAULTS_ONLY = {
|
|
|
107
108
|
update_check: v.optional(flag),
|
|
108
109
|
skill_hint: v.optional(flag),
|
|
109
110
|
};
|
|
111
|
+
/**
|
|
112
|
+
* Written into the file's `defaults` at the value the code applies when the file says nothing (WC-CMD-30). Left out:
|
|
113
|
+
* objects, and settings whose absence means something no value says (`timeout_ms`, `color`, `requests_per_minute`,
|
|
114
|
+
* the embedding model), since writing any value would change what they do.
|
|
115
|
+
*/
|
|
116
|
+
const SHARED_SETTINGS = [
|
|
117
|
+
{ key: "limit", default: DEFAULT_LIMIT, description: "how many items a list shows at once" },
|
|
118
|
+
{ key: "keep_runs_for_days", default: DEFAULT_KEEP_RUNS_FOR_DAYS, description: "days a run is kept" },
|
|
119
|
+
{
|
|
120
|
+
key: "sends_per_hour",
|
|
121
|
+
default: DEFAULT_SENDS_PER_HOUR,
|
|
122
|
+
description: "the most messages an account sends in an hour; a bot has none unless its section sets one",
|
|
123
|
+
},
|
|
124
|
+
{ key: "update_check", default: true, description: "check once a day for a newer version" },
|
|
125
|
+
{
|
|
126
|
+
key: "skill_hint",
|
|
127
|
+
default: true,
|
|
128
|
+
description: "tell an agent once a day that `skill install` would give it this tool's guide",
|
|
129
|
+
},
|
|
130
|
+
{ key: "record", default: true, description: "keep every run in runs list; false keeps failed runs only" },
|
|
131
|
+
{
|
|
132
|
+
key: "record_input",
|
|
133
|
+
default: true,
|
|
134
|
+
description: 'keep what was typed in a run\'s command line: true, false, or "failed" for failed runs only',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
key: "keep_successful_runs_for_days",
|
|
138
|
+
default: KEEP_SUCCESSFUL_RUNS_FOR_DAYS,
|
|
139
|
+
description: "days a successful run is kept",
|
|
140
|
+
},
|
|
141
|
+
{ key: "runs_size_limit_mb", default: RUNS_SIZE_LIMIT_MB, description: "the most megabytes all runs may take" },
|
|
142
|
+
{ key: "sender_colors", default: false, description: "color each sender's name in a conversation" },
|
|
143
|
+
{ key: "search_catch_up", default: false, description: "prepare local search after a gaps repair" },
|
|
144
|
+
{
|
|
145
|
+
key: "history_dialog_days",
|
|
146
|
+
default: DEFAULT_HISTORY_LIMITS.dialogDays,
|
|
147
|
+
description: "days of a one-to-one chat's pushed history the store keeps",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
key: "history_group_days",
|
|
151
|
+
default: DEFAULT_HISTORY_LIMITS.groupDays,
|
|
152
|
+
description: "days of a group's pushed history the store keeps",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
key: "history_group_members_max",
|
|
156
|
+
default: DEFAULT_HISTORY_LIMITS.groupMembersMax,
|
|
157
|
+
description: "a group with more people than this keeps no pushed history",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
key: "picture_search",
|
|
161
|
+
default: false,
|
|
162
|
+
description: "search and images embed read the pictures themselves, with a model on this computer",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
key: "search_reranker",
|
|
166
|
+
default: DEFAULT_RERANK_MODEL,
|
|
167
|
+
description: "the reranker that orders search's first hits, or off",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
key: "embedding_provider",
|
|
171
|
+
default: AI_DEFAULTS.embeddingProvider,
|
|
172
|
+
description: "local or openai: what turns text into vectors for search by meaning",
|
|
173
|
+
},
|
|
174
|
+
];
|
|
110
175
|
/** Where `profile` came from when none was named and the command needs none. */
|
|
111
176
|
export const NONE_NAMED = "none named";
|
|
112
177
|
/** A name for a command that speaks for no account; never chosen for one that does. */
|
|
@@ -174,12 +239,18 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
174
239
|
const allSettings = [...Object.keys(profileEntries), ...botOnlyKeys, ...defaultsOnlyKeys].filter((setting) => app.messenger !== false ||
|
|
175
240
|
!MESSENGER_ONLY_SETTINGS.includes(setting) ||
|
|
176
241
|
(app.sends === true && setting === "sends_per_hour"));
|
|
242
|
+
const readSchema = extension.schema ?? schema;
|
|
243
|
+
// An app with a schema of its own may refuse a shared key in `defaults`; writing it would break that file.
|
|
244
|
+
const declaredSettings = [...SHARED_SETTINGS, ...(extension.settings ?? [])]
|
|
245
|
+
.filter(({ key, default: value }) => allSettings.includes(key) && v.safeParse(readSchema, { defaults: { [key]: value } }).success)
|
|
246
|
+
.map((setting) => ({ ...setting, key: `defaults.${setting.key}` }));
|
|
247
|
+
// `defaults` needs no field of its own, so it is the one section created: every key then shows (RISK-2125).
|
|
248
|
+
const withDefaults = (raw, except = []) => withSettingDefaults(isScope(raw) && !Object.hasOwn(raw, "defaults") ? { ...raw, defaults: {} } : raw, declaredSettings.filter(({ key }) => !except.includes(key)));
|
|
177
249
|
const PROFILE = envName(app, "PROFILE");
|
|
178
250
|
const PROFILE_LOCK = envName(app, "PROFILE_LOCK");
|
|
179
251
|
const TIMEOUT = envName(app, "TIMEOUT");
|
|
180
252
|
const REQUESTS_PER_MINUTE = envName(app, "REQUESTS_PER_MINUTE");
|
|
181
253
|
const configPathFor = ({ env = process.env, configDir }) => configFilePath(configDir ?? resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).config);
|
|
182
|
-
const readSchema = extension.schema ?? schema;
|
|
183
254
|
const context = (file) => ({ file, tool: app.command });
|
|
184
255
|
/** The file with every problem found; what loads is the file without the settings they name. */
|
|
185
256
|
const loadConfig = (path) => {
|
|
@@ -225,7 +296,7 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
225
296
|
const configPath = configPathFor(options);
|
|
226
297
|
try {
|
|
227
298
|
if (!options.readOnly)
|
|
228
|
-
ensureDefaultConfig(configPath);
|
|
299
|
+
ensureDefaultConfig(configPath, withDefaults(DEFAULT_CONFIG).config);
|
|
229
300
|
}
|
|
230
301
|
catch (error) {
|
|
231
302
|
throw new CliError("configuration_error", error instanceof Error ? error.message : String(error));
|
|
@@ -435,7 +506,7 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
435
506
|
throw new CliError("validation_error", `${setting} has no problem to remove — name the profile, or add --defaults, to unset a setting`);
|
|
436
507
|
if (!known && valueAt(raw, at) === undefined)
|
|
437
508
|
throw new CliError("validation_error", `no setting called "${setting}" — one of: ${allSettings.join(", ")}`);
|
|
438
|
-
saveConfigFile(path, withoutPath(raw, at).config);
|
|
509
|
+
saveConfigFile(path, withDefaults(withoutPath(raw, at).config, [at.join(".")]).config);
|
|
439
510
|
return null;
|
|
440
511
|
}
|
|
441
512
|
if (!known)
|
|
@@ -456,13 +527,25 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
456
527
|
if (added)
|
|
457
528
|
throw new CliError("validation_error", `${setting} cannot be "${value}": ${added.what}`);
|
|
458
529
|
const checked = v.safeParse(readSchema, changed);
|
|
459
|
-
saveConfigFile(path, checked.success ? checked.output : changed);
|
|
530
|
+
saveConfigFile(path, withDefaults(checked.success ? checked.output : changed).config);
|
|
460
531
|
return valueAt(checked.success ? checked.output : changed, at) ?? null;
|
|
461
532
|
};
|
|
533
|
+
/** Each declared setting the file lacks, at its default, on the file as written; the keys added. */
|
|
534
|
+
const migrate = (path) => {
|
|
535
|
+
const read = readConfigFile(path);
|
|
536
|
+
if ("issue" in read)
|
|
537
|
+
throw new CliError("configuration_error", issueMessage([read.issue]));
|
|
538
|
+
const { config, added } = withDefaults(read.raw);
|
|
539
|
+
if (added.length > 0)
|
|
540
|
+
saveConfigFile(path, config);
|
|
541
|
+
return added;
|
|
542
|
+
};
|
|
462
543
|
return {
|
|
463
544
|
resolveSettings,
|
|
464
545
|
configuredProfiles,
|
|
465
546
|
changeSetting,
|
|
547
|
+
migrate,
|
|
548
|
+
declaredSettings,
|
|
466
549
|
allSettings,
|
|
467
550
|
defaultsOnlyKeys,
|
|
468
551
|
schema,
|
|
@@ -8,6 +8,8 @@ export declare const appDoctorCommand: (app: import("./entry.js").AppIdentity, c
|
|
|
8
8
|
kind?: import("./settings.js").ProfileKind | undefined;
|
|
9
9
|
root?: boolean;
|
|
10
10
|
}) => unknown;
|
|
11
|
+
migrate: (path: string) => string[];
|
|
12
|
+
declaredSettings: import("./setting-defaults.js").DeclaredSetting[];
|
|
11
13
|
allSettings: string[];
|
|
12
14
|
defaultsOnlyKeys: string[];
|
|
13
15
|
schema: import("valibot").StrictObjectSchema<{
|
|
@@ -188,6 +190,8 @@ export declare const appMcpCommand: (app: import("./entry.js").AppIdentity, conf
|
|
|
188
190
|
kind?: import("./settings.js").ProfileKind | undefined;
|
|
189
191
|
root?: boolean;
|
|
190
192
|
}) => unknown;
|
|
193
|
+
migrate: (path: string) => string[];
|
|
194
|
+
declaredSettings: import("./setting-defaults.js").DeclaredSetting[];
|
|
191
195
|
allSettings: string[];
|
|
192
196
|
defaultsOnlyKeys: string[];
|
|
193
197
|
schema: import("valibot").StrictObjectSchema<{
|
|
@@ -375,6 +379,8 @@ export declare const configCommand: (app: import("./entry.js").AppIdentity, conf
|
|
|
375
379
|
kind?: import("./settings.js").ProfileKind | undefined;
|
|
376
380
|
root?: boolean;
|
|
377
381
|
}) => unknown;
|
|
382
|
+
migrate: (path: string) => string[];
|
|
383
|
+
declaredSettings: import("./setting-defaults.js").DeclaredSetting[];
|
|
378
384
|
allSettings: string[];
|
|
379
385
|
defaultsOnlyKeys: string[];
|
|
380
386
|
schema: import("valibot").StrictObjectSchema<{
|
|
@@ -590,6 +596,8 @@ export declare const completeCommand: (messenger: import("./index.js").Messenger
|
|
|
590
596
|
kind?: import("./settings.js").ProfileKind | undefined;
|
|
591
597
|
root?: boolean;
|
|
592
598
|
}) => unknown;
|
|
599
|
+
migrate: (path: string) => string[];
|
|
600
|
+
declaredSettings: import("./setting-defaults.js").DeclaredSetting[];
|
|
593
601
|
allSettings: string[];
|
|
594
602
|
defaultsOnlyKeys: string[];
|
|
595
603
|
schema: import("valibot").StrictObjectSchema<{
|
|
@@ -4,3 +4,9 @@
|
|
|
4
4
|
* not hold stays, so a forward or a reply to an unsaved email is still found.
|
|
5
5
|
*/
|
|
6
6
|
export declare const mailCore: (body: string, earlier: string[]) => string;
|
|
7
|
+
/**
|
|
8
|
+
* What an email says itself, for a model that should not read earlier mail or the sender's signature: no quoted
|
|
9
|
+
* lines, nothing after a reply or Outlook header, no signature or mobile footer. A forwarded message is kept whole,
|
|
10
|
+
* since it is what was sent. When nothing would be left — an email that is only a quote — the body stays as it is.
|
|
11
|
+
*/
|
|
12
|
+
export declare const mailOwnText: (body: string) => string;
|
|
@@ -86,3 +86,76 @@ export const mailCore = (body, earlier) => {
|
|
|
86
86
|
const kept = trimEdges(lines.filter((_, i) => !drop.has(i)));
|
|
87
87
|
return withoutSignOff(withoutGreeting(kept)).join("\n").trim();
|
|
88
88
|
};
|
|
89
|
+
const QUOTED = /^\s*>/;
|
|
90
|
+
const ATTRIBUTION_END = /(wrote|написал(\(а\)|а|и)?|escribió|a écrit|schrieb\s.+|<[^<>\s]+@[^<>\s]+>)\s*:\s*$/iu;
|
|
91
|
+
/** The first line of an attribution a client wrapped in two: "On Mon, 3 Nov 2026 at 10:00, Alice Example <". */
|
|
92
|
+
const ATTRIBUTION_START = /^(on|le|am|el|в)\s/iu;
|
|
93
|
+
const DATED = /^(on|le|am|el|в|\d)/iu;
|
|
94
|
+
const ORIGINAL = /^-{2,}\s*(original message|ursprüngliche nachricht|message d'origine|mensaje original|исходное сообщение)\s*-{2,}$/iu;
|
|
95
|
+
const FROM_LINE = /^(from|von|de|от)\s?:\s*\S/iu;
|
|
96
|
+
const DATE_LINE = /^(sent|date|gesendet|datum|envoyé|enviado|fecha|отправлено|дата)\s?:/iu;
|
|
97
|
+
const FORWARD = /^(-{2,}\s*(forwarded message|weitergeleitete nachricht|message transféré|mensaje reenviado|пересылаемое сообщение)\s*-{2,}|begin forwarded message:)$/iu;
|
|
98
|
+
const RULE = /^_{5,}$/;
|
|
99
|
+
const MOBILE = /^(sent from my |von meinem .+ gesendet$|envoyé de mon |отправлено с |enviado desde mi )/iu;
|
|
100
|
+
/** Where an attribution ending at `i` begins, or -1 when the line is not one. */
|
|
101
|
+
const attributionAt = (lines, i) => {
|
|
102
|
+
const line = (lines[i] ?? "").trim();
|
|
103
|
+
if (!ATTRIBUTION_END.test(line))
|
|
104
|
+
return -1;
|
|
105
|
+
const prior = (lines[i - 1] ?? "").trim();
|
|
106
|
+
return prior && ATTRIBUTION_START.test(prior) && !ATTRIBUTION_START.test(line) ? i - 1 : i;
|
|
107
|
+
};
|
|
108
|
+
/** An Outlook header — From, then Sent or Date within the next lines — or its "Original Message" divider. */
|
|
109
|
+
const outlookAt = (lines, i) => {
|
|
110
|
+
const line = (lines[i] ?? "").trim();
|
|
111
|
+
if (ORIGINAL.test(line))
|
|
112
|
+
return true;
|
|
113
|
+
return FROM_LINE.test(line) && lines.slice(i + 1, i + 4).some((next) => DATE_LINE.test(next.trim()));
|
|
114
|
+
};
|
|
115
|
+
const nextText = (lines, from) => lines.slice(from).find((line) => line.trim()) ?? "";
|
|
116
|
+
/** The lines an email adds, in reading order: quotes dropped, and everything after a reply header cut. */
|
|
117
|
+
const ownLines = (lines) => {
|
|
118
|
+
const kept = [];
|
|
119
|
+
for (let i = 0; i < lines.length; i++) {
|
|
120
|
+
const line = lines[i] ?? "";
|
|
121
|
+
if (QUOTED.test(line))
|
|
122
|
+
continue;
|
|
123
|
+
if (outlookAt(lines, i)) {
|
|
124
|
+
const last = kept.findLastIndex((one) => one.trim());
|
|
125
|
+
return last !== -1 && RULE.test((kept[last] ?? "").trim()) ? kept.slice(0, last) : kept;
|
|
126
|
+
}
|
|
127
|
+
const start = attributionAt(lines, i);
|
|
128
|
+
if (start === -1) {
|
|
129
|
+
kept.push(line);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (start < i)
|
|
133
|
+
kept.pop();
|
|
134
|
+
if (QUOTED.test(nextText(lines, i + 1)))
|
|
135
|
+
continue;
|
|
136
|
+
// Without quote marks only a dated header is one: "Here is what Bob wrote:" is not.
|
|
137
|
+
const header = lines
|
|
138
|
+
.slice(start, i + 1)
|
|
139
|
+
.join(" ")
|
|
140
|
+
.trim();
|
|
141
|
+
if (/\d/.test(header) && (header.includes("@") || DATED.test(header)) && header.length <= 300)
|
|
142
|
+
return kept;
|
|
143
|
+
if (start < i)
|
|
144
|
+
kept.push(lines[start] ?? "");
|
|
145
|
+
kept.push(line);
|
|
146
|
+
}
|
|
147
|
+
return kept;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* What an email says itself, for a model that should not read earlier mail or the sender's signature: no quoted
|
|
151
|
+
* lines, nothing after a reply or Outlook header, no signature or mobile footer. A forwarded message is kept whole,
|
|
152
|
+
* since it is what was sent. When nothing would be left — an email that is only a quote — the body stays as it is.
|
|
153
|
+
*/
|
|
154
|
+
export const mailOwnText = (body) => {
|
|
155
|
+
const lines = body.replace(/\r\n?/g, "\n").split("\n");
|
|
156
|
+
const forward = lines.findIndex((line) => FORWARD.test(line.trim()));
|
|
157
|
+
const head = forward === -1 ? lines : lines.slice(0, forward);
|
|
158
|
+
const own = withoutSignOff(trimEdges(ownLines(head).filter((line) => !MOBILE.test(line.trim()))));
|
|
159
|
+
const text = [...own, ...(forward === -1 ? [] : ["", ...lines.slice(forward)])].join("\n").trim();
|
|
160
|
+
return text || body.trim();
|
|
161
|
+
};
|
|
@@ -75,4 +75,9 @@ export interface AdminCalculated {
|
|
|
75
75
|
graph: ReturnType<typeof rankReplyGraph>["quality"];
|
|
76
76
|
}
|
|
77
77
|
export declare const adminEntityKey: (row: Pick<AdminRow, "account" | "id">) => string;
|
|
78
|
+
/** Median and nearest-rank p90 of reply delays, the numbers every response-time answer reports. */
|
|
79
|
+
export declare const delayStats: (delays: number[]) => {
|
|
80
|
+
medianMilliseconds: number | null;
|
|
81
|
+
p90Milliseconds: number | null;
|
|
82
|
+
};
|
|
78
83
|
export declare const calculateAdminStatistics: (nodes: AdminNode[], stays: AdminStay[], options: AdminOptions, inspect: () => void) => AdminCalculated;
|
|
@@ -6,7 +6,8 @@ import { rankingQuestion } from "./rankings-options.js";
|
|
|
6
6
|
export const adminEntityKey = (row) => JSON.stringify([row.account.provider, row.account.account, row.id]);
|
|
7
7
|
const personKey = (node) => JSON.stringify([node.account.provider, node.account.account, node.sender]);
|
|
8
8
|
const locator = (node) => formatLocator({ ...node.account, chat: node.chatId, message: node.id });
|
|
9
|
-
|
|
9
|
+
/** Median and nearest-rank p90 of reply delays, the numbers every response-time answer reports. */
|
|
10
|
+
export const delayStats = (delays) => {
|
|
10
11
|
const sorted = delays.toSorted((a, b) => a - b);
|
|
11
12
|
const middle = Math.floor(sorted.length / 2);
|
|
12
13
|
return {
|
|
@@ -103,7 +104,7 @@ export const calculateAdminStatistics = (nodes, stays, options, inspect) => {
|
|
|
103
104
|
account: { provider: person.provider, account: person.account },
|
|
104
105
|
questions: eligible.filter((node) => node.account.provider === person.provider && node.account.account === person.account).length,
|
|
105
106
|
answered: rows.length,
|
|
106
|
-
...
|
|
107
|
+
...delayStats(rows.map((row) => row.contribution)),
|
|
107
108
|
status: "observed",
|
|
108
109
|
}, rows);
|
|
109
110
|
}
|
|
@@ -154,7 +155,7 @@ export const calculateAdminStatistics = (nodes, stays, options, inspect) => {
|
|
|
154
155
|
pending: end > options.cutoff,
|
|
155
156
|
questions: held.length,
|
|
156
157
|
answered: delays.length,
|
|
157
|
-
...
|
|
158
|
+
...delayStats(delays),
|
|
158
159
|
status: "partial",
|
|
159
160
|
}, contributions);
|
|
160
161
|
}
|