@primusclaw/personal-agent 0.2.0 → 0.3.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/dist/index.js +99 -11
- package/dist/types/locales/link.d.ts +1 -0
- package/dist/types/ports/store/link.d.ts +19 -0
- package/dist/types/proactive/deliver.d.ts +7 -1
- package/dist/types/proactive/deps.d.ts +6 -0
- package/dist/types/protocol/claw-api/assistant-events.d.ts +5 -0
- package/dist/types/routing/credentials.d.ts +9 -0
- package/dist/types/routing/features.d.ts +2 -1
- package/dist/types/routing/personal-router.d.ts +13 -0
- package/dist/types/subscriptions/subscriptions.d.ts +6 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9557,7 +9557,8 @@ You will get a short code; send \`/link CODE\` here (valid for 10 minutes, works
|
|
|
9557
9557
|
`,
|
|
9558
9558
|
tooManyWrong: (p) => `Too many wrong codes. Wait ${String(p.minutes)} minute${p.minutes !== 1 ? "s" : ""} and try again.`,
|
|
9559
9559
|
linkingPaused: "Linking is paused for a few minutes because of too many wrong codes. Please try again later.",
|
|
9560
|
-
badCode: "That code is not valid or has expired. Get a new one on the console page."
|
|
9560
|
+
badCode: "That code is not valid or has expired. Get a new one on the console page.",
|
|
9561
|
+
identityRefused: "That needs you in person: confirm your identity once on this card, then ask again."
|
|
9561
9562
|
},
|
|
9562
9563
|
zh: {
|
|
9563
9564
|
linkSteps: (p) => `${p.lead}\u8981\u4F7F\u7528 ${p.bot}\uFF0C\u8BF7\u5148\u8FDE\u4E0A\u4F60\u7684 PrimusClaw \u8D26\u53F7\u3002\u6253\u5F00\u8FD9\u4E2A\u9875\u9762\uFF0C\u767B\u5F55\u540E\u9009\u62E9 Connect Microsoft Teams\uFF1A
|
|
@@ -9579,7 +9580,8 @@ ${p.page}
|
|
|
9579
9580
|
`,
|
|
9580
9581
|
tooManyWrong: (p) => `Too many wrong codes. Wait ${String(p.minutes)} minute${p.minutes !== 1 ? "s" : ""} and try again.`,
|
|
9581
9582
|
linkingPaused: "Linking is paused for a few minutes because of too many wrong codes. Please try again later.",
|
|
9582
|
-
badCode: "That code is not valid or has expired. Get a new one on the console page."
|
|
9583
|
+
badCode: "That code is not valid or has expired. Get a new one on the console page.",
|
|
9584
|
+
identityRefused: "\u8FD9\u4E00\u6B65\u9700\u8981\u4F60\u672C\u4EBA\uFF1A\u5728\u8FD9\u5F20\u5361\u7247\u4E0A\u786E\u8BA4\u4E00\u6B21\u8EAB\u4EFD\uFF0C\u7136\u540E\u518D\u8BF4\u4E00\u904D\u5C31\u884C\u3002"
|
|
9583
9585
|
}
|
|
9584
9586
|
});
|
|
9585
9587
|
var LINK_KEY_NAME = "PrimusClaw Teams";
|
|
@@ -9682,6 +9684,28 @@ var Credentials = class {
|
|
|
9682
9684
|
await chat.say(linkText.in("en").platformDown);
|
|
9683
9685
|
return void 0;
|
|
9684
9686
|
}
|
|
9687
|
+
/**
|
|
9688
|
+
* After an answer of the assistant that was refused for want of a confirmed identity (`needs_identity`: a person whose chats
|
|
9689
|
+
* run on their key because they never confirmed it asked for something only they may do, such as a review as them): the card
|
|
9690
|
+
* that confirms it, unless the person has confirmed by now. Nothing when chats do not run on the exchange, or it cannot be
|
|
9691
|
+
* asked. Never before that: connecting a machine and the rest of what a key may do are not held behind it.
|
|
9692
|
+
*/
|
|
9693
|
+
async identityAfterRefusal(chat) {
|
|
9694
|
+
if (this.chatExchange === void 0) return false;
|
|
9695
|
+
const token = await this.chatExchange.tokenFor(chat.sender.id);
|
|
9696
|
+
if (token.ok || token.error.kind !== "not_bound") return false;
|
|
9697
|
+
return this.sendIdentityCard(chat, linkText.in(chat.lang()).identityRefused);
|
|
9698
|
+
}
|
|
9699
|
+
/** The binding card with `lead`; false (nothing sent) when the identity service gives no link: a key's owner is not sent the `/link` steps. */
|
|
9700
|
+
async sendIdentityCard(chat, lead) {
|
|
9701
|
+
const request = await this.deps.exchange.bindingRequest(chat.sender.id, chat.sender.email);
|
|
9702
|
+
if (!request.ok) {
|
|
9703
|
+
this.deps.logger.warn("binding request failed", { kind: request.error.kind });
|
|
9704
|
+
return false;
|
|
9705
|
+
}
|
|
9706
|
+
await chat.send(this.views.binding(request.value.confirmUrl, lead));
|
|
9707
|
+
return true;
|
|
9708
|
+
}
|
|
9685
9709
|
/** A fresh credential per call when chats run on exchanged tokens (they expire); undefined: the key as it is. */
|
|
9686
9710
|
provider(userId, key) {
|
|
9687
9711
|
return this.chatExchange === void 0 ? void 0 : () => this.fresh(userId, key);
|
|
@@ -12973,16 +12997,58 @@ var PersonalRouter = class {
|
|
|
12973
12997
|
text;
|
|
12974
12998
|
stopPress;
|
|
12975
12999
|
showHere;
|
|
13000
|
+
/** A person's bound id is read and kept one message at a time, so a late lookup of an earlier token never wins. */
|
|
13001
|
+
boundLock = new KeyedLock();
|
|
12976
13002
|
async route(event, chat) {
|
|
12977
13003
|
const { store, subscriptions } = this.ctx.deps;
|
|
12978
13004
|
const userId = chat.sender.id;
|
|
12979
13005
|
store.references.saveReference(userId, wireOf(event));
|
|
12980
13006
|
if (event.type === "action" && event.invoke) store.members.notePersonalClick(userId);
|
|
12981
|
-
|
|
12982
|
-
|
|
13007
|
+
if (store.links.getKey(userId) !== null) {
|
|
13008
|
+
await this.backfill(userId);
|
|
13009
|
+
if (subscriptions !== void 0) await subscriptions.ensure(userId);
|
|
13010
|
+
} else {
|
|
13011
|
+
await this.boundLock.run(userId, () => this.reachBound(userId));
|
|
13012
|
+
}
|
|
12983
13013
|
if (event.type === "message") await this.message(event, chat);
|
|
12984
13014
|
else await this.click(event, chat);
|
|
12985
13015
|
}
|
|
13016
|
+
/**
|
|
13017
|
+
* A person with no link whose chats run on an exchanged token (bound at the identity service): their Claw user id is
|
|
13018
|
+
* read with the token and their subscription made with it, as a link's are, so their 1:1 chat reaches the assistant
|
|
13019
|
+
* from this message on (the assistant reaches only a person whose Claw user id and subscription it knows).
|
|
13020
|
+
*
|
|
13021
|
+
* The id is read again whenever the token is not the one it was read with (tokens last minutes, so at most once a token):
|
|
13022
|
+
* a person who rebinds to another account is followed to it, and one no longer bound is forgotten, so the assistant
|
|
13023
|
+
* never keeps answering for an account the person no longer holds (nor for one whose owner a new token cannot tell yet).
|
|
13024
|
+
* A subscription is made or mended only with a token whose owner is the id kept.
|
|
13025
|
+
*/
|
|
13026
|
+
async reachBound(userId) {
|
|
13027
|
+
const { credentials, owners } = this.ctx;
|
|
13028
|
+
const { store, subscriptions } = this.ctx.deps;
|
|
13029
|
+
if (!credentials.onExchange) return;
|
|
13030
|
+
let token;
|
|
13031
|
+
try {
|
|
13032
|
+
token = await credentials.background(userId);
|
|
13033
|
+
} catch (error) {
|
|
13034
|
+
if (error instanceof CredentialUnavailable) return;
|
|
13035
|
+
throw error;
|
|
13036
|
+
}
|
|
13037
|
+
if (token === void 0) {
|
|
13038
|
+
store.links.forgetBound(userId);
|
|
13039
|
+
return;
|
|
13040
|
+
}
|
|
13041
|
+
const kept = store.links.boundClawUser(userId, token);
|
|
13042
|
+
if (kept?.current !== true) {
|
|
13043
|
+
const clawUserId = await owners.userIdOf(token);
|
|
13044
|
+
if (clawUserId === void 0) {
|
|
13045
|
+
if (kept !== null) store.links.forgetBound(userId);
|
|
13046
|
+
return;
|
|
13047
|
+
}
|
|
13048
|
+
store.links.setBoundClawUser(userId, clawUserId, token);
|
|
13049
|
+
}
|
|
13050
|
+
if (subscriptions !== void 0) await subscriptions.ensure(userId, token);
|
|
13051
|
+
}
|
|
12986
13052
|
/** Linked before the Claw user id was kept: it is read now. */
|
|
12987
13053
|
async backfill(userId) {
|
|
12988
13054
|
const { links: links2 } = this.ctx.deps.store;
|
|
@@ -19398,8 +19464,27 @@ var Deliveries = class {
|
|
|
19398
19464
|
return this.sendRemoteAsk(userId, conversation, event, remote);
|
|
19399
19465
|
return void 0;
|
|
19400
19466
|
}
|
|
19401
|
-
/**
|
|
19467
|
+
/**
|
|
19468
|
+
* What the assistant said, then, when its turn was refused something for want of a confirmed identity (`needs_identity`) and
|
|
19469
|
+
* this is the person's own chat (a group binding's key has `#`), the card that confirms it. A card that could not be sent never
|
|
19470
|
+
* makes the message undelivered.
|
|
19471
|
+
*/
|
|
19402
19472
|
async sendSaid(userId, conversation, event) {
|
|
19473
|
+
const sent = await this.sendSaidWords(userId, conversation, event);
|
|
19474
|
+
const offer = this.deps.identityAfterRefusal;
|
|
19475
|
+
if (sent && offer !== void 0 && event["type"] === "assistant.say" && event["needs_identity"] === true && !userId.includes("#")) {
|
|
19476
|
+
try {
|
|
19477
|
+
await offer(userId, conversation, str5(event["turn_message_id"]) || str5(event["event_id"]));
|
|
19478
|
+
} catch (error) {
|
|
19479
|
+
this.deps.logger.warn("the identity card after a refused answer could not be sent", {
|
|
19480
|
+
error: error instanceof Error ? error.name : "unknown"
|
|
19481
|
+
});
|
|
19482
|
+
}
|
|
19483
|
+
}
|
|
19484
|
+
return sent;
|
|
19485
|
+
}
|
|
19486
|
+
/** What the assistant said (or a work item that needs the person and was not said). */
|
|
19487
|
+
async sendSaidWords(userId, conversation, event) {
|
|
19403
19488
|
const card = await this.remoteCard(userId, conversation, event);
|
|
19404
19489
|
if (card !== void 0) return card;
|
|
19405
19490
|
let words3 = workEventText(parseWorkItem2(event["item"]) ?? null, event["text"]);
|
|
@@ -20071,13 +20156,17 @@ var Subscriptions = class {
|
|
|
20071
20156
|
return "no_subscription";
|
|
20072
20157
|
}
|
|
20073
20158
|
}
|
|
20074
|
-
/**
|
|
20075
|
-
|
|
20159
|
+
/**
|
|
20160
|
+
* Make sure this person has a subscription (with the assistant's types when it is on). It is made and mended with
|
|
20161
|
+
* `credential` when one is given (a person with no link, whose chats run on an exchanged token), else with their linked
|
|
20162
|
+
* key. Never throws.
|
|
20163
|
+
*/
|
|
20164
|
+
async ensure(aadId, credential2) {
|
|
20076
20165
|
try {
|
|
20166
|
+
const key = credential2 ?? this.store.links.getKey(aadId);
|
|
20077
20167
|
let found = this.store.notifySubs.getNotifySub(aadId);
|
|
20078
|
-
if (found !== null && this.assistantOnFor(aadId)) found = await this.reconcile(aadId, found.subscriptionId);
|
|
20168
|
+
if (found !== null && this.assistantOnFor(aadId)) found = await this.reconcile(aadId, found.subscriptionId, key);
|
|
20079
20169
|
if (found !== null) return;
|
|
20080
|
-
const key = this.store.links.getKey(aadId);
|
|
20081
20170
|
if (key === null || !this.store.notifySubs.claimNotifyAttempt(aadId, RETRY_SECONDS)) return;
|
|
20082
20171
|
await this.create(aadId, key);
|
|
20083
20172
|
} catch (error) {
|
|
@@ -20117,8 +20206,7 @@ var Subscriptions = class {
|
|
|
20117
20206
|
* at, and mended with one PATCH, at most every UPGRADE_RETRY_SECONDS. One Claw no longer has is forgotten (the caller makes a new
|
|
20118
20207
|
* one); one Claw refuses even the assistant's types (400, 404) takes the person back to sessions (`types_version` 0).
|
|
20119
20208
|
*/
|
|
20120
|
-
async reconcile(aadId, subscriptionId) {
|
|
20121
|
-
const key = this.store.links.getKey(aadId);
|
|
20209
|
+
async reconcile(aadId, subscriptionId, key) {
|
|
20122
20210
|
if (key === null || !this.store.notifySubs.claimNotifyAttempt(aadId, UPGRADE_RETRY_SECONDS)) {
|
|
20123
20211
|
return this.store.notifySubs.getNotifySub(aadId);
|
|
20124
20212
|
}
|
|
@@ -25,6 +25,7 @@ export declare const linkText: import("./catalog.js").Catalog<{
|
|
|
25
25
|
}) => string;
|
|
26
26
|
linkingPaused: string;
|
|
27
27
|
badCode: string;
|
|
28
|
+
identityRefused: string;
|
|
28
29
|
}>;
|
|
29
30
|
/** The name the linked key has in SaFE (the console creates it under this name). */
|
|
30
31
|
export declare const LINK_KEY_NAME = "PrimusClaw Teams";
|
|
@@ -30,7 +30,26 @@ export interface LinkRepository {
|
|
|
30
30
|
setClawUserId(userId: string, clawUserId: string): void;
|
|
31
31
|
/** Linked before the Claw user id was kept (or the lookup failed at link time). */
|
|
32
32
|
needsClawUserId(userId: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The link's Claw user id; for a person with no link, the one kept for their binding at the identity service (their
|
|
35
|
+
* chats run on exchanged tokens). The assistant reaches either by it.
|
|
36
|
+
*/
|
|
33
37
|
clawUserIdOf(userId: string): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* A person with no link, bound at the identity service: the Claw user id kept for them, and whether it was read with
|
|
40
|
+
* `credential` (the token they write with now); null when none is kept.
|
|
41
|
+
*/
|
|
42
|
+
boundClawUser(userId: string, credential: string): {
|
|
43
|
+
clawUserId: string;
|
|
44
|
+
current: boolean;
|
|
45
|
+
} | null;
|
|
46
|
+
/**
|
|
47
|
+
* Keep the Claw user id read with `credential` for a bound person with no link. When it names another account than the
|
|
48
|
+
* one kept, their subscription (made with the earlier account's token) is forgotten, to be made again with this one.
|
|
49
|
+
*/
|
|
50
|
+
setBoundClawUser(userId: string, clawUserId: string, credential: string): void;
|
|
51
|
+
/** A person with no link is no longer bound: forget their kept Claw user id and their subscription. */
|
|
52
|
+
forgetBound(userId: string): void;
|
|
34
53
|
/** Every linked person, in a stable order. */
|
|
35
54
|
linkedUsers(): string[];
|
|
36
55
|
userForClawUser(clawUserId: string): string | null;
|
|
@@ -17,8 +17,14 @@ export declare class Deliveries {
|
|
|
17
17
|
* question (its card edited). Undefined when the message goes on as words.
|
|
18
18
|
*/
|
|
19
19
|
private remoteCard;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* What the assistant said, then, when its turn was refused something for want of a confirmed identity (`needs_identity`) and
|
|
22
|
+
* this is the person's own chat (a group binding's key has `#`), the card that confirms it. A card that could not be sent never
|
|
23
|
+
* makes the message undelivered.
|
|
24
|
+
*/
|
|
21
25
|
private sendSaid;
|
|
26
|
+
/** What the assistant said (or a work item that needs the person and was not said). */
|
|
27
|
+
private sendSaidWords;
|
|
22
28
|
/** The client locale of the message being handled ("" for a push or a sweep: Python CLIENT_LOCALE's default). */
|
|
23
29
|
private clientLocale;
|
|
24
30
|
/** The chat is an agent's window now: direct chat is on for the person and the chat is attached to a session. */
|
|
@@ -74,4 +74,10 @@ export interface ProactiveDeps {
|
|
|
74
74
|
* handled (`/resume`, quiet hours off) answer in their client's language; a push or a sweep reads none. Default: none.
|
|
75
75
|
*/
|
|
76
76
|
readonly requestScope?: RequestScope;
|
|
77
|
+
/**
|
|
78
|
+
* After an assistant message whose turn was refused for want of a confirmed identity (`needs_identity`), in the person's own
|
|
79
|
+
* chat: the card that confirms it (`Credentials.identityAfterRefusal`), once per turn (`turn`: the message the turn answers,
|
|
80
|
+
* else the say's own id; a later say of the same turn replaces the first and carries the field too). Default: none.
|
|
81
|
+
*/
|
|
82
|
+
readonly identityAfterRefusal?: (userId: string, conversation: Conversation, turn: string) => Promise<void>;
|
|
77
83
|
}
|
|
@@ -171,6 +171,11 @@ export interface AssistantSayPayload extends AssistantEventBase {
|
|
|
171
171
|
replaces?: string;
|
|
172
172
|
/** The message id of the turn a system notice answers or a router say belongs to (what an `amends` or a `replaces` is matched by). */
|
|
173
173
|
turn_message_id?: string;
|
|
174
|
+
/**
|
|
175
|
+
* Present when the turn was refused something only the person may do because their message came with a key, not with a confirmed
|
|
176
|
+
* identity (`not_attended`): a channel that can bind the person's identity offers it with this message; one that cannot ignores it.
|
|
177
|
+
*/
|
|
178
|
+
needs_identity?: true;
|
|
174
179
|
}
|
|
175
180
|
/** The system's own message, sent when an item has needed the user for a while and nobody has said so. */
|
|
176
181
|
export interface WorkItemNeedsYouPayload extends AssistantEventBase {
|
|
@@ -25,6 +25,15 @@ export declare class Credentials {
|
|
|
25
25
|
get onExchange(): boolean;
|
|
26
26
|
/** What to call Claw with for this person, or undefined after they were told what to do (`ask`). */
|
|
27
27
|
credential(chat: Chat, ask?: boolean): Promise<string | undefined>;
|
|
28
|
+
/**
|
|
29
|
+
* After an answer of the assistant that was refused for want of a confirmed identity (`needs_identity`: a person whose chats
|
|
30
|
+
* run on their key because they never confirmed it asked for something only they may do, such as a review as them): the card
|
|
31
|
+
* that confirms it, unless the person has confirmed by now. Nothing when chats do not run on the exchange, or it cannot be
|
|
32
|
+
* asked. Never before that: connecting a machine and the rest of what a key may do are not held behind it.
|
|
33
|
+
*/
|
|
34
|
+
identityAfterRefusal(chat: Chat): Promise<boolean>;
|
|
35
|
+
/** The binding card with `lead`; false (nothing sent) when the identity service gives no link: a key's owner is not sent the `/link` steps. */
|
|
36
|
+
private sendIdentityCard;
|
|
28
37
|
/** A fresh credential per call when chats run on exchanged tokens (they expire); undefined: the key as it is. */
|
|
29
38
|
provider(userId: string, key: string): CredentialProvider;
|
|
30
39
|
/** The person's exchanged token, else `fallback` (a turn can outlive a 10-minute token: each Claw call asks again). */
|
|
@@ -134,7 +134,8 @@ export interface RemoteChat {
|
|
|
134
134
|
}
|
|
135
135
|
/** The person's notification subscription in Claw (Python `subscriptions.Subscriptions`); undefined when notifications are off. */
|
|
136
136
|
export interface Subscriptions {
|
|
137
|
-
|
|
137
|
+
/** Made or mended with `credential` when given (a person with no link: their exchanged token), else the linked key. */
|
|
138
|
+
ensure(userId: string, credential?: string): Promise<void>;
|
|
138
139
|
/** Before the key is revoked: Claw needs it to delete the subscription. */
|
|
139
140
|
remove(userId: string, key: string): Promise<void>;
|
|
140
141
|
}
|
|
@@ -15,8 +15,21 @@ export declare class PersonalRouter {
|
|
|
15
15
|
private readonly text;
|
|
16
16
|
private readonly stopPress;
|
|
17
17
|
private readonly showHere;
|
|
18
|
+
/** A person's bound id is read and kept one message at a time, so a late lookup of an earlier token never wins. */
|
|
19
|
+
private readonly boundLock;
|
|
18
20
|
constructor(ctx: RouterContext, sets: CommandSets, text: PersonalText);
|
|
19
21
|
route(event: InboundMessage | InboundAction, chat: Chat): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* A person with no link whose chats run on an exchanged token (bound at the identity service): their Claw user id is
|
|
24
|
+
* read with the token and their subscription made with it, as a link's are, so their 1:1 chat reaches the assistant
|
|
25
|
+
* from this message on (the assistant reaches only a person whose Claw user id and subscription it knows).
|
|
26
|
+
*
|
|
27
|
+
* The id is read again whenever the token is not the one it was read with (tokens last minutes, so at most once a token):
|
|
28
|
+
* a person who rebinds to another account is followed to it, and one no longer bound is forgotten, so the assistant
|
|
29
|
+
* never keeps answering for an account the person no longer holds (nor for one whose owner a new token cannot tell yet).
|
|
30
|
+
* A subscription is made or mended only with a token whose owner is the id kept.
|
|
31
|
+
*/
|
|
32
|
+
private reachBound;
|
|
20
33
|
/** Linked before the Claw user id was kept: it is read now. */
|
|
21
34
|
private backfill;
|
|
22
35
|
private message;
|
|
@@ -51,8 +51,12 @@ export declare class Subscriptions {
|
|
|
51
51
|
* mended first and the user id filled in, so what is said is the state after trying. Never throws.
|
|
52
52
|
*/
|
|
53
53
|
reachable(aadId: string): Promise<Unreachable>;
|
|
54
|
-
/**
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Make sure this person has a subscription (with the assistant's types when it is on). It is made and mended with
|
|
56
|
+
* `credential` when one is given (a person with no link, whose chats run on an exchanged token), else with their linked
|
|
57
|
+
* key. Never throws.
|
|
58
|
+
*/
|
|
59
|
+
ensure(aadId: string, credential?: string): Promise<void>;
|
|
56
60
|
private create;
|
|
57
61
|
/**
|
|
58
62
|
* With the assistant on, the recorded subscription must be there in Claw, enabled and asking for the assistant's events: looked
|