@wirecat/cli-core 0.361.0 → 0.363.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 +30 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/meeting-findings-input.d.ts +2 -1
- package/dist/cli/meeting-findings-input.js +10 -7
- package/dist/cli/program.d.ts +1 -1
- package/dist/cli/program.js +1 -1
- package/dist/context/evidence.d.ts +6 -0
- package/dist/context/evidence.js +4 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/meetings/findings-rules.d.ts +60 -0
- package/dist/meetings/findings-rules.js +117 -0
- package/dist/meetings/findings.d.ts +10 -48
- package/dist/meetings/findings.js +10 -116
- package/dist/meetings/index.d.ts +1 -0
- package/dist/meetings/index.js +1 -0
- package/dist/services/context-evidence.d.ts +2 -1
- package/dist/services/context-evidence.js +1 -0
- package/dist/services/index.d.ts +3 -2
- package/dist/services/index.js +2 -1
- package/dist/services/project-gathering.d.ts +1 -0
- package/dist/services/project-gathering.js +3 -0
- package/dist/services/proposal-tasks.js +5 -0
- package/dist/store/evidence-findings.d.ts +63 -0
- package/dist/store/evidence-findings.js +15 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.js +1 -0
- package/dist/store/sqlite/context-evidence.d.ts +9 -1
- package/dist/store/sqlite/context-evidence.js +43 -1
- package/dist/store/sqlite/evidence-findings.d.ts +3 -0
- package/dist/store/sqlite/evidence-findings.js +172 -0
- package/dist/store/sqlite/findings-core.d.ts +17 -0
- package/dist/store/sqlite/findings-core.js +108 -0
- package/dist/store/sqlite/meeting-findings.js +47 -121
- package/dist/store/sqlite/proposed-actions.d.ts +4 -1
- package/dist/store/sqlite/proposed-actions.js +12 -4
- package/dist/store/store.d.ts +5 -1
- package/dist/store/store.js +10 -1
- package/dist/terminal/login-qr.d.ts +15 -11
- package/dist/terminal/login-qr.js +30 -42
- package/dist/terminal/qr.d.ts +7 -5
- package/dist/terminal/qr.js +21 -47
- package/package.json +1 -1
- package/parity.json +1 -7
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { findingHash, MEETING_FINDINGS_KIND, meetingCueFingerprint, meetingFindingFingerprint, parseMeetingReference, validateMeetingFindings, } from "../../meetings/index.js";
|
|
3
|
-
import {
|
|
4
|
-
import { authorActor
|
|
5
|
-
import { requiredThing } from "./notes.js";
|
|
3
|
+
import { findingsCoreOver } from "./findings-core.js";
|
|
4
|
+
import { authorActor } from "./memories.js";
|
|
6
5
|
import { proposalOperationsOver } from "./proposed-actions.js";
|
|
7
|
-
import { taskRowOf } from "./things.js";
|
|
8
6
|
import { write } from "./transaction.js";
|
|
9
|
-
import { workTaskOperationsOver } from "./work-tasks.js";
|
|
10
7
|
const stale = () => {
|
|
11
8
|
throw new CliError("validation_error", "meeting findings changed since they were read; stage a new batch", {
|
|
12
9
|
reason: "meeting-findings.stale",
|
|
@@ -18,8 +15,6 @@ const invalid = (message) => {
|
|
|
18
15
|
export const meetingFindingsStoreOver = (context) => {
|
|
19
16
|
const { database } = context;
|
|
20
17
|
const proposals = proposalOperationsOver(context);
|
|
21
|
-
const facts = factOperationsOver(context);
|
|
22
|
-
const tasks = workTaskOperationsOver(context);
|
|
23
18
|
const accountId = (key) => {
|
|
24
19
|
if (!key ||
|
|
25
20
|
typeof key.provider !== "string" ||
|
|
@@ -68,109 +63,56 @@ export const meetingFindingsStoreOver = (context) => {
|
|
|
68
63
|
receipt: proposal.result,
|
|
69
64
|
};
|
|
70
65
|
};
|
|
66
|
+
const core = findingsCoreOver(context, MEETING_FINDINGS_KIND);
|
|
71
67
|
const snapshot = (account, input) => {
|
|
72
68
|
let readBytes = 0;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
seen.
|
|
84
|
-
|
|
85
|
-
const person = requiredThing(database, finding.assignee, "the assignee");
|
|
86
|
-
if (person.type !== "person")
|
|
87
|
-
return invalid("the assignee must be a stored person");
|
|
88
|
-
const personRow = database.prepare("SELECT id,name,updated_at FROM persons WHERE id=?").get(person.id);
|
|
89
|
-
if (!personRow)
|
|
90
|
-
throw new CliError("not_found", "the assignee is unavailable");
|
|
91
|
-
seen.set(finding.assignee, personRow);
|
|
92
|
-
}
|
|
93
|
-
for (const evidence of finding.evidence) {
|
|
94
|
-
const parsed = parseMeetingReference(evidence.reference);
|
|
95
|
-
if (parsed.accountId !== account)
|
|
96
|
-
throw new CliError("not_found", "the cited cue belongs to another account");
|
|
97
|
-
const membership = database
|
|
98
|
-
.prepare("SELECT id,updated_at,confirmed,source FROM links WHERE from_type='meeting' AND from_id=? AND to_type='project' AND to_id=? AND kind='member-of' AND anchor IS NULL AND confirmed=1")
|
|
99
|
-
.get(parsed.meetingId, project.id);
|
|
100
|
-
if (!membership)
|
|
101
|
-
return invalid("each cited meeting needs confirmed membership in the finding's project");
|
|
102
|
-
seen.set(`${finding.project}/${parsed.meetingId}`, membership);
|
|
103
|
-
if (seen.has(evidence.reference)) {
|
|
104
|
-
if (seen.get(evidence.reference) !== evidence.fingerprint)
|
|
105
|
-
return stale();
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
const query = "FROM meeting_transcript_rows r JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id JOIN meetings m ON m.id=t.meeting_id WHERE m.id=? AND m.account_id=? AND m.deleted_at IS NULL AND t.id=? AND t.deleted_at IS NULL AND t.superseded_at IS NULL AND r.position=?";
|
|
109
|
-
const params = [parsed.meetingId, account, parsed.transcriptId, parsed.cuePosition];
|
|
110
|
-
const budget = database
|
|
111
|
-
.prepare(`SELECT length(CAST(r.text AS BLOB))+coalesce(length(CAST(r.speaker_name AS BLOB)),0)+coalesce(length(CAST(t.content_hash AS BLOB)),0) AS bytes ${query}`)
|
|
112
|
-
.get(...params);
|
|
113
|
-
if (!budget)
|
|
114
|
-
return stale();
|
|
115
|
-
readBytes += Number(budget.bytes);
|
|
116
|
-
if (!Number.isSafeInteger(readBytes) || readBytes > 65536)
|
|
117
|
-
return invalid("finding evidence exceeds the 65536-byte stored text budget");
|
|
118
|
-
const row = database
|
|
119
|
-
.prepare(`SELECT t.content_hash,r.start_ms,r.end_ms,r.speaker_participant_id,r.speaker_name,r.text ${query}`)
|
|
120
|
-
.get(...params);
|
|
121
|
-
if (!row)
|
|
69
|
+
return core.snapshot(input, (evidence, finding, projectId, seen) => {
|
|
70
|
+
const parsed = parseMeetingReference(evidence.reference);
|
|
71
|
+
if (parsed.accountId !== account)
|
|
72
|
+
throw new CliError("not_found", "the cited cue belongs to another account");
|
|
73
|
+
const membership = database
|
|
74
|
+
.prepare("SELECT id,updated_at,confirmed,source FROM links WHERE from_type='meeting' AND from_id=? AND to_type='project' AND to_id=? AND kind='member-of' AND anchor IS NULL AND confirmed=1")
|
|
75
|
+
.get(parsed.meetingId, projectId);
|
|
76
|
+
if (!membership)
|
|
77
|
+
return invalid("each cited meeting needs confirmed membership in the finding's project");
|
|
78
|
+
seen.set(`${finding.project}/${parsed.meetingId}`, membership);
|
|
79
|
+
if (seen.has(evidence.reference)) {
|
|
80
|
+
if (seen.get(evidence.reference) !== evidence.fingerprint)
|
|
122
81
|
return stale();
|
|
123
|
-
|
|
124
|
-
reference: evidence.reference,
|
|
125
|
-
transcriptId: parsed.transcriptId,
|
|
126
|
-
contentHash: row.content_hash == null ? null : String(row.content_hash),
|
|
127
|
-
position: parsed.cuePosition,
|
|
128
|
-
startMs: Number(row.start_ms),
|
|
129
|
-
endMs: Number(row.end_ms),
|
|
130
|
-
speakerParticipantId: row.speaker_participant_id == null ? null : Number(row.speaker_participant_id),
|
|
131
|
-
speakerName: row.speaker_name == null ? null : String(row.speaker_name),
|
|
132
|
-
text: String(row.text),
|
|
133
|
-
});
|
|
134
|
-
if (fingerprint !== evidence.fingerprint)
|
|
135
|
-
return stale();
|
|
136
|
-
seen.set(evidence.reference, fingerprint);
|
|
82
|
+
return;
|
|
137
83
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
evidence,
|
|
155
|
-
|
|
84
|
+
const query = "FROM meeting_transcript_rows r JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id JOIN meetings m ON m.id=t.meeting_id WHERE m.id=? AND m.account_id=? AND m.deleted_at IS NULL AND t.id=? AND t.deleted_at IS NULL AND t.superseded_at IS NULL AND r.position=?";
|
|
85
|
+
const params = [parsed.meetingId, account, parsed.transcriptId, parsed.cuePosition];
|
|
86
|
+
const budget = database
|
|
87
|
+
.prepare(`SELECT length(CAST(r.text AS BLOB))+coalesce(length(CAST(r.speaker_name AS BLOB)),0)+coalesce(length(CAST(t.content_hash AS BLOB)),0) AS bytes ${query}`)
|
|
88
|
+
.get(...params);
|
|
89
|
+
if (!budget)
|
|
90
|
+
return stale();
|
|
91
|
+
readBytes += Number(budget.bytes);
|
|
92
|
+
if (!Number.isSafeInteger(readBytes) || readBytes > 65536)
|
|
93
|
+
return invalid("finding evidence exceeds the 65536-byte stored text budget");
|
|
94
|
+
const row = database
|
|
95
|
+
.prepare(`SELECT t.content_hash,r.start_ms,r.end_ms,r.speaker_participant_id,r.speaker_name,r.text ${query}`)
|
|
96
|
+
.get(...params);
|
|
97
|
+
if (!row)
|
|
98
|
+
return stale();
|
|
99
|
+
const fingerprint = meetingCueFingerprint({
|
|
100
|
+
reference: evidence.reference,
|
|
101
|
+
transcriptId: parsed.transcriptId,
|
|
102
|
+
contentHash: row.content_hash == null ? null : String(row.content_hash),
|
|
103
|
+
position: parsed.cuePosition,
|
|
104
|
+
startMs: Number(row.start_ms),
|
|
105
|
+
endMs: Number(row.end_ms),
|
|
106
|
+
speakerParticipantId: row.speaker_participant_id == null ? null : Number(row.speaker_participant_id),
|
|
107
|
+
speakerName: row.speaker_name == null ? null : String(row.speaker_name),
|
|
108
|
+
text: String(row.text),
|
|
156
109
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
title: finding.title,
|
|
162
|
-
type: finding.type,
|
|
163
|
-
description: `${finding.commitment}: ${finding.reason}`,
|
|
164
|
-
...(finding.dueAt ? { dueAt: new Date(finding.dueAt) } : {}),
|
|
165
|
-
}, agent);
|
|
166
|
-
const id = taskRowOf(database, task.id);
|
|
167
|
-
if (id === undefined)
|
|
168
|
-
throw new CliError("configuration_error", "created finding task is unavailable");
|
|
169
|
-
linkEvidence(context, { type: finding.type, id }, evidence, "agent");
|
|
170
|
-
if (finding.assignee)
|
|
171
|
-
tasks.assign(task.id, { to: finding.assignee, role: "assignee" }, agent);
|
|
172
|
-
return task.ref;
|
|
110
|
+
if (fingerprint !== evidence.fingerprint)
|
|
111
|
+
return stale();
|
|
112
|
+
seen.set(evidence.reference, fingerprint);
|
|
113
|
+
});
|
|
173
114
|
};
|
|
115
|
+
const previous = core.previous;
|
|
174
116
|
return {
|
|
175
117
|
stage: async (key, raw, agent) => {
|
|
176
118
|
const input = validateMeetingFindings(raw);
|
|
@@ -248,23 +190,7 @@ export const meetingFindingsStoreOver = (context) => {
|
|
|
248
190
|
bot: proposal.proposedBy.name,
|
|
249
191
|
...(input.agentModel === null ? {} : { model: input.agentModel }),
|
|
250
192
|
};
|
|
251
|
-
const items = batch.input
|
|
252
|
-
const fingerprint = meetingFindingFingerprint(finding);
|
|
253
|
-
const prior = previous(account, fingerprint);
|
|
254
|
-
if (prior) {
|
|
255
|
-
if (accepted.includes(finding.id) && prior.status === "rejected")
|
|
256
|
-
return invalid("this unchanged finding was already rejected; changed evidence needs a new candidate");
|
|
257
|
-
return { ...prior, id: finding.id, reused: true };
|
|
258
|
-
}
|
|
259
|
-
const status = accepted.includes(finding.id) ? "accepted" : "rejected";
|
|
260
|
-
return {
|
|
261
|
-
id: finding.id,
|
|
262
|
-
fingerprint,
|
|
263
|
-
status,
|
|
264
|
-
reference: status === "accepted" ? create(finding, agent) : null,
|
|
265
|
-
reused: false,
|
|
266
|
-
};
|
|
267
|
-
});
|
|
193
|
+
const items = core.outcomes(account, batch.input, accepted, agent);
|
|
268
194
|
return view(proposals.approve(id, {
|
|
269
195
|
previewFingerprint: batch.fingerprint,
|
|
270
196
|
accepted,
|
|
@@ -63,8 +63,11 @@ export interface ProposedActionsStore {
|
|
|
63
63
|
failed(id: string, error: string): Promise<ProposedAction>;
|
|
64
64
|
judge(id: string, verdict: ProposedAction["verdict"]): Promise<ProposedAction>;
|
|
65
65
|
}
|
|
66
|
-
type ProposalOperations = {
|
|
66
|
+
type ProposalOperations = Omit<{
|
|
67
67
|
[K in keyof ProposedActionsStore]: ProposedActionsStore[K] extends (...args: infer A) => Promise<infer R> ? (...args: A) => R : never;
|
|
68
|
+
}, "approve"> & {
|
|
69
|
+
/** `by` records an agent deciding for the owner; the public store always records the owner. */
|
|
70
|
+
approve(id: string, result?: unknown, by?: Author): ProposedAction;
|
|
68
71
|
};
|
|
69
72
|
export declare const proposalOperationsOver: (context: StoreContext) => ProposalOperations;
|
|
70
73
|
export declare const proposedActionsStoreOver: (context: StoreContext) => ProposedActionsStore;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { MEETING_FINDINGS_KIND } from "../../meetings/index.js";
|
|
3
|
+
import { EVIDENCE_FINDINGS_KIND } from "../evidence-findings.js";
|
|
3
4
|
import { authorActor } from "./memories.js";
|
|
4
5
|
import { requiredThing } from "./notes.js";
|
|
5
6
|
import { referenceOfThing, storedThing } from "./things.js";
|
|
@@ -60,8 +61,8 @@ export const proposalOperationsOver = (context) => {
|
|
|
60
61
|
.run(to, now(), ...values, Number(id));
|
|
61
62
|
return actionOf(rowOf(id));
|
|
62
63
|
});
|
|
63
|
-
const decide = (id, to, result) => {
|
|
64
|
-
const { actor } = authorActor(context,
|
|
64
|
+
const decide = (id, to, result, by = "owner") => {
|
|
65
|
+
const { actor } = authorActor(context, by);
|
|
65
66
|
return move(id, ["proposed"], to, ", decided_by_type = ?, decided_by_id = ?, decided_at = ?, result = ?", [
|
|
66
67
|
actor.type,
|
|
67
68
|
actor.id,
|
|
@@ -109,7 +110,7 @@ export const proposalOperationsOver = (context) => {
|
|
|
109
110
|
.all(status ?? null, status ?? null, Math.min(Math.max(limit, 1), 500))
|
|
110
111
|
.map(actionOf);
|
|
111
112
|
},
|
|
112
|
-
approve: (id, result) => decide(id, "approved", result),
|
|
113
|
+
approve: (id, result, by) => decide(id, "approved", result, by),
|
|
113
114
|
reject: (id) => decide(id, "rejected"),
|
|
114
115
|
executed: (id, result) => move(id, ["approved"], "executed", ", executed_at = ?, result = ?", [
|
|
115
116
|
now(),
|
|
@@ -133,15 +134,22 @@ export const proposalOperationsOver = (context) => {
|
|
|
133
134
|
export const proposedActionsStoreOver = (context) => {
|
|
134
135
|
const operations = proposalOperationsOver(context);
|
|
135
136
|
const generic = (id) => {
|
|
136
|
-
|
|
137
|
+
const kind = operations.get(id).kind;
|
|
138
|
+
if (kind === MEETING_FINDINGS_KIND)
|
|
137
139
|
throw new CliError("validation_error", "review meeting findings through the dedicated findings command", {
|
|
138
140
|
reason: "meeting-findings.dedicated-review",
|
|
139
141
|
});
|
|
142
|
+
if (kind === EVIDENCE_FINDINGS_KIND)
|
|
143
|
+
throw new CliError("validation_error", "review evidence findings through the dedicated findings command", {
|
|
144
|
+
reason: "evidence-findings.dedicated-review",
|
|
145
|
+
});
|
|
140
146
|
};
|
|
141
147
|
return {
|
|
142
148
|
propose: async (input) => {
|
|
143
149
|
if (input.kind === MEETING_FINDINGS_KIND)
|
|
144
150
|
throw new CliError("validation_error", "stage meeting findings through the dedicated findings command");
|
|
151
|
+
if (input.kind === EVIDENCE_FINDINGS_KIND)
|
|
152
|
+
throw new CliError("validation_error", "stage evidence findings through the dedicated findings command");
|
|
145
153
|
return operations.propose(input);
|
|
146
154
|
},
|
|
147
155
|
get: async (id) => operations.get(id),
|
package/dist/store/store.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { ContextEvidenceReader, ContextIndex } from "../context/evidence.js";
|
|
1
|
+
import type { ContextEvidenceReader, ContextIndex, ContextRecordReader } from "../context/evidence.js";
|
|
2
2
|
import { type AccountStore } from "./account-store.js";
|
|
3
3
|
import { type AccountsStore } from "./accounts.js";
|
|
4
4
|
import { type AttachmentsStore } from "./attachments.js";
|
|
5
5
|
import { type ChatsStore } from "./chats.js";
|
|
6
6
|
import { type ConversationsStore } from "./conversations.js";
|
|
7
7
|
import { type CountersStore } from "./counters.js";
|
|
8
|
+
import type { EvidenceFindingsStore } from "./evidence-findings.js";
|
|
8
9
|
import type { MeetingFindingsStore } from "./meeting-findings.js";
|
|
9
10
|
import { type MentionsStore } from "./mentions.js";
|
|
10
11
|
import { type MessagesStore } from "./messages.js";
|
|
@@ -35,6 +36,7 @@ import { type VectorsStore } from "./vectors.js";
|
|
|
35
36
|
export interface MessageStore {
|
|
36
37
|
readonly contextEvidence?: ContextEvidenceReader;
|
|
37
38
|
readonly contextIndex?: ContextIndex;
|
|
39
|
+
readonly contextRecords?: ContextRecordReader;
|
|
38
40
|
/** Accounts, account groups and which account each app profile is. */
|
|
39
41
|
readonly accounts: AccountsStore;
|
|
40
42
|
/** One account's chats: the list, what left, what is complete, their metadata. */
|
|
@@ -77,6 +79,8 @@ export interface MessageStore {
|
|
|
77
79
|
readonly catalog: CatalogStore;
|
|
78
80
|
/** Choices that hold until replaced, each with its evidence. */
|
|
79
81
|
readonly meetingFindings: MeetingFindingsStore;
|
|
82
|
+
/** Cited facts and follow-ups about one project from stored messages, mail and documents, for review. */
|
|
83
|
+
readonly evidenceFindings: EvidenceFindingsStore;
|
|
80
84
|
readonly facts: FactsStore;
|
|
81
85
|
/** What agents concluded: summaries, digests, facts, preferences, each with evidence and a scope. */
|
|
82
86
|
readonly memories: MemoriesStore;
|
package/dist/store/store.js
CHANGED
|
@@ -16,8 +16,9 @@ import { agentActionsStoreOver } from "./sqlite/agent-actions.js";
|
|
|
16
16
|
import * as attachmentTexts from "./sqlite/attachment-texts.js";
|
|
17
17
|
import { botUpdateStoreOver } from "./sqlite/bot-updates.js";
|
|
18
18
|
import { catalogStoreOver } from "./sqlite/catalog.js";
|
|
19
|
-
import { contextEvidenceOver, contextIndexOver } from "./sqlite/context-evidence.js";
|
|
19
|
+
import { contextEvidenceOver, contextIndexOver, contextRecordsOver } from "./sqlite/context-evidence.js";
|
|
20
20
|
import { mailStoreOver } from "./sqlite/emails.js";
|
|
21
|
+
import { evidenceFindingsStoreOver } from "./sqlite/evidence-findings.js";
|
|
21
22
|
import { factsStoreOver } from "./sqlite/facts.js";
|
|
22
23
|
import { involvementStoreOver } from "./sqlite/involvements.js";
|
|
23
24
|
import { knowledgeStoreOver } from "./sqlite/knowledge.js";
|
|
@@ -82,6 +83,7 @@ const storeOver = (context) => {
|
|
|
82
83
|
};
|
|
83
84
|
const contextEvidence = once(() => contextEvidenceOver(context));
|
|
84
85
|
const contextIndex = once(() => contextIndexOver(context, stemmerFor));
|
|
86
|
+
const contextRecords = once(() => contextRecordsOver(context));
|
|
85
87
|
const searches = once(() => searchesStoreOver(context));
|
|
86
88
|
const queries = once(() => queriesStoreOver(context, stemmerFor));
|
|
87
89
|
const searchIndex = once(() => searchIndexStoreOver(context));
|
|
@@ -93,6 +95,7 @@ const storeOver = (context) => {
|
|
|
93
95
|
const notes = once(() => ({ ...notesStoreOver(context), ...noteSearchOver(context) }));
|
|
94
96
|
const catalog = once(() => catalogStoreOver(context));
|
|
95
97
|
const meetingFindings = once(() => meetingFindingsStoreOver(context));
|
|
98
|
+
const evidenceFindings = once(() => evidenceFindingsStoreOver(context));
|
|
96
99
|
const facts = once(() => factsStoreOver(context));
|
|
97
100
|
const memories = once(() => memoriesStoreOver(context));
|
|
98
101
|
const proposedActions = once(() => proposedActionsStoreOver(context));
|
|
@@ -105,6 +108,9 @@ const storeOver = (context) => {
|
|
|
105
108
|
get contextIndex() {
|
|
106
109
|
return contextIndex();
|
|
107
110
|
},
|
|
111
|
+
get contextRecords() {
|
|
112
|
+
return contextRecords();
|
|
113
|
+
},
|
|
108
114
|
meetingVectors: meetingVectorsOver(context),
|
|
109
115
|
get accounts() {
|
|
110
116
|
return areas.accounts().whole;
|
|
@@ -175,6 +181,9 @@ const storeOver = (context) => {
|
|
|
175
181
|
get meetingFindings() {
|
|
176
182
|
return meetingFindings();
|
|
177
183
|
},
|
|
184
|
+
get evidenceFindings() {
|
|
185
|
+
return evidenceFindings();
|
|
186
|
+
},
|
|
178
187
|
get facts() {
|
|
179
188
|
return facts();
|
|
180
189
|
},
|
|
@@ -1,30 +1,34 @@
|
|
|
1
|
-
import { CliError } from "@wirecat/cli-base";
|
|
2
1
|
/** Whether a person can scan a code drawn here and answer what the login asks next. */
|
|
3
2
|
export declare const atTerminal: (input: NodeJS.ReadableStream & {
|
|
4
3
|
isTTY?: boolean;
|
|
5
4
|
}) => boolean;
|
|
6
|
-
/**
|
|
7
|
-
export declare const
|
|
5
|
+
/** What an agent reads off the plain-text code: the same words a person reads under the drawn one. */
|
|
6
|
+
export declare const PASTE_QR: string;
|
|
8
7
|
export interface LoginQrOptions {
|
|
9
|
-
/** `--qr-file`: the PNG to write instead of drawing the code. */
|
|
10
|
-
qrFile?: string;
|
|
11
8
|
/** Where the code and the narrow-terminal hint go; `--quiet` must not hide either. */
|
|
12
9
|
diagnostic: (text: string) => void;
|
|
13
|
-
/** Where the
|
|
10
|
+
/** Where where-to-scan and the agent's instruction go. */
|
|
14
11
|
note: (text: string) => void;
|
|
15
12
|
/** How far the code sits right, to line up under a setup step. */
|
|
16
13
|
indent?: number;
|
|
17
14
|
/** Read on every code, so a widened terminal draws the next one. Unknown draws the code. */
|
|
18
15
|
columns?: () => number | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Whether a person reads the diagnostic stream in a terminal. Off one, the code is plain text for an agent to
|
|
18
|
+
* paste into the chat (`plainQr`). Defaults to whether stderr is a terminal.
|
|
19
|
+
*/
|
|
20
|
+
terminal?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Machine output (`--json`, `--jsonl`, or no terminal): each code is also one JSON line on the diagnostic stream,
|
|
23
|
+
* `{"event":"qr","qr":{"text","expires_at","scan"}}`. stdout keeps only the answer, which comes after the scan.
|
|
24
|
+
*/
|
|
25
|
+
json?: boolean;
|
|
19
26
|
}
|
|
20
27
|
export interface LoginQr {
|
|
21
|
-
readonly path: string | undefined;
|
|
22
28
|
/**
|
|
23
29
|
* `scan` says where to scan; it follows the code, and is not said when the terminal is too narrow to draw one.
|
|
24
30
|
* Never throws for a code that does not fit: the messengers call it from inside their own event loops.
|
|
25
31
|
*/
|
|
26
|
-
show: (link: string, scan?: string) => void;
|
|
27
|
-
/** The image logs the account in while it is valid; call this in `finally`. */
|
|
28
|
-
close: () => void;
|
|
32
|
+
show: (link: string, scan?: string, expiresAt?: Date) => void;
|
|
29
33
|
}
|
|
30
|
-
export declare const loginQr: ({
|
|
34
|
+
export declare const loginQr: ({ diagnostic, note, indent: pad, columns, terminal, json, }: LoginQrOptions) => LoginQr;
|
|
@@ -1,48 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { CliError, indent, writeSecurely } from "@wirecat/cli-base";
|
|
1
|
+
import { indent } from "@wirecat/cli-base";
|
|
4
2
|
import { inputPolicy } from "../cli/input-policy.js";
|
|
5
|
-
import {
|
|
3
|
+
import { plainQr, terminalQr } from "./qr.js";
|
|
6
4
|
const MARGIN = 2;
|
|
7
5
|
/** Whether a person can scan a code drawn here and answer what the login asks next. */
|
|
8
6
|
export const atTerminal = (input) => input.isTTY === true && !inputPolicy(input).noInput;
|
|
9
|
-
/**
|
|
10
|
-
export const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
written = true;
|
|
24
|
-
note(`the QR code is in ${path} — it is replaced when the code renews`);
|
|
25
|
-
if (scan)
|
|
26
|
-
note(scan);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const drawn = terminalQr(link);
|
|
30
|
-
const need = drawn.width + pad + MARGIN;
|
|
31
|
-
const width = columns();
|
|
32
|
-
if (width !== undefined && need > width) {
|
|
33
|
-
diagnostic(`the terminal is ${width} columns wide and the QR code needs ${need} — widen it, ` +
|
|
34
|
-
"or run again with --qr-file <png>");
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
diagnostic(`\n${indent(drawn.text, pad + MARGIN)}`);
|
|
7
|
+
/** What an agent reads off the plain-text code: the same words a person reads under the drawn one. */
|
|
8
|
+
export const PASTE_QR = "agents: paste the QR code above into the chat as a code block, exactly as printed; " +
|
|
9
|
+
"when a new one is printed, paste it and say the old one expired";
|
|
10
|
+
export const loginQr = ({ diagnostic, note, indent: pad = 0, columns = () => process.stderr.columns, terminal = process.stderr.isTTY === true, json = false, }) => ({
|
|
11
|
+
show: (link, scan, expiresAt) => {
|
|
12
|
+
if (!terminal) {
|
|
13
|
+
const text = plainQr(link);
|
|
14
|
+
diagnostic(`\n${text}\n`);
|
|
15
|
+
if (json)
|
|
16
|
+
diagnostic(JSON.stringify({
|
|
17
|
+
event: "qr",
|
|
18
|
+
qr: { text, expires_at: expiresAt?.toISOString() ?? null, scan: scan ?? null },
|
|
19
|
+
}));
|
|
20
|
+
note(PASTE_QR);
|
|
38
21
|
if (scan)
|
|
39
22
|
note(scan);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const drawn = terminalQr(link);
|
|
26
|
+
const need = drawn.width + pad + MARGIN;
|
|
27
|
+
const width = columns();
|
|
28
|
+
if (width !== undefined && need > width) {
|
|
29
|
+
diagnostic(`the terminal is ${width} columns wide and the QR code needs ${need} — widen it`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
diagnostic(`\n${indent(drawn.text, pad + MARGIN)}`);
|
|
33
|
+
if (scan)
|
|
34
|
+
note(scan);
|
|
35
|
+
},
|
|
36
|
+
});
|
package/dist/terminal/qr.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The same code as a PNG, `scale` pixels a module, for a person or an agent to pass on as an image.
|
|
3
|
-
* 8-bit greyscale, one filter-free row per pixel line: the smallest PNG any decoder reads.
|
|
4
|
-
*/
|
|
5
|
-
export declare const qrPng: (link: string, scale?: number) => Uint8Array;
|
|
6
1
|
/**
|
|
7
2
|
* A QR code as text, two modules per character cell with half blocks, so it fits a terminal
|
|
8
3
|
* without being twice as tall as it is wide. `width` is in columns, for deciding whether it fits.
|
|
@@ -11,3 +6,10 @@ export declare const terminalQr: (link: string) => {
|
|
|
11
6
|
text: string;
|
|
12
7
|
width: number;
|
|
13
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* The same code as plain text, for an agent to paste into a chat where no colour survives. Without colours a glyph
|
|
11
|
+
* takes the foreground colour, so the light modules and the quiet zone are the glyphs: on a dark background the code
|
|
12
|
+
* reads dark on light, and every line starts and ends with a glyph, so nothing trims it. Half blocks keep a long
|
|
13
|
+
* WhatsApp code under 80 columns; full blocks would need about 140 and wrap.
|
|
14
|
+
*/
|
|
15
|
+
export declare const plainQr: (link: string) => string;
|
package/dist/terminal/qr.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { deflateSync } from "node:zlib";
|
|
2
1
|
import qrcode from "qrcode-generator";
|
|
3
2
|
const QUIET_ZONE = 4;
|
|
4
3
|
// Dark modules on a light ground whatever the terminal's theme: a phone camera reads an inverted
|
|
@@ -18,52 +17,6 @@ const modules = (link) => {
|
|
|
18
17
|
};
|
|
19
18
|
return { size, dark };
|
|
20
19
|
};
|
|
21
|
-
/**
|
|
22
|
-
* The same code as a PNG, `scale` pixels a module, for a person or an agent to pass on as an image.
|
|
23
|
-
* 8-bit greyscale, one filter-free row per pixel line: the smallest PNG any decoder reads.
|
|
24
|
-
*/
|
|
25
|
-
export const qrPng = (link, scale = 8) => {
|
|
26
|
-
const { size, dark } = modules(link);
|
|
27
|
-
const side = size * scale;
|
|
28
|
-
const pixels = Buffer.alloc((side + 1) * side, 0xff);
|
|
29
|
-
for (let y = 0; y < side; y++) {
|
|
30
|
-
pixels[y * (side + 1)] = 0;
|
|
31
|
-
for (let x = 0; x < side; x++) {
|
|
32
|
-
if (dark(Math.floor(y / scale), Math.floor(x / scale)))
|
|
33
|
-
pixels[y * (side + 1) + 1 + x] = 0;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const header = Buffer.alloc(13);
|
|
37
|
-
header.writeUInt32BE(side, 0);
|
|
38
|
-
header.writeUInt32BE(side, 4);
|
|
39
|
-
header.set([8, 0, 0, 0, 0], 8);
|
|
40
|
-
return Buffer.concat([
|
|
41
|
-
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
42
|
-
chunk("IHDR", header),
|
|
43
|
-
chunk("IDAT", deflateSync(pixels)),
|
|
44
|
-
chunk("IEND", Buffer.alloc(0)),
|
|
45
|
-
]);
|
|
46
|
-
};
|
|
47
|
-
const chunk = (type, data) => {
|
|
48
|
-
const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
49
|
-
const out = Buffer.alloc(body.length + 8);
|
|
50
|
-
out.writeUInt32BE(data.length, 0);
|
|
51
|
-
body.copy(out, 4);
|
|
52
|
-
out.writeUInt32BE(crc32(body), body.length + 4);
|
|
53
|
-
return out;
|
|
54
|
-
};
|
|
55
|
-
const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
56
|
-
let c = n;
|
|
57
|
-
for (let k = 0; k < 8; k++)
|
|
58
|
-
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
59
|
-
return c >>> 0;
|
|
60
|
-
});
|
|
61
|
-
const crc32 = (bytes) => {
|
|
62
|
-
let c = 0xffffffff;
|
|
63
|
-
for (const byte of bytes)
|
|
64
|
-
c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
|
|
65
|
-
return (c ^ 0xffffffff) >>> 0;
|
|
66
|
-
};
|
|
67
20
|
/**
|
|
68
21
|
* A QR code as text, two modules per character cell with half blocks, so it fits a terminal
|
|
69
22
|
* without being twice as tall as it is wide. `width` is in columns, for deciding whether it fits.
|
|
@@ -82,3 +35,24 @@ export const terminalQr = (link) => {
|
|
|
82
35
|
}
|
|
83
36
|
return { text: lines.join("\n"), width: size };
|
|
84
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* The same code as plain text, for an agent to paste into a chat where no colour survives. Without colours a glyph
|
|
40
|
+
* takes the foreground colour, so the light modules and the quiet zone are the glyphs: on a dark background the code
|
|
41
|
+
* reads dark on light, and every line starts and ends with a glyph, so nothing trims it. Half blocks keep a long
|
|
42
|
+
* WhatsApp code under 80 columns; full blocks would need about 140 and wrap.
|
|
43
|
+
*/
|
|
44
|
+
export const plainQr = (link) => {
|
|
45
|
+
const { size, dark } = modules(link);
|
|
46
|
+
const light = (row, col) => row < size && !dark(row, col);
|
|
47
|
+
const lines = [];
|
|
48
|
+
for (let row = 0; row < size; row += 2) {
|
|
49
|
+
let line = "";
|
|
50
|
+
for (let col = 0; col < size; col++) {
|
|
51
|
+
const top = light(row, col);
|
|
52
|
+
const bottom = light(row + 1, col);
|
|
53
|
+
line += top && bottom ? "█" : top ? "▀" : bottom ? "▄" : " ";
|
|
54
|
+
}
|
|
55
|
+
lines.push(line);
|
|
56
|
+
}
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.363.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
|
@@ -810,10 +810,6 @@
|
|
|
810
810
|
"value": "<url>",
|
|
811
811
|
"meaning": "the tunnel's https address the browser apps use"
|
|
812
812
|
},
|
|
813
|
-
"--qr-file": {
|
|
814
|
-
"value": "<png>",
|
|
815
|
-
"meaning": "write the QR code to this PNG instead of drawing it, for an agent to pass on"
|
|
816
|
-
},
|
|
817
813
|
"--question": {
|
|
818
814
|
"meaning": "match only questions"
|
|
819
815
|
},
|
|
@@ -4053,7 +4049,6 @@
|
|
|
4053
4049
|
"in": ["tg"],
|
|
4054
4050
|
"reason": "Telegram asks each client for its own app from my.telegram.org; MAX has no such step; WhatsApp has no application registration"
|
|
4055
4051
|
},
|
|
4056
|
-
"--qr-file": "all",
|
|
4057
4052
|
"--sms": {
|
|
4058
4053
|
"in": ["tg"],
|
|
4059
4054
|
"reason": "asks Telegram to resend its login code by SMS; MAX has no such choice; WhatsApp links a device by QR or pairing code, never by an SMS code"
|
|
@@ -4068,8 +4063,7 @@
|
|
|
4068
4063
|
"in": ["tg"],
|
|
4069
4064
|
"reason": "Telegram requires an application from my.telegram.org; MAX has no application registration step; WhatsApp has no application registration"
|
|
4070
4065
|
},
|
|
4071
|
-
"--method": "all"
|
|
4072
|
-
"--qr-file": "all"
|
|
4066
|
+
"--method": "all"
|
|
4073
4067
|
}
|
|
4074
4068
|
},
|
|
4075
4069
|
"skill": {
|