@wirecat/cli-core 0.528.0 → 0.530.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 +45 -0
- package/dist/cli/config-command.d.ts +11 -1
- package/dist/cli/config-command.js +12 -10
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/messenger/download-command.d.ts +2 -1
- package/dist/cli/messenger/download-command.js +2 -1
- package/dist/cli/messenger/tasks-command.d.ts +2 -0
- package/dist/cli/messenger/tasks-command.js +1 -1
- package/dist/cli/shared-command-exports.d.ts +1 -0
- package/dist/entities/index.d.ts +12 -0
- package/dist/entities/task-results.d.ts +10 -0
- package/dist/entities/task-results.js +2 -0
- package/dist/entities/task.d.ts +3 -0
- package/dist/entities/task.js +3 -0
- package/dist/files/index.d.ts +1 -0
- package/dist/files/index.js +1 -0
- package/dist/files/safe-name.d.ts +5 -0
- package/dist/files/safe-name.js +18 -0
- package/dist/mail/sender-scores.js +6 -1
- package/dist/meetings/findings-rules.d.ts +6 -1
- package/dist/meetings/findings-rules.js +14 -4
- package/dist/services/file-download.d.ts +2 -5
- package/dist/services/file-download.js +3 -18
- package/dist/store/evidence-findings.d.ts +4 -1
- package/dist/store/evidence-findings.js +9 -3
- package/dist/store/people.d.ts +4 -0
- package/dist/store/people.js +12 -0
- package/dist/store/sqlite/context-evidence.js +14 -0
- package/dist/store/sqlite/evidence-findings.js +16 -8
- package/dist/store/sqlite/findings-core.d.ts +2 -2
- package/dist/store/sqlite/findings-core.js +20 -11
- package/dist/store/sqlite/task-board.d.ts +11 -2
- package/dist/store/sqlite/task-board.js +38 -5
- package/dist/store/sqlite/work-tasks.d.ts +21 -3
- package/dist/store/sqlite/work-tasks.js +39 -8
- package/dist/tasks/priority.js +5 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,51 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.530.0 — 01.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`configCommand(app, config, { ownerOnly })`**: settings and permission keys an agent with `mcp.authority` still
|
|
15
|
+
cannot change over MCP, beside `mcp.*` and `bot.api` — for a mail CLI, the server hosts its password goes to, and
|
|
16
|
+
the key that lets an agent mail other people. A permission key there covers the keys under it.
|
|
17
|
+
- **`safeName` from `./files`**: the file-name cleaner downloads use (no path, no leading dot, no control or direction
|
|
18
|
+
character; Windows' reserved names refused), for a CLI that saves a file someone else named.
|
|
19
|
+
|
|
20
|
+
## 0.529.0 — 01.11.2026
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`people.add({ name })`** adds a person known by name only, to assign a task to before any message names them.
|
|
25
|
+
`tasks.taskPeople({ limit })` lists everyone the store knows, the owner first, then by how many live tasks name
|
|
26
|
+
them; it took only the owner and people with a task role.
|
|
27
|
+
- **`tasks.create` takes `status`**, so a task starts in its column with its due date and agent in one write.
|
|
28
|
+
- **`priorityOf` reads a score with a decimal point**, `0.0` to `1.0`; a bare `0` … `4` keeps the old scale.
|
|
29
|
+
- **`when:` in the project task query**: `overdue`, `today`, `upcoming` (the 7 days after today), `later` or
|
|
30
|
+
`none`, by the owner's calendar day. `boardCounts` counts them in `due`, and counts tasks by `agent`; each project
|
|
31
|
+
count carries the `ref` a task's `project` names. A task carries `projectKey` and `projectName`
|
|
32
|
+
(`project_key`, `project_name` in the `task` and `task_card` entities).
|
|
33
|
+
- **A findings batch can be about a person, an organization, a task or nobody**, and cite a `note:`: a fact finding
|
|
34
|
+
takes an optional `subject` and may name no project; a task finding still names its project. A batch holds at most
|
|
35
|
+
one project. `contextRecordOf` reads notes, so a note's fingerprint can be cited. `evidenceFindings.list({ project })`
|
|
36
|
+
also finds a batch whose findings name that project.
|
|
37
|
+
- **`dateOrNone`** from `./cli`: the `--due` parser of the shared `tasks update`, for a CLI's own `tasks add`.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **`overdue:true` no longer matches a task due later today**: it is open work due before today, in the owner's zone.
|
|
42
|
+
- **A task's history gives due dates and other times as ISO**, as the task does, not epoch milliseconds.
|
|
43
|
+
- **Scoring mail senders no longer hides a broken store.** `scoreSenders` treated any failure of the account lookup
|
|
44
|
+
as "no such account" and returned quietly; now only `not_found` does, and a locked or damaged store fails the
|
|
45
|
+
sync as it does elsewhere.
|
|
46
|
+
|
|
47
|
+
### Changed — may break callers
|
|
48
|
+
|
|
49
|
+
- **`EvidenceFindingsBatch.project` is `null`** when the findings name no project, and the batch carries `subject`.
|
|
50
|
+
A finding's `project` is optional in `FindingsInput`; meeting findings still require it.
|
|
51
|
+
- **Needs `@wirecat/cli-base` 0.71.x** (peer range `>=0.71.0 <0.72.0`, was 0.68.x). 0.71.0 lets `--to` take an e-mail
|
|
52
|
+
address and `--subject` a subject line in the shared command vocabulary, which mail-cli uses. A CLI on cli-base
|
|
53
|
+
0.68 or 0.69 moves to 0.71 together with this version.
|
|
54
|
+
|
|
10
55
|
## 0.528.0 — 01.11.2026
|
|
11
56
|
|
|
12
57
|
### Added
|
|
@@ -6,8 +6,18 @@ import { type Configuration } from "./settings.js";
|
|
|
6
6
|
* The settings in force and where each came from, and changing them. Printed whole: no field the
|
|
7
7
|
* configuration accepts can hold a secret — the schema has nowhere to put one.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Settings and permission keys an agent with `mcp.authority` still may not change, beyond the ones that let it in:
|
|
11
|
+
* where the password goes (a server host), or a permission the app reads only from the owner's file.
|
|
12
|
+
*/
|
|
13
|
+
export interface OwnerOnlySettings {
|
|
14
|
+
settings?: readonly string[];
|
|
15
|
+
/** A key here covers the keys under it too: `messages.send.others` covers `messages.send.others.x`. */
|
|
16
|
+
permissions?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export declare const configCommand: (app: AppIdentity, config: Configuration, { permissionKey, ownerOnly, }?: {
|
|
10
19
|
permissionKey?: PermissionKeySource | undefined;
|
|
20
|
+
ownerOnly?: OwnerOnlySettings;
|
|
11
21
|
}) => Command;
|
|
12
22
|
/**
|
|
13
23
|
* A key no command or write is checked against would be saved and do nothing (BUG-137) — whether it
|
|
@@ -10,11 +10,7 @@ import { knownBeside, knownPermissionKeys, unknownPermissionIssues, } from "./pe
|
|
|
10
10
|
import { fromFile, NONE_NAMED } from "./settings.js";
|
|
11
11
|
import { nextStep } from "./steps.js";
|
|
12
12
|
import { changeStoreSetting, isStoreSetting, offersStoreSetting, storeSettings, storeSettingsOf, } from "./store-settings.js";
|
|
13
|
-
|
|
14
|
-
* The settings in force and where each came from, and changing them. Printed whole: no field the
|
|
15
|
-
* configuration accepts can hold a secret — the schema has nowhere to put one.
|
|
16
|
-
*/
|
|
17
|
-
export const configCommand = (app, config, { permissionKey = config.permissions } = {}) => {
|
|
13
|
+
export const configCommand = (app, config, { permissionKey = config.permissions, ownerOnly = {}, } = {}) => {
|
|
18
14
|
const command = new Command("config").description("the settings in force, and where each one came from");
|
|
19
15
|
// Only a messenger has bots, and with them the personal and bot sections of the file. Elsewhere the options stay
|
|
20
16
|
// accepted, so a script that passes one still runs, but help, `commands --json` and MCP leave them out.
|
|
@@ -120,7 +116,7 @@ export const configCommand = (app, config, { permissionKey = config.permissions
|
|
|
120
116
|
throw new CliError("permission_error", `this process is locked to profile ${settings.profile} (${lock}) — ${root ? setting : "--defaults"} changes other profiles`);
|
|
121
117
|
}
|
|
122
118
|
if (environmentOf(this).agent)
|
|
123
|
-
refuseGuardedKey(setting, action === "set" ? String(given) : undefined);
|
|
119
|
+
refuseGuardedKey(setting, action === "set" ? String(given) : undefined, ownerOnly);
|
|
124
120
|
if (action === "set")
|
|
125
121
|
refuseUnknownKey(this, setting, String(given), permissionKey);
|
|
126
122
|
const saved = config.changeSetting(settings.configPath, {
|
|
@@ -166,7 +162,11 @@ export const refuseUnknownKey = (command, setting, value, permissionKey) => {
|
|
|
166
162
|
* profile, in the defaults, or by replacing `permissions` whole. Only the owner, in a terminal, turns it on or off
|
|
167
163
|
* (NEED-1662, NEED-1679).
|
|
168
164
|
*/
|
|
169
|
-
const refuseGuardedKey = (setting, value) => {
|
|
165
|
+
const refuseGuardedKey = (setting, value, ownerOnly) => {
|
|
166
|
+
if (ownerOnly.settings?.includes(setting))
|
|
167
|
+
throw new CliError("permission_error", `an agent cannot change ${setting} — the owner sets it in a terminal`, {
|
|
168
|
+
setting,
|
|
169
|
+
});
|
|
170
170
|
const keys = setting.startsWith("permissions.")
|
|
171
171
|
? [setting.slice("permissions.".length)]
|
|
172
172
|
: setting === "permissions"
|
|
@@ -174,11 +174,13 @@ const refuseGuardedKey = (setting, value) => {
|
|
|
174
174
|
? ["mcp"]
|
|
175
175
|
: objectKeys(value)
|
|
176
176
|
: [];
|
|
177
|
-
const
|
|
177
|
+
const under = (key, guarded) => key === guarded || key.startsWith(`${guarded}.`);
|
|
178
|
+
const listed = keys.flatMap((key) => (ownerOnly.permissions ?? []).filter((guarded) => under(key, guarded)));
|
|
179
|
+
const guarded = keys.some((key) => under(key, BOT_API_KEY))
|
|
178
180
|
? BOT_API_KEY
|
|
179
|
-
: keys.some((key) => key
|
|
181
|
+
: keys.some((key) => under(key, "mcp"))
|
|
180
182
|
? AUTHORITY_KEY
|
|
181
|
-
:
|
|
183
|
+
: listed[0];
|
|
182
184
|
if (!guarded)
|
|
183
185
|
return;
|
|
184
186
|
throw new CliError("permission_error", `an agent cannot change permissions.${guarded} — the owner sets it in a terminal`, { permission: guarded });
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -106,6 +106,7 @@ export { STEMS_EVERY_MS, STEMS_SLICE_MS, type StemFills, stemFills } from "./mes
|
|
|
106
106
|
export type { Running, ServerOptions, ServerProcess, ServerSystem } from "./messenger/server-command.js";
|
|
107
107
|
export { storeSummary } from "./messenger/store-maintenance-command.js";
|
|
108
108
|
export { type Saving, stored } from "./messenger/stored.js";
|
|
109
|
+
export { dateOrNone } from "./messenger/tasks-command.js";
|
|
109
110
|
export { MODEL_CATALOGUE_QUERY, type ModelCatalogueOptions } from "./model-catalogue-command.js";
|
|
110
111
|
export { window, withPaging } from "./paging.js";
|
|
111
112
|
export { type CommandPermissions, commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, type PermissionKeyOf, type PermissionKeySource, refuseUnknownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -95,6 +95,7 @@ export { DAY_MS, FIRST_FETCH_MS, memberFetches } from "./messenger/serve-members
|
|
|
95
95
|
export { STEMS_EVERY_MS, STEMS_SLICE_MS, stemFills } from "./messenger/serve-stems.js";
|
|
96
96
|
export { storeSummary } from "./messenger/store-maintenance-command.js";
|
|
97
97
|
export { stored } from "./messenger/stored.js";
|
|
98
|
+
export { dateOrNone } from "./messenger/tasks-command.js";
|
|
98
99
|
export { MODEL_CATALOGUE_QUERY } from "./model-catalogue-command.js";
|
|
99
100
|
export { window, withPaging } from "./paging.js";
|
|
100
101
|
export { commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, refuseUnknownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type { Id } from "../../domain/models.js";
|
|
3
|
-
export {
|
|
3
|
+
export { safeName } from "../../files/safe-name.js";
|
|
4
|
+
export { downloadMessage, type Saved, save } from "../../services/file-download.js";
|
|
4
5
|
import { type Messenger } from "./context.js";
|
|
5
6
|
/** `messages download`: every file of one message, or with `--all` of a whole chat, never over a file already there. */
|
|
6
7
|
export declare const downloadSubcommand: (messages: Command, messenger: Messenger) => Command;
|
|
@@ -7,7 +7,8 @@ import { NOT_FILES } from "../../domain/attachments.js";
|
|
|
7
7
|
import { FETCHING, keyOf } from "../../services/archive.js";
|
|
8
8
|
import { batchProgress, mergeBatches } from "../../services/batch.js";
|
|
9
9
|
import { saveFiles } from "../../services/file-download.js";
|
|
10
|
-
export {
|
|
10
|
+
export { safeName } from "../../files/safe-name.js";
|
|
11
|
+
export { downloadMessage, save } from "../../services/file-download.js";
|
|
11
12
|
import { parseDuration } from "../../domain/duration.js";
|
|
12
13
|
import { OFFLINE } from "../../services/deps.js";
|
|
13
14
|
import { patiently } from "../../services/patience.js";
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { type Messenger } from "./context.js";
|
|
3
|
+
/** `none` clears a date; anything else is a time ahead. Not `null`: commander stores a parser's `null` as "". */
|
|
4
|
+
export declare const dateOrNone: (flag: string) => (typed: string) => "none" | Date;
|
|
3
5
|
/** What waits on the owner — kept in the local store by `src/tasks/`; nothing here reaches the messenger. */
|
|
4
6
|
export declare const tasksCommand: (messenger: Messenger) => Command;
|
|
5
7
|
export declare const tasksStatsCommand: (messenger: Messenger) => Command;
|
|
@@ -92,7 +92,7 @@ const pairLine = (pair) => [
|
|
|
92
92
|
`(${pair.reasons.join(", ")})`,
|
|
93
93
|
].join(" ");
|
|
94
94
|
/** `none` clears a date; anything else is a time ahead. Not `null`: commander stores a parser's `null` as "". */
|
|
95
|
-
const dateOrNone = (flag) => (typed) => typed.trim().toLowerCase() === "none" ? "none" : new Date(aheadOf(typed, flag));
|
|
95
|
+
export const dateOrNone = (flag) => (typed) => typed.trim().toLowerCase() === "none" ? "none" : new Date(aheadOf(typed, flag));
|
|
96
96
|
const durationOf = (flag) => (typed) => {
|
|
97
97
|
const wanted = typed.trim();
|
|
98
98
|
const ms = Date.now() - momentOf(wanted, flag);
|
|
@@ -528,6 +528,7 @@ export declare const configCommand: (app: import("./entry.js").AppIdentity, conf
|
|
|
528
528
|
permissions?: import("./permission-keys.js").CommandPermissions | undefined;
|
|
529
529
|
}, args_2?: {
|
|
530
530
|
permissionKey?: import("./permission-keys.js").PermissionKeySource | undefined;
|
|
531
|
+
ownerOnly?: import("./config-command.js").OwnerOnlySettings;
|
|
531
532
|
} | undefined) => import("commander").Command;
|
|
532
533
|
export declare const guideCommand: (app: Pick<import("./entry.js").AppIdentity, "command">, options: import("./guide.js").GuideOptions) => import("commander").Command;
|
|
533
534
|
export declare const addMeetingBatchCommand: (program: import("commander").Command, deps: {
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -4761,6 +4761,8 @@ export declare const ENTITIES: {
|
|
|
4761
4761
|
readonly state: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
|
|
4762
4762
|
readonly origin: v.OptionalSchema<v.PicklistSchema<readonly ["rule", "agent", "owner"], undefined>, undefined>;
|
|
4763
4763
|
readonly project: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
4764
|
+
readonly project_key: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
4765
|
+
readonly project_name: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
4764
4766
|
readonly account: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
4765
4767
|
readonly chat: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
4766
4768
|
readonly source_message: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
@@ -4829,6 +4831,8 @@ export declare const ENTITIES: {
|
|
|
4829
4831
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
4830
4832
|
readonly key: v.StringSchema<undefined>;
|
|
4831
4833
|
readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
4834
|
+
readonly project_key: v.StringSchema<undefined>;
|
|
4835
|
+
readonly project_name: v.StringSchema<undefined>;
|
|
4832
4836
|
readonly title: v.StringSchema<undefined>;
|
|
4833
4837
|
readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4834
4838
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
@@ -4933,6 +4937,8 @@ export declare const ENTITIES: {
|
|
|
4933
4937
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
4934
4938
|
readonly key: v.StringSchema<undefined>;
|
|
4935
4939
|
readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
4940
|
+
readonly project_key: v.StringSchema<undefined>;
|
|
4941
|
+
readonly project_name: v.StringSchema<undefined>;
|
|
4936
4942
|
readonly title: v.StringSchema<undefined>;
|
|
4937
4943
|
readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4938
4944
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
@@ -5038,6 +5044,8 @@ export declare const ENTITIES: {
|
|
|
5038
5044
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
5039
5045
|
readonly key: v.StringSchema<undefined>;
|
|
5040
5046
|
readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
5047
|
+
readonly project_key: v.StringSchema<undefined>;
|
|
5048
|
+
readonly project_name: v.StringSchema<undefined>;
|
|
5041
5049
|
readonly title: v.StringSchema<undefined>;
|
|
5042
5050
|
readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
5043
5051
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
@@ -5171,6 +5179,8 @@ export declare const ENTITIES: {
|
|
|
5171
5179
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
5172
5180
|
readonly key: v.StringSchema<undefined>;
|
|
5173
5181
|
readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
5182
|
+
readonly project_key: v.StringSchema<undefined>;
|
|
5183
|
+
readonly project_name: v.StringSchema<undefined>;
|
|
5174
5184
|
readonly title: v.StringSchema<undefined>;
|
|
5175
5185
|
readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
5176
5186
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
@@ -5269,6 +5279,8 @@ export declare const ENTITIES: {
|
|
|
5269
5279
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
5270
5280
|
readonly key: v.StringSchema<undefined>;
|
|
5271
5281
|
readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
5282
|
+
readonly project_key: v.StringSchema<undefined>;
|
|
5283
|
+
readonly project_name: v.StringSchema<undefined>;
|
|
5272
5284
|
readonly title: v.StringSchema<undefined>;
|
|
5273
5285
|
readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
5274
5286
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
@@ -4,6 +4,8 @@ export declare const taskCard: v.StrictObjectSchema<{
|
|
|
4
4
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
5
5
|
readonly key: v.StringSchema<undefined>;
|
|
6
6
|
readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
7
|
+
readonly project_key: v.StringSchema<undefined>;
|
|
8
|
+
readonly project_name: v.StringSchema<undefined>;
|
|
7
9
|
readonly title: v.StringSchema<undefined>;
|
|
8
10
|
readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
9
11
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
@@ -108,6 +110,8 @@ export declare const taskCreated: v.StrictObjectSchema<{
|
|
|
108
110
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
109
111
|
readonly key: v.StringSchema<undefined>;
|
|
110
112
|
readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
113
|
+
readonly project_key: v.StringSchema<undefined>;
|
|
114
|
+
readonly project_name: v.StringSchema<undefined>;
|
|
111
115
|
readonly title: v.StringSchema<undefined>;
|
|
112
116
|
readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
113
117
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
@@ -213,6 +217,8 @@ export declare const taskLinks: v.UnionSchema<[v.StrictObjectSchema<{
|
|
|
213
217
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
214
218
|
readonly key: v.StringSchema<undefined>;
|
|
215
219
|
readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
220
|
+
readonly project_key: v.StringSchema<undefined>;
|
|
221
|
+
readonly project_name: v.StringSchema<undefined>;
|
|
216
222
|
readonly title: v.StringSchema<undefined>;
|
|
217
223
|
readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
218
224
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
@@ -346,6 +352,8 @@ export declare const taskMerge: v.StrictObjectSchema<{
|
|
|
346
352
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
347
353
|
readonly key: v.StringSchema<undefined>;
|
|
348
354
|
readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
355
|
+
readonly project_key: v.StringSchema<undefined>;
|
|
356
|
+
readonly project_name: v.StringSchema<undefined>;
|
|
349
357
|
readonly title: v.StringSchema<undefined>;
|
|
350
358
|
readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
351
359
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
|
@@ -444,6 +452,8 @@ export declare const taskUnmerge: v.StrictObjectSchema<{
|
|
|
444
452
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
445
453
|
readonly key: v.StringSchema<undefined>;
|
|
446
454
|
readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
455
|
+
readonly project_key: v.StringSchema<undefined>;
|
|
456
|
+
readonly project_name: v.StringSchema<undefined>;
|
|
447
457
|
readonly title: v.StringSchema<undefined>;
|
|
448
458
|
readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
449
459
|
readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
|
package/dist/entities/task.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export declare const task: v.StrictObjectSchema<{
|
|
|
19
19
|
readonly state: v.PicklistSchema<readonly ["open", "todo", "in_progress", "done", "dismissed"], undefined>;
|
|
20
20
|
readonly origin: v.OptionalSchema<v.PicklistSchema<readonly ["rule", "agent", "owner"], undefined>, undefined>;
|
|
21
21
|
readonly project: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
22
|
+
/** The project's key and name, as `tasks counts` names it. */
|
|
23
|
+
readonly project_key: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
24
|
+
readonly project_name: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
22
25
|
readonly account: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
23
26
|
/** The chat it came from, as a `chat:` reference. */
|
|
24
27
|
readonly chat: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
package/dist/entities/task.js
CHANGED
|
@@ -22,6 +22,9 @@ export const task = v.strictObject({
|
|
|
22
22
|
state: v.picklist(TASK_STATES),
|
|
23
23
|
origin: v.optional(v.picklist(TASK_ORIGINS)),
|
|
24
24
|
project: v.optional(v.nullable(text)),
|
|
25
|
+
/** The project's key and name, as `tasks counts` names it. */
|
|
26
|
+
project_key: v.optional(v.nullable(text)),
|
|
27
|
+
project_name: v.optional(v.nullable(text)),
|
|
25
28
|
account: v.optional(text),
|
|
26
29
|
/** The chat it came from, as a `chat:` reference. */
|
|
27
30
|
chat: v.optional(v.nullable(reference)),
|
package/dist/files/index.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export { acquireLock, type FileLock } from "./lock.js";
|
|
|
4
4
|
export type { NativeWriterLeaseLayout } from "./native-writer-lease.js";
|
|
5
5
|
export { nativeWriterLeaseStore } from "./native-writer-lease.js";
|
|
6
6
|
export { openOrdinaryFile, sameOrdinaryFile, syncDirectory } from "./ordinary-file.js";
|
|
7
|
+
export { safeName } from "./safe-name.js";
|
|
7
8
|
export type { WriterLease, WriterLeaseAcquisition, WriterLeaseInfo, WriterLeaseOptions, WriterLeaseStore, } from "./writer-lease.js";
|
package/dist/files/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { readBoundedUtf8File } from "./bounded-utf8.js";
|
|
|
3
3
|
export { acquireLock } from "./lock.js";
|
|
4
4
|
export { nativeWriterLeaseStore } from "./native-writer-lease.js";
|
|
5
5
|
export { openOrdinaryFile, sameOrdinaryFile, syncDirectory } from "./ordinary-file.js";
|
|
6
|
+
export { safeName } from "./safe-name.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A name another person chose cannot climb out of the folder, cannot be a dot file we would then
|
|
3
|
+
* hide, and carries no control or direction character to rewrite the terminal or disguise its extension.
|
|
4
|
+
*/
|
|
5
|
+
export declare const safeName: (name: string | undefined, platform?: NodeJS.Platform) => string | undefined;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* A name another person chose cannot climb out of the folder, cannot be a dot file we would then
|
|
4
|
+
* hide, and carries no control or direction character to rewrite the terminal or disguise its extension.
|
|
5
|
+
*/
|
|
6
|
+
export const safeName = (name, platform = process.platform) => {
|
|
7
|
+
let plain = name === undefined
|
|
8
|
+
? ""
|
|
9
|
+
: basename(name.replaceAll("\\", "/"))
|
|
10
|
+
.replace(/[\p{Cc}---]/gu, "")
|
|
11
|
+
.replace(/^\.+/, "");
|
|
12
|
+
if (platform === "win32") {
|
|
13
|
+
plain = plain.replace(/[<>:"|?*]/g, "_").replace(/[. ]+$/, "");
|
|
14
|
+
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(plain))
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return plain === "" ? undefined : plain;
|
|
18
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
1
2
|
import { domainsToAge, groupKey, probeMail } from "./probe.js";
|
|
2
3
|
import { decide } from "./rules.js";
|
|
3
4
|
import { FRAUD_SIGNALS, FREE_MAIL, registrableOf, WEIGHTS } from "./score.js";
|
|
@@ -22,7 +23,11 @@ export const scoreSenders = async (store, address, ages, now = Date.now(), rules
|
|
|
22
23
|
const done = last === undefined ? undefined : JSON.parse(last);
|
|
23
24
|
if (done !== undefined && done.version === SCORE_VERSION && now - done.at < DAY)
|
|
24
25
|
return undefined;
|
|
25
|
-
const stored = await store.accounts.storedAccount(key).catch(() =>
|
|
26
|
+
const stored = await store.accounts.storedAccount(key).catch((error) => {
|
|
27
|
+
if (error instanceof CliError && error.code === "not_found")
|
|
28
|
+
return null;
|
|
29
|
+
throw error;
|
|
30
|
+
});
|
|
26
31
|
if (stored === null)
|
|
27
32
|
return undefined;
|
|
28
33
|
const items = (await store.mail.emailFacts({ accountId: stored.id })).map((email) => {
|
|
@@ -6,7 +6,10 @@ export interface FindingEvidence {
|
|
|
6
6
|
}
|
|
7
7
|
interface FindingBase {
|
|
8
8
|
id: string;
|
|
9
|
-
|
|
9
|
+
/** Every task finding has one; a fact finding may have none when its rules allow it. */
|
|
10
|
+
project?: string;
|
|
11
|
+
/** Who or what a fact is about: `person:`, `organization:`, `task:` or `project:`. */
|
|
12
|
+
subject?: string;
|
|
10
13
|
evidence: FindingEvidence[];
|
|
11
14
|
}
|
|
12
15
|
export interface FactFinding extends FindingBase {
|
|
@@ -54,6 +57,8 @@ export interface FindingsRules {
|
|
|
54
57
|
/** "cue" */
|
|
55
58
|
cited: string;
|
|
56
59
|
reference: (raw: string) => string;
|
|
60
|
+
/** A fact finding may name no project, and any finding may name a subject. */
|
|
61
|
+
anySubject?: boolean;
|
|
57
62
|
}
|
|
58
63
|
export declare const validateFindings: (input: FindingsInput, rules: FindingsRules) => FindingsInput;
|
|
59
64
|
export declare const findingFingerprint: ({ id: _id, ...finding }: Finding) => string;
|
|
@@ -16,6 +16,7 @@ const fields = (value, allowed, noun) => {
|
|
|
16
16
|
if (Object.keys(value).some((key) => !allowed.includes(key)))
|
|
17
17
|
invalid(`unknown ${noun} field`);
|
|
18
18
|
};
|
|
19
|
+
const SUBJECT = /^(person|organization|project):[1-9]\d*$|^task:[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
19
20
|
export const validateFindings = (input, rules) => {
|
|
20
21
|
if (!input || typeof input !== "object" || Buffer.byteLength(JSON.stringify(input), "utf8") > 65536)
|
|
21
22
|
return invalid(`${rules.plural} take at most 65536 UTF-8 bytes`);
|
|
@@ -36,9 +37,13 @@ export const validateFindings = (input, rules) => {
|
|
|
36
37
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(id) || ids.has(id))
|
|
37
38
|
return invalid("finding IDs must be unique simple keys");
|
|
38
39
|
ids.add(id);
|
|
39
|
-
const project = text(finding.project, 64);
|
|
40
|
-
if (!/^project:[1-9]\d*$/.test(project))
|
|
40
|
+
const project = finding.project === undefined && rules.anySubject ? undefined : text(finding.project, 64);
|
|
41
|
+
if (project !== undefined && !/^project:[1-9]\d*$/.test(project))
|
|
41
42
|
return invalid("a finding names a project reference");
|
|
43
|
+
if (finding.subject !== undefined && (!rules.anySubject || !SUBJECT.test(text(finding.subject, 80))))
|
|
44
|
+
return invalid("a finding's subject is a person:, organization:, task: or project: reference");
|
|
45
|
+
if (finding.kind === "task" && project === undefined)
|
|
46
|
+
return invalid("a task finding names the project the task goes in");
|
|
42
47
|
if (!Array.isArray(finding.evidence) || finding.evidence.length < 1 || finding.evidence.length > 8)
|
|
43
48
|
return invalid(`a finding requires 1–8 cited ${rules.cited}s`);
|
|
44
49
|
const evidence = finding.evidence
|
|
@@ -56,9 +61,14 @@ export const validateFindings = (input, rules) => {
|
|
|
56
61
|
.sort((a, b) => a.reference.localeCompare(b.reference));
|
|
57
62
|
if (new Set(evidence.map((item) => item.reference)).size !== evidence.length)
|
|
58
63
|
return invalid(`duplicate evidence ${rules.cited}`);
|
|
59
|
-
const base = {
|
|
64
|
+
const base = {
|
|
65
|
+
id,
|
|
66
|
+
...(project === undefined ? {} : { project }),
|
|
67
|
+
...(finding.subject === undefined ? {} : { subject: finding.subject }),
|
|
68
|
+
evidence,
|
|
69
|
+
};
|
|
60
70
|
if (finding.kind === "fact") {
|
|
61
|
-
fields(finding, ["id", "project", "evidence", "kind", "type", "statement"], rules.singular);
|
|
71
|
+
fields(finding, ["id", "project", "subject", "evidence", "kind", "type", "statement"], rules.singular);
|
|
62
72
|
if (!FACT_TYPES.includes(finding.type))
|
|
63
73
|
return invalid("unknown fact finding type");
|
|
64
74
|
return { ...base, kind: "fact", type: finding.type, statement: text(finding.statement, 2000) };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RefreshedFile } from "../domain/attachments.js";
|
|
2
2
|
import type { Id } from "../domain/models.js";
|
|
3
|
+
import { safeName } from "../files/safe-name.js";
|
|
3
4
|
import type { RemoteFile } from "../ports/messenger.js";
|
|
4
5
|
import { batchProgress } from "./batch.js";
|
|
5
6
|
import type { MessagesService } from "./messages.js";
|
|
@@ -39,11 +40,7 @@ export declare const saveFiles: (messages: Pick<MessagesService, "keepDownloaded
|
|
|
39
40
|
maxErrorPercent: number;
|
|
40
41
|
};
|
|
41
42
|
}>;
|
|
42
|
-
|
|
43
|
-
* A name another person chose cannot climb out of the folder, cannot be a dot file we would then
|
|
44
|
-
* hide, and carries no control or direction character to rewrite the terminal or disguise its extension.
|
|
45
|
-
*/
|
|
46
|
-
export declare const safeName: (name: string | undefined, platform?: NodeJS.Platform) => string | undefined;
|
|
43
|
+
export { safeName };
|
|
47
44
|
/**
|
|
48
45
|
* The bytes go to a temporary name first and are then hard-linked to the real one, which fails if the
|
|
49
46
|
* name is taken — so an existing file survives, and an interrupted download leaves no half file under
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createWriteStream, mkdirSync } from "node:fs";
|
|
2
2
|
import { link, rm } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
4
|
import { Readable, Transform } from "node:stream";
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
6
|
import { CliError } from "@wirecat/cli-base";
|
|
7
|
+
import { safeName } from "../files/safe-name.js";
|
|
7
8
|
import { batchProgress } from "./batch.js";
|
|
8
9
|
/** The files the messenger gave a new reference for while their bytes were read, to save without a path. */
|
|
9
10
|
export const refreshedFiles = (files) => files.flatMap(({ kind, name, position, providerRef }) => providerRef === undefined || Object.keys(providerRef).length === 0
|
|
@@ -90,23 +91,7 @@ const EXTENSIONS = {
|
|
|
90
91
|
};
|
|
91
92
|
/** Telegram sends a photo with no name and no type; it is always a JPEG. */
|
|
92
93
|
const BY_KIND = { photo: "jpg" };
|
|
93
|
-
|
|
94
|
-
* A name another person chose cannot climb out of the folder, cannot be a dot file we would then
|
|
95
|
-
* hide, and carries no control or direction character to rewrite the terminal or disguise its extension.
|
|
96
|
-
*/
|
|
97
|
-
export const safeName = (name, platform = process.platform) => {
|
|
98
|
-
let plain = name === undefined
|
|
99
|
-
? ""
|
|
100
|
-
: basename(name.replaceAll("\\", "/"))
|
|
101
|
-
.replace(/[\p{Cc}---]/gu, "")
|
|
102
|
-
.replace(/^\.+/, "");
|
|
103
|
-
if (platform === "win32") {
|
|
104
|
-
plain = plain.replace(/[<>:"|?*]/g, "_").replace(/[. ]+$/, "");
|
|
105
|
-
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(plain))
|
|
106
|
-
return undefined;
|
|
107
|
-
}
|
|
108
|
-
return plain === "" ? undefined : plain;
|
|
109
|
-
};
|
|
94
|
+
export { safeName };
|
|
110
95
|
/**
|
|
111
96
|
* The bytes go to a temporary name first and are then hard-linked to the real one, which fails if the
|
|
112
97
|
* name is taken — so an existing file survives, and an interrupted download leaves no half file under
|
|
@@ -20,7 +20,10 @@ export interface EvidenceFindingsReceipt extends FindingsReceipt {
|
|
|
20
20
|
}
|
|
21
21
|
export interface EvidenceFindingsBatch {
|
|
22
22
|
id: string;
|
|
23
|
-
project
|
|
23
|
+
/** `null` when its findings name no one project. */
|
|
24
|
+
project: string | null;
|
|
25
|
+
/** Who or what every finding is about, when they agree: a project, person, organization or task; else `null`. */
|
|
26
|
+
subject: string | null;
|
|
24
27
|
input: FindingsInput;
|
|
25
28
|
fingerprint: string;
|
|
26
29
|
status: "proposed" | "reviewed";
|
|
@@ -3,13 +3,19 @@ import { formatLocator, parseLocator } from "../domain/locator.js";
|
|
|
3
3
|
import { validateFindings } from "../meetings/index.js";
|
|
4
4
|
export const EVIDENCE_FINDINGS_KIND = "evidence_findings";
|
|
5
5
|
const reference = (raw) => {
|
|
6
|
-
if (/^document:[1-9]\d*$/.test(raw))
|
|
6
|
+
if (/^(document|note):[1-9]\d*$/.test(raw))
|
|
7
7
|
return raw;
|
|
8
8
|
try {
|
|
9
9
|
return formatLocator(parseLocator(raw));
|
|
10
10
|
}
|
|
11
11
|
catch {
|
|
12
|
-
throw new CliError("validation_error", "finding evidence is a msg: or
|
|
12
|
+
throw new CliError("validation_error", "finding evidence is a msg:, document: or note: reference");
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
|
-
export const validateEvidenceFindings = (input) => validateFindings(input, {
|
|
15
|
+
export const validateEvidenceFindings = (input) => validateFindings(input, {
|
|
16
|
+
plural: "evidence findings",
|
|
17
|
+
singular: "evidence finding",
|
|
18
|
+
cited: "reference",
|
|
19
|
+
reference,
|
|
20
|
+
anySubject: true,
|
|
21
|
+
});
|
package/dist/store/people.d.ts
CHANGED
|
@@ -59,6 +59,10 @@ export interface PeopleStore extends AccountPeople {
|
|
|
59
59
|
unlinkIdentity(identity: IdentityRef, options: LinkOptions): Promise<PersonRecord>;
|
|
60
60
|
/** The identity, and everyone linked with it, is the owner. */
|
|
61
61
|
linkToOwner(identity: IdentityRef): Promise<PersonRecord>;
|
|
62
|
+
/** A person known by name only, with no identity yet: someone to assign a task to before any message names them. */
|
|
63
|
+
add(input: {
|
|
64
|
+
name: string;
|
|
65
|
+
}): Promise<PersonRecord>;
|
|
62
66
|
}
|
|
63
67
|
export declare const accountPeopleOver: (context: StoreContext) => AccountPeople;
|
|
64
68
|
export declare const peopleStoreOver: (context: StoreContext, keyed: AccountPeople) => PeopleStore;
|
package/dist/store/people.js
CHANGED
|
@@ -62,6 +62,18 @@ export const peopleStoreOver = (context, keyed) => {
|
|
|
62
62
|
personOf: async (identity) => personLinks.personOf(context, identity),
|
|
63
63
|
personSeenInAccounts: async (uid, accountIds) => personSeenInAccounts(context, uid, accountIds),
|
|
64
64
|
personByUid: async (uid) => personLinks.personByUid(context, uid),
|
|
65
|
+
add: async ({ name }) => {
|
|
66
|
+
const text = typeof name === "string" ? name.trim() : "";
|
|
67
|
+
if (!text || text.length > 200)
|
|
68
|
+
throw new CliError("validation_error", "a person's name takes 1–200 characters");
|
|
69
|
+
const id = write(database, () => {
|
|
70
|
+
const at = context.now();
|
|
71
|
+
return Number(database
|
|
72
|
+
.prepare("INSERT INTO persons (name, owner, created_at, updated_at) VALUES (?, 0, ?, ?) RETURNING id")
|
|
73
|
+
.get(text, at, at)?.id);
|
|
74
|
+
});
|
|
75
|
+
return personLinks.personByUid(context, String(id));
|
|
76
|
+
},
|
|
65
77
|
owner: async () => {
|
|
66
78
|
try {
|
|
67
79
|
return personLinks.personByUid(context, String(ownerPerson(database).id));
|
|
@@ -258,6 +258,20 @@ export const contextRecordOf = (database, ref) => {
|
|
|
258
258
|
}
|
|
259
259
|
: null;
|
|
260
260
|
}
|
|
261
|
+
const note = /^note:([1-9]\d*)$/.exec(ref);
|
|
262
|
+
if (note) {
|
|
263
|
+
const row = database
|
|
264
|
+
.prepare("SELECT title, body, revision FROM notes WHERE id=? AND deleted_at IS NULL")
|
|
265
|
+
.get(Number(note[1]));
|
|
266
|
+
return row
|
|
267
|
+
? {
|
|
268
|
+
ref,
|
|
269
|
+
title: row.title == null ? null : String(row.title),
|
|
270
|
+
text: String(row.body),
|
|
271
|
+
revision: Number(row.revision),
|
|
272
|
+
}
|
|
273
|
+
: null;
|
|
274
|
+
}
|
|
261
275
|
let at;
|
|
262
276
|
try {
|
|
263
277
|
at = parseLocator(ref);
|
|
@@ -33,13 +33,16 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
33
33
|
return invalid("stored findings exceed the read budget");
|
|
34
34
|
return proposals.get(id);
|
|
35
35
|
};
|
|
36
|
+
/** The one project its findings name, or `null` when none does. */
|
|
37
|
+
const projectOf = (input) => input.findings.find((finding) => finding.project)?.project ?? null;
|
|
36
38
|
const view = (proposal) => {
|
|
37
39
|
const input = proposal.payload;
|
|
38
|
-
if (!input || !
|
|
40
|
+
if (!input || !["proposed", "approved", "rejected"].includes(proposal.status))
|
|
39
41
|
return invalid("invalid stored findings batch");
|
|
40
42
|
return {
|
|
41
43
|
id: proposal.id,
|
|
42
|
-
project:
|
|
44
|
+
project: projectOf(input),
|
|
45
|
+
subject: proposal.target,
|
|
43
46
|
input: { schemaVersion: 1, key: input.key, extractor: input.extractor, findings: input.findings },
|
|
44
47
|
fingerprint: findingHash({ input, author: proposal.proposedBy.id }),
|
|
45
48
|
status: proposal.status === "proposed" ? "proposed" : "reviewed",
|
|
@@ -94,9 +97,12 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
94
97
|
return {
|
|
95
98
|
stage: async (raw, rawAgent) => {
|
|
96
99
|
const input = validateEvidenceFindings(raw);
|
|
97
|
-
const projects = new Set(input.findings.
|
|
98
|
-
if (projects.size
|
|
99
|
-
return invalid("an evidence findings batch is about one project");
|
|
100
|
+
const projects = new Set(input.findings.flatMap((finding) => finding.project ?? []));
|
|
101
|
+
if (projects.size > 1)
|
|
102
|
+
return invalid("an evidence findings batch is about one project at most");
|
|
103
|
+
const subjects = new Set(input.findings.map((finding) => finding.subject));
|
|
104
|
+
const shared = subjects.size === 1 ? [...subjects][0] : undefined;
|
|
105
|
+
const target = shared ?? [...projects][0];
|
|
100
106
|
const by = findingsAgent(rawAgent);
|
|
101
107
|
return write(database, () => {
|
|
102
108
|
const { actor } = authorActor(context, by);
|
|
@@ -118,7 +124,7 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
118
124
|
return view(proposals.propose({
|
|
119
125
|
kind: EVIDENCE_FINDINGS_KIND,
|
|
120
126
|
by,
|
|
121
|
-
target:
|
|
127
|
+
...(target === undefined ? {} : { target }),
|
|
122
128
|
payload: { ...input, inputHash, snapshotHash, snapshotParts, agentModel: by.model ?? null },
|
|
123
129
|
}));
|
|
124
130
|
});
|
|
@@ -134,8 +140,10 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
134
140
|
if (projectId === undefined)
|
|
135
141
|
return invalid("a findings list filters by a project reference");
|
|
136
142
|
const ids = database
|
|
137
|
-
.prepare("SELECT id FROM proposed_actions WHERE account_id IS NULL AND kind=? AND (? IS NULL OR (target_type='project' AND target_id=?)
|
|
138
|
-
|
|
143
|
+
.prepare("SELECT id FROM proposed_actions WHERE account_id IS NULL AND kind=? AND (? IS NULL OR (target_type='project' AND target_id=?) " +
|
|
144
|
+
"OR EXISTS (SELECT 1 FROM json_each(payload,'$.findings') f WHERE json_extract(f.value,'$.project')=?)) " +
|
|
145
|
+
"AND (? IS NULL OR status=?) ORDER BY id DESC LIMIT ? OFFSET ?")
|
|
146
|
+
.all(EVIDENCE_FINDINGS_KIND, projectId, projectId === null ? null : Number(projectId), projectId === null ? null : `project:${projectId}`, status ?? null, status === "reviewed" ? "approved" : (status ?? null), limit + 1, offset);
|
|
139
147
|
return { items: ids.slice(0, limit).map((row) => view(load(String(row.id)))), hasMore: ids.length > limit };
|
|
140
148
|
},
|
|
141
149
|
review: async (id, decision) => write(database, () => {
|
|
@@ -11,8 +11,8 @@ export declare const findingsAgent: (agent: FindingsAgent) => FindingsAgent;
|
|
|
11
11
|
* including a reference it already saw for another finding.
|
|
12
12
|
*/
|
|
13
13
|
export declare const findingsCoreOver: (context: StoreContext, kind: string) => {
|
|
14
|
-
snapshot: (input: FindingsInput, cite: (evidence: FindingEvidence, finding: Finding, projectId: string, seen: Map<string, unknown>) => void) => string;
|
|
15
|
-
snapshotParts: (input: FindingsInput, cite: (evidence: FindingEvidence, finding: Finding, projectId: string, seen: Map<string, unknown>) => void) => Record<string, string>;
|
|
14
|
+
snapshot: (input: FindingsInput, cite: (evidence: FindingEvidence, finding: Finding, projectId: string | null, seen: Map<string, unknown>) => void) => string;
|
|
15
|
+
snapshotParts: (input: FindingsInput, cite: (evidence: FindingEvidence, finding: Finding, projectId: string | null, seen: Map<string, unknown>) => void) => Record<string, string>;
|
|
16
16
|
previous: (account: number | null, fingerprint: string) => FindingOutcome | undefined;
|
|
17
17
|
outcomes: (account: number | null, input: FindingsInput, accepted: string[], agent: FindingsAgent) => FindingOutcome[];
|
|
18
18
|
};
|
|
@@ -30,15 +30,21 @@ export const findingsCoreOver = (context, kind) => {
|
|
|
30
30
|
const cited = (input, cite) => {
|
|
31
31
|
const seen = new Map();
|
|
32
32
|
for (const finding of input.findings) {
|
|
33
|
-
|
|
34
|
-
if (project
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
let projectId = null;
|
|
34
|
+
if (finding.project !== undefined) {
|
|
35
|
+
const project = requiredThing(database, finding.project, "the project");
|
|
36
|
+
if (project.type !== "project")
|
|
37
|
+
return invalid("a finding needs an owner project");
|
|
38
|
+
const projectRow = database
|
|
39
|
+
.prepare("SELECT id,key,name,description,type,scope,updated_at FROM projects WHERE id=? AND account_id IS NULL AND deleted_at IS NULL")
|
|
40
|
+
.get(project.id);
|
|
41
|
+
if (!projectRow)
|
|
42
|
+
throw new CliError("not_found", "the owner project is unavailable");
|
|
43
|
+
seen.set(finding.project, projectRow);
|
|
44
|
+
projectId = String(project.id);
|
|
45
|
+
}
|
|
46
|
+
if (finding.subject !== undefined)
|
|
47
|
+
seen.set(finding.subject, requiredThing(database, finding.subject, "the subject"));
|
|
42
48
|
if (finding.kind === "task" && finding.assignee) {
|
|
43
49
|
const person = requiredThing(database, finding.assignee, "the assignee");
|
|
44
50
|
if (person.type !== "person")
|
|
@@ -49,7 +55,7 @@ export const findingsCoreOver = (context, kind) => {
|
|
|
49
55
|
seen.set(finding.assignee, personRow);
|
|
50
56
|
}
|
|
51
57
|
for (const evidence of finding.evidence)
|
|
52
|
-
cite(evidence, finding,
|
|
58
|
+
cite(evidence, finding, projectId, seen);
|
|
53
59
|
}
|
|
54
60
|
return seen;
|
|
55
61
|
};
|
|
@@ -68,12 +74,15 @@ export const findingsCoreOver = (context, kind) => {
|
|
|
68
74
|
const fact = facts.add({
|
|
69
75
|
type: finding.type,
|
|
70
76
|
statement: finding.statement,
|
|
71
|
-
project: finding.project,
|
|
77
|
+
...(finding.project === undefined ? {} : { project: finding.project }),
|
|
78
|
+
...(finding.subject === undefined ? {} : { subject: finding.subject }),
|
|
72
79
|
evidence,
|
|
73
80
|
by: agent,
|
|
74
81
|
});
|
|
75
82
|
return facts.accept(fact.ref).ref;
|
|
76
83
|
}
|
|
84
|
+
if (finding.project === undefined)
|
|
85
|
+
return invalid("a task finding names the project the task goes in");
|
|
77
86
|
const task = tasks.create({
|
|
78
87
|
project: finding.project,
|
|
79
88
|
title: finding.title,
|
|
@@ -6,8 +6,17 @@ export type TaskSort = (typeof TASK_SORTS)[number];
|
|
|
6
6
|
export declare const PROJECT_TASK_FIELDS: readonly FieldSpec[];
|
|
7
7
|
/** A `blocked-by` task that is not done holds the task back; so does one deleted or out of reach. */
|
|
8
8
|
export declare const UNFINISHED_BLOCKER: string;
|
|
9
|
-
/**
|
|
10
|
-
export declare const
|
|
9
|
+
/** Each due group as SQL over `t`, from day starts in the owner's zone; the filter and the counts share them. */
|
|
10
|
+
export declare const dueGroups: (now: number, zone: string) => {
|
|
11
|
+
readonly overdue: `t.status NOT IN ('done', 'dismissed') AND t.due_at IS NOT NULL AND t.due_at < ${number}`;
|
|
12
|
+
readonly today: `t.due_at >= ${number} AND t.due_at < ${number}`;
|
|
13
|
+
readonly upcoming: `t.due_at >= ${number} AND t.due_at < ${number}`;
|
|
14
|
+
readonly later: `t.due_at >= ${number}`;
|
|
15
|
+
readonly none: "t.due_at IS NULL";
|
|
16
|
+
};
|
|
17
|
+
export type DueGroup = keyof ReturnType<typeof dueGroups>;
|
|
18
|
+
/** `now` and `zone` place today; one value per answer, so the list and its counts agree. */
|
|
19
|
+
export declare const projectTaskColumns: (now: number, zone: string) => ColumnMap;
|
|
11
20
|
/** Live tasks of the owner's projects; an account's inbox is listed by its account, never here. */
|
|
12
21
|
export declare const BOARD_SCOPE = "t.deleted_at IS NULL AND t.project_id IN (SELECT id FROM projects WHERE deleted_at IS NULL AND length(key) <> 10)";
|
|
13
22
|
/** The chosen order first, then priority, age and id, so a page boundary never splits a tie. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
import { OPERATORS_BY_TYPE, walkQuery, } from "@wirecat/cli-base/query";
|
|
2
|
+
import { dayBoundary, OPERATORS_BY_TYPE, walkQuery, } from "@wirecat/cli-base/query";
|
|
3
3
|
import { TASK_TYPES } from "../../domain/knowledge.js";
|
|
4
4
|
import { taskWordsOf } from "../../search/task-similarity.js";
|
|
5
5
|
import { PRIORITY_LABELS, priorityBand, TASK_STATES } from "../../tasks/index.js";
|
|
@@ -28,7 +28,12 @@ export const PROJECT_TASK_FIELDS = [
|
|
|
28
28
|
}),
|
|
29
29
|
field("agent", "kw", "agent:hermes", { relation: "the agent working on it, by name; none or any" }),
|
|
30
30
|
field("due", "time", "due:[today TO 2026-12-31]", { relation: "the due date; NOT due:* has none" }),
|
|
31
|
-
field("overdue", "bool", "overdue:true", { relation: "open work
|
|
31
|
+
field("overdue", "bool", "overdue:true", { relation: "open work due before today" }),
|
|
32
|
+
field("when", "enum", "when:today", {
|
|
33
|
+
values: ["overdue", "today", "upcoming", "later", "none"],
|
|
34
|
+
relation: "the due date as a board groups it: overdue (open work due before today), today, upcoming (the next 7 days " +
|
|
35
|
+
"after today), later, or none",
|
|
36
|
+
}),
|
|
32
37
|
field("closed", "time", "closed:>=14d", { relation: "when it was done or dismissed" }),
|
|
33
38
|
field("ready", "bool", "ready:true", { relation: "open work whose blocked-by tasks are all done" }),
|
|
34
39
|
field("text", "txt", "text:invoice", { relation: "words of the title and description; a bare word means this" }),
|
|
@@ -131,8 +136,28 @@ const truth = (node, sql) => ({
|
|
|
131
136
|
sql: node.value.toLowerCase() === "true" ? sql : `NOT (${sql})`,
|
|
132
137
|
params: [],
|
|
133
138
|
});
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
const SPAN = { start: 0, end: 0 };
|
|
140
|
+
const UPCOMING_DAYS = 7;
|
|
141
|
+
const dayIn = (at, zone, ahead) => {
|
|
142
|
+
const date = new Date(`${new Intl.DateTimeFormat("en-CA", { timeZone: zone, year: "numeric", month: "2-digit", day: "2-digit" }).format(at)}T00:00:00Z`);
|
|
143
|
+
date.setUTCDate(date.getUTCDate() + ahead);
|
|
144
|
+
return dayBoundary(date.toISOString().slice(0, 10), zone, false, SPAN);
|
|
145
|
+
};
|
|
146
|
+
/** Each due group as SQL over `t`, from day starts in the owner's zone; the filter and the counts share them. */
|
|
147
|
+
export const dueGroups = (now, zone) => {
|
|
148
|
+
const today = dayIn(now, zone, 0);
|
|
149
|
+
const tomorrow = dayIn(now, zone, 1);
|
|
150
|
+
const after = dayIn(now, zone, UPCOMING_DAYS + 1);
|
|
151
|
+
return {
|
|
152
|
+
overdue: `${OPEN} AND t.due_at IS NOT NULL AND t.due_at < ${today}`,
|
|
153
|
+
today: `t.due_at >= ${today} AND t.due_at < ${tomorrow}`,
|
|
154
|
+
upcoming: `t.due_at >= ${tomorrow} AND t.due_at < ${after}`,
|
|
155
|
+
later: `t.due_at >= ${after}`,
|
|
156
|
+
none: "t.due_at IS NULL",
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
/** `now` and `zone` place today; one value per answer, so the list and its counts agree. */
|
|
160
|
+
export const projectTaskColumns = (now, zone) => ({
|
|
136
161
|
state: { type: "enum", sql: "t.status" },
|
|
137
162
|
kind: { type: "enum", sql: "t.type" },
|
|
138
163
|
project: { where: projectWhere },
|
|
@@ -141,7 +166,15 @@ export const projectTaskColumns = (now) => ({
|
|
|
141
166
|
agent: { where: agentWhere },
|
|
142
167
|
due: { type: "time", sql: "t.due_at" },
|
|
143
168
|
closed: { type: "time", sql: "t.closed_at" },
|
|
144
|
-
overdue: { where: (node) => truth(node,
|
|
169
|
+
overdue: { where: (node) => truth(node, dueGroups(now, zone).overdue) },
|
|
170
|
+
when: {
|
|
171
|
+
where: (node) => {
|
|
172
|
+
const group = dueGroups(now, zone)[node.value.toLowerCase()];
|
|
173
|
+
if (group === undefined)
|
|
174
|
+
throw new CliError("validation_error", "when is overdue, today, upcoming, later or none");
|
|
175
|
+
return { sql: group, params: [] };
|
|
176
|
+
},
|
|
177
|
+
},
|
|
145
178
|
ready: { where: (node) => truth(node, `${OPEN} AND NOT EXISTS (${UNFINISHED_BLOCKER})`) },
|
|
146
179
|
text: {
|
|
147
180
|
where: (node) => {
|
|
@@ -6,7 +6,7 @@ import type { Actor } from "./actors.js";
|
|
|
6
6
|
import { type Author } from "./memories.js";
|
|
7
7
|
import type { StoreContext } from "./open.js";
|
|
8
8
|
import { type Where } from "./query-filter.js";
|
|
9
|
-
import { type TaskSort } from "./task-board.js";
|
|
9
|
+
import { type DueGroup, type TaskSort } from "./task-board.js";
|
|
10
10
|
import { type CloseSuggestion } from "./task-close.js";
|
|
11
11
|
import { type TaskRelationsStore } from "./task-relations.js";
|
|
12
12
|
export declare const TASK_ROLES: readonly ["assignee", "reviewer", "watcher"];
|
|
@@ -31,7 +31,7 @@ export interface TaskFields {
|
|
|
31
31
|
/** A name the caller checked against its category list; every change is a `category` event. */
|
|
32
32
|
category?: string;
|
|
33
33
|
}
|
|
34
|
-
export interface NewWorkTask extends Omit<TaskFields, "
|
|
34
|
+
export interface NewWorkTask extends Omit<TaskFields, "reason"> {
|
|
35
35
|
/** `project:<id>` or the project's key. */
|
|
36
36
|
project: string;
|
|
37
37
|
title: string;
|
|
@@ -48,6 +48,9 @@ export interface TaskDetails {
|
|
|
48
48
|
ref: string;
|
|
49
49
|
key: string;
|
|
50
50
|
project: string;
|
|
51
|
+
/** The project's key and name, as `boardCounts` names it. */
|
|
52
|
+
projectKey: string;
|
|
53
|
+
projectName: string;
|
|
51
54
|
title: string;
|
|
52
55
|
description: string | null;
|
|
53
56
|
type: TaskType;
|
|
@@ -109,8 +112,10 @@ export interface BoardOptions {
|
|
|
109
112
|
/** How many tasks match, and how many hold each value: the board's pickers show these beside their choices. */
|
|
110
113
|
export interface BoardCounts {
|
|
111
114
|
total: number;
|
|
115
|
+
/** `value` is the key; `ref` the `project:<id>` a task's `project` names. */
|
|
112
116
|
project: {
|
|
113
117
|
value: string;
|
|
118
|
+
ref: string;
|
|
114
119
|
name: string;
|
|
115
120
|
count: number;
|
|
116
121
|
}[];
|
|
@@ -129,6 +134,16 @@ export interface BoardCounts {
|
|
|
129
134
|
value: TaskState;
|
|
130
135
|
count: number;
|
|
131
136
|
}[];
|
|
137
|
+
/** The groups `when:` takes, in board order; they do not overlap. */
|
|
138
|
+
due: {
|
|
139
|
+
value: DueGroup;
|
|
140
|
+
count: number;
|
|
141
|
+
}[];
|
|
142
|
+
/** The agent's name, or `none`. */
|
|
143
|
+
agent: {
|
|
144
|
+
value: string;
|
|
145
|
+
count: number;
|
|
146
|
+
}[];
|
|
132
147
|
}
|
|
133
148
|
export interface TaskEvent {
|
|
134
149
|
kind: (typeof TASK_EVENT_KINDS)[number];
|
|
@@ -176,7 +191,10 @@ export interface WorkTasks extends TaskRelationsStore {
|
|
|
176
191
|
total: number;
|
|
177
192
|
}>;
|
|
178
193
|
/** The owner and everyone who holds a role on a live task, by name: who the board offers to assign. */
|
|
179
|
-
|
|
194
|
+
/** Everyone the store knows, the owner first, then by how many live tasks name them. */
|
|
195
|
+
taskPeople(options?: {
|
|
196
|
+
limit?: number;
|
|
197
|
+
}): Promise<{
|
|
180
198
|
person: string;
|
|
181
199
|
name: string | null;
|
|
182
200
|
owner: boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { timezoneOf } from "@wirecat/cli-base/query";
|
|
2
3
|
import { TASK_TYPES, WORK_TASK_TYPES } from "../../domain/knowledge.js";
|
|
3
4
|
import { checkedScore, isClosedState, legacyPriority, PRIORITY_LABELS, priorityBand, priorityLabel, TASK_STATES, } from "../../tasks/index.js";
|
|
4
5
|
import { folded } from "./folded.js";
|
|
@@ -7,7 +8,7 @@ import { CORPORA, drainCorpus } from "./note-index.js";
|
|
|
7
8
|
import { requiredThing } from "./notes.js";
|
|
8
9
|
import { queryFilter } from "./query-filter.js";
|
|
9
10
|
import { stemmerCache } from "./stems.js";
|
|
10
|
-
import { BOARD_SCOPE, boardOrder, projectTaskColumns, withoutField } from "./task-board.js";
|
|
11
|
+
import { BOARD_SCOPE, boardOrder, dueGroups, projectTaskColumns, withoutField, } from "./task-board.js";
|
|
11
12
|
import { closeSuggestionOf, SUGGESTION_COLUMNS } from "./task-close.js";
|
|
12
13
|
import { INBOX_KEY_LENGTH, inboxKey } from "./task-keys.js";
|
|
13
14
|
import { taskRelationsStoreOver } from "./task-relations.js";
|
|
@@ -53,6 +54,13 @@ const textOf = (value, what, max) => {
|
|
|
53
54
|
const claimOf = (row, at) => row.claimed_by != null && Number(row.claimed_until) >= at
|
|
54
55
|
? { claimedBy: String(row.claimed_by), claimedUntil: iso(row.claimed_until) }
|
|
55
56
|
: { claimedBy: null, claimedUntil: null };
|
|
57
|
+
/** A change to a time column is kept as epoch ms; it reads as ISO, as the task itself does. */
|
|
58
|
+
const timesAsIso = (changes) => Object.fromEntries(Object.entries(changes).map(([key, pair]) => [
|
|
59
|
+
key,
|
|
60
|
+
key.endsWith("_at") && Array.isArray(pair)
|
|
61
|
+
? pair.map((value) => (typeof value === "number" ? new Date(value).toISOString() : value))
|
|
62
|
+
: pair,
|
|
63
|
+
]));
|
|
56
64
|
/** What a write made on a stale copy is refused with: the revision the caller read is not the task's now. */
|
|
57
65
|
export const checkRevision = (row, expected) => {
|
|
58
66
|
if (expected === undefined || Number(row.revision) === expected)
|
|
@@ -101,6 +109,10 @@ export const workTaskOperationsOver = (context) => {
|
|
|
101
109
|
}
|
|
102
110
|
return parent;
|
|
103
111
|
};
|
|
112
|
+
const projectNamed = (id) => {
|
|
113
|
+
const project = database.prepare("SELECT key, name FROM projects WHERE id = ?").get(id);
|
|
114
|
+
return { projectKey: String(project.key), projectName: String(project.name) };
|
|
115
|
+
};
|
|
104
116
|
const detailsOf = (row) => {
|
|
105
117
|
const id = Number(row.id);
|
|
106
118
|
const reference = (taskRow) => `task:${taskIdOf(database, taskRow)}`;
|
|
@@ -109,6 +121,7 @@ export const workTaskOperationsOver = (context) => {
|
|
|
109
121
|
ref: reference(id),
|
|
110
122
|
key: String(row.key),
|
|
111
123
|
project: `project:${row.project_id}`,
|
|
124
|
+
...projectNamed(Number(row.project_id)),
|
|
112
125
|
title: String(row.title),
|
|
113
126
|
description: row.description == null ? null : String(row.description),
|
|
114
127
|
type: row.type,
|
|
@@ -141,7 +154,7 @@ export const workTaskOperationsOver = (context) => {
|
|
|
141
154
|
const boardWhere = (query, zone, at) => {
|
|
142
155
|
if (query === undefined)
|
|
143
156
|
return { sql: BOARD_SCOPE, params: [] };
|
|
144
|
-
const where = queryFilter(query, projectTaskColumns(at), {
|
|
157
|
+
const where = queryFilter(query, projectTaskColumns(at, timezoneOf(zone)), {
|
|
145
158
|
command: "tasks list",
|
|
146
159
|
now: at,
|
|
147
160
|
...(zone ? { zone } : {}),
|
|
@@ -483,8 +496,13 @@ export const workTaskOperationsOver = (context) => {
|
|
|
483
496
|
.prepare(`SELECT ${select}, count(*) AS n FROM tasks t ${from} WHERE ${where.sql} GROUP BY ${group}`)
|
|
484
497
|
.all(...where.params);
|
|
485
498
|
};
|
|
486
|
-
const project = grouped("project", "p.key, p.name", "JOIN projects p ON p.id = t.project_id", "p.id")
|
|
487
|
-
.map((row) => ({
|
|
499
|
+
const project = grouped("project", "p.id, p.key, p.name", "JOIN projects p ON p.id = t.project_id", "p.id")
|
|
500
|
+
.map((row) => ({
|
|
501
|
+
value: String(row.key),
|
|
502
|
+
ref: `project:${row.id}`,
|
|
503
|
+
name: String(row.name),
|
|
504
|
+
count: Number(row.n),
|
|
505
|
+
}))
|
|
488
506
|
.sort((a, b) => a.value.localeCompare(b.value));
|
|
489
507
|
const assigned = grouped("assignee", "a.assignee_id AS person, pe.name", "LEFT JOIN task_assignments a ON a.task_id = t.id AND a.assignee_type = 'person' AND a.role = 'assignee' " +
|
|
490
508
|
"LEFT JOIN persons pe ON pe.id = a.assignee_id", "a.assignee_id");
|
|
@@ -501,6 +519,17 @@ export const workTaskOperationsOver = (context) => {
|
|
|
501
519
|
priorities.set(label, (priorities.get(label) ?? 0) + Number(row.n));
|
|
502
520
|
}
|
|
503
521
|
const states = new Map(grouped("state", "t.status", "", "t.status").map((row) => [row.status, Number(row.n)]));
|
|
522
|
+
const groups = dueGroups(at, timezoneOf(zone));
|
|
523
|
+
const dueWhere = boardWhere(withoutField(withoutField(query, "when"), "overdue"), zone, at);
|
|
524
|
+
const due = Object.keys(groups).map((value) => ({
|
|
525
|
+
value,
|
|
526
|
+
count: Number(database
|
|
527
|
+
.prepare(`SELECT count(*) AS n FROM tasks t WHERE ${dueWhere.sql} AND ${groups[value]}`)
|
|
528
|
+
.get(...dueWhere.params)?.n ?? 0),
|
|
529
|
+
}));
|
|
530
|
+
const agent = grouped("agent", "t.agent", "", "t.agent")
|
|
531
|
+
.map((row) => ({ value: row.agent == null ? "none" : String(row.agent), count: Number(row.n) }))
|
|
532
|
+
.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
|
|
504
533
|
return {
|
|
505
534
|
total: countOf(boardWhere(query, zone, at)),
|
|
506
535
|
project,
|
|
@@ -510,14 +539,16 @@ export const workTaskOperationsOver = (context) => {
|
|
|
510
539
|
count: priorities.get(value) ?? 0,
|
|
511
540
|
})),
|
|
512
541
|
state: TASK_STATES.map((value) => ({ value, count: states.get(value) ?? 0 })),
|
|
542
|
+
due,
|
|
543
|
+
agent,
|
|
513
544
|
};
|
|
514
545
|
},
|
|
515
|
-
taskPeople: async () => database
|
|
546
|
+
taskPeople: async ({ limit = 500 } = {}) => database
|
|
516
547
|
.prepare("SELECT p.id, p.name, p.owner, count(DISTINCT t.id) AS n FROM persons p " +
|
|
517
548
|
"LEFT JOIN task_assignments a ON a.assignee_type = 'person' AND a.assignee_id = p.id " +
|
|
518
549
|
"LEFT JOIN tasks t ON t.id = a.task_id AND t.deleted_at IS NULL " +
|
|
519
|
-
"
|
|
520
|
-
.all()
|
|
550
|
+
"GROUP BY p.id ORDER BY p.owner DESC, n DESC, p.name IS NULL, p.name COLLATE NOCASE, p.id LIMIT ?")
|
|
551
|
+
.all(Math.min(Math.max(limit, 1), 5000))
|
|
521
552
|
.map((row) => ({
|
|
522
553
|
person: `person:${row.id}`,
|
|
523
554
|
name: row.name == null ? null : String(row.name),
|
|
@@ -539,7 +570,7 @@ export const workTaskOperationsOver = (context) => {
|
|
|
539
570
|
id: Number(row.actor_id),
|
|
540
571
|
name: row.actor_name == null ? null : String(row.actor_name),
|
|
541
572
|
},
|
|
542
|
-
changes: row.changes == null ? null : JSON.parse(String(row.changes)),
|
|
573
|
+
changes: row.changes == null ? null : timesAsIso(JSON.parse(String(row.changes))),
|
|
543
574
|
at: iso(row.created_at),
|
|
544
575
|
})),
|
|
545
576
|
};
|
package/dist/tasks/priority.js
CHANGED
|
@@ -33,7 +33,11 @@ export const priorityOf = (value) => {
|
|
|
33
33
|
return MIDDLE[word];
|
|
34
34
|
if (/^[0-4]$/.test(word))
|
|
35
35
|
return LEGACY_PRIORITY_SCORES[Number(word)];
|
|
36
|
-
|
|
36
|
+
// A bare 0 or 1 is the old scale, where they mean the opposite; the decimal point says a 0–1 score is meant.
|
|
37
|
+
if (/^(0?\.\d+|[01]\.\d*)$/.test(word) && Number(word) <= 1)
|
|
38
|
+
return Number(word);
|
|
39
|
+
throw new CliError("validation_error", `a priority is ${PRIORITY_LABELS.join(", ")}, none, or a score from 0.0 to 1.0 with its decimal point; ` +
|
|
40
|
+
"0 (urgent) … 4 (low) still work as critical, high, medium, low, low");
|
|
37
41
|
};
|
|
38
42
|
/** `priorityOf` for a command-line option: Commander stores a parser's `null` as an empty string, so `none` stays a word. */
|
|
39
43
|
export const priorityOption = (value) => priorityOf(value) ?? "none";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.530.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
"@types/node": "^22.20.5",
|
|
252
252
|
"@types/pngjs": "6.0.5",
|
|
253
253
|
"@vitest/coverage-v8": "^5.0.5",
|
|
254
|
-
"@wirecat/cli-base": "0.
|
|
254
|
+
"@wirecat/cli-base": "0.71.0",
|
|
255
255
|
"@wirecat/cli-core-legacy-tags": "npm:@wirecat/cli-core@0.414.0",
|
|
256
256
|
"commander": "15.0.0",
|
|
257
257
|
"cspell": "10.4.0",
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
},
|
|
272
272
|
"peerDependencies": {
|
|
273
273
|
"@napi-rs/canvas": ">=0.1.69 <2",
|
|
274
|
-
"@wirecat/cli-base": ">=0.
|
|
274
|
+
"@wirecat/cli-base": ">=0.71.0 <0.72.0",
|
|
275
275
|
"commander": ">=15"
|
|
276
276
|
},
|
|
277
277
|
"bin": {
|