@wirecat/cli-core 0.412.0 → 0.414.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 +29 -0
- package/dist/cli/entry.d.ts +59 -0
- package/dist/cli/entry.js +137 -0
- package/dist/cli/first-run.d.ts +4 -1
- package/dist/cli/first-run.js +9 -6
- package/dist/cli/help/command-help.js +8 -5
- package/dist/cli/help/first-screen.js +3 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/program.js +1 -0
- package/dist/errors/reasons.js +16 -1
- package/dist/files/native-writer-lease.js +17 -6
- package/dist/http/public-url.js +2 -0
- package/dist/mcp/agent.js +4 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/oauth/authorize.d.ts +21 -0
- package/dist/oauth/authorize.js +130 -0
- package/dist/oauth/index.d.ts +5 -0
- package/dist/oauth/index.js +4 -0
- package/dist/oauth/keeper.d.ts +41 -0
- package/dist/oauth/keeper.js +136 -0
- package/dist/oauth/provider.d.ts +54 -0
- package/dist/oauth/provider.js +1 -0
- package/dist/oauth/requests.d.ts +11 -0
- package/dist/oauth/requests.js +129 -0
- package/dist/oauth/storage.d.ts +48 -0
- package/dist/oauth/storage.js +197 -0
- package/dist/ports/app.d.ts +5 -0
- package/dist/services/project-gathering.d.ts +2 -0
- package/dist/services/project-gathering.js +18 -2
- package/dist/store/people.js +4 -1
- package/dist/store/sqlite/person-links.d.ts +6 -0
- package/dist/store/sqlite/person-links.js +27 -0
- package/dist/store/types.d.ts +2 -0
- package/package.json +5 -1
|
@@ -5,6 +5,7 @@ import { excerpt } from "./excerpt.js";
|
|
|
5
5
|
import { attachNeighbours, documentMatches, } 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 PAGE = 20;
|
|
8
9
|
const CONTEXTS = ["person-only", "sender", "linked-chat", "project"];
|
|
9
10
|
const contextOf = (lane) => lane.kinds.includes("linked-chat")
|
|
10
11
|
? "linked-chat"
|
|
@@ -84,6 +85,12 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
84
85
|
const oversized = new Map();
|
|
85
86
|
let stopped = null;
|
|
86
87
|
const open = (u) => !terminal.has(u.state) && u.state !== "read-limited";
|
|
88
|
+
const linkedLanes = new Set(plan.linkedChats.flatMap((c) => (c.lane === null ? [] : [c.lane])));
|
|
89
|
+
// One even share per linked chat plus one for the word and sender lanes, so one busy chat cannot fill a run; never
|
|
90
|
+
// below a page, so every chat still gets its whole first page.
|
|
91
|
+
const linkedLimit = Math.max(PAGE, Math.floor(plan.budgets.maxItems / (linkedLanes.size + 1)));
|
|
92
|
+
const linkedTaken = new Map();
|
|
93
|
+
const capped = new Set();
|
|
87
94
|
/** Records another lane finding a reference this run already returned; true when it had. */
|
|
88
95
|
const again = (ref, lane) => {
|
|
89
96
|
const known = byRef.get(ref);
|
|
@@ -133,11 +140,13 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
133
140
|
byRef.set(item.ref, entry);
|
|
134
141
|
items.push(entry);
|
|
135
142
|
usage.items++;
|
|
143
|
+
if (linkedLanes.has(lane.id))
|
|
144
|
+
linkedTaken.set(lane.id, (linkedTaken.get(lane.id) ?? 0) + 1);
|
|
136
145
|
};
|
|
137
146
|
// A resumed run starts at its shallowest open unit: below it no unit is due, and an empty round would end the run.
|
|
138
147
|
const shallowest = progress.reduce((low, u) => (open(u) ? Math.min(low, u.pages) : low), Number.POSITIVE_INFINITY);
|
|
139
148
|
for (let round = shallowest; !stopped; round++) {
|
|
140
|
-
const due = progress.flatMap((u, index) =>
|
|
149
|
+
const due = progress.flatMap((u, index) => open(u) && u.pages <= round && !capped.has(units[index]?.lane.id ?? "") ? [index] : []);
|
|
141
150
|
if (!due.length)
|
|
142
151
|
break;
|
|
143
152
|
for (const index of due) {
|
|
@@ -145,6 +154,11 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
145
154
|
if (!unit || !state)
|
|
146
155
|
continue;
|
|
147
156
|
signal?.throwIfAborted();
|
|
157
|
+
const room = linkedLanes.has(unit.lane.id) ? linkedLimit - (linkedTaken.get(unit.lane.id) ?? 0) : PAGE;
|
|
158
|
+
if (room <= 0) {
|
|
159
|
+
capped.add(unit.lane.id);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
148
162
|
stopped =
|
|
149
163
|
usage.reads >= plan.budgets.maxReads
|
|
150
164
|
? "reads"
|
|
@@ -187,7 +201,7 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
187
201
|
}
|
|
188
202
|
};
|
|
189
203
|
try {
|
|
190
|
-
let page = await readPage(Math.min(plan.budgets.maxPageBytes, plan.budgets.maxReadBytes - usage.readBytes),
|
|
204
|
+
let page = await readPage(Math.min(plan.budgets.maxPageBytes, plan.budgets.maxReadBytes - usage.readBytes), Math.min(PAGE, room));
|
|
191
205
|
// A large record another lane already returned is not read again; its page already moves past it.
|
|
192
206
|
const big = page.oversized && !again(page.oversized.ref, unit.lane) ? page.oversized : undefined;
|
|
193
207
|
if (big && big.bytes <= CONTEXT_MAX_READ_BYTES) {
|
|
@@ -288,6 +302,8 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
288
302
|
: units.every((unit, index) => unit.lane.id !== lane || progress[index]?.state === "unavailable")
|
|
289
303
|
? "unavailable"
|
|
290
304
|
: "in-scope",
|
|
305
|
+
items: lane === null ? 0 : (linkedTaken.get(lane) ?? 0),
|
|
306
|
+
capped: lane !== null && capped.has(lane),
|
|
291
307
|
})),
|
|
292
308
|
fetch: fetched.map((f, index) => ({ source: index, ...f })),
|
|
293
309
|
index: indexed,
|
package/dist/store/people.js
CHANGED
|
@@ -12,8 +12,11 @@ export const accountPeopleOver = (context) => {
|
|
|
12
12
|
return {
|
|
13
13
|
savePeople: async (key, people) => write(database, () => {
|
|
14
14
|
const accountKey = accountPk(key);
|
|
15
|
-
for (const person of people)
|
|
15
|
+
for (const person of people) {
|
|
16
16
|
identityPk(accountKey, key.provider, person.id, person.name, person);
|
|
17
|
+
if (person.email)
|
|
18
|
+
personLinks.joinByAddress(context, { provider: key.provider, id: person.id }, person.email, key.provider);
|
|
19
|
+
}
|
|
17
20
|
}),
|
|
18
21
|
contacts: async (key, options) => {
|
|
19
22
|
const accountKey = findAccountPk(key);
|
|
@@ -14,6 +14,12 @@ export declare const linkIdentityToPerson: (context: StoreContext, identityPk: n
|
|
|
14
14
|
export declare const linkIdentities: (context: StoreContext, person: IdentityRef, other: IdentityRef, options: LinkOptions) => PersonRecord;
|
|
15
15
|
/** Moves every identity of `from` to `to`, as `linkIdentities` does once it has found both people. */
|
|
16
16
|
export declare const mergePerson: (context: StoreContext, from: number, to: number, options: LinkOptions) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Joins the identity to the person of the mail contact with this exact address, making that contact when mail has
|
|
19
|
+
* not seen it yet, so whichever import comes second lands on one person. The contact is seen by no account, so it is
|
|
20
|
+
* no one's mail correspondent. An owner's unlink wins: two identities the owner separated stay apart.
|
|
21
|
+
*/
|
|
22
|
+
export declare const joinByAddress: (context: StoreContext, ref: IdentityRef, address: string, by: string) => void;
|
|
17
23
|
/** Gives the identity a person of its own again; the ones left keep theirs. */
|
|
18
24
|
export declare const unlinkIdentity: (context: StoreContext, ref: IdentityRef, options: LinkOptions) => PersonRecord;
|
|
19
25
|
/** Messages that name the person: by id where the messenger said so, or by `@username` in the text. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { and, eq, inArray, isNull, sql } from "./drizzle/core.js";
|
|
3
|
+
import { identityOf } from "./identities.js";
|
|
3
4
|
import { resolvePersonLinks } from "./person-resolution.js";
|
|
4
5
|
import { accountIdentities, accounts, chats, identities, identityLinkEvents, identityLinks, messages, persons, } from "./schema.js";
|
|
5
6
|
import { hitsWhere } from "./search.js";
|
|
@@ -135,6 +136,32 @@ export const mergePerson = (context, from, to, options) => {
|
|
|
135
136
|
for (const { identityPk } of together)
|
|
136
137
|
move(context, identityPk, from, to, options);
|
|
137
138
|
};
|
|
139
|
+
/** The owner once moved one of the two identities away from a person the other belonged to. */
|
|
140
|
+
const separatedByOwner = ({ database }, one, other) => database
|
|
141
|
+
.prepare(`SELECT 1 FROM identity_link_events moved
|
|
142
|
+
JOIN identity_link_events held ON held.to_person_id = moved.from_person_id
|
|
143
|
+
WHERE moved.author = 'owner' AND ((moved.identity_id = ? AND held.identity_id = ?)
|
|
144
|
+
OR (moved.identity_id = ? AND held.identity_id = ?))
|
|
145
|
+
LIMIT 1`)
|
|
146
|
+
.get(one, other, other, one) !== undefined;
|
|
147
|
+
/**
|
|
148
|
+
* Joins the identity to the person of the mail contact with this exact address, making that contact when mail has
|
|
149
|
+
* not seen it yet, so whichever import comes second lands on one person. The contact is seen by no account, so it is
|
|
150
|
+
* no one's mail correspondent. An owner's unlink wins: two identities the owner separated stay apart.
|
|
151
|
+
*/
|
|
152
|
+
export const joinByAddress = (context, ref, address, by) => {
|
|
153
|
+
const key = address.trim().toLowerCase();
|
|
154
|
+
if (!/^[^\s@]+@[^\s@]+$/.test(key) || ref.provider === "email")
|
|
155
|
+
return;
|
|
156
|
+
const found = required(context, ref);
|
|
157
|
+
if (!identityRow(context, { provider: "email", id: key }))
|
|
158
|
+
identityOf(context, "email", key, null);
|
|
159
|
+
const contact = required(context, { provider: "email", id: key });
|
|
160
|
+
if (contact.personPk === found.personPk || separatedByOwner(context, found.pk, contact.pk))
|
|
161
|
+
return;
|
|
162
|
+
mergePerson(context, found.personPk, contact.personPk, { method: "same-email", by });
|
|
163
|
+
resolvePersonLinks(context.database, recordOf(context, contact.personPk).identities.flatMap(({ name, username }) => [name, username]));
|
|
164
|
+
};
|
|
138
165
|
/** Gives the identity a person of its own again; the ones left keep theirs. */
|
|
139
166
|
export const unlinkIdentity = (context, ref, options) => {
|
|
140
167
|
const found = required(context, ref);
|
package/dist/store/types.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface PersonFacts {
|
|
|
32
32
|
username?: string | null;
|
|
33
33
|
isBot?: boolean | null;
|
|
34
34
|
description?: string | null;
|
|
35
|
+
/** An address the provider shows for them: they become one person with the mail contact of that exact address. */
|
|
36
|
+
email?: string | null;
|
|
35
37
|
}
|
|
36
38
|
/** One identity in one messenger, by the messenger's own id. `provider` is any string: `email` is one. */
|
|
37
39
|
export interface IdentityRef {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.414.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",
|
|
@@ -128,6 +128,10 @@
|
|
|
128
128
|
"./update": {
|
|
129
129
|
"types": "./dist/update/index.d.ts",
|
|
130
130
|
"default": "./dist/update/index.js"
|
|
131
|
+
},
|
|
132
|
+
"./oauth": {
|
|
133
|
+
"types": "./dist/oauth/index.d.ts",
|
|
134
|
+
"default": "./dist/oauth/index.js"
|
|
131
135
|
}
|
|
132
136
|
},
|
|
133
137
|
"scripts": {
|