@wirecat/cli-core 0.510.0 → 0.512.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 +40 -0
- package/dist/calendar/mcp/index.d.ts +46 -10
- package/dist/calendar/mcp/index.js +60 -25
- package/dist/cli/list-query.d.ts +4 -2
- package/dist/cli/list-query.js +3 -3
- package/dist/entities/index.d.ts +8 -8
- package/dist/entities/task-results.d.ts +7 -7
- package/dist/entities/task.d.ts +1 -1
- package/dist/services/task-rules.js +2 -2
- package/dist/services/tasks.d.ts +1 -1
- package/dist/services/tasks.js +7 -5
- package/dist/store/index.d.ts +4 -1
- package/dist/store/index.js +2 -0
- package/dist/store/sqlite/knowledge.js +3 -2
- package/dist/store/sqlite/person-preparation.js +1 -1
- package/dist/store/sqlite/task-board.d.ts +16 -0
- package/dist/store/sqlite/task-board.js +133 -0
- package/dist/store/sqlite/task-candidates.js +1 -1
- package/dist/store/sqlite/task-category-settings.js +1 -1
- package/dist/store/sqlite/task-close-store.js +2 -1
- package/dist/store/sqlite/task-pairs.js +4 -3
- package/dist/store/sqlite/task-relations.js +3 -2
- package/dist/store/sqlite/tasks.js +5 -4
- package/dist/store/sqlite/work-tasks.d.ts +52 -2
- package/dist/store/sqlite/work-tasks.js +103 -10
- package/dist/tasks/index.d.ts +1 -1
- package/dist/tasks/index.js +1 -1
- package/dist/tasks/model.d.ts +5 -2
- package/dist/tasks/model.js +5 -2
- package/dist/tasks/service.js +2 -2
- package/package.json +1 -1
- package/parity.json +39 -0
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { TASK_TYPES, WORK_TASK_TYPES } from "../../domain/knowledge.js";
|
|
3
|
-
import { TASK_STATES } from "../../tasks/index.js";
|
|
3
|
+
import { isClosedState, TASK_STATES } from "../../tasks/index.js";
|
|
4
4
|
import { folded } from "./folded.js";
|
|
5
5
|
import { authorActor } from "./memories.js";
|
|
6
|
+
import { CORPORA, drainCorpus } from "./note-index.js";
|
|
6
7
|
import { requiredThing } from "./notes.js";
|
|
8
|
+
import { queryFilter } from "./query-filter.js";
|
|
9
|
+
import { stemmerCache } from "./stems.js";
|
|
10
|
+
import { BOARD_SCOPE, boardOrder, projectTaskColumns, withoutField } from "./task-board.js";
|
|
7
11
|
import { closeSuggestionOf, SUGGESTION_COLUMNS } from "./task-close.js";
|
|
8
|
-
import { inboxKey } from "./task-keys.js";
|
|
12
|
+
import { INBOX_KEY_LENGTH, inboxKey } from "./task-keys.js";
|
|
9
13
|
import { taskRelationsStoreOver } from "./task-relations.js";
|
|
10
14
|
import { taskIdOf, taskRowOf, thingOf } from "./things.js";
|
|
11
15
|
import { write } from "./transaction.js";
|
|
@@ -120,6 +124,18 @@ export const workTaskOperationsOver = (context) => {
|
|
|
120
124
|
updatedAt: iso(row.updated_at),
|
|
121
125
|
};
|
|
122
126
|
};
|
|
127
|
+
const stemmerFor = stemmerCache();
|
|
128
|
+
const boardWhere = (query, zone, at) => {
|
|
129
|
+
if (query === undefined)
|
|
130
|
+
return { sql: BOARD_SCOPE, params: [] };
|
|
131
|
+
const where = queryFilter(query, projectTaskColumns(at), {
|
|
132
|
+
command: "tasks list",
|
|
133
|
+
now: at,
|
|
134
|
+
...(zone ? { zone } : {}),
|
|
135
|
+
});
|
|
136
|
+
return { sql: `${BOARD_SCOPE} AND (${where.sql})`, params: where.params };
|
|
137
|
+
};
|
|
138
|
+
const countOf = (where) => Number(database.prepare(`SELECT count(*) AS n FROM tasks t WHERE ${where.sql}`).get(...where.params)?.n ?? 0);
|
|
123
139
|
const rowById = (id) => database.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
124
140
|
const details = (id) => detailsOf(rowById(id));
|
|
125
141
|
const edit = (task, fields, actor, created = false) => {
|
|
@@ -154,12 +170,21 @@ export const workTaskOperationsOver = (context) => {
|
|
|
154
170
|
if (fields.status !== undefined) {
|
|
155
171
|
if (!TASK_STATES.includes(fields.status))
|
|
156
172
|
throw new CliError("validation_error", `a task's status is one of ${TASK_STATES.join(", ")}`);
|
|
157
|
-
|
|
173
|
+
if ((fields.status === "todo" || fields.status === "in_progress") &&
|
|
174
|
+
String(database.prepare("SELECT key FROM projects WHERE id = ?").get(Number(before.project_id))?.key).length ===
|
|
175
|
+
INBOX_KEY_LENGTH)
|
|
176
|
+
throw new CliError("validation_error", "a task from a message is open, done or dismissed; todo and in_progress are for a project task");
|
|
177
|
+
const closed = isClosedState(fields.status);
|
|
178
|
+
const reopened = !closed && isClosedState(String(before.status));
|
|
158
179
|
if (before.status !== fields.status) {
|
|
159
180
|
change("status", fields.status, "status");
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
181
|
+
if (fields.status === "in_progress" && before.started_at == null)
|
|
182
|
+
change("started_at", at);
|
|
183
|
+
if (closed || reopened) {
|
|
184
|
+
change("closed_at", closed ? at : null);
|
|
185
|
+
change("closed_by_type", closed ? actor.type : null);
|
|
186
|
+
change("closed_by_id", closed ? actor.id : null);
|
|
187
|
+
}
|
|
163
188
|
if (closed) {
|
|
164
189
|
change("claimed_by", null);
|
|
165
190
|
change("claimed_until", null);
|
|
@@ -168,6 +193,11 @@ export const workTaskOperationsOver = (context) => {
|
|
|
168
193
|
}
|
|
169
194
|
else
|
|
170
195
|
change("close_certainty", null);
|
|
196
|
+
// Why it was closed no longer holds once it is open again.
|
|
197
|
+
if (reopened && fields.reason === undefined) {
|
|
198
|
+
change("close_reason", null);
|
|
199
|
+
change("close_reason_folded", null);
|
|
200
|
+
}
|
|
171
201
|
}
|
|
172
202
|
}
|
|
173
203
|
if (fields.reason !== undefined) {
|
|
@@ -296,11 +326,11 @@ export const workTaskOperationsOver = (context) => {
|
|
|
296
326
|
const at = now();
|
|
297
327
|
const won = write(database, () => database
|
|
298
328
|
.prepare("UPDATE tasks SET claimed_by = ?, claimed_until = ?, updated_at = ? " +
|
|
299
|
-
"WHERE id = ? AND status
|
|
329
|
+
"WHERE id = ? AND status NOT IN ('done', 'dismissed') AND (claimed_by IS NULL OR claimed_until < ? OR claimed_by = ?) RETURNING id")
|
|
300
330
|
.get(holder, until.getTime(), at, task, at, holder));
|
|
301
331
|
if (!won) {
|
|
302
332
|
const row = rowById(task);
|
|
303
|
-
if (row.status
|
|
333
|
+
if (isClosedState(String(row.status)))
|
|
304
334
|
throw new CliError("validation_error", `task ${String(row.key)} is ${String(row.status)}; only an open task is claimed`);
|
|
305
335
|
throw new CliError("validation_error", `${String(row.claimed_by)} holds task ${String(row.key)} until ${iso(row.claimed_until)}; ` +
|
|
306
336
|
"wait for it, or ask them to release it", { reason: "task.claimed", holder: String(row.claimed_by), until: iso(row.claimed_until) });
|
|
@@ -322,12 +352,75 @@ export const workTaskOperationsOver = (context) => {
|
|
|
322
352
|
});
|
|
323
353
|
return details(task);
|
|
324
354
|
},
|
|
355
|
+
board: async (query, { limit, offset = 0, sort = "priority", descending = false, zone }) => {
|
|
356
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)
|
|
357
|
+
throw new CliError("validation_error", "a page of tasks holds 1–500");
|
|
358
|
+
drainCorpus(database, CORPORA.task, stemmerFor);
|
|
359
|
+
const where = boardWhere(query, zone, now());
|
|
360
|
+
const rows = database
|
|
361
|
+
.prepare(`SELECT t.* FROM tasks t WHERE ${where.sql} ORDER BY ${boardOrder(sort, descending)} LIMIT ? OFFSET ?`)
|
|
362
|
+
.all(...where.params, limit, offset);
|
|
363
|
+
return { items: rows.map(detailsOf), total: countOf(where) };
|
|
364
|
+
},
|
|
365
|
+
boardCounts: async (query, { zone } = {}) => {
|
|
366
|
+
drainCorpus(database, CORPORA.task, stemmerFor);
|
|
367
|
+
const at = now();
|
|
368
|
+
const without = (name) => boardWhere(withoutField(query, name), zone, at);
|
|
369
|
+
const grouped = (name, select, from, group) => {
|
|
370
|
+
const where = without(name);
|
|
371
|
+
return database
|
|
372
|
+
.prepare(`SELECT ${select}, count(*) AS n FROM tasks t ${from} WHERE ${where.sql} GROUP BY ${group}`)
|
|
373
|
+
.all(...where.params);
|
|
374
|
+
};
|
|
375
|
+
const project = grouped("project", "p.key, p.name", "JOIN projects p ON p.id = t.project_id", "p.id")
|
|
376
|
+
.map((row) => ({ value: String(row.key), name: String(row.name), count: Number(row.n) }))
|
|
377
|
+
.sort((a, b) => a.value.localeCompare(b.value));
|
|
378
|
+
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' " +
|
|
379
|
+
"LEFT JOIN persons pe ON pe.id = a.assignee_id", "a.assignee_id");
|
|
380
|
+
const assignee = assigned
|
|
381
|
+
.map((row) => ({
|
|
382
|
+
value: row.person == null ? "none" : `person:${row.person}`,
|
|
383
|
+
name: row.name == null ? null : String(row.name),
|
|
384
|
+
count: Number(row.n),
|
|
385
|
+
}))
|
|
386
|
+
.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
|
|
387
|
+
const priorities = new Map(grouped("priority", "t.priority", "", "t.priority").map((row) => [row.priority, Number(row.n)]));
|
|
388
|
+
const states = new Map(grouped("state", "t.status", "", "t.status").map((row) => [row.status, Number(row.n)]));
|
|
389
|
+
return {
|
|
390
|
+
total: countOf(boardWhere(query, zone, at)),
|
|
391
|
+
project,
|
|
392
|
+
assignee,
|
|
393
|
+
priority: [0, 1, 2, 3, 4, null].map((value) => ({ value, count: priorities.get(value) ?? 0 })),
|
|
394
|
+
state: TASK_STATES.map((value) => ({ value, count: states.get(value) ?? 0 })),
|
|
395
|
+
};
|
|
396
|
+
},
|
|
397
|
+
taskPeople: async () => database
|
|
398
|
+
.prepare("SELECT p.id, p.name, p.owner, count(DISTINCT t.id) AS n FROM persons p " +
|
|
399
|
+
"LEFT JOIN task_assignments a ON a.assignee_type = 'person' AND a.assignee_id = p.id " +
|
|
400
|
+
"LEFT JOIN tasks t ON t.id = a.task_id AND t.deleted_at IS NULL " +
|
|
401
|
+
"WHERE p.owner = 1 OR t.id IS NOT NULL GROUP BY p.id ORDER BY p.owner DESC, p.name COLLATE NOCASE, p.id")
|
|
402
|
+
.all()
|
|
403
|
+
.map((row) => ({
|
|
404
|
+
person: `person:${row.id}`,
|
|
405
|
+
name: row.name == null ? null : String(row.name),
|
|
406
|
+
owner: Number(row.owner) === 1,
|
|
407
|
+
tasks: Number(row.n),
|
|
408
|
+
})),
|
|
325
409
|
events: async (id) => database
|
|
326
|
-
.prepare("SELECT actor_type, actor_id, kind, changes, created_at
|
|
410
|
+
.prepare("SELECT e.actor_type, e.actor_id, e.kind, e.changes, e.created_at, " +
|
|
411
|
+
"CASE e.actor_type WHEN 'person' THEN p.name WHEN 'bot' THEN b.name END AS actor_name FROM task_events e " +
|
|
412
|
+
"LEFT JOIN persons p ON e.actor_type = 'person' AND p.id = e.actor_id " +
|
|
413
|
+
"LEFT JOIN bots b ON e.actor_type = 'bot' AND b.id = e.actor_id WHERE e.task_id = ? ORDER BY e.id")
|
|
327
414
|
.all(required(id))
|
|
328
415
|
.map((row) => ({
|
|
329
416
|
kind: row.kind,
|
|
330
|
-
actor: row.actor_type == null
|
|
417
|
+
actor: row.actor_type == null
|
|
418
|
+
? null
|
|
419
|
+
: {
|
|
420
|
+
type: row.actor_type,
|
|
421
|
+
id: Number(row.actor_id),
|
|
422
|
+
name: row.actor_name == null ? null : String(row.actor_name),
|
|
423
|
+
},
|
|
331
424
|
changes: row.changes == null ? null : JSON.parse(String(row.changes)),
|
|
332
425
|
at: iso(row.created_at),
|
|
333
426
|
})),
|
package/dist/tasks/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { type ClosedState, closeTask, TASK_KINDS, TASK_ORIGINS, TASK_STATES, TASK_VERDICTS, type Task, type TaskKind, type TaskOrigin, type TaskState, type TaskVerdict, } from "./model.js";
|
|
1
|
+
export { CLOSED_STATES, type ClosedState, closeTask, isClosedState, TASK_KINDS, TASK_ORIGINS, TASK_STATES, TASK_VERDICTS, type Task, type TaskKind, type TaskOrigin, type TaskState, type TaskVerdict, } from "./model.js";
|
|
2
2
|
export { type CloseOptions, createTaskService, type GroupStats, type NewTask, type TaskService, type TaskServiceOptions, } from "./service.js";
|
|
3
3
|
export { matches, type TaskFilter, type TaskStore } from "./store.js";
|
package/dist/tasks/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { closeTask, TASK_KINDS, TASK_ORIGINS, TASK_STATES, TASK_VERDICTS, } from "./model.js";
|
|
1
|
+
export { CLOSED_STATES, closeTask, isClosedState, TASK_KINDS, TASK_ORIGINS, TASK_STATES, TASK_VERDICTS, } from "./model.js";
|
|
2
2
|
export { createTaskService, } from "./service.js";
|
|
3
3
|
export { matches } from "./store.js";
|
package/dist/tasks/model.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export declare const TASK_KINDS: readonly ["question", "request", "mention", "promise"];
|
|
2
|
-
|
|
2
|
+
/** `todo` and `in_progress` are steps of a project task's work; a task from a message is only ever open or closed. */
|
|
3
|
+
export declare const TASK_STATES: readonly ["open", "todo", "in_progress", "done", "dismissed"];
|
|
4
|
+
export declare const CLOSED_STATES: readonly ["done", "dismissed"];
|
|
3
5
|
export declare const TASK_ORIGINS: readonly ["rule", "agent", "owner"];
|
|
4
6
|
export declare const TASK_VERDICTS: readonly ["useful", "not_useful"];
|
|
5
7
|
export type TaskKind = (typeof TASK_KINDS)[number];
|
|
6
8
|
export type TaskState = (typeof TASK_STATES)[number];
|
|
7
9
|
export type TaskOrigin = (typeof TASK_ORIGINS)[number];
|
|
8
10
|
export type TaskVerdict = (typeof TASK_VERDICTS)[number];
|
|
9
|
-
export type ClosedState =
|
|
11
|
+
export type ClosedState = (typeof CLOSED_STATES)[number];
|
|
12
|
+
export declare const isClosedState: (state: string) => state is ClosedState;
|
|
10
13
|
/**
|
|
11
14
|
* A task points at what it is about and never holds its text: `source` is a locator the host
|
|
12
15
|
* resolves (`msg:…` for a message), so a deleted message leaves nothing behind here.
|
package/dist/tasks/model.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
export const TASK_KINDS = ["question", "request", "mention", "promise"];
|
|
3
|
-
|
|
3
|
+
/** `todo` and `in_progress` are steps of a project task's work; a task from a message is only ever open or closed. */
|
|
4
|
+
export const TASK_STATES = ["open", "todo", "in_progress", "done", "dismissed"];
|
|
5
|
+
export const CLOSED_STATES = ["done", "dismissed"];
|
|
4
6
|
export const TASK_ORIGINS = ["rule", "agent", "owner"];
|
|
5
7
|
export const TASK_VERDICTS = ["useful", "not_useful"];
|
|
8
|
+
export const isClosedState = (state) => CLOSED_STATES.includes(state);
|
|
6
9
|
export function closeTask(task, state, by, at, reason) {
|
|
7
|
-
if (task.state
|
|
10
|
+
if (isClosedState(task.state))
|
|
8
11
|
throw new CliError("validation_error", `task ${task.id} is already ${task.state}`, { reason: "task.closed" });
|
|
9
12
|
const { claimedBy: _by, claimedUntil: _until, ...unclaimed } = task;
|
|
10
13
|
return { ...unclaimed, state, closedAt: at, closedBy: by, ...(reason === undefined ? {} : { reason }) };
|
package/dist/tasks/service.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
import { closeTask } from "./model.js";
|
|
2
|
+
import { closeTask, isClosedState } from "./model.js";
|
|
3
3
|
export function createTaskService({ store, now = () => new Date(), newId = () => crypto.randomUUID(), }) {
|
|
4
4
|
async function find(id) {
|
|
5
5
|
const task = await store.get(id);
|
|
@@ -36,7 +36,7 @@ export function createTaskService({ store, now = () => new Date(), newId = () =>
|
|
|
36
36
|
for (const task of await store.list(filter))
|
|
37
37
|
byGroup.set(task.group, [...(byGroup.get(task.group) ?? []), task]);
|
|
38
38
|
return [...byGroup].map(([group, tasks]) => {
|
|
39
|
-
const open = tasks.filter((task) => task.state
|
|
39
|
+
const open = tasks.filter((task) => !isClosedState(task.state));
|
|
40
40
|
const closeMs = tasks.flatMap((task) => task.closedAt ? [task.closedAt.getTime() - task.createdAt.getTime()] : []);
|
|
41
41
|
const oldestOpenAt = open.map((task) => task.createdAt).sort((a, b) => a.getTime() - b.getTime())[0];
|
|
42
42
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.512.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",
|
package/parity.json
CHANGED
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"--ai": {
|
|
24
24
|
"meaning": "call the configured reply model with stored message data; requires reply consent, otherwise uses fallback"
|
|
25
25
|
},
|
|
26
|
+
"--alias": {
|
|
27
|
+
"value": "<name>",
|
|
28
|
+
"meaning": "another name of the project, searched on its own; repeat for more"
|
|
29
|
+
},
|
|
26
30
|
"--all": {
|
|
27
31
|
"meaning": "every row, no paging",
|
|
28
32
|
"note": "chats requests confirm and reject take every pending request instead — counted against the hourly limit first — so it differs on purpose (Help text rule 4)"
|
|
@@ -261,6 +265,10 @@
|
|
|
261
265
|
"value": "<n>",
|
|
262
266
|
"meaning": "messages before and after each hit"
|
|
263
267
|
},
|
|
268
|
+
"--context-query": {
|
|
269
|
+
"value": "<words>",
|
|
270
|
+
"meaning": "more words that all appear in one text; repeat for another"
|
|
271
|
+
},
|
|
264
272
|
"--correct": {
|
|
265
273
|
"value": "<n>",
|
|
266
274
|
"meaning": "with --quiz: the right answer's position, from 1"
|
|
@@ -3669,6 +3677,37 @@
|
|
|
3669
3677
|
}
|
|
3670
3678
|
}
|
|
3671
3679
|
},
|
|
3680
|
+
"projects": {
|
|
3681
|
+
"in": [],
|
|
3682
|
+
"planned": {
|
|
3683
|
+
"max": "max-cli#886, on the cli-core release with this row",
|
|
3684
|
+
"tg": "tg-cli#798, on the cli-core release with this row",
|
|
3685
|
+
"wa": "wa backlog: mount projectsCommand once wa is on cli-core 0.504.0"
|
|
3686
|
+
}
|
|
3687
|
+
},
|
|
3688
|
+
"projects evidence": {
|
|
3689
|
+
"in": [],
|
|
3690
|
+
"planned": {
|
|
3691
|
+
"max": "max-cli#886, on the cli-core release with this row",
|
|
3692
|
+
"tg": "tg-cli#798, on the cli-core release with this row",
|
|
3693
|
+
"wa": "wa backlog: mount projectsCommand once wa is on cli-core 0.504.0"
|
|
3694
|
+
},
|
|
3695
|
+
"options": {
|
|
3696
|
+
"--after-n": "all",
|
|
3697
|
+
"--alias": "all",
|
|
3698
|
+
"--before-n": "all",
|
|
3699
|
+
"--before-time": "all",
|
|
3700
|
+
"--contact": "all",
|
|
3701
|
+
"--context-query": "all",
|
|
3702
|
+
"--cursor": "all",
|
|
3703
|
+
"--limit": "all",
|
|
3704
|
+
"--max-pages": "all",
|
|
3705
|
+
"--no-catch-up": "all",
|
|
3706
|
+
"--since-time": "all",
|
|
3707
|
+
"--sync-first": "all",
|
|
3708
|
+
"--sync-time": "all"
|
|
3709
|
+
}
|
|
3710
|
+
},
|
|
3672
3711
|
"reactions": {
|
|
3673
3712
|
"in": "all"
|
|
3674
3713
|
},
|