@wirecat/cli-core 0.580.0 → 0.582.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 +32 -0
- package/dist/mail/cli/index.d.ts +2 -0
- package/dist/mail/cli/review.js +19 -3
- package/dist/mail/findings.d.ts +4 -1
- package/dist/mail/findings.js +3 -2
- package/dist/mail/marks.d.ts +10 -0
- package/dist/mail/marks.js +16 -3
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/proposals/index.d.ts +1 -1
- package/dist/services/proposals/index.js +1 -1
- package/dist/services/proposals/kinds/organization.d.ts +7 -0
- package/dist/services/proposals/kinds/organization.js +22 -5
- package/dist/store/knowledge-query.js +7 -1
- package/dist/store/sqlite/knowledge.d.ts +2 -2
- package/dist/store/sqlite/knowledge.js +2 -21
- package/dist/store/sqlite/organizations.d.ts +14 -0
- package/dist/store/sqlite/organizations.js +48 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
+
## 0.582.0 — 05.11.2026
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`knowledge.updateOrganization(organization, { name })` renames an organization**, given as `organization:<id>` or
|
|
13
|
+
by its id; its kind and scope stay. An agent's rename is a proposal for the owner: the `organization_rename`
|
|
14
|
+
proposal kind and `proposeOrganizationRename(store, { organization, name }, by, reason?)`. It shows in `review` as
|
|
15
|
+
an `organization` row (`rename organization:3 to ...`); confirming it renames, rejecting it changes nothing. No
|
|
16
|
+
schema change.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **`knowledge.organizations` answers a `name:` term.** `ORGANIZATIONS_QUERY` listed `name`, but the store had no
|
|
21
|
+
column for it, so `organizations list name:acme` failed. The match ignores case in any script, and `name:*acme*`
|
|
22
|
+
finds a name that holds a word.
|
|
23
|
+
|
|
24
|
+
## 0.581.0 — 05.11.2026
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`review --holder` gives the findings key.** A host that mounts its findings command (`MailMount.findings`)
|
|
29
|
+
gets a `mail-findings.add` step with `key` before the acknowledge step, so an agent copies the key instead of
|
|
30
|
+
building it from the cursor. The text answer names it too.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **A run after `messages reset` can stage its findings.** The reset counts on the mark (`resets`), and the findings
|
|
35
|
+
key of a mark moved back is `<holder>:<through_id>-r<resets>`, such as `mail-triage:277352-r1`. The runs after a
|
|
36
|
+
reset reached the through id of a batch staged before it and were refused (`mail-findings.key-used`), so their
|
|
37
|
+
mark stayed. Keys of marks never moved back keep their shape, and the same review still gives the same key, so
|
|
38
|
+
a retry of one run stages nothing twice.
|
|
39
|
+
|
|
8
40
|
## 0.580.0 — 05.11.2026
|
|
9
41
|
|
|
10
42
|
### Added
|
package/dist/mail/cli/index.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface MailMount {
|
|
|
15
15
|
acknowledge?: string;
|
|
16
16
|
/** `null`: the host mounts no thread read, so a review offers none. */
|
|
17
17
|
conversation?: string | null;
|
|
18
|
+
/** The host's command that stages a findings batch with `--key`; `review --holder` then gives that key as a step. */
|
|
19
|
+
findings?: string;
|
|
18
20
|
/** In snake_case, as a step names it: memo's `source`, with `email:<account>`. */
|
|
19
21
|
mailbox?: {
|
|
20
22
|
option: string;
|
package/dist/mail/cli/review.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
3
|
import { defineQuery } from "@wirecat/cli-base/query";
|
|
4
|
-
import { defineSteps, stepOf } from "@wirecat/cli-base/standards";
|
|
4
|
+
import { defineSteps, MAX_NEXT_STEPS, stepOf, } from "@wirecat/cli-base/standards";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { environmentOf, outputFor } from "../../cli/context.js";
|
|
7
7
|
import { LOCAL_READ } from "../../cli/effects.js";
|
|
@@ -36,7 +36,18 @@ export const MAIL_REVIEW_QUERY = defineQuery({
|
|
|
36
36
|
/** The mail review's steps where a host mounted them; a host lists them beside its own so its step checks know them. */
|
|
37
37
|
export const mailReviewSteps = (mount = {}) => {
|
|
38
38
|
const mailbox = mount.mailbox ? [{ name: mount.mailbox.option, required: true }] : [];
|
|
39
|
+
const findings = mount.findings
|
|
40
|
+
? [
|
|
41
|
+
{
|
|
42
|
+
id: "mail-findings.add",
|
|
43
|
+
command: mount.findings,
|
|
44
|
+
args: [{ name: "key", required: true }, ...mailbox],
|
|
45
|
+
why: "stage what these emails ask under this key, before acknowledging them",
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
: [];
|
|
39
49
|
return defineSteps([
|
|
50
|
+
...findings,
|
|
40
51
|
{
|
|
41
52
|
id: "mail-messages.acknowledge",
|
|
42
53
|
command: mount.acknowledge ?? "messages acknowledge",
|
|
@@ -197,7 +208,12 @@ const holderReview = async (command, mail, holder, query) => {
|
|
|
197
208
|
const step = stepsFor(mail.mount, mailbox);
|
|
198
209
|
const answer = reviewAnswer(found, mailbox, true, step);
|
|
199
210
|
const skipped = Object.entries(read.skipped).filter(([, count]) => count > 0);
|
|
200
|
-
|
|
211
|
+
const stage = read.findingsKey === undefined ? [] : step("mail-findings.add", { key: read.findingsKey });
|
|
212
|
+
answer.next = [
|
|
213
|
+
...stage,
|
|
214
|
+
...step("mail-messages.acknowledge", { holder, cursor: read.cursor }),
|
|
215
|
+
...(answer.next ?? []),
|
|
216
|
+
].slice(0, MAX_NEXT_STEPS);
|
|
201
217
|
answer.missing = [
|
|
202
218
|
...(answer.missing ?? []),
|
|
203
219
|
...(read.held > 0 ? [{ field: "items", why: `${read.held} partial emails held until they are whole` }] : []),
|
|
@@ -217,7 +233,7 @@ const holderReview = async (command, mail, holder, query) => {
|
|
|
217
233
|
const output = outputFor(command);
|
|
218
234
|
renderAnswer(command, output, answer, () => {
|
|
219
235
|
output.renderer.note(read.mark
|
|
220
|
-
? `${holder}: past ${read.mark.throughId}; acknowledge with --cursor ${read.cursor}${read.more ? "; more after these" : ""}`
|
|
236
|
+
? `${holder}: past ${read.mark.throughId}; findings key ${read.findingsKey}; acknowledge with --cursor ${read.cursor}${read.more ? "; more after these" : ""}`
|
|
221
237
|
: `${holder} has no mark yet: it starts from now — acknowledge with --cursor ${read.cursor}`);
|
|
222
238
|
return emails.map((hit) => `${hitText(hit)}${markText(marks.get(hit.id), hit.excerptOnly)}\n`).join("");
|
|
223
239
|
});
|
package/dist/mail/findings.d.ts
CHANGED
|
@@ -131,7 +131,10 @@ export interface MailOrderUpdateFinding extends Cited {
|
|
|
131
131
|
export type MailFinding = MailTaskFinding | MailFactFinding | MailTaskUpdateFinding | MailMemoryFinding | MailLinkFinding | MailNewContactFinding | MailInvoiceFinding | MailPaymentFinding | MailOrderFinding | MailOrderUpdateFinding;
|
|
132
132
|
export interface MailFindingsInput {
|
|
133
133
|
schemaVersion: 1;
|
|
134
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* `<holder>:<through_id>`, with `-r<resets>` after the owner moved the mark back: one batch per run of a holder over
|
|
136
|
+
* the mail it was given. `review --holder` gives it.
|
|
137
|
+
*/
|
|
135
138
|
key: string;
|
|
136
139
|
extractor: string;
|
|
137
140
|
findings: MailFinding[];
|
package/dist/mail/findings.js
CHANGED
|
@@ -347,8 +347,9 @@ const one = (raw, ids) => {
|
|
|
347
347
|
* The shape of a mail findings batch: 1–30 findings, each citing 1–8 `msg:email/…` references with fingerprints.
|
|
348
348
|
* What needs the store — the fingerprints, partial mail, the per-sender cap, the targets — the store checks.
|
|
349
349
|
*/
|
|
350
|
-
const KEY = /^[a-z0-9][a-z0-9-]{0,39}:[1-9]\d
|
|
351
|
-
const KEY_SHAPE = "a mail findings key is <holder>:<through_id>,
|
|
350
|
+
const KEY = /^[a-z0-9][a-z0-9-]{0,39}:[1-9]\d*(-r[1-9]\d*)?$/;
|
|
351
|
+
const KEY_SHAPE = "a mail findings key is the one `review --holder` gave: <holder>:<through_id>, and -r<resets> after a reset, " +
|
|
352
|
+
"such as mail-triage:9123456 or mail-triage:9123456-r1";
|
|
352
353
|
export const validateMailFindings = (input) => {
|
|
353
354
|
const batch = validateBatch(input, RULES, MAIL_FINDINGS_PER_RUN, one);
|
|
354
355
|
if (!KEY.test(batch.key))
|
package/dist/mail/marks.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface MailMark {
|
|
|
22
22
|
suspicious: number[];
|
|
23
23
|
read: number;
|
|
24
24
|
skipped: Record<SkipReason, number>;
|
|
25
|
+
/** How often the owner moved the mark back: a findings key names it, so a run after a reset never reuses one. */
|
|
26
|
+
resets: number;
|
|
25
27
|
/** ms. */
|
|
26
28
|
updatedAt: number;
|
|
27
29
|
}
|
|
@@ -30,6 +32,12 @@ export declare const markOf: (store: MessageStore, mailbox: AccountKey, holder:
|
|
|
30
32
|
export declare const marksOf: (store: MessageStore, mailbox: AccountKey) => Promise<MailMark[]>;
|
|
31
33
|
/** The mark's state, as a cursor `acknowledge` takes back: the last id read, and when it was read. */
|
|
32
34
|
export declare const cursorOf: (throughId: number, readAt: number) => string;
|
|
35
|
+
/**
|
|
36
|
+
* The findings key of one run: `<holder>:<through_id>`, and `-r<resets>` once the owner moved the mark back, so the
|
|
37
|
+
* runs after a reset never reach a key a batch staged before it holds. The same review gives the same key: a retry
|
|
38
|
+
* of one run stages nothing twice.
|
|
39
|
+
*/
|
|
40
|
+
export declare const findingsKeyOf: (holder: string, throughId: number, resets: number) => string;
|
|
33
41
|
/** A bulk email that is not surely a newsletter: the holder gets its subject and first lines only. */
|
|
34
42
|
export interface BulkExcerpt {
|
|
35
43
|
email: StoredHit;
|
|
@@ -45,6 +53,8 @@ export interface MarkRead {
|
|
|
45
53
|
excerpts: BulkExcerpt[];
|
|
46
54
|
/** What `acknowledge --cursor` takes once the holder is done with these. */
|
|
47
55
|
cursor: string;
|
|
56
|
+
/** The key a findings batch over these emails takes; `undefined` while nothing is stored to read. */
|
|
57
|
+
findingsKey: string | undefined;
|
|
48
58
|
/** More is stored after this batch: read again after acknowledging. */
|
|
49
59
|
more: boolean;
|
|
50
60
|
held: number;
|
package/dist/mail/marks.js
CHANGED
|
@@ -39,6 +39,7 @@ const parsed = (holder, value, at) => {
|
|
|
39
39
|
suspicious: Array.isArray(saved.suspicious) ? saved.suspicious.filter((id) => typeof id === "number") : [],
|
|
40
40
|
read: saved.read ?? 0,
|
|
41
41
|
skipped: { ...noneSkipped(), ...saved.skipped },
|
|
42
|
+
resets: typeof saved.resets === "number" ? saved.resets : 0,
|
|
42
43
|
updatedAt: at,
|
|
43
44
|
};
|
|
44
45
|
}
|
|
@@ -80,6 +81,12 @@ const verdict = (email, now) => {
|
|
|
80
81
|
};
|
|
81
82
|
/** The mark's state, as a cursor `acknowledge` takes back: the last id read, and when it was read. */
|
|
82
83
|
export const cursorOf = (throughId, readAt) => `${throughId}.${readAt}`;
|
|
84
|
+
/**
|
|
85
|
+
* The findings key of one run: `<holder>:<through_id>`, and `-r<resets>` once the owner moved the mark back, so the
|
|
86
|
+
* runs after a reset never reach a key a batch staged before it holds. The same review gives the same key: a retry
|
|
87
|
+
* of one run stages nothing twice.
|
|
88
|
+
*/
|
|
89
|
+
export const findingsKeyOf = (holder, throughId, resets) => `${holder}:${throughId}${resets > 0 ? `-r${resets}` : ""}`;
|
|
83
90
|
const parseCursor = (cursor) => {
|
|
84
91
|
const match = /^(\d+)\.(\d+)$/.exec(cursor.trim());
|
|
85
92
|
if (!match)
|
|
@@ -140,10 +147,14 @@ const examine = async (store, mailbox, holder, { now = Date.now(), limit = MARK_
|
|
|
140
147
|
skipped: noneSkipped(),
|
|
141
148
|
mark: saved,
|
|
142
149
|
};
|
|
150
|
+
const keyed = (throughId) => ({
|
|
151
|
+
cursor: cursorOf(throughId, now),
|
|
152
|
+
findingsKey: throughId > 0 ? findingsKeyOf(holder, throughId, saved?.resets ?? 0) : undefined,
|
|
153
|
+
});
|
|
143
154
|
if (!account)
|
|
144
|
-
return { ...empty,
|
|
155
|
+
return { ...empty, ...keyed(0) };
|
|
145
156
|
if (!saved)
|
|
146
|
-
return { ...empty,
|
|
157
|
+
return { ...empty, ...keyed(await store.mail.newestEmailId(account.id)) };
|
|
147
158
|
const mark = after === undefined ? saved : { ...saved, throughId: after, held: [], suspicious: [] };
|
|
148
159
|
const found = await store.mail.emailsAfter(account.id, mark.throughId, limit + 1, [...mark.held, ...mark.suspicious]);
|
|
149
160
|
// Mail a running sync stored is read once that sync ended: it files bulk mail only at its end.
|
|
@@ -179,7 +190,7 @@ const examine = async (store, mailbox, holder, { now = Date.now(), limit = MARK_
|
|
|
179
190
|
wanted: wanted.sort((a, b) => a - b),
|
|
180
191
|
sorted: new Map([...sorted].filter(([, one]) => one.verdict === "excerpt")),
|
|
181
192
|
oldestStoredAt,
|
|
182
|
-
|
|
193
|
+
...keyed(batch.at(-1)?.id ?? mark.throughId),
|
|
183
194
|
more: newer.length > batch.length,
|
|
184
195
|
held,
|
|
185
196
|
skipped,
|
|
@@ -312,6 +323,7 @@ export const acknowledgeMark = async (store, mailbox, holder, cursor, now = Date
|
|
|
312
323
|
suspicious: [...new Set(suspicious)].sort((a, b) => a - b).slice(-MOST_HELD),
|
|
313
324
|
read,
|
|
314
325
|
skipped,
|
|
326
|
+
resets: mark?.resets ?? 0,
|
|
315
327
|
};
|
|
316
328
|
await store.sync.setSyncState(mailbox, key, JSON.stringify(next));
|
|
317
329
|
return { holder, ...next, updatedAt: now };
|
|
@@ -397,6 +409,7 @@ export const rewindMark = async (store, mailbox, holder, to, { now = Date.now(),
|
|
|
397
409
|
suspicious: mark.suspicious.filter((id) => id <= through),
|
|
398
410
|
read: mark.read,
|
|
399
411
|
skipped: mark.skipped,
|
|
412
|
+
resets: mark.resets + 1,
|
|
400
413
|
};
|
|
401
414
|
if (!dry)
|
|
402
415
|
await store.sync.setSyncState(mailbox, key, JSON.stringify(next));
|
package/dist/services/index.d.ts
CHANGED
|
@@ -188,7 +188,7 @@ export { type PackageUpgradeOutcome, type PackageUpgradePorts, type PackageUpgra
|
|
|
188
188
|
export { type PersonMeetingContext, type PersonMeetingContextItem, type PersonMeetingContextOptions, type PersonMeetingReadStore, personMeetingContext, } from "./person-meeting-context.js";
|
|
189
189
|
export { privatePeopleService } from "./private-people.js";
|
|
190
190
|
export { approveAsTask, type ProposalTaskStore, proposalSource } from "./proposal-tasks.js";
|
|
191
|
-
export { type Decided, type Decision, emptyProposalRegistry, FACT_SUMMARIES, ORGANIZATION_ADD_KIND, type OrganizationProposal, type ProposalFilter, type ProposalKind, type ProposalRegistry, type ProposalRow, parseProposalIds, proposalRegistry, proposeOrganization, } from "./proposals/index.js";
|
|
191
|
+
export { type Decided, type Decision, emptyProposalRegistry, FACT_SUMMARIES, ORGANIZATION_ADD_KIND, ORGANIZATION_RENAME_KIND, type OrganizationProposal, type OrganizationRenameProposal, type ProposalFilter, type ProposalKind, type ProposalRegistry, type ProposalRow, parseProposalIds, proposalRegistry, proposeOrganization, proposeOrganizationRename, } from "./proposals/index.js";
|
|
192
192
|
export type { RetentionQuery, RetentionService } from "./retention.js";
|
|
193
193
|
export { retentionService } from "./retention.js";
|
|
194
194
|
export { RESOURCES_SEARCHED, type SearchAllFound, type SearchAllItem, type SearchAllMessenger, type SearchAllRequest, type SearchedResource, searchAll, } from "./search-all.js";
|
package/dist/services/index.js
CHANGED
|
@@ -146,7 +146,7 @@ export { upgradePackage, } from "./package-upgrade.js";
|
|
|
146
146
|
export { personMeetingContext, } from "./person-meeting-context.js";
|
|
147
147
|
export { privatePeopleService } from "./private-people.js";
|
|
148
148
|
export { approveAsTask, proposalSource } from "./proposal-tasks.js";
|
|
149
|
-
export { emptyProposalRegistry, FACT_SUMMARIES, ORGANIZATION_ADD_KIND, parseProposalIds, proposalRegistry, proposeOrganization, } from "./proposals/index.js";
|
|
149
|
+
export { emptyProposalRegistry, FACT_SUMMARIES, ORGANIZATION_ADD_KIND, ORGANIZATION_RENAME_KIND, parseProposalIds, proposalRegistry, proposeOrganization, proposeOrganizationRename, } from "./proposals/index.js";
|
|
150
150
|
export { retentionService } from "./retention.js";
|
|
151
151
|
export { RESOURCES_SEARCHED, searchAll, } from "./search-all.js";
|
|
152
152
|
export { RESOURCES_SEARCHED_WITH_MEETINGS, searchAllIncludingMeetings, searchAllWithMeetings, } from "./search-all-meetings.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { parseProposalIds } from "./ids.js";
|
|
2
2
|
export { FACT_SUMMARIES } from "./kinds/findings.js";
|
|
3
|
-
export { ORGANIZATION_ADD_KIND, type OrganizationProposal, proposeOrganization } from "./kinds/organization.js";
|
|
3
|
+
export { ORGANIZATION_ADD_KIND, ORGANIZATION_RENAME_KIND, type OrganizationProposal, type OrganizationRenameProposal, proposeOrganization, proposeOrganizationRename, } from "./kinds/organization.js";
|
|
4
4
|
export { emptyProposalRegistry, proposalRegistry } from "./registry.js";
|
|
5
5
|
export type { Decided, Decision, ProposalFilter, ProposalKind, ProposalRegistry, ProposalRow } from "./types.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { parseProposalIds } from "./ids.js";
|
|
2
2
|
export { FACT_SUMMARIES } from "./kinds/findings.js";
|
|
3
|
-
export { ORGANIZATION_ADD_KIND, proposeOrganization } from "./kinds/organization.js";
|
|
3
|
+
export { ORGANIZATION_ADD_KIND, ORGANIZATION_RENAME_KIND, proposeOrganization, proposeOrganizationRename, } from "./kinds/organization.js";
|
|
4
4
|
export { emptyProposalRegistry, proposalRegistry } from "./registry.js";
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type { Author, MessageStore, Organization, ProposedAction } from "../../../store/index.js";
|
|
2
2
|
import type { ProposalKind } from "../types.js";
|
|
3
3
|
export declare const ORGANIZATION_ADD_KIND = "organization_add";
|
|
4
|
+
export declare const ORGANIZATION_RENAME_KIND = "organization_rename";
|
|
4
5
|
export interface OrganizationProposal {
|
|
5
6
|
name: string;
|
|
6
7
|
kind: Organization["kind"];
|
|
7
8
|
scope?: Organization["scope"];
|
|
8
9
|
}
|
|
10
|
+
export interface OrganizationRenameProposal {
|
|
11
|
+
organization: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
9
14
|
/** An agent's new organization waits as a stored proposal; the organization exists only once it is confirmed. */
|
|
10
15
|
export declare const proposeOrganization: (store: Pick<MessageStore, "proposedActions">, input: OrganizationProposal, by: Author, reason?: string) => Promise<ProposedAction>;
|
|
16
|
+
/** An agent's rename waits as a stored proposal too; the organization keeps its name until it is confirmed. */
|
|
17
|
+
export declare const proposeOrganizationRename: (store: Pick<MessageStore, "proposedActions">, input: OrganizationRenameProposal, by: Author, reason?: string) => Promise<ProposedAction>;
|
|
11
18
|
export declare const organizationKind: (store: Pick<MessageStore, "proposedActions" | "knowledge">) => ProposalKind;
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { proposerOfAction } from "../../../store/index.js";
|
|
2
2
|
import { actionStatus, kept, pushed, storedStatus } from "./shared.js";
|
|
3
3
|
export const ORGANIZATION_ADD_KIND = "organization_add";
|
|
4
|
+
export const ORGANIZATION_RENAME_KIND = "organization_rename";
|
|
4
5
|
const rowOf = (action) => {
|
|
6
|
+
const renamed = action.kind === ORGANIZATION_RENAME_KIND;
|
|
5
7
|
const { name, kind } = action.payload;
|
|
8
|
+
const { organization } = action.payload;
|
|
6
9
|
return {
|
|
7
10
|
id: action.id,
|
|
8
11
|
kind: "organization",
|
|
9
12
|
status: actionStatus(action.status),
|
|
10
13
|
project: null,
|
|
11
|
-
subject:
|
|
14
|
+
subject: renamed ? organization : "organization:new",
|
|
12
15
|
object: null,
|
|
13
|
-
summary: `add ${kind} ${name}`,
|
|
16
|
+
summary: renamed ? `rename ${organization} to ${name}` : `add ${kind} ${name}`,
|
|
14
17
|
proposedBy: proposerOfAction(action),
|
|
15
18
|
reason: action.reason,
|
|
16
19
|
evidence: [],
|
|
@@ -27,13 +30,20 @@ export const proposeOrganization = (store, input, by, reason) => store.proposedA
|
|
|
27
30
|
payload: input,
|
|
28
31
|
...(reason === undefined ? {} : { reason }),
|
|
29
32
|
});
|
|
33
|
+
/** An agent's rename waits as a stored proposal too; the organization keeps its name until it is confirmed. */
|
|
34
|
+
export const proposeOrganizationRename = (store, input, by, reason) => store.proposedActions.propose({
|
|
35
|
+
kind: ORGANIZATION_RENAME_KIND,
|
|
36
|
+
by,
|
|
37
|
+
payload: input,
|
|
38
|
+
...(reason === undefined ? {} : { reason }),
|
|
39
|
+
});
|
|
30
40
|
export const organizationKind = (store) => ({
|
|
31
41
|
kind: "organization",
|
|
32
|
-
stored: [ORGANIZATION_ADD_KIND],
|
|
42
|
+
stored: [ORGANIZATION_ADD_KIND, ORGANIZATION_RENAME_KIND],
|
|
33
43
|
list: async (filter) => {
|
|
34
44
|
const status = storedStatus(filter);
|
|
35
45
|
const actions = await store.proposedActions.list({
|
|
36
|
-
kinds: [ORGANIZATION_ADD_KIND],
|
|
46
|
+
kinds: [ORGANIZATION_ADD_KIND, ORGANIZATION_RENAME_KIND],
|
|
37
47
|
...(status === undefined ? {} : { status }),
|
|
38
48
|
...pushed(filter),
|
|
39
49
|
});
|
|
@@ -45,7 +55,14 @@ export const organizationKind = (store) => ({
|
|
|
45
55
|
await store.proposedActions.reject(id, by, reason);
|
|
46
56
|
return { id, kind: "organization", status: "rejected", made: [] };
|
|
47
57
|
}
|
|
48
|
-
const
|
|
58
|
+
const action = await store.proposedActions.get(id);
|
|
59
|
+
if (action.kind === ORGANIZATION_RENAME_KIND) {
|
|
60
|
+
const { organization, name } = action.payload;
|
|
61
|
+
const renamed = await store.knowledge.updateOrganization(organization, { name });
|
|
62
|
+
await store.proposedActions.approve(id, by, { organization: renamed.ref });
|
|
63
|
+
return { id, kind: "organization", status: "confirmed", made: [] };
|
|
64
|
+
}
|
|
65
|
+
const { name, kind, scope } = action.payload;
|
|
49
66
|
const made = await store.knowledge.addOrganization({ name, kind, ...(scope === undefined ? {} : { scope }) });
|
|
50
67
|
await store.proposedActions.approve(id, by, { organization: made.ref });
|
|
51
68
|
return { id, kind: "organization", status: "confirmed", made: [made.ref] };
|
|
@@ -6,7 +6,13 @@ export const ORGANIZATIONS_QUERY = {
|
|
|
6
6
|
backend: "sql",
|
|
7
7
|
fields: queryFieldsOf(organization, {
|
|
8
8
|
native: ["id", "kind", "scope", "created_at"],
|
|
9
|
-
examples: {
|
|
9
|
+
examples: {
|
|
10
|
+
id: "id:1",
|
|
11
|
+
kind: "kind:company",
|
|
12
|
+
name: "name:*acme*",
|
|
13
|
+
scope: "scope:work",
|
|
14
|
+
created_at: "created_at>=2026-01-01",
|
|
15
|
+
},
|
|
10
16
|
}),
|
|
11
17
|
};
|
|
12
18
|
export const PROJECTS_QUERY = {
|
|
@@ -5,6 +5,7 @@ import { type MeetingProjectsStore } from "./meeting-projects.js";
|
|
|
5
5
|
import type { Author } from "./memories.js";
|
|
6
6
|
import { type Link } from "./notes.js";
|
|
7
7
|
import type { StoreContext } from "./open.js";
|
|
8
|
+
import { type OrganizationsStore } from "./organizations.js";
|
|
8
9
|
import type { ProposedAction } from "./proposed-actions.js";
|
|
9
10
|
import { type KnowledgeRelation, type LinkDecision, type RelateInput, type RelationUpdate, type Scored } from "./relation-proposals.js";
|
|
10
11
|
import type { TagCreationOptions } from "./tag-creation.js";
|
|
@@ -100,7 +101,7 @@ export interface Reminder {
|
|
|
100
101
|
receipt: string | null;
|
|
101
102
|
}
|
|
102
103
|
export type LabelledType = "note" | "document" | "person" | "organization" | "project" | "task" | "meeting" | "folder";
|
|
103
|
-
export interface KnowledgeStore extends VocabularyStore, MeetingProjectsStore {
|
|
104
|
+
export interface KnowledgeStore extends VocabularyStore, MeetingProjectsStore, OrganizationsStore {
|
|
104
105
|
taskIds(key: AccountKey, options?: {
|
|
105
106
|
state?: "open" | "done" | "dismissed";
|
|
106
107
|
limit?: number;
|
|
@@ -159,7 +160,6 @@ export interface KnowledgeStore extends VocabularyStore, MeetingProjectsStore {
|
|
|
159
160
|
name: string;
|
|
160
161
|
scope?: Organization["scope"];
|
|
161
162
|
}): Promise<Organization>;
|
|
162
|
-
organizations(query?: QueryAst): Promise<Organization[]>;
|
|
163
163
|
/** A project; its key is made from the name when none is given. */
|
|
164
164
|
addProject(input: {
|
|
165
165
|
name: string;
|
|
@@ -14,6 +14,7 @@ import { linkKind, RELATION_KINDS } from "./link-kinds.js";
|
|
|
14
14
|
import { meetingProjectsStoreOver } from "./meeting-projects.js";
|
|
15
15
|
import { holdingText } from "./note-index.js";
|
|
16
16
|
import { insertLink, linkOf } from "./notes.js";
|
|
17
|
+
import { organizationOf, organizationsStoreOver } from "./organizations.js";
|
|
17
18
|
import { queryFilter } from "./query-filter.js";
|
|
18
19
|
import { relationOf, relationProposalsOver, } from "./relation-proposals.js";
|
|
19
20
|
import { addTags as addThingTags, canonicalTagNamed, createTag, labelsOf, liveTagging, removeTags as removeThingTags, setMainTopic, } from "./tags.js";
|
|
@@ -205,14 +206,6 @@ export const knowledgeStoreOver = (context) => {
|
|
|
205
206
|
throw new CliError("not_found", "no annotation with that id");
|
|
206
207
|
return found;
|
|
207
208
|
};
|
|
208
|
-
const organizationOf = (row) => ({
|
|
209
|
-
id: String(row.id),
|
|
210
|
-
ref: `organization:${row.id}`,
|
|
211
|
-
kind: row.kind,
|
|
212
|
-
name: String(row.name),
|
|
213
|
-
scope: row.scope,
|
|
214
|
-
createdAt: iso(row.created_at),
|
|
215
|
-
});
|
|
216
209
|
const projectOf = (row) => ({
|
|
217
210
|
id: String(row.id),
|
|
218
211
|
ref: `project:${row.id}`,
|
|
@@ -294,6 +287,7 @@ export const knowledgeStoreOver = (context) => {
|
|
|
294
287
|
const tagsOf = (list) => [...new Set(list.map(canonicalTagName))];
|
|
295
288
|
return {
|
|
296
289
|
...vocabularyStoreOver(context),
|
|
290
|
+
...organizationsStoreOver(context),
|
|
297
291
|
...meetingProjectsStoreOver(context),
|
|
298
292
|
taskIds: async (key, options = {}) => {
|
|
299
293
|
const { limit, offset } = bounded(options.limit, options.offset);
|
|
@@ -445,19 +439,6 @@ export const knowledgeStoreOver = (context) => {
|
|
|
445
439
|
.get(oneOf(kind, ORGANIZATION_KINDS, "an organization's kind"), textOf(name, 200), oneOf(scope, SCOPES, "a scope"), at, at);
|
|
446
440
|
return organizationOf(row);
|
|
447
441
|
},
|
|
448
|
-
organizations: async (query) => {
|
|
449
|
-
const where = query &&
|
|
450
|
-
queryFilter(query, {
|
|
451
|
-
id: { type: "kw", sql: "CAST(id AS TEXT)" },
|
|
452
|
-
kind: { type: "enum", sql: "kind" },
|
|
453
|
-
scope: { type: "enum", sql: "scope" },
|
|
454
|
-
created_at: { type: "time", sql: "created_at" },
|
|
455
|
-
});
|
|
456
|
-
return database
|
|
457
|
-
.prepare(`SELECT * FROM organizations WHERE deleted_at IS NULL${where ? ` AND (${where.sql})` : ""} ORDER BY name, id LIMIT 500`)
|
|
458
|
-
.all(...(where?.params ?? []))
|
|
459
|
-
.map(organizationOf);
|
|
460
|
-
},
|
|
461
442
|
addProject: async ({ name, key, type = "other", organization, scope = "personal", description }) => write(database, () => {
|
|
462
443
|
const title = textOf(name, 200);
|
|
463
444
|
const organizationId = organization === undefined
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type QueryAst } from "@wirecat/cli-base/query";
|
|
2
|
+
import type { Organization } from "./knowledge.js";
|
|
3
|
+
import type { StoreContext } from "./open.js";
|
|
4
|
+
type Row = Record<string, unknown>;
|
|
5
|
+
export declare const organizationOf: (row: Row) => Organization;
|
|
6
|
+
export interface OrganizationsStore {
|
|
7
|
+
organizations(query?: QueryAst): Promise<Organization[]>;
|
|
8
|
+
/** Renames an organization given as `organization:<id>` or by its id; its kind and scope stay. */
|
|
9
|
+
updateOrganization(organization: string, input: {
|
|
10
|
+
name: string;
|
|
11
|
+
}): Promise<Organization>;
|
|
12
|
+
}
|
|
13
|
+
export declare const organizationsStoreOver: ({ database, now }: StoreContext) => OrganizationsStore;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { evaluate, walkQuery } from "@wirecat/cli-base/query";
|
|
3
|
+
import { ORGANIZATIONS_QUERY } from "../knowledge-query.js";
|
|
4
|
+
import { queryFilter } from "./query-filter.js";
|
|
5
|
+
import { thingOf } from "./things.js";
|
|
6
|
+
import { write } from "./transaction.js";
|
|
7
|
+
export const organizationOf = (row) => ({
|
|
8
|
+
id: String(row.id),
|
|
9
|
+
ref: `organization:${row.id}`,
|
|
10
|
+
kind: row.kind,
|
|
11
|
+
name: String(row.name),
|
|
12
|
+
scope: row.scope,
|
|
13
|
+
createdAt: new Date(Number(row.created_at)).toISOString(),
|
|
14
|
+
});
|
|
15
|
+
export const organizationsStoreOver = ({ database, now }) => ({
|
|
16
|
+
organizations: async (query) => {
|
|
17
|
+
// `lower()` folds ASCII only and an organization has no folded copy, so a name filter runs on the rows.
|
|
18
|
+
if (query && walkQuery(query.root).some((node) => node.field === "name"))
|
|
19
|
+
return database.prepare("SELECT * FROM organizations WHERE deleted_at IS NULL ORDER BY name, id").all()
|
|
20
|
+
.map(organizationOf)
|
|
21
|
+
.filter((one) => evaluate(query, ORGANIZATIONS_QUERY.fields, { ...one, created_at: one.createdAt }))
|
|
22
|
+
.slice(0, 500);
|
|
23
|
+
const where = query &&
|
|
24
|
+
queryFilter(query, {
|
|
25
|
+
id: { type: "kw", sql: "CAST(id AS TEXT)" },
|
|
26
|
+
kind: { type: "enum", sql: "kind" },
|
|
27
|
+
scope: { type: "enum", sql: "scope" },
|
|
28
|
+
created_at: { type: "time", sql: "created_at" },
|
|
29
|
+
});
|
|
30
|
+
return database
|
|
31
|
+
.prepare(`SELECT * FROM organizations WHERE deleted_at IS NULL${where ? ` AND (${where.sql})` : ""} ORDER BY name, id LIMIT 500`)
|
|
32
|
+
.all(...(where?.params ?? []))
|
|
33
|
+
.map(organizationOf);
|
|
34
|
+
},
|
|
35
|
+
updateOrganization: async (organization, { name }) => write(database, () => {
|
|
36
|
+
const title = name.trim();
|
|
37
|
+
if (!title || title.length > 200)
|
|
38
|
+
throw new CliError("validation_error", "text takes 1–200 characters");
|
|
39
|
+
const thing = thingOf(database, organization.includes(":") ? organization : `organization:${organization}`);
|
|
40
|
+
const row = thing?.type === "organization" &&
|
|
41
|
+
database
|
|
42
|
+
.prepare("UPDATE organizations SET name = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL RETURNING *")
|
|
43
|
+
.get(title, now(), thing.id);
|
|
44
|
+
if (!row)
|
|
45
|
+
throw new CliError("not_found", `no organization ${organization}`);
|
|
46
|
+
return organizationOf(row);
|
|
47
|
+
}),
|
|
48
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.582.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",
|