@wirecat/cli-core 0.374.0 → 0.376.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/context/evidence-response.js +3 -2
- package/dist/context/evidence.d.ts +8 -4
- package/dist/context/evidence.js +19 -8
- package/dist/search/lucene/registry.js +1 -1
- package/dist/services/context-evidence.js +2 -1
- package/dist/services/index.d.ts +2 -2
- package/dist/services/meeting-communications-selection.d.ts +2 -1
- package/dist/services/project-gathering-request.d.ts +24 -2
- package/dist/services/project-gathering-request.js +61 -3
- package/dist/services/project-gathering.d.ts +12 -1
- package/dist/services/project-gathering.js +33 -8
- package/dist/store/sqlite/context-evidence.js +35 -21
- package/dist/store/sqlite/knowledge.js +15 -5
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,46 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.376.0 — 20.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`gatherProjectEvidence` reads a project's linked chats.** `linkedChats: [{ provider, account, chat }]` (at
|
|
15
|
+
most 16, the host's confirmed `member-of` links) become lanes of their own (`kinds: ["linked-chat"]`,
|
|
16
|
+
`query: null`, `chat: <reference>`), read whole through the selected source of the chat's own account. Items
|
|
17
|
+
found only that way have `context: "linked-chat"` and no `around`; a reference a word lane also finds keeps
|
|
18
|
+
`context: "project"`. The result's `linkedChats` gives each entry's `ref`, `source`, `lane` and `state`
|
|
19
|
+
(`in-scope`, `out-of-scope` when its account is not in `sources`, never read; `unavailable` when not stored).
|
|
20
|
+
Checkpoints stay version 1; a run without linked chats keeps its fingerprint, and a changed link set is refused
|
|
21
|
+
with "start again without --cursor". See [project gathering](docs/dev/PROJECT-GATHERING.md).
|
|
22
|
+
- **The context reader reads one chat.** `chat` (a chat or mail thread external id) in place of `query` returns
|
|
23
|
+
that chat's records newest first, including a chat hidden from search, with `coverage.matching: "chat"`.
|
|
24
|
+
Refused on document and meeting sources and together with `query`. Old cursors stay valid.
|
|
25
|
+
- **`knowledge.relate` links a chat to a project.** A `chat:<provider>/<account>/<chat>` reference may be the
|
|
26
|
+
`from` of a `member-of` relationship to a `project:`, and `confirmRelation` confirms it. The reference carries
|
|
27
|
+
its account; a scope key of another account is refused. A chat is related to nothing else.
|
|
28
|
+
|
|
29
|
+
### Changed — may break callers
|
|
30
|
+
|
|
31
|
+
- **`ProjectLane.query` is `string | null`** (null for a linked chat's lane), and `ProjectEvidenceItem.context`
|
|
32
|
+
adds `"linked-chat"`. `ContextEvidenceRequest.query` is optional and `coverage.matching` adds `"chat"`.
|
|
33
|
+
- **`store.contextRecords.read` resolves a message of a chat hidden from search**, since its reference names
|
|
34
|
+
the chat, so evidence from a linked hidden chat can be staged and its fingerprint rechecked.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **A resumed project gathering read nothing when every open unit already had a page** (for example a lane
|
|
39
|
+
stopped after its first page): it now starts at the shallowest open unit's round.
|
|
40
|
+
|
|
41
|
+
## 0.375.0 — 20.10.2026
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **A WhatsApp chat id works unquoted in a query.** `chat:133…@lid` and `chat:120…@g.us` parse as one term; before,
|
|
46
|
+
`@lid` was refused with `query.unsupported-operator`. The fix is in cli-base 0.47.3, so cli-core now needs
|
|
47
|
+
`@wirecat/cli-base >=0.47.3`: a CLI pinned to 0.47.0–0.47.2 must raise its own cli-base pin with this release.
|
|
48
|
+
Emails in the `email` field help and the search docs are unquoted too.
|
|
49
|
+
|
|
10
50
|
## 0.374.0 — 20.10.2026
|
|
11
51
|
|
|
12
52
|
### Added
|
|
@@ -8,7 +8,7 @@ export const validateContextEvidencePage = (page, request) => {
|
|
|
8
8
|
throw new CliError("invalid_response", "context reader returned invalid or out-of-scope evidence");
|
|
9
9
|
};
|
|
10
10
|
try {
|
|
11
|
-
const { source, after, before, limit, maxReadBytes } = contextEvidenceRequest(request);
|
|
11
|
+
const { source, chat, after, before, limit, maxReadBytes } = contextEvidenceRequest(request);
|
|
12
12
|
if (!page || contextSourceKey(contextSource(page.source)) !== contextSourceKey(source))
|
|
13
13
|
fail();
|
|
14
14
|
if (!Array.isArray(page.items) ||
|
|
@@ -23,7 +23,7 @@ export const validateContextEvidencePage = (page, request) => {
|
|
|
23
23
|
typeof c.hasMore !== "boolean" ||
|
|
24
24
|
typeof c.hasUndated !== "boolean" ||
|
|
25
25
|
!["ready", "pending", "missing", "unknown"].includes(c.index) ||
|
|
26
|
-
c.matching !== "literal-words" ||
|
|
26
|
+
c.matching !== (chat === undefined ? "literal-words" : "chat") ||
|
|
27
27
|
c.completeness !== "unknown" ||
|
|
28
28
|
c.consistency !== "snapshot-per-page")
|
|
29
29
|
fail();
|
|
@@ -83,6 +83,7 @@ export const validateContextEvidencePage = (page, request) => {
|
|
|
83
83
|
const locator = parseLocator(item.ref);
|
|
84
84
|
if (locator.provider !== source.provider ||
|
|
85
85
|
locator.account !== source.account ||
|
|
86
|
+
(chat !== undefined && locator.chat !== chat) ||
|
|
86
87
|
formatLocator(locator) !== item.ref)
|
|
87
88
|
fail();
|
|
88
89
|
text.push(locator.chat, locator.message);
|
|
@@ -8,7 +8,10 @@ export interface ContextSource {
|
|
|
8
8
|
}
|
|
9
9
|
export interface ContextEvidenceRequest {
|
|
10
10
|
source: ContextSource;
|
|
11
|
-
query
|
|
11
|
+
/** Required unless `chat` is given; a chat read takes no query. */
|
|
12
|
+
query?: string;
|
|
13
|
+
/** One stored chat or mail thread of the source's account, by its external id; refused for documents and meetings. */
|
|
14
|
+
chat?: string;
|
|
12
15
|
after: number;
|
|
13
16
|
before: number;
|
|
14
17
|
limit?: number;
|
|
@@ -52,7 +55,7 @@ export interface ContextEvidencePage {
|
|
|
52
55
|
hasMore: boolean;
|
|
53
56
|
hasUndated: boolean;
|
|
54
57
|
completeness: "unknown";
|
|
55
|
-
matching: "literal-words";
|
|
58
|
+
matching: "literal-words" | "chat";
|
|
56
59
|
consistency: "snapshot-per-page";
|
|
57
60
|
};
|
|
58
61
|
}
|
|
@@ -80,11 +83,12 @@ export declare const fittingRows: (sizes: readonly number[], limit: number, maxR
|
|
|
80
83
|
export interface ContextRecordReader {
|
|
81
84
|
read(ref: string): Promise<Pick<ContextEvidenceItem, "ref" | "title" | "text" | "revision"> | null>;
|
|
82
85
|
}
|
|
83
|
-
export declare const contextCursorScope: (request: Pick<ContextEvidenceRequest, "source" | "query" | "after" | "before">) => string;
|
|
86
|
+
export declare const contextCursorScope: (request: Pick<ContextEvidenceRequest, "source" | "query" | "chat" | "after" | "before">) => string;
|
|
84
87
|
export declare const contextSource: (value: ContextSource) => ContextSource;
|
|
85
88
|
export declare const contextEvidenceRequest: (input: ContextEvidenceRequest) => {
|
|
86
89
|
source: ContextSource;
|
|
87
|
-
query: string;
|
|
90
|
+
query: string | undefined;
|
|
91
|
+
chat: string | undefined;
|
|
88
92
|
words: RegExpMatchArray | [];
|
|
89
93
|
after: number;
|
|
90
94
|
before: number;
|
package/dist/context/evidence.js
CHANGED
|
@@ -24,9 +24,11 @@ export const contextCursorScope = (request) => createHash("sha256")
|
|
|
24
24
|
request.source.accountId,
|
|
25
25
|
request.source.provider,
|
|
26
26
|
request.source.account,
|
|
27
|
-
request.query,
|
|
27
|
+
request.query ?? null,
|
|
28
28
|
request.after,
|
|
29
29
|
request.before,
|
|
30
|
+
// Only a chat read hashes its chat, so cursors issued before chat reads existed stay valid.
|
|
31
|
+
...(request.chat === undefined ? [] : [request.chat]),
|
|
30
32
|
]))
|
|
31
33
|
.digest("hex");
|
|
32
34
|
const fields = (value, allowed, prefix) => {
|
|
@@ -51,7 +53,7 @@ export const contextSource = (value) => {
|
|
|
51
53
|
};
|
|
52
54
|
export const contextEvidenceRequest = (input) => {
|
|
53
55
|
const source = contextSource(input?.source);
|
|
54
|
-
fields(input, ["source", "query", "after", "before", "limit", "maxReadBytes", "cursor", "signal"], "");
|
|
56
|
+
fields(input, ["source", "query", "chat", "after", "before", "limit", "maxReadBytes", "cursor", "signal"], "");
|
|
55
57
|
const integer = (value, name, low, high) => {
|
|
56
58
|
if (!Number.isSafeInteger(value) || value < low || value > high)
|
|
57
59
|
throw new CliError("validation_error", `${name} must be a whole number from ${low} to ${high}`);
|
|
@@ -61,11 +63,20 @@ export const contextEvidenceRequest = (input) => {
|
|
|
61
63
|
const before = integer(input.before, "before", after + 1, 8_640_000_000_000_000);
|
|
62
64
|
if (before - after > 90 * 86400000)
|
|
63
65
|
throw new CliError("validation_error", "context time window must not exceed 90 days");
|
|
66
|
+
const chat = input.chat;
|
|
67
|
+
if (chat !== undefined) {
|
|
68
|
+
if (typeof chat !== "string" || !chat.trim() || Buffer.byteLength(chat) > 1024)
|
|
69
|
+
throw new CliError("validation_error", "chat needs 1–1024 UTF-8 bytes");
|
|
70
|
+
if (source.kind !== "message" && source.kind !== "mail")
|
|
71
|
+
throw new CliError("validation_error", "only a message or mail source reads one chat");
|
|
72
|
+
if (input.query !== undefined)
|
|
73
|
+
throw new CliError("validation_error", "a chat read takes no query");
|
|
74
|
+
}
|
|
64
75
|
const query = input.query;
|
|
65
|
-
if (typeof query !== "string" || !query.trim() || Buffer.byteLength(query) > 2048)
|
|
76
|
+
if (chat === undefined && (typeof query !== "string" || !query.trim() || Buffer.byteLength(query) > 2048))
|
|
66
77
|
throw new CliError("validation_error", "query needs 1–2048 UTF-8 bytes of literal words");
|
|
67
|
-
const words = query
|
|
68
|
-
if (!words.length || words.length > 16)
|
|
78
|
+
const words = query?.match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
79
|
+
if (chat === undefined && (!words.length || words.length > 16))
|
|
69
80
|
throw new CliError("validation_error", "query needs 1–16 literal words");
|
|
70
81
|
const limit = integer(input.limit ?? 10, "limit", 1, 20);
|
|
71
82
|
const maxReadBytes = integer(input.maxReadBytes ?? 65536, "maxReadBytes", 1, CONTEXT_MAX_READ_BYTES);
|
|
@@ -80,7 +91,7 @@ export const contextEvidenceRequest = (input) => {
|
|
|
80
91
|
id: integer(input.cursor.id, "cursor.id", 1, Number.MAX_SAFE_INTEGER),
|
|
81
92
|
scope: input.cursor.scope,
|
|
82
93
|
};
|
|
83
|
-
if (cursor && cursor.scope !== contextCursorScope({ source, query, after, before }))
|
|
84
|
-
throw new CliError("validation_error", "cursor belongs to another source, query or time window");
|
|
85
|
-
return { source, query, words, after, before, limit, maxReadBytes, cursor, signal: input.signal };
|
|
94
|
+
if (cursor && cursor.scope !== contextCursorScope({ source, query, chat, after, before }))
|
|
95
|
+
throw new CliError("validation_error", "cursor belongs to another source, query, chat or time window");
|
|
96
|
+
return { source, query, chat, words, after, before, limit, maxReadBytes, cursor, signal: input.signal };
|
|
86
97
|
};
|
|
@@ -355,7 +355,7 @@ export const QUERY_FIELDS = [
|
|
|
355
355
|
'phone:"8 900 000-00-01"',
|
|
356
356
|
"a phone found in the text, written any way; a number without + is read in phone_countries",
|
|
357
357
|
],
|
|
358
|
-
["email",
|
|
358
|
+
["email", "email:ana@example.com", "an address found in the text, lower-cased"],
|
|
359
359
|
["url", 'url:"https://example.com/a"', "a link found in the text, without utm_*, fbclid and the fragment"],
|
|
360
360
|
["domain", "domain:google.com", "the site of a link or an address in the text; docs.google.com means google.com"],
|
|
361
361
|
[
|
|
@@ -21,7 +21,8 @@ export const createContextEvidenceReader = (port, allowed, limits) => {
|
|
|
21
21
|
const reservation = budget.reserve(validated.maxReadBytes);
|
|
22
22
|
const request = Object.freeze({
|
|
23
23
|
source: Object.freeze({ ...source }),
|
|
24
|
-
query: validated.query,
|
|
24
|
+
...(validated.query === undefined ? {} : { query: validated.query }),
|
|
25
|
+
...(validated.chat === undefined ? {} : { chat: validated.chat }),
|
|
25
26
|
after: validated.after,
|
|
26
27
|
before: validated.before,
|
|
27
28
|
limit: validated.limit,
|
package/dist/services/index.d.ts
CHANGED
|
@@ -3,13 +3,13 @@ export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, C
|
|
|
3
3
|
export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
|
|
4
4
|
export { excerpt } from "./excerpt.js";
|
|
5
5
|
export { type DownloadedModel, downloadImageModel, downloadTextModel, type ModelDownloadOptions, } from "./model-downloads.js";
|
|
6
|
-
export type { ProjectEvidenceItem, ProjectGathering, ProjectGatheringPorts, ProjectGatheringStop, ProjectIndexCatchUp, ProjectSourceFetcher, } from "./project-gathering.js";
|
|
6
|
+
export type { ProjectEvidenceContext, ProjectEvidenceItem, ProjectGathering, ProjectGatheringPorts, ProjectGatheringStop, ProjectIndexCatchUp, ProjectSourceFetcher, } from "./project-gathering.js";
|
|
7
7
|
export { gatherProjectEvidence } from "./project-gathering.js";
|
|
8
8
|
export type { ProjectNeighbour, ProjectNeighbourReader } from "./project-gathering-around.js";
|
|
9
9
|
export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
|
|
10
10
|
export type { EvidenceMention, EvidenceMentions } from "./project-gathering-mentions.js";
|
|
11
11
|
export { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
12
|
-
export type { ProjectFetchState, ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, ProjectUnitState, } from "./project-gathering-request.js";
|
|
12
|
+
export type { ProjectFetchState, ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, ProjectLinkedChat, ProjectUnitState, } from "./project-gathering-request.js";
|
|
13
13
|
import { type AccountService } from "./account.js";
|
|
14
14
|
import { type AdminService } from "./admin.js";
|
|
15
15
|
import { type AdminStatisticsService } from "./admin-statistics.js";
|
|
@@ -14,7 +14,8 @@ export declare const captureCommunicationSelections: (port: MeetingCommunication
|
|
|
14
14
|
id: any;
|
|
15
15
|
request: {
|
|
16
16
|
source: import("./context-evidence.js").ContextSource;
|
|
17
|
-
query: string;
|
|
17
|
+
query: string | undefined;
|
|
18
|
+
chat: string | undefined;
|
|
18
19
|
after: number;
|
|
19
20
|
before: number;
|
|
20
21
|
limit: number;
|
|
@@ -12,6 +12,8 @@ export interface ProjectGatheringRequest {
|
|
|
12
12
|
reference?: string;
|
|
13
13
|
}[];
|
|
14
14
|
sources: ContextSource[];
|
|
15
|
+
/** Chats the host confirmed as the project's; each is read whole, through the source of its own account. */
|
|
16
|
+
linkedChats?: ProjectLinkedChat[];
|
|
15
17
|
scope: "explicit" | "all-connected";
|
|
16
18
|
lookbackDays?: number;
|
|
17
19
|
after?: number;
|
|
@@ -28,6 +30,11 @@ export interface ProjectGatheringRequest {
|
|
|
28
30
|
checkpoint?: string;
|
|
29
31
|
signal?: AbortSignal;
|
|
30
32
|
}
|
|
33
|
+
export interface ProjectLinkedChat {
|
|
34
|
+
provider: string;
|
|
35
|
+
account: string;
|
|
36
|
+
chat: string;
|
|
37
|
+
}
|
|
31
38
|
export interface ProjectGatheringBudgets {
|
|
32
39
|
maxReads?: number;
|
|
33
40
|
maxReadBytes?: number;
|
|
@@ -35,12 +42,15 @@ export interface ProjectGatheringBudgets {
|
|
|
35
42
|
maxElapsedMs?: number;
|
|
36
43
|
maxItems?: number;
|
|
37
44
|
}
|
|
38
|
-
export type ProjectLaneKind = "project" | "key" | "alias" | "keyword" | "person";
|
|
45
|
+
export type ProjectLaneKind = "project" | "key" | "alias" | "keyword" | "person" | "linked-chat";
|
|
39
46
|
export interface ProjectLane {
|
|
40
47
|
id: string;
|
|
41
48
|
kinds: ProjectLaneKind[];
|
|
42
|
-
|
|
49
|
+
/** Null for a linked chat's lane, which reads every message of the chat. */
|
|
50
|
+
query: string | null;
|
|
43
51
|
people: string[];
|
|
52
|
+
/** A linked chat's lane only: the chat's reference. */
|
|
53
|
+
chat?: string;
|
|
44
54
|
}
|
|
45
55
|
export type ProjectUnitState = "pending" | "partial" | "done" | "unavailable" | "failed" | "read-limited";
|
|
46
56
|
export interface ProjectUnitProgress {
|
|
@@ -67,6 +77,8 @@ export interface ProjectGatheringCheckpoint {
|
|
|
67
77
|
code: string | null;
|
|
68
78
|
}[];
|
|
69
79
|
units: ProjectUnitProgress[];
|
|
80
|
+
/** Present only when the run reads linked chats: a hash of their references. */
|
|
81
|
+
linkedChats?: string;
|
|
70
82
|
seen: string[];
|
|
71
83
|
totals: {
|
|
72
84
|
reads: number;
|
|
@@ -90,6 +102,15 @@ export declare const projectGatheringRequest: (input: ProjectGatheringRequest, n
|
|
|
90
102
|
}[];
|
|
91
103
|
lanes: ProjectLane[];
|
|
92
104
|
sources: ContextSource[];
|
|
105
|
+
linkedChats: {
|
|
106
|
+
provider: string;
|
|
107
|
+
account: string;
|
|
108
|
+
chat: string;
|
|
109
|
+
ref: string;
|
|
110
|
+
source: number | null;
|
|
111
|
+
lane: string | null;
|
|
112
|
+
}[];
|
|
113
|
+
linkedKey: string | undefined;
|
|
93
114
|
scope: "all-connected" | "explicit";
|
|
94
115
|
lookbackDays: number | null;
|
|
95
116
|
window: {
|
|
@@ -126,6 +147,7 @@ export declare const projectUnits: (plan: ProjectGatheringPlan, window: {
|
|
|
126
147
|
after: number;
|
|
127
148
|
before: number;
|
|
128
149
|
};
|
|
150
|
+
chat: string | undefined;
|
|
129
151
|
}[];
|
|
130
152
|
export declare const seenKey: (ref: string) => string;
|
|
131
153
|
export declare const encodeCheckpoint: (checkpoint: ProjectGatheringCheckpoint) => string;
|
|
@@ -2,10 +2,11 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { CliError } from "@wirecat/cli-base";
|
|
3
3
|
import { contextSource } from "../context/evidence.js";
|
|
4
4
|
import { contextSourceKey } from "../context/evidence-response.js";
|
|
5
|
+
import { formatReference } from "../domain/references.js";
|
|
5
6
|
const DAY = 86400000;
|
|
6
7
|
const MAX_TIME = 8_640_000_000_000_000;
|
|
7
8
|
export const CONTEXT_CHUNK_DAYS = 90;
|
|
8
|
-
const LIMITS = { lanes: 16, sources: 32, units: 512, lookbackDays: 730, people: 16 };
|
|
9
|
+
const LIMITS = { lanes: 16, sources: 32, units: 512, lookbackDays: 730, people: 16, linkedChats: 16 };
|
|
9
10
|
const BUDGETS = {
|
|
10
11
|
maxReads: [48, 512],
|
|
11
12
|
maxReadBytes: [2097152, 16777216],
|
|
@@ -49,6 +50,7 @@ export const projectGatheringRequest = (input, now) => {
|
|
|
49
50
|
"keywords",
|
|
50
51
|
"people",
|
|
51
52
|
"sources",
|
|
53
|
+
"linkedChats",
|
|
52
54
|
"scope",
|
|
53
55
|
"lookbackDays",
|
|
54
56
|
"after",
|
|
@@ -90,10 +92,33 @@ export const projectGatheringRequest = (input, now) => {
|
|
|
90
92
|
const sources = sourceList.map((s) => contextSource(s));
|
|
91
93
|
if (new Set(sources.map(contextSourceKey)).size !== sources.length)
|
|
92
94
|
fail("sources lists one source twice", "sources");
|
|
95
|
+
const linkedChats = list(input.linkedChats, "linkedChats", LIMITS.linkedChats).map((value, i) => {
|
|
96
|
+
const name = `linkedChats.${i}`;
|
|
97
|
+
if (!value || typeof value !== "object")
|
|
98
|
+
return fail(`${name} needs provider, account and chat`, name);
|
|
99
|
+
known(value, ["provider", "account", "chat"], `${name}.`);
|
|
100
|
+
const given = value;
|
|
101
|
+
const provider = text(given.provider, `${name}.provider`, 128);
|
|
102
|
+
const account = text(given.account, `${name}.account`, 1024);
|
|
103
|
+
const chat = text(given.chat, `${name}.chat`, 1024);
|
|
104
|
+
const kind = provider === "email" ? "mail" : "message";
|
|
105
|
+
// The chat's own account must be a selected source; any other account would widen the owner's scope.
|
|
106
|
+
const source = sources.findIndex((s) => s.kind === kind && s.provider === provider && s.account === account);
|
|
107
|
+
return {
|
|
108
|
+
provider,
|
|
109
|
+
account,
|
|
110
|
+
chat,
|
|
111
|
+
ref: formatReference({ type: "chat", provider, account, chat }),
|
|
112
|
+
source: source < 0 ? null : source,
|
|
113
|
+
lane: null,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
if (new Set(linkedChats.map((c) => c.ref)).size !== linkedChats.length)
|
|
117
|
+
fail("linkedChats lists one chat twice", "linkedChats");
|
|
93
118
|
const lanes = [];
|
|
94
119
|
const lane = (kind, query, person) => {
|
|
95
120
|
const key = query.toLowerCase();
|
|
96
|
-
const found = lanes.find((l) => l.query
|
|
121
|
+
const found = lanes.find((l) => l.query?.toLowerCase() === key) ?? {
|
|
97
122
|
id: `l${lanes.length + 1}`,
|
|
98
123
|
kinds: [],
|
|
99
124
|
query,
|
|
@@ -117,6 +142,12 @@ export const projectGatheringRequest = (input, now) => {
|
|
|
117
142
|
lane("person", p.name, p.reference ?? p.name);
|
|
118
143
|
if (lanes.length > LIMITS.lanes)
|
|
119
144
|
fail(`at most ${LIMITS.lanes} distinct search lanes`, "keywords");
|
|
145
|
+
for (const linked of linkedChats) {
|
|
146
|
+
if (linked.source === null)
|
|
147
|
+
continue;
|
|
148
|
+
linked.lane = `l${lanes.length + 1}`;
|
|
149
|
+
lanes.push({ id: linked.lane, kinds: ["linked-chat"], query: null, people: [], chat: linked.ref });
|
|
150
|
+
}
|
|
120
151
|
const fetch = input.fetch ?? "none";
|
|
121
152
|
const index = input.index ?? "update";
|
|
122
153
|
if (index !== "update" && index !== "as-is")
|
|
@@ -171,6 +202,11 @@ export const projectGatheringRequest = (input, now) => {
|
|
|
171
202
|
fetch,
|
|
172
203
|
]))
|
|
173
204
|
.digest("hex");
|
|
205
|
+
const linkedKey = linkedChats.length
|
|
206
|
+
? createHash("sha256")
|
|
207
|
+
.update(JSON.stringify(linkedChats.map((c) => c.ref)))
|
|
208
|
+
.digest("hex")
|
|
209
|
+
: undefined;
|
|
174
210
|
const runKey = input.runKey === undefined ? fingerprint : text(input.runKey, "runKey", 256);
|
|
175
211
|
if (input.checkpoint !== undefined && typeof input.checkpoint !== "string")
|
|
176
212
|
fail("checkpoint must be text", "checkpoint");
|
|
@@ -180,6 +216,8 @@ export const projectGatheringRequest = (input, now) => {
|
|
|
180
216
|
people,
|
|
181
217
|
lanes,
|
|
182
218
|
sources,
|
|
219
|
+
linkedChats,
|
|
220
|
+
linkedKey,
|
|
183
221
|
scope: input.scope,
|
|
184
222
|
lookbackDays,
|
|
185
223
|
window,
|
|
@@ -201,7 +239,25 @@ export const contextChunks = (after, before) => {
|
|
|
201
239
|
return chunks;
|
|
202
240
|
};
|
|
203
241
|
export const projectUnits = (plan, window) => {
|
|
204
|
-
const
|
|
242
|
+
const chunks = contextChunks(window.after, window.before);
|
|
243
|
+
const literal = chunks.flatMap((chunk) => plan.lanes
|
|
244
|
+
.filter((lane) => lane.query !== null)
|
|
245
|
+
.flatMap((lane) => plan.sources.map((source, sourceIndex) => ({
|
|
246
|
+
lane,
|
|
247
|
+
source,
|
|
248
|
+
sourceIndex,
|
|
249
|
+
chunk,
|
|
250
|
+
chat: undefined,
|
|
251
|
+
}))));
|
|
252
|
+
// Appended after every literal unit: a checkpoint from a run without links keeps its unit order, and in each
|
|
253
|
+
// round a literal lane labels a shared hit before the linked chat does.
|
|
254
|
+
const linked = chunks.flatMap((chunk) => plan.linkedChats.flatMap(({ lane, source, chat }) => {
|
|
255
|
+
const found = plan.lanes.find((l) => l.id === lane);
|
|
256
|
+
return found && source !== null
|
|
257
|
+
? [{ lane: found, source: plan.sources[source], sourceIndex: source, chunk, chat }]
|
|
258
|
+
: [];
|
|
259
|
+
}));
|
|
260
|
+
const units = [...literal, ...linked];
|
|
205
261
|
if (units.length > LIMITS.units)
|
|
206
262
|
fail(`${units.length} lane × source × 90-day reads exceed ${LIMITS.units}; narrow the lanes, sources or window`);
|
|
207
263
|
return units;
|
|
@@ -227,6 +283,8 @@ export const decodeCheckpoint = (text, plan) => {
|
|
|
227
283
|
value.after < 0 ||
|
|
228
284
|
value.before <= value.after)
|
|
229
285
|
fail("invalid project gathering checkpoint", "checkpoint");
|
|
286
|
+
if ((value.linkedChats ?? null) !== (plan.linkedKey ?? null))
|
|
287
|
+
fail("the project's linked chats changed; start again without --cursor", "checkpoint");
|
|
230
288
|
if (value.runKey !== plan.runKey)
|
|
231
289
|
fail("checkpoint belongs to another run key", "checkpoint");
|
|
232
290
|
if (value.fingerprint !== plan.fingerprint)
|
|
@@ -32,11 +32,13 @@ export interface ProjectEvidenceItem extends Omit<ContextEvidenceItem, "text"> {
|
|
|
32
32
|
text: string;
|
|
33
33
|
textTruncated: boolean;
|
|
34
34
|
lanes: string[];
|
|
35
|
-
context:
|
|
35
|
+
context: ProjectEvidenceContext;
|
|
36
36
|
fingerprint: string;
|
|
37
37
|
around?: ProjectNeighbour[] | null;
|
|
38
38
|
aroundIssue?: string | null;
|
|
39
39
|
}
|
|
40
|
+
/** Strongest first: a hit that names the project, then one only from a linked chat, then one naming only a person. */
|
|
41
|
+
export type ProjectEvidenceContext = "project" | "linked-chat" | "person-only";
|
|
40
42
|
export type ProjectGatheringStop = "reads" | "read-bytes" | "elapsed" | "items" | null;
|
|
41
43
|
export declare const gatherProjectEvidence: (ports: ProjectGatheringPorts, request: ProjectGatheringRequest) => Promise<{
|
|
42
44
|
schemaVersion: 1;
|
|
@@ -62,6 +64,15 @@ export declare const gatherProjectEvidence: (ports: ProjectGatheringPorts, reque
|
|
|
62
64
|
sources: ContextSource[];
|
|
63
65
|
};
|
|
64
66
|
lanes: ProjectLane[];
|
|
67
|
+
linkedChats: {
|
|
68
|
+
provider: string;
|
|
69
|
+
account: string;
|
|
70
|
+
chat: string;
|
|
71
|
+
ref: string;
|
|
72
|
+
source: number | null;
|
|
73
|
+
lane: string | null;
|
|
74
|
+
state: "in-scope" | "out-of-scope" | "unavailable";
|
|
75
|
+
}[];
|
|
65
76
|
fetch: {
|
|
66
77
|
state: ProjectFetchState;
|
|
67
78
|
history: "complete" | "partial" | "unknown" | null;
|
|
@@ -5,6 +5,12 @@ import { excerpt } from "./excerpt.js";
|
|
|
5
5
|
import { attachNeighbours, documentPassage, } from "./project-gathering-around.js";
|
|
6
6
|
import { decodeCheckpoint, encodeCheckpoint, projectGatheringRequest, projectUnits, seenKey, } from "./project-gathering-request.js";
|
|
7
7
|
const terminal = new Set(["done", "unavailable", "failed"]);
|
|
8
|
+
const CONTEXTS = ["person-only", "linked-chat", "project"];
|
|
9
|
+
const contextOf = (lane) => lane.kinds.includes("linked-chat")
|
|
10
|
+
? "linked-chat"
|
|
11
|
+
: lane.kinds.every((k) => k === "person")
|
|
12
|
+
? "person-only"
|
|
13
|
+
: "project";
|
|
8
14
|
export const gatherProjectEvidence = async (ports, request) => {
|
|
9
15
|
if (!ports?.evidence || typeof ports.evidence.read !== "function")
|
|
10
16
|
throw new CliError("configuration_error", "stored context reads are unavailable in this store");
|
|
@@ -81,8 +87,9 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
81
87
|
if (known) {
|
|
82
88
|
if (!known.lanes.includes(lane.id))
|
|
83
89
|
known.lanes.push(lane.id);
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
const context = contextOf(lane);
|
|
91
|
+
if (CONTEXTS.indexOf(context) > CONTEXTS.indexOf(known.context))
|
|
92
|
+
known.context = context;
|
|
86
93
|
return true;
|
|
87
94
|
}
|
|
88
95
|
if (!seen.has(seenKey(ref)))
|
|
@@ -94,7 +101,7 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
94
101
|
if (again(item.ref, lane))
|
|
95
102
|
return;
|
|
96
103
|
seen.add(seenKey(item.ref));
|
|
97
|
-
const short = excerpt(item.text, lane.query, 300, { literal: true });
|
|
104
|
+
const short = excerpt(item.text, lane.query ?? "", 300, { literal: true });
|
|
98
105
|
const fingerprint = contextEvidenceFingerprint(item);
|
|
99
106
|
const entry = {
|
|
100
107
|
...item,
|
|
@@ -104,9 +111,9 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
104
111
|
textTruncated: short !== item.text.replace(/\s+/g, " ").trim(),
|
|
105
112
|
fingerprint,
|
|
106
113
|
lanes: [lane.id],
|
|
107
|
-
context: lane
|
|
114
|
+
context: contextOf(lane),
|
|
108
115
|
};
|
|
109
|
-
if (kind === "document" && plan.around.before + plan.around.after) {
|
|
116
|
+
if (kind === "document" && lane.query !== null && plan.around.before + plan.around.after) {
|
|
110
117
|
entry.around = [];
|
|
111
118
|
for (const part of documentPassage(item.text, lane.query, plan.around)) {
|
|
112
119
|
const bytes = Buffer.byteLength(part.text);
|
|
@@ -122,7 +129,9 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
122
129
|
items.push(entry);
|
|
123
130
|
usage.items++;
|
|
124
131
|
};
|
|
125
|
-
|
|
132
|
+
// A resumed run starts at its shallowest open unit: below it no unit is due, and an empty round would end the run.
|
|
133
|
+
const shallowest = progress.reduce((low, u) => (open(u) ? Math.min(low, u.pages) : low), Number.POSITIVE_INFINITY);
|
|
134
|
+
for (let round = shallowest; !stopped; round++) {
|
|
126
135
|
const due = progress.flatMap((u, index) => (open(u) && u.pages <= round ? [index] : []));
|
|
127
136
|
if (!due.length)
|
|
128
137
|
break;
|
|
@@ -152,7 +161,7 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
152
161
|
try {
|
|
153
162
|
const page = await reader.read({
|
|
154
163
|
source: unit.source,
|
|
155
|
-
query: unit.lane.query,
|
|
164
|
+
...(unit.chat === undefined ? { query: unit.lane.query ?? undefined } : { chat: unit.chat }),
|
|
156
165
|
after: unit.chunk.after,
|
|
157
166
|
before: unit.chunk.before,
|
|
158
167
|
limit,
|
|
@@ -214,7 +223,9 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
214
223
|
}
|
|
215
224
|
}
|
|
216
225
|
}
|
|
217
|
-
await attachNeighbours(
|
|
226
|
+
await attachNeighbours(
|
|
227
|
+
// A linked chat's neighbours are already its own lane's evidence.
|
|
228
|
+
items.filter((item) => item.context !== "linked-chat"), ports.neighbours, plan.around, {
|
|
218
229
|
remaining: () => plan.budgets.maxReadBytes - usage.readBytes - usage.aroundBytes,
|
|
219
230
|
charge: (bytes) => {
|
|
220
231
|
usage.aroundBytes += bytes;
|
|
@@ -236,6 +247,7 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
236
247
|
asOf: window.asOf,
|
|
237
248
|
fetched,
|
|
238
249
|
units: progress.map((u) => (u.state === "read-limited" ? { ...u, state: "partial" } : u)),
|
|
250
|
+
...(plan.linkedKey === undefined ? {} : { linkedChats: plan.linkedKey }),
|
|
239
251
|
seen: [...seen],
|
|
240
252
|
totals,
|
|
241
253
|
};
|
|
@@ -255,6 +267,19 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
255
267
|
},
|
|
256
268
|
scope: { origin: plan.scope, sources: plan.sources },
|
|
257
269
|
lanes: plan.lanes,
|
|
270
|
+
linkedChats: plan.linkedChats.map(({ provider, account, chat, ref, source, lane }) => ({
|
|
271
|
+
provider,
|
|
272
|
+
account,
|
|
273
|
+
chat,
|
|
274
|
+
ref,
|
|
275
|
+
source,
|
|
276
|
+
lane,
|
|
277
|
+
state: source === null
|
|
278
|
+
? "out-of-scope"
|
|
279
|
+
: units.every((unit, index) => unit.lane.id !== lane || progress[index]?.state === "unavailable")
|
|
280
|
+
? "unavailable"
|
|
281
|
+
: "in-scope",
|
|
282
|
+
})),
|
|
258
283
|
fetch: fetched.map((f, index) => ({ source: index, ...f })),
|
|
259
284
|
index: indexed,
|
|
260
285
|
units: units.map((unit, index) => ({
|
|
@@ -19,6 +19,9 @@ const corpora = {
|
|
|
19
19
|
timeKind: "'sent'",
|
|
20
20
|
join: "JOIN chats c ON c.id=n.chat_id AND c.account_id=n.account_id",
|
|
21
21
|
where: "c.searchable=1",
|
|
22
|
+
chats: "chats",
|
|
23
|
+
// A read that names the chat sees it even when it is hidden from search (searchable=0).
|
|
24
|
+
named: "c.external_id=?",
|
|
22
25
|
},
|
|
23
26
|
mail: {
|
|
24
27
|
table: "emails",
|
|
@@ -32,6 +35,8 @@ const corpora = {
|
|
|
32
35
|
timeKind: "'sent'",
|
|
33
36
|
join: "JOIN email_threads c ON c.id=n.email_thread_id AND c.account_id=n.account_id",
|
|
34
37
|
where: "a.provider='email'",
|
|
38
|
+
chats: "email_threads",
|
|
39
|
+
named: "a.provider='email' AND c.external_id=?",
|
|
35
40
|
},
|
|
36
41
|
document: {
|
|
37
42
|
table: "documents",
|
|
@@ -45,17 +50,19 @@ const corpora = {
|
|
|
45
50
|
timeKind: contextDocumentTimeKind,
|
|
46
51
|
join: "",
|
|
47
52
|
where: "n.body IS NOT NULL AND CASE WHEN json_valid(n.metadata) THEN coalesce(json_extract(n.metadata, '$.catalog.active'),1) ELSE 1 END=1",
|
|
53
|
+
chats: null,
|
|
54
|
+
named: null,
|
|
48
55
|
},
|
|
49
56
|
};
|
|
50
57
|
/** No index fills, model loads or record hydration outside the read transaction. */
|
|
51
58
|
export const contextEvidenceOver = ({ database }) => ({
|
|
52
59
|
async read(original) {
|
|
53
60
|
const request = contextEvidenceRequest(original);
|
|
54
|
-
const { source, words, after, before, limit, maxReadBytes, cursor, signal } = request;
|
|
61
|
+
const { source, words, chat, after, before, limit, maxReadBytes, cursor, signal } = request;
|
|
55
62
|
if (source.kind === "meeting")
|
|
56
63
|
return readMeetingContextEvidence(database, request);
|
|
57
64
|
const corpus = corpora[source.kind];
|
|
58
|
-
const scope = contextCursorScope({ source, query: request.query, after, before });
|
|
65
|
+
const scope = contextCursorScope({ source, query: request.query, chat, after, before });
|
|
59
66
|
signal?.throwIfAborted();
|
|
60
67
|
return read(database, () => {
|
|
61
68
|
const coverage = {
|
|
@@ -64,25 +71,35 @@ export const contextEvidenceOver = ({ database }) => ({
|
|
|
64
71
|
hasMore: false,
|
|
65
72
|
hasUndated: false,
|
|
66
73
|
completeness: "unknown",
|
|
67
|
-
matching: "literal-words",
|
|
74
|
+
matching: chat === undefined ? "literal-words" : "chat",
|
|
68
75
|
consistency: "snapshot-per-page",
|
|
69
76
|
};
|
|
70
77
|
const empty = () => ({ source, items: [], next: null, readBytes: 0, coverage });
|
|
71
|
-
const
|
|
78
|
+
const accountParams = [source.accountId, source.provider, source.account];
|
|
72
79
|
const account = database
|
|
73
80
|
.prepare(`SELECT 1 FROM accounts WHERE id=? AND provider=? AND external_id=?${source.kind === "mail" ? " AND provider='email'" : ""}`)
|
|
74
|
-
.get(...
|
|
81
|
+
.get(...accountParams);
|
|
75
82
|
if (!account)
|
|
76
83
|
return empty();
|
|
84
|
+
if (chat !== undefined &&
|
|
85
|
+
!database
|
|
86
|
+
.prepare(`SELECT 1 FROM ${corpus.chats} WHERE account_id=? AND external_id=?`)
|
|
87
|
+
.get(source.accountId, chat))
|
|
88
|
+
return empty();
|
|
77
89
|
coverage.available = true;
|
|
78
|
-
|
|
90
|
+
const scopeParams = chat === undefined ? accountParams : [...accountParams, chat];
|
|
91
|
+
const base = `FROM ${corpus.table} n JOIN accounts a ON a.id=n.account_id ${corpus.join}
|
|
92
|
+
WHERE n.account_id=? AND a.provider=? AND a.external_id=? AND n.deleted_at IS NULL
|
|
93
|
+
AND ${chat === undefined ? corpus.where : corpus.named}`;
|
|
94
|
+
// A chat read uses no word index, so words still waiting for it hide nothing.
|
|
95
|
+
if (chat !== undefined)
|
|
96
|
+
coverage.index = "ready";
|
|
97
|
+
else if (!database.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(corpus.index)) {
|
|
79
98
|
coverage.index = "missing";
|
|
80
99
|
return empty();
|
|
81
100
|
}
|
|
82
|
-
const base = `FROM ${corpus.table} n JOIN accounts a ON a.id=n.account_id ${corpus.join}
|
|
83
|
-
WHERE n.account_id=? AND a.provider=? AND a.external_id=? AND n.deleted_at IS NULL AND ${corpus.where}`;
|
|
84
101
|
coverage.hasUndated = Boolean(database.prepare(`SELECT 1 ${base} AND ${corpus.at} IS NULL LIMIT 1`).get(...scopeParams));
|
|
85
|
-
if (source.kind === "message") {
|
|
102
|
+
if (chat === undefined && source.kind === "message") {
|
|
86
103
|
const state = database
|
|
87
104
|
.prepare("SELECT watermark,filled_through FROM search_index_state WHERE name='message_words'")
|
|
88
105
|
.get();
|
|
@@ -95,7 +112,7 @@ export const contextEvidenceOver = ({ database }) => ({
|
|
|
95
112
|
? "pending"
|
|
96
113
|
: "ready";
|
|
97
114
|
}
|
|
98
|
-
else {
|
|
115
|
+
else if (chat === undefined) {
|
|
99
116
|
coverage.index = database
|
|
100
117
|
.prepare(`SELECT 1 ${base} AND ${corpus.at}>=? AND ${corpus.at}<?
|
|
101
118
|
AND EXISTS(SELECT 1 FROM ${source.kind === "mail" ? "email" : "document"}_index_pending p
|
|
@@ -108,16 +125,13 @@ export const contextEvidenceOver = ({ database }) => ({
|
|
|
108
125
|
${corpus.title} AS title,${corpus.body} AS body,${corpus.url} AS url,${corpus.at} AS at,
|
|
109
126
|
n.updated_at,${corpus.revision} AS revision,${corpus.timeKind} AS time_kind ${base}
|
|
110
127
|
AND ${corpus.at}>=? AND ${corpus.at}<?
|
|
111
|
-
AND n.id IN (SELECT rowid FROM ${corpus.index} WHERE ${corpus.index} MATCH ?)
|
|
112
|
-
${source.kind === "message" ? `AND n.normalized_text IS NOT NULL AND n.normalizer_version=${NORMALIZER_VERSION}` : `AND NOT EXISTS(SELECT 1 FROM ${source.kind === "mail" ? "email" : "document"}_index_pending p WHERE p.id=n.id AND p.indexable_type='${source.kind === "mail" ? "email" : "document"}')`}
|
|
128
|
+
${chat !== undefined ? "" : `AND n.id IN (SELECT rowid FROM ${corpus.index} WHERE ${corpus.index} MATCH ?)`}
|
|
129
|
+
${chat !== undefined ? "" : source.kind === "message" ? `AND n.normalized_text IS NOT NULL AND n.normalizer_version=${NORMALIZER_VERSION}` : `AND NOT EXISTS(SELECT 1 FROM ${source.kind === "mail" ? "email" : "document"}_index_pending p WHERE p.id=n.id AND p.indexable_type='${source.kind === "mail" ? "email" : "document"}')`}
|
|
113
130
|
${cursor ? `AND (${corpus.at}<? OR (${corpus.at}=? AND n.id<?))` : ""}
|
|
114
131
|
ORDER BY ${corpus.at} DESC,n.id DESC LIMIT ?`;
|
|
115
|
-
const params = [
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
before,
|
|
119
|
-
words.map((w) => `normalized_text:"${normalize(w)}"`).join(" AND "),
|
|
120
|
-
];
|
|
132
|
+
const params = [...scopeParams, after, before];
|
|
133
|
+
if (chat === undefined)
|
|
134
|
+
params.push(words.map((w) => `normalized_text:"${normalize(w)}"`).join(" AND "));
|
|
121
135
|
if (cursor)
|
|
122
136
|
params.push(cursor.at, cursor.at, cursor.id);
|
|
123
137
|
params.push(limit + 1);
|
|
@@ -217,9 +231,9 @@ export const contextRecordOf = (database, ref) => {
|
|
|
217
231
|
const corpus = at.provider === "email" ? corpora.mail : corpora.message;
|
|
218
232
|
const row = database
|
|
219
233
|
.prepare(`SELECT ${corpus.title} AS title,${corpus.body} AS body FROM ${corpus.table} n JOIN accounts a ON a.id=n.account_id
|
|
220
|
-
${corpus.join} WHERE a.provider=? AND a.external_id=? AND
|
|
221
|
-
AND n.deleted_at IS NULL AND ${corpus.
|
|
222
|
-
.get(at.provider, at.account, at.
|
|
234
|
+
${corpus.join} WHERE a.provider=? AND a.external_id=? AND n.external_id=?
|
|
235
|
+
AND n.deleted_at IS NULL AND ${corpus.named}`)
|
|
236
|
+
.get(at.provider, at.account, at.message, at.chat);
|
|
223
237
|
return row
|
|
224
238
|
? { ref, title: row.title == null ? null : String(row.title), text: String(row.body), revision: null }
|
|
225
239
|
: null;
|
|
@@ -222,11 +222,18 @@ export const knowledgeStoreOver = (context) => {
|
|
|
222
222
|
throw new CliError("not_found", "no relationship with that id");
|
|
223
223
|
return row;
|
|
224
224
|
};
|
|
225
|
-
|
|
225
|
+
/** A chat may only be the member: its reference names its own account, so it is never another account's. */
|
|
226
|
+
const related = (key, reference, member = false) => {
|
|
226
227
|
checked(key);
|
|
227
228
|
const thing = thingOf(database, reference);
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
const chat = member && (thing?.type === "chat" || thing?.type === "email_thread");
|
|
230
|
+
if (!thing || !(RELATED.includes(thing.type) || chat) || stateOfThing(database, thing) !== "available")
|
|
231
|
+
throw new CliError("not_found", `reference needs a stored person:, organization:, project:${member ? ", task: or chat:" : " or task:"}`);
|
|
232
|
+
if (chat && key !== null) {
|
|
233
|
+
const parsed = parseReference(reference);
|
|
234
|
+
if (parsed.provider !== key.provider || parsed.account !== key.account)
|
|
235
|
+
throw new CliError("validation_error", "the chat belongs to another account");
|
|
236
|
+
}
|
|
230
237
|
return thing;
|
|
231
238
|
};
|
|
232
239
|
const reminderOf = (row) => ({
|
|
@@ -473,10 +480,13 @@ export const knowledgeStoreOver = (context) => {
|
|
|
473
480
|
.all()
|
|
474
481
|
.map(projectOf),
|
|
475
482
|
relate: async (key, input) => {
|
|
476
|
-
const from = related(key, input.from);
|
|
483
|
+
const from = related(key, input.from, true);
|
|
477
484
|
const to = related(key, input.to);
|
|
478
485
|
if ((from.type === to.type && from.id === to.id) || !RELATION_KINDS.includes(input.kind))
|
|
479
486
|
throw new CliError("validation_error", "a relationship needs distinct references and a supported kind");
|
|
487
|
+
if ((from.type === "chat" || from.type === "email_thread") &&
|
|
488
|
+
(input.kind !== "member-of" || to.type !== "project"))
|
|
489
|
+
throw new CliError("validation_error", "a chat is related only as member-of a project");
|
|
480
490
|
const existing = database
|
|
481
491
|
.prepare(`${RELATIONS} AND from_type=? AND from_id=? AND to_type=? AND to_id=? AND kind=?`)
|
|
482
492
|
.get(from.type, from.id, to.type, to.id, input.kind);
|
|
@@ -514,7 +524,7 @@ export const knowledgeStoreOver = (context) => {
|
|
|
514
524
|
checked(key);
|
|
515
525
|
const row = relation(id);
|
|
516
526
|
const link = linkOf(database, row);
|
|
517
|
-
related(key, link.from);
|
|
527
|
+
related(key, link.from, true);
|
|
518
528
|
related(key, String(link.to));
|
|
519
529
|
database
|
|
520
530
|
.prepare("UPDATE links SET confirmed=1, source='owner', author='owner', updated_at=? WHERE id=?")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.376.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",
|
|
@@ -202,7 +202,7 @@
|
|
|
202
202
|
"@types/node": "^22.20.4",
|
|
203
203
|
"@types/pngjs": "6.0.5",
|
|
204
204
|
"@vitest/coverage-v8": "^5.0.2",
|
|
205
|
-
"@wirecat/cli-base": "0.47.
|
|
205
|
+
"@wirecat/cli-base": "0.47.3",
|
|
206
206
|
"commander": "15.0.0",
|
|
207
207
|
"cspell": "10.3.6",
|
|
208
208
|
"drizzle-kit": "1.0.0-rc.4",
|
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
},
|
|
223
223
|
"peerDependencies": {
|
|
224
224
|
"@napi-rs/canvas": ">=0.1.69 <2",
|
|
225
|
-
"@wirecat/cli-base": ">=0.47.
|
|
225
|
+
"@wirecat/cli-base": ">=0.47.3 <0.48.0",
|
|
226
226
|
"commander": ">=15"
|
|
227
227
|
},
|
|
228
228
|
"bin": {
|