toolroll 0.9.16 → 0.9.18
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 +28 -0
- package/dist/assignment-brief.js +11 -4
- package/dist/browser/workspace.js +14 -14
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/chat-actions.d.ts +7 -0
- package/dist/chat-actions.js +40 -4
- package/dist/chat-ask.js +29 -15
- package/dist/chat-channel.js +3 -1
- package/dist/chat-decide.d.ts +203 -0
- package/dist/chat-decide.js +554 -0
- package/dist/chat-delivery-state.d.ts +7 -0
- package/dist/chat-delivery.d.ts +3 -0
- package/dist/chat-delivery.js +63 -17
- package/dist/chat-quiet.d.ts +4 -1
- package/dist/chat-quiet.js +6 -2
- package/dist/chat-warmth.d.ts +21 -0
- package/dist/chat-warmth.js +36 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-api.d.ts +3 -1
- package/dist/discord-api.js +4 -2
- package/dist/discord-chat.d.ts +3 -2
- package/dist/discord-chat.js +35 -6
- package/dist/lead-about.d.ts +35 -0
- package/dist/lead-about.js +62 -0
- package/dist/lead-capabilities.d.ts +64 -0
- package/dist/lead-capabilities.js +199 -0
- package/dist/lead-commitments.js +13 -7
- package/dist/lead-context.d.ts +5 -3
- package/dist/lead-context.js +29 -7
- package/dist/lead-follow.d.ts +1 -1
- package/dist/lead-follow.js +3 -2
- package/dist/lead-identity.js +1 -1
- package/dist/lead-people.d.ts +55 -0
- package/dist/lead-people.js +128 -0
- package/dist/lead-voice.d.ts +3 -2
- package/dist/lead-voice.js +4 -3
- package/dist/mate-cli.d.ts +3 -0
- package/dist/mate-cli.js +15 -14
- package/dist/mate-contract.d.ts +1 -1
- package/dist/mate-contract.js +6 -4
- package/dist/mate-progress.js +2 -0
- package/dist/mate-tools.d.ts +8 -0
- package/dist/mate-tools.js +112 -6
- package/dist/mate.d.ts +26 -1
- package/dist/mate.js +89 -25
- package/dist/modes.d.ts +6 -0
- package/dist/modes.js +6 -0
- package/dist/operate.js +20 -8
- package/dist/phase-routing.d.ts +1 -1
- package/dist/project-delete.js +3 -0
- package/dist/provider-auth.d.ts +4 -0
- package/dist/provider-auth.js +1 -1
- package/dist/reply-shape.d.ts +96 -0
- package/dist/reply-shape.js +452 -0
- package/dist/result-shots.d.ts +51 -0
- package/dist/result-shots.js +119 -0
- package/dist/serve.js +76 -49
- package/dist/slack-chat.d.ts +2 -0
- package/dist/slack-chat.js +38 -7
- package/dist/store.d.ts +36 -3
- package/dist/store.js +163 -16
- package/dist/team-runtime.js +1 -1
- package/dist/teams-chat.js +17 -1
- package/dist/telegram-mate.d.ts +12 -0
- package/dist/telegram-mate.js +27 -11
- package/dist/telegram-team.js +6 -3
- package/dist/telegram.d.ts +5 -3
- package/dist/telegram.js +227 -21
- package/dist/test-temp.js +1 -1
- package/package.json +1 -1
package/dist/chat-delivery.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { answerChatQuestionPrompt, applyChatQuestionTap, questionParts } from "./teammate-question.js";
|
|
3
3
|
import { applyChatAskTap, chatAskText } from "./chat-ask.js";
|
|
4
4
|
import { channelRepos, resolveChannelMate, proposalPreview, proposalOutcomeText, confirmedLink, confirmedCardText, replyContextFor, mirrorToTaskChat, focusContextFor, taskInCeiling, tooLongText, } from "./chat-channel.js";
|
|
5
|
-
import { MATE_MESSAGE_MAX_CHARS, runMateTurn } from "./mate.js";
|
|
5
|
+
import { MATE_MESSAGE_MAX_CHARS, mateFailureText, runMateTurn } from "./mate.js";
|
|
6
|
+
import { shapeReplyParts } from "./reply-shape.js";
|
|
7
|
+
import { warmTurn } from "./chat-warmth.js";
|
|
6
8
|
import { confirmMateProposal, dismissMateProposal, } from "./mate-doors.js";
|
|
7
9
|
import { ceilingDigestOf, verifyApproverStanding } from "./principal.js";
|
|
8
10
|
import { ChatState, ChatDeliveryError, chatHash, } from "./chat-delivery-state.js";
|
|
@@ -17,7 +19,8 @@ import { leadChannelOf } from "./lead-context.js";
|
|
|
17
19
|
import { promiseChannelOf } from "./lead-commitments.js";
|
|
18
20
|
import { phoneText, PHONE_HELP, phoneCommand, phoneStatus, phoneTaskView, phoneTaskChoices, phoneTaskListText, resolvePhoneTask, phoneFocusText, PHONE_NO_MATCH, PHONE_BACK_TO_LEAD } from "./telegram-status.js";
|
|
19
21
|
import { applyRoomInbound, conversationRow, roomCardApprover, roomCommand, roomGrantAllowed, roomMessagesAfter, roomMessageText, teamDomain } from "./chat-rooms.js";
|
|
20
|
-
import { isTelegramProgressNotification, proposalTaskOf } from "./store.js";
|
|
22
|
+
import { isTelegramProgressNotification, proposalTaskOf, RESULT_SHOTS_KIND } from "./store.js";
|
|
23
|
+
import { resultShotsFor } from "./result-shots.js";
|
|
21
24
|
import { messageTeammate } from "./teammate-desk.js";
|
|
22
25
|
import { CHAT_APP_NAMES } from "./flow-triggers.js";
|
|
23
26
|
export const chatObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
|
|
@@ -25,7 +28,8 @@ export const chatObject = (v) => v !== null && typeof v === "object" && !Array.i
|
|
|
25
28
|
: {};
|
|
26
29
|
const object = chatObject;
|
|
27
30
|
const nowOf = (options) => options.clock?.() ?? new Date();
|
|
28
|
-
|
|
31
|
+
const CHAT_PART_SIZE = 2800;
|
|
32
|
+
export const splitChatText = (text, size = CHAT_PART_SIZE) => {
|
|
29
33
|
const parts = [];
|
|
30
34
|
for (let at = 0; at < text.length;) {
|
|
31
35
|
let end = Math.min(at + size, text.length);
|
|
@@ -221,7 +225,7 @@ export async function processChatEvent(options) {
|
|
|
221
225
|
return true;
|
|
222
226
|
}
|
|
223
227
|
if (event.session !== null && event.session !== resolved.session.id) {
|
|
224
|
-
state.plan(event.id, [{ text: "
|
|
228
|
+
state.plan(event.id, [{ text: "This conversation was restarted after you sent that, so it wasn't answered. Send your message again." }], now);
|
|
225
229
|
return true;
|
|
226
230
|
}
|
|
227
231
|
state
|
|
@@ -263,6 +267,9 @@ export async function processChatEvent(options) {
|
|
|
263
267
|
}
|
|
264
268
|
// An unknown surface names no channel rather than a guess.
|
|
265
269
|
const channel = leadChannelOf(options.label);
|
|
270
|
+
// Only ever the owner's own message in their own chat: never the bot's, never a room.
|
|
271
|
+
const warm = warmTurn(options.warm !== undefined && event.kind === "message" && event.member !== identity.bot && event.member === binding.member && event.channel === binding.channel
|
|
272
|
+
? options.warm(event, binding) : {});
|
|
266
273
|
const outcome = await runMateTurn({
|
|
267
274
|
store,
|
|
268
275
|
who: resolved.who,
|
|
@@ -282,19 +289,20 @@ export async function processChatEvent(options) {
|
|
|
282
289
|
evidenceRoot: options.evidenceRoot,
|
|
283
290
|
mediaDelivery: "documents",
|
|
284
291
|
...(channel === undefined ? {} : { channel }),
|
|
292
|
+
onProgress: warm.onProgress,
|
|
285
293
|
revalidate: async () => {
|
|
286
294
|
try {
|
|
287
295
|
await channelAccess(options, binding, resolved.who.ceilingDigest);
|
|
288
296
|
return { ok: true };
|
|
289
297
|
}
|
|
290
|
-
catch {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
};
|
|
298
|
+
catch (error) {
|
|
299
|
+
const code = error instanceof ChatDeliveryError ? error.code : "";
|
|
300
|
+
const reason = code === "Connected projects changed" ? "projects-changed"
|
|
301
|
+
: code.endsWith("account access changed") ? "member-changed" : "access-changed";
|
|
302
|
+
return { ok: false, reason };
|
|
295
303
|
}
|
|
296
304
|
},
|
|
297
|
-
});
|
|
305
|
+
}).finally(warm.stop);
|
|
298
306
|
if (outcome.ok && !outcome.replayed && context?.task)
|
|
299
307
|
mirrorToTaskChat(store, resolved.who, context.task, options.label, text, outcome.reply, nowOf(options));
|
|
300
308
|
if (!state.owns(identity.installation, options.owner, nowOf(options)) ||
|
|
@@ -327,7 +335,7 @@ export async function processChatEvent(options) {
|
|
|
327
335
|
if (turn?.state !== "answered") {
|
|
328
336
|
state.plan(event.id, [
|
|
329
337
|
{
|
|
330
|
-
text:
|
|
338
|
+
text: mateFailureText(turn?.failureReason ?? null),
|
|
331
339
|
},
|
|
332
340
|
], nowOf(options));
|
|
333
341
|
return true;
|
|
@@ -342,7 +350,9 @@ export async function processChatEvent(options) {
|
|
|
342
350
|
.listMateMessages(turn.thread, 200)
|
|
343
351
|
.find((message) => message.turn === turn.id && message.role === "assistant")?.text ?? "The reply is no longer in the saved thread.";
|
|
344
352
|
// The lead's answer about one task names it: a reply to it stays on that task.
|
|
345
|
-
|
|
353
|
+
// Split as written, then shape each part: no cut ever lands inside a link, code or a bold anchor.
|
|
354
|
+
const shapedParts = shapeReplyParts(reply, options.partSize ?? CHAT_PART_SIZE, { asked: text, appOrigin: options.origin() });
|
|
355
|
+
const parts = (shapedParts.length > 0 ? shapedParts : splitChatText("")).map((text) => (about === null ? { text, voice: true } : { text, voice: true, task: about.task, ...(about.run === null ? {} : { run: about.run }) }));
|
|
346
356
|
// The lead's question to its owner: its options as buttons, then "Something else", in their own chat (any thread);
|
|
347
357
|
// in a room a tap is not their message, so the question and its options go out as text.
|
|
348
358
|
const ask = store.mateAsk(turn.id);
|
|
@@ -581,8 +591,13 @@ export async function planRoomMessages(options) {
|
|
|
581
591
|
if (carrier === null)
|
|
582
592
|
break;
|
|
583
593
|
const parts = [];
|
|
584
|
-
if (text !== null)
|
|
585
|
-
|
|
594
|
+
if (text !== null) {
|
|
595
|
+
if (message.role === "assistant")
|
|
596
|
+
for (const part of shapeReplyParts(text, options.partSize ?? CHAT_PART_SIZE, { appOrigin: options.origin() }))
|
|
597
|
+
parts.push({ text: part, voice: true, channel: room.chat });
|
|
598
|
+
else
|
|
599
|
+
parts.push({ text, channel: room.chat });
|
|
600
|
+
}
|
|
586
601
|
if (message.role === "assistant" && message.turn !== null)
|
|
587
602
|
for (const proposal of store.listMateProposals(row.thread, ["pending"]).filter(one => one.turn === message.turn))
|
|
588
603
|
parts.push({ text: "", proposal: proposal.id, channel: room.chat });
|
|
@@ -654,6 +669,32 @@ function planFinished(options, binding, taskRef, run, now, notification = null)
|
|
|
654
669
|
store.setChatBatchMessage(batch.id, String(part.id), shown);
|
|
655
670
|
return true;
|
|
656
671
|
}
|
|
672
|
+
/** Screenshots with a result (result-shots.ts), planned after the result's own message: each an upload that follows
|
|
673
|
+
* it in its thread (Slack, Discord), or one link to them (Teams). A failed check is one plain line instead. */
|
|
674
|
+
function planResultShots(options, binding, notification, repos, id, now) {
|
|
675
|
+
const { state, store, identity } = options;
|
|
676
|
+
const plan = resultShotsFor(store, options.evidenceRoot, repos, notification);
|
|
677
|
+
if (plan.kind === "none" || notification.taskId === null || notification.run === null)
|
|
678
|
+
return;
|
|
679
|
+
const where = { task: notification.taskId, run: notification.run };
|
|
680
|
+
// The result's message here: its finished-work batch (quiet), or its progress card (every step).
|
|
681
|
+
const follows = (() => {
|
|
682
|
+
const batch = notification.taskRef === null ? null : store.chatBatchMessageFor(`${state.channel}:${binding.id}`, notification.taskRef, notification.run);
|
|
683
|
+
if (batch !== null)
|
|
684
|
+
return Number(batch);
|
|
685
|
+
const card = state.prepare("SELECT part FROM chat_progress WHERE binding=? AND run=?").get(binding.id, notification.run);
|
|
686
|
+
return card === undefined ? null : Number(card.part);
|
|
687
|
+
})();
|
|
688
|
+
const image = (artifact, sha256) => ({ taskId: where.task, run: where.run, artifact, sha256 });
|
|
689
|
+
const parts = plan.kind === "line" ? [{ text: plan.text, ...where }]
|
|
690
|
+
// Teams has no file upload here: one message that links to the saved result.
|
|
691
|
+
: state.channel === "teams" ? [{ text: plan.shots.length === 1 ? plan.shots[0].caption : `${plan.shots[0].caption} · ${plan.shots.length} screenshots`,
|
|
692
|
+
image: image(plan.shots[0].artifact, plan.shots[0].sha256), shot: { follows: null }, ...where }]
|
|
693
|
+
: plan.shots.map(one => ({ text: one.caption, image: image(one.artifact, one.sha256), shot: { follows }, ...where }));
|
|
694
|
+
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
695
|
+
ts: "", thread: "", payload: "{}", created: now.toISOString() });
|
|
696
|
+
state.plan(id, parts, now);
|
|
697
|
+
}
|
|
657
698
|
/** One progress card per exact result; separate urgent facts retain their own review link. */
|
|
658
699
|
export async function planChatNotifications(options) {
|
|
659
700
|
const state = options.state, { store, identity } = options, bindings = state.bindings(identity.installation).filter(one => state.live(one));
|
|
@@ -680,10 +721,11 @@ export async function planChatNotifications(options) {
|
|
|
680
721
|
const personal = notification.recipient !== null;
|
|
681
722
|
if (notification.createdAt >= binding.created &&
|
|
682
723
|
notification.resolvedAt === null &&
|
|
683
|
-
// A promise the lead made on another chat is reported there (lead-commitments.ts)
|
|
724
|
+
// A promise the lead made on another chat is reported there (lead-commitments.ts); one its owner asked for on
|
|
725
|
+
// this chat is always said here, however quiet the lead's own work is kept.
|
|
684
726
|
(promiseChannelOf(notification) ?? state.channel) === state.channel &&
|
|
685
727
|
// Pings follow responsibility: the lead's work, this person's own act and a muted project stay in the console.
|
|
686
|
-
store.pingAllowed(notification, binding.approver) &&
|
|
728
|
+
(promiseChannelOf(notification) === state.channel || store.pingAllowed(notification, binding.approver)) &&
|
|
687
729
|
// A flow decision for "anyone who approves" reaches every approver who can see the project.
|
|
688
730
|
// A notification addressed to this person reaches them whatever project
|
|
689
731
|
// their channel follows (a sign-in pause is the installation's, v108).
|
|
@@ -695,9 +737,13 @@ export async function planChatNotifications(options) {
|
|
|
695
737
|
// Only when I'm needed (the default): the task's one card is edited in place, and a new message
|
|
696
738
|
// follows only when this person is needed. Every step keeps the branches below unchanged.
|
|
697
739
|
const quiet = !personal && isTaskFact(notification) && store.notificationPreference(binding.approver).mode === "quiet";
|
|
740
|
+
if (notification.kind === RESULT_SHOTS_KIND) {
|
|
741
|
+
planResultShots(options, binding, notification, repos, id, now);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
698
744
|
if (notification.kind === LEAD_SAY_KIND) {
|
|
699
745
|
// The lead's words (lead-voice.ts): one message, repainted in place when a later say joins it.
|
|
700
|
-
const content = { text: leadSayText(store, notification),
|
|
746
|
+
const content = { text: leadSayText(store, notification, binding.approver),
|
|
701
747
|
...(notification.link ? { link: { label: "Open", path: notification.link } } : {}) };
|
|
702
748
|
const earlier = leadSayEarlier(store, notification).map(one => state.prepare("SELECT id FROM chat_part WHERE event=?")
|
|
703
749
|
.get(chatHash(`${options.state.channel}:notice:${binding.id}:${one}`))).find(one => one !== undefined);
|
package/dist/chat-quiet.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { type ProgressEntity } from "./telegram-progress.js";
|
|
2
2
|
import { type PhoneTaskLink } from "./telegram-status.js";
|
|
3
3
|
import { type ChatBatch, type Notification, type Store } from "./store.js";
|
|
4
|
-
|
|
4
|
+
import { type DecideTarget } from "./chat-decide.js";
|
|
5
|
+
/** `also`: more buttons after `link`, such as [Look first] beside [Merge]. `target`: the one card a chat may act on in
|
|
6
|
+
* place (chat-decide.ts), when the message is about one result, plan, failure or pull request. */
|
|
5
7
|
export type QuietView = {
|
|
6
8
|
text: string;
|
|
7
9
|
entities: ProgressEntity[];
|
|
8
10
|
link: PhoneTaskLink;
|
|
9
11
|
also?: PhoneTaskLink[];
|
|
12
|
+
target?: DecideTarget;
|
|
10
13
|
};
|
|
11
14
|
export declare function needsPerson(row: Pick<Notification, "dedupeKey" | "kind" | "pushClass" | "recipient">): boolean;
|
|
12
15
|
/** A fact about one placed task: in quiet mode it lands on that task's card. */
|
package/dist/chat-quiet.js
CHANGED
|
@@ -12,6 +12,7 @@ import { asksToFinish, batchLine, chatText, chatTitle, factLinkLabel, mentions,
|
|
|
12
12
|
import { phoneText, projectLabel } from "./telegram-status.js";
|
|
13
13
|
import { isLifecycleNotification } from "./store.js";
|
|
14
14
|
import { leadSubjectOf } from "./lead-voice.js";
|
|
15
|
+
import { decideFallbackLink, decideTargetOf } from "./chat-decide.js";
|
|
15
16
|
/** What always makes a new message, even in quiet mode: a question or approval waiting, a failure or other
|
|
16
17
|
* attention fact, a result Ready for review, a security alert or release, and anything addressed to one person
|
|
17
18
|
* (a sign-in or plan-limit pause, an evening digest). */
|
|
@@ -132,7 +133,8 @@ function lineOf(store, item, now, root, viewer) {
|
|
|
132
133
|
const subject = leadSubjectOf(store, row, viewer);
|
|
133
134
|
const words = chatText(phoneText(row.body === "" ? subject : `${subject}\n\n${row.body}`, 2500), [{ id: ref.externalId, title: summary }]);
|
|
134
135
|
const { phrase, headline } = updatePhrase(row);
|
|
135
|
-
|
|
136
|
+
const target = decideTargetOf(row);
|
|
137
|
+
return { also: [], target, link: decideFallbackLink(target) ?? (row.link === null ? { label: "Open task", path: chatControlHref("task", ref.externalId) } : { label: factLinkLabel(row.link), path: row.link }),
|
|
136
138
|
fact: { summary, headline, checks: null, report: false, completedBy: null, update: { words: mentions(words, summary) ? words : `${summary} · ${words}`, phrase } } };
|
|
137
139
|
}
|
|
138
140
|
const run = item.run === null ? null : store.getRun(item.run);
|
|
@@ -156,6 +158,7 @@ function lineOf(store, item, now, root, viewer) {
|
|
|
156
158
|
// A result ready for a person offers its real next step, then a look first.
|
|
157
159
|
if (asksToFinish(fact))
|
|
158
160
|
return { fact, also: [{ label: READY_ACTIONS.look, path: chatResultHref(ref.externalId, run.id, "changes") }],
|
|
161
|
+
target: pullRequest ? { kind: "merge", taskId: ref.externalId, run: run.id } : { kind: "result", taskId: ref.externalId, run: run.id },
|
|
159
162
|
link: pullRequest ? { label: READY_ACTIONS.merge, path: `/t/${encodeURIComponent(ref.externalId)}#merge` } : { label: READY_ACTIONS.complete, path: chatResultHref(ref.externalId, run.id) } };
|
|
160
163
|
return { fact, link: card.link, also: [] };
|
|
161
164
|
}
|
|
@@ -168,7 +171,8 @@ export function finishedView(store, batch, now, root, viewer) {
|
|
|
168
171
|
const text = batchLine(lines.map(one => one.fact));
|
|
169
172
|
if (lines.length > 1)
|
|
170
173
|
return { text, entities: [], link: { label: "Open", path: "/tasks" } };
|
|
171
|
-
|
|
174
|
+
const target = lines[0].target ?? null;
|
|
175
|
+
return { text, entities: [], link: lines[0].link, ...(lines[0].also.length === 0 ? {} : { also: lines[0].also }), ...(target === null ? {} : { target }) };
|
|
172
176
|
}
|
|
173
177
|
// ---- the evening digest ----------------------------------------------------------
|
|
174
178
|
const FAILURE = /fail|exhausted|fenced/;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Small warm touches while the lead works on a chat app: once a turn will take more than a few seconds (its first tool
|
|
2
|
+
* step starts), a 👍 on the owner's message and the app's typing indicator until the turn ends. Best effort only: an
|
|
3
|
+
* app or permission that does not allow it is skipped silently, and nothing here can fail, delay or change the turn. */
|
|
4
|
+
import type { MateProgress } from "./mate-progress.js";
|
|
5
|
+
export declare const WARM_EMOJI = "\uD83D\uDC4D";
|
|
6
|
+
/** Telegram and Teams show typing for about five seconds, Discord for ten: refreshed inside the shorter window. */
|
|
7
|
+
export declare const TYPING_REFRESH_MS = 4000;
|
|
8
|
+
export type WarmHooks = {
|
|
9
|
+
/** React to the owner's own message. Absent: the app (or this bot's permission) has no reactions. */
|
|
10
|
+
react?: () => Promise<unknown>;
|
|
11
|
+
/** Show the app's typing indicator once. Absent: the app has none for bots. */
|
|
12
|
+
typing?: () => Promise<unknown>;
|
|
13
|
+
};
|
|
14
|
+
export type WarmTurn = {
|
|
15
|
+
onProgress: (event: MateProgress) => void;
|
|
16
|
+
stop: () => void;
|
|
17
|
+
};
|
|
18
|
+
/** The turn's progress listener: on the first tool step, react once and keep typing shown until `stop`. */
|
|
19
|
+
export declare function warmTurn(hooks: WarmHooks, options?: {
|
|
20
|
+
refreshMs?: number;
|
|
21
|
+
}): WarmTurn;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const WARM_EMOJI = "👍";
|
|
2
|
+
/** Telegram and Teams show typing for about five seconds, Discord for ten: refreshed inside the shorter window. */
|
|
3
|
+
export const TYPING_REFRESH_MS = 4_000;
|
|
4
|
+
/** The turn's progress listener: on the first tool step, react once and keep typing shown until `stop`. */
|
|
5
|
+
export function warmTurn(hooks, options = {}) {
|
|
6
|
+
let started = false;
|
|
7
|
+
let stopped = false;
|
|
8
|
+
let timer = null;
|
|
9
|
+
const quietly = (call) => {
|
|
10
|
+
if (call === undefined || stopped)
|
|
11
|
+
return;
|
|
12
|
+
try {
|
|
13
|
+
void call().catch(() => undefined);
|
|
14
|
+
}
|
|
15
|
+
catch { /* skipped silently */ }
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
onProgress: event => {
|
|
19
|
+
if (stopped || event.kind !== "tool" || started)
|
|
20
|
+
return;
|
|
21
|
+
started = true;
|
|
22
|
+
quietly(hooks.react);
|
|
23
|
+
quietly(hooks.typing);
|
|
24
|
+
if (hooks.typing !== undefined) {
|
|
25
|
+
timer = setInterval(() => quietly(hooks.typing), options.refreshMs ?? TYPING_REFRESH_MS);
|
|
26
|
+
timer.unref?.();
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
stop: () => {
|
|
30
|
+
stopped = true;
|
|
31
|
+
if (timer !== null)
|
|
32
|
+
clearInterval(timer);
|
|
33
|
+
timer = null;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -125,6 +125,7 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
|
|
|
125
125
|
export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
|
|
126
126
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
127
127
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
128
|
+
version?: never;
|
|
128
129
|
active: boolean;
|
|
129
130
|
phase: string;
|
|
130
131
|
detail: string;
|
|
@@ -138,7 +139,6 @@ export declare function desktopUpdateStatus(stateDir: string): {
|
|
|
138
139
|
wasRunning?: never;
|
|
139
140
|
backupPath?: never;
|
|
140
141
|
updateId?: never;
|
|
141
|
-
version?: never;
|
|
142
142
|
buildId?: never;
|
|
143
143
|
workDir?: never;
|
|
144
144
|
} | {
|
package/dist/discord-api.d.ts
CHANGED
|
@@ -5,12 +5,14 @@ export declare const discordId: (v: unknown) => v is string;
|
|
|
5
5
|
export type DiscordCredentials = ChatIdentity & {
|
|
6
6
|
botToken: string;
|
|
7
7
|
};
|
|
8
|
-
export type DiscordApi = (method: "GET" | "POST" | "PATCH", path: string, body?: Record<string, unknown>, file?: {
|
|
8
|
+
export type DiscordApi = (method: "GET" | "POST" | "PATCH" | "PUT", path: string, body?: Record<string, unknown>, file?: {
|
|
9
9
|
bytes: Uint8Array;
|
|
10
10
|
name: string;
|
|
11
11
|
}) => Promise<Record<string, unknown>>;
|
|
12
12
|
export declare class DiscordError extends ChatDeliveryError {
|
|
13
13
|
}
|
|
14
|
+
/** Discord's 403: the bot may not do this here (for a file, no Attach Files permission). */
|
|
15
|
+
export declare const DISCORD_REFUSED = "Discord refused access. Check the app and direct-message permissions.";
|
|
14
16
|
export declare function discordApi(token: string, fetcher?: typeof fetch): DiscordApi;
|
|
15
17
|
export declare const discordCredentialFile: (dir: string) => string;
|
|
16
18
|
export declare function loadDiscordCredentials(dir: string): DiscordCredentials | null;
|
package/dist/discord-api.js
CHANGED
|
@@ -8,10 +8,12 @@ export { object };
|
|
|
8
8
|
export const discordId = (v) => typeof v === "string" && /^[1-9][0-9]{16,19}$/.test(v);
|
|
9
9
|
export class DiscordError extends ChatDeliveryError {
|
|
10
10
|
}
|
|
11
|
+
/** Discord's 403: the bot may not do this here (for a file, no Attach Files permission). */
|
|
12
|
+
export const DISCORD_REFUSED = "Discord refused access. Check the app and direct-message permissions.";
|
|
11
13
|
export function discordApi(token, fetcher = fetch) {
|
|
12
14
|
return async (method, path, body, file) => {
|
|
13
15
|
// Only relative API routes; never send the bot credential to supplied URLs.
|
|
14
|
-
if (!/^\/(?:users\/@me|users\/[0-9]+|oauth2\/applications\/@me|channels\/[0-9]+(?:\/messages(?:\/[0-9]+)?(?:\?limit=100)?)?|interactions\/[0-9]+\/[A-Za-z0-9._-]+\/callback)$/.test(path))
|
|
16
|
+
if (!/^\/(?:users\/@me|users\/[0-9]+|oauth2\/applications\/@me|channels\/[0-9]+(?:\/typing|\/messages\/[0-9]+\/reactions\/%F0%9F%91%8D\/@me|\/messages(?:\/[0-9]+)?(?:\?limit=100)?)?|interactions\/[0-9]+\/[A-Za-z0-9._-]+\/callback)$/.test(path))
|
|
15
17
|
throw new DiscordError("Invalid Discord request");
|
|
16
18
|
let response;
|
|
17
19
|
const headers = { Authorization: `Bot ${token}` };
|
|
@@ -56,7 +58,7 @@ export function discordApi(token, fetcher = fetch) {
|
|
|
56
58
|
const message = response.status === 401
|
|
57
59
|
? "Discord access expired. Disconnect and reconnect with the current bot token."
|
|
58
60
|
: response.status === 403
|
|
59
|
-
?
|
|
61
|
+
? DISCORD_REFUSED
|
|
60
62
|
: response.status === 404
|
|
61
63
|
? "The Discord message or conversation is unavailable. Open the saved chat."
|
|
62
64
|
: response.status === 413
|
package/dist/discord-chat.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ChatState, type ChatIdentity } from "./chat-delivery-state.js";
|
|
2
2
|
import { type ChatDeliveryOptions } from "./chat-delivery.js";
|
|
3
|
+
import { discordPlain } from "./reply-shape.js";
|
|
3
4
|
import { type DiscordApi } from "./discord-api.js";
|
|
4
5
|
export type DiscordChatOptions = Omit<ChatDeliveryOptions, "state" | "label" | "member" | "partSize" | "maxProposal"> & {
|
|
5
6
|
api: DiscordApi;
|
|
@@ -11,6 +12,6 @@ export declare const planDiscordNotifications: (options: DiscordChatOptions) =>
|
|
|
11
12
|
/** Save only normalized input. Interaction credentials and pairing codes never enter SQLite. */
|
|
12
13
|
export declare function receiveDiscord(state: ChatState, identity: ChatIdentity, type: string, raw: unknown, now: Date): boolean;
|
|
13
14
|
/** Escape Discord's presentation syntax; allowed_mentions also prevents actual pings. */
|
|
14
|
-
export
|
|
15
|
-
export declare function discordCard(text: string, buttons?: Record<string, unknown>[]): Record<string, unknown>;
|
|
15
|
+
export { discordPlain };
|
|
16
|
+
export declare function discordCard(text: string, buttons?: Record<string, unknown>[], voice?: boolean): Record<string, unknown>;
|
|
16
17
|
export declare function deliverDiscordPart(options: DiscordChatOptions): Promise<boolean>;
|
package/dist/discord-chat.js
CHANGED
|
@@ -2,20 +2,28 @@
|
|
|
2
2
|
import { chatQuestionButtons } from "./teammate-question.js";
|
|
3
3
|
import { chatAskButtons } from "./chat-ask.js";
|
|
4
4
|
import { chatFlowButtons } from "./chat-flow.js";
|
|
5
|
+
import { refuseResultShots, resultShotsPruned } from "./result-shots.js";
|
|
5
6
|
import { channelInbox } from "./chat-inbox.js";
|
|
6
7
|
import { roomCommand } from "./chat-rooms.js";
|
|
7
8
|
import { ChatState, ChatDeliveryError, chatHash, } from "./chat-delivery-state.js";
|
|
8
9
|
import { processChatEvent, planChatNotifications, channelAccess, chatObject as object, planRoomMessages, } from "./chat-delivery.js";
|
|
9
10
|
import { chatResultHref } from "./chat-controls.js";
|
|
10
11
|
import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
|
|
12
|
+
import { discordPlain, renderReply } from "./reply-shape.js";
|
|
13
|
+
import { WARM_EMOJI } from "./chat-warmth.js";
|
|
11
14
|
import { proposalPreview, proposalLink, proposalOutcomeText, armedCardText, armedYesLabel, } from "./chat-channel.js";
|
|
12
15
|
import { resultImageFileName, safeResultImageCaption, verifyResultImage, } from "./chat-evidence.js";
|
|
13
|
-
import { discordId, discordMember, DiscordError, } from "./discord-api.js";
|
|
16
|
+
import { discordId, discordMember, DiscordError, DISCORD_REFUSED, } from "./discord-api.js";
|
|
14
17
|
export const discordDelivery = (options) => ({
|
|
15
18
|
...options,
|
|
16
19
|
state: new ChatState(options.store, "discord"),
|
|
17
20
|
label: "Discord",
|
|
18
21
|
member: (member, channel) => discordMember(options.api, member, channel),
|
|
22
|
+
// A 👍 on the owner's message and "typing…" while the lead works; a missing permission is skipped silently.
|
|
23
|
+
warm: (event) => ({
|
|
24
|
+
react: () => options.api("PUT", `/channels/${event.channel}/messages/${event.ts}/reactions/${encodeURIComponent(WARM_EMOJI)}/@me`),
|
|
25
|
+
typing: () => options.api("POST", `/channels/${event.channel}/typing`),
|
|
26
|
+
}),
|
|
19
27
|
partSize: 1800,
|
|
20
28
|
maxProposal: 3400,
|
|
21
29
|
});
|
|
@@ -140,8 +148,16 @@ function link(origin, target) {
|
|
|
140
148
|
}
|
|
141
149
|
}
|
|
142
150
|
/** Escape Discord's presentation syntax; allowed_mentions also prevents actual pings. */
|
|
143
|
-
export
|
|
144
|
-
export function discordCard(text, buttons = []) {
|
|
151
|
+
export { discordPlain };
|
|
152
|
+
export function discordCard(text, buttons = [], voice = false) {
|
|
153
|
+
// The lead's own reply: one description in Discord's Markdown (bold anchors, labelled links), no title line promoted to a header.
|
|
154
|
+
if (voice)
|
|
155
|
+
return {
|
|
156
|
+
content: "",
|
|
157
|
+
embeds: [{ description: renderReply(text, "discord").slice(0, 4096), color: 0x297b70 }],
|
|
158
|
+
components: Array.from({ length: Math.min(5, Math.ceil(buttons.length / 5)) }, (_, row) => ({ type: 1, components: buttons.slice(row * 5, row * 5 + 5) })),
|
|
159
|
+
allowed_mentions: { parse: [], replied_user: false },
|
|
160
|
+
};
|
|
145
161
|
const lines = text.split("\n"), first = lines[0] ?? "Toolroll", title = discordPlain(first).length <= 256 &&
|
|
146
162
|
first.length <= 150 &&
|
|
147
163
|
lines.length > 1
|
|
@@ -190,6 +206,13 @@ export async function deliverDiscordPart(options) {
|
|
|
190
206
|
!repos.includes(store.lookupRef(content.task)?.repo ?? ""))
|
|
191
207
|
throw new DiscordError("Connected projects changed");
|
|
192
208
|
let text = content.text, buttons = [], file;
|
|
209
|
+
if (content.image && content.shot && resultShotsPruned(options.evidenceRoot, content.image.run)) {
|
|
210
|
+
state.prepare("UPDATE chat_part SET state='dropped',problem='Removed by retention' WHERE id=?").run(row.id);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
// A screenshot sent with a result replies to that result's message once it is placed.
|
|
214
|
+
const follows = content.shot?.follows == null ? undefined : state.prepare("SELECT message FROM chat_part WHERE id=? AND state='sent'").get(content.shot.follows)?.message;
|
|
215
|
+
const thread = discordId(follows) ? String(follows) : event.thread;
|
|
193
216
|
if (content.image) {
|
|
194
217
|
const image = verifyResultImage(store, options.evidenceRoot, repos, content.image);
|
|
195
218
|
if (!image.ok)
|
|
@@ -336,7 +359,7 @@ export async function deliverDiscordPart(options) {
|
|
|
336
359
|
throw new DiscordError("Screenshot evidence changed before upload");
|
|
337
360
|
file.bytes = fresh.bytes;
|
|
338
361
|
}
|
|
339
|
-
const args = discordCard(text, buttons);
|
|
362
|
+
const args = discordCard(text, buttons, content.voice === true && !content.proposal && !content.image);
|
|
340
363
|
if (file)
|
|
341
364
|
args.attachments = [
|
|
342
365
|
{ id: 0, filename: file.name, description: text.slice(0, 1024) },
|
|
@@ -344,9 +367,9 @@ export async function deliverDiscordPart(options) {
|
|
|
344
367
|
if (!target) {
|
|
345
368
|
args.nonce = nonce;
|
|
346
369
|
args.enforce_nonce = true;
|
|
347
|
-
if (discordId(
|
|
370
|
+
if (discordId(thread))
|
|
348
371
|
args.message_reference = {
|
|
349
|
-
message_id:
|
|
372
|
+
message_id: thread,
|
|
350
373
|
channel_id: destination,
|
|
351
374
|
fail_if_not_exists: false,
|
|
352
375
|
};
|
|
@@ -370,6 +393,12 @@ export async function deliverDiscordPart(options) {
|
|
|
370
393
|
return true;
|
|
371
394
|
}
|
|
372
395
|
catch (error) {
|
|
396
|
+
// No permission to attach files here: one plain line instead of the result's screenshots.
|
|
397
|
+
const content = JSON.parse(row.payload);
|
|
398
|
+
if (content.image && content.shot && error instanceof DiscordError && error.code === DISCORD_REFUSED) {
|
|
399
|
+
refuseResultShots(state, store, row, content, "Discord", options.clock?.() ?? new Date());
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
373
402
|
const problem = error instanceof ChatDeliveryError
|
|
374
403
|
? error
|
|
375
404
|
: new DiscordError("Discord delivery is waiting to retry", 15_000, true);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** What your lead knows about you (Settings → Lead): a short list of the owner's own lasting preferences, the same
|
|
2
|
+
* across projects ("keep copy terse", "I test myself"). Stored beside the lead's name and persona; only lines the
|
|
3
|
+
* owner confirmed (a remember about-you card) or wrote themselves are kept, never a guess. */
|
|
4
|
+
import type { Store } from "./store.js";
|
|
5
|
+
export declare const ABOUT_YOU_MAX_LINES = 20;
|
|
6
|
+
/** Each line is under 200 characters. */
|
|
7
|
+
export declare const ABOUT_YOU_LINE_MAX = 199;
|
|
8
|
+
/** This person's confirmed lines; an older store reads as none. */
|
|
9
|
+
export declare function aboutYouOf(store: Store, account: string | null | undefined): string[];
|
|
10
|
+
/** One line as it is saved, or the problem with it. */
|
|
11
|
+
export declare function checkAboutYouLine(value: unknown): {
|
|
12
|
+
ok: true;
|
|
13
|
+
line: string;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
/** The Settings form's text, one line each: the saved list, or the problem with it. Blank lines and repeats go. */
|
|
19
|
+
export declare function checkAboutYou(text: string): {
|
|
20
|
+
ok: true;
|
|
21
|
+
lines: string[];
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
/** Save the whole list for this person (already checked). Their lead's name and persona are left as they are. */
|
|
27
|
+
export declare function saveAboutYou(store: Store, account: string, lines: readonly string[], now: Date): void;
|
|
28
|
+
/** The list once a confirmed line is added, or once it replaces line `replaces` (1-based). */
|
|
29
|
+
export declare function withAboutYouLine(lines: readonly string[], line: string, replaces: number): {
|
|
30
|
+
ok: true;
|
|
31
|
+
lines: string[];
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { scanForSecrets } from "./evidence.js";
|
|
2
|
+
export const ABOUT_YOU_MAX_LINES = 20;
|
|
3
|
+
/** Each line is under 200 characters. */
|
|
4
|
+
export const ABOUT_YOU_LINE_MAX = 199;
|
|
5
|
+
/** This person's confirmed lines; an older store reads as none. */
|
|
6
|
+
export function aboutYouOf(store, account) {
|
|
7
|
+
if (account == null || account === "")
|
|
8
|
+
return [];
|
|
9
|
+
try {
|
|
10
|
+
return store.leadAbout(account);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** One line as it is saved, or the problem with it. */
|
|
17
|
+
export function checkAboutYouLine(value) {
|
|
18
|
+
const line = typeof value === "string" ? value.replace(/^\s*[-*•]\s*/, "").replace(/\s+/g, " ").trim() : "";
|
|
19
|
+
if (line.length < 2)
|
|
20
|
+
return { ok: false, message: "Say it in a few words." };
|
|
21
|
+
if (line.length > ABOUT_YOU_LINE_MAX)
|
|
22
|
+
return { ok: false, message: `Keep each line under ${ABOUT_YOU_LINE_MAX + 1} characters.` };
|
|
23
|
+
if (/[\u0000-\u001f\u007f---�]/.test(line) || scanForSecrets(line).length > 0)
|
|
24
|
+
return { ok: false, message: "Use plain words, with no passwords or keys." };
|
|
25
|
+
return { ok: true, line };
|
|
26
|
+
}
|
|
27
|
+
/** The Settings form's text, one line each: the saved list, or the problem with it. Blank lines and repeats go. */
|
|
28
|
+
export function checkAboutYou(text) {
|
|
29
|
+
const lines = [];
|
|
30
|
+
// A problem names the line as the person sees it in the box, blank lines and repeats counted.
|
|
31
|
+
for (const [index, raw] of text.replace(/\r\n?/g, "\n").split("\n").entries()) {
|
|
32
|
+
if (raw.trim() === "")
|
|
33
|
+
continue;
|
|
34
|
+
const checked = checkAboutYouLine(raw);
|
|
35
|
+
if (!checked.ok)
|
|
36
|
+
return { ok: false, message: `Line ${index + 1}: ${checked.message}` };
|
|
37
|
+
if (!lines.some(one => one.toLowerCase() === checked.line.toLowerCase()))
|
|
38
|
+
lines.push(checked.line);
|
|
39
|
+
}
|
|
40
|
+
if (lines.length > ABOUT_YOU_MAX_LINES)
|
|
41
|
+
return { ok: false, message: `Keep it to ${ABOUT_YOU_MAX_LINES} lines.` };
|
|
42
|
+
return { ok: true, lines };
|
|
43
|
+
}
|
|
44
|
+
/** Save the whole list for this person (already checked). Their lead's name and persona are left as they are. */
|
|
45
|
+
export function saveAboutYou(store, account, lines, now) {
|
|
46
|
+
store.setLeadAbout(account, lines, now);
|
|
47
|
+
}
|
|
48
|
+
/** The list once a confirmed line is added, or once it replaces line `replaces` (1-based). */
|
|
49
|
+
export function withAboutYouLine(lines, line, replaces) {
|
|
50
|
+
if (replaces > 0) {
|
|
51
|
+
if (replaces > lines.length)
|
|
52
|
+
return { ok: false, message: "That line is no longer in what your lead knows about you." };
|
|
53
|
+
const next = [...lines];
|
|
54
|
+
next[replaces - 1] = line;
|
|
55
|
+
return { ok: true, lines: next.filter((one, index) => index === replaces - 1 || one.toLowerCase() !== line.toLowerCase()) };
|
|
56
|
+
}
|
|
57
|
+
if (lines.some(one => one.toLowerCase() === line.toLowerCase()))
|
|
58
|
+
return { ok: false, message: "Your lead already knows this." };
|
|
59
|
+
if (lines.length >= ABOUT_YOU_MAX_LINES)
|
|
60
|
+
return { ok: false, message: `What your lead knows about you is full (${ABOUT_YOU_MAX_LINES} lines). Replace a line, or tidy it in Settings → Lead.` };
|
|
61
|
+
return { ok: true, lines: [...lines, line] };
|
|
62
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the lead can rely on right now, in one read (get_capabilities): the agents a project's route would run, the
|
|
3
|
+
* workers that would run them, the project's tools and skills, the integrations, and the project's checks. Every entry
|
|
4
|
+
* says what it lets the lead do and, when it can't, the one next step and the settings control (show_control) that
|
|
5
|
+
* takes the owner there. Everything here reads what is already recorded — runner readiness reports, sign-in pauses,
|
|
6
|
+
* plan-limit readings, tool tests, skill tests, integration checks — and never probes anything that spends.
|
|
7
|
+
*/
|
|
8
|
+
import type { Store } from "./store.js";
|
|
9
|
+
import type { ChatControl } from "./chat-controls.js";
|
|
10
|
+
import type { ProviderId } from "./provider.js";
|
|
11
|
+
import { type AuthMode } from "./keys.js";
|
|
12
|
+
import { type Integration } from "./integrations.js";
|
|
13
|
+
/** One thing the lead may depend on: its state in a few words, what it lets the lead do, and when it can't, the next step. */
|
|
14
|
+
export type Capability = {
|
|
15
|
+
name: string;
|
|
16
|
+
state: string;
|
|
17
|
+
ok: boolean;
|
|
18
|
+
lets: string;
|
|
19
|
+
next: string | null;
|
|
20
|
+
link: ChatControl | null;
|
|
21
|
+
};
|
|
22
|
+
export type ProjectCapabilities = {
|
|
23
|
+
repo: string;
|
|
24
|
+
agents: (Capability & {
|
|
25
|
+
provider: string;
|
|
26
|
+
model: string;
|
|
27
|
+
roles: string[];
|
|
28
|
+
})[];
|
|
29
|
+
agentsProblem: Capability | null;
|
|
30
|
+
tools: Capability[];
|
|
31
|
+
skills: Capability[];
|
|
32
|
+
checks: Capability & {
|
|
33
|
+
level: string;
|
|
34
|
+
quickCheck: boolean;
|
|
35
|
+
releaseCheck: boolean;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export type Capabilities = {
|
|
39
|
+
projects: ProjectCapabilities[];
|
|
40
|
+
workers: (Capability & {
|
|
41
|
+
online: boolean;
|
|
42
|
+
capacity: number;
|
|
43
|
+
running: string[];
|
|
44
|
+
})[];
|
|
45
|
+
integrations: (Capability & {
|
|
46
|
+
kind: string;
|
|
47
|
+
account: string | null;
|
|
48
|
+
usedBy: string[];
|
|
49
|
+
lastError: string | null;
|
|
50
|
+
})[];
|
|
51
|
+
};
|
|
52
|
+
/** The integrations exactly as get_integrations reads them, with what each lets the lead do and the next step. */
|
|
53
|
+
export declare function integrationCapabilities(list: readonly Integration[]): Capabilities["integrations"];
|
|
54
|
+
/** Every capability, for one project or every admitted one. Read-only and spends nothing. */
|
|
55
|
+
export declare function capabilitiesOf(store: Store, input: {
|
|
56
|
+
repos: readonly string[];
|
|
57
|
+
admitted: readonly string[];
|
|
58
|
+
actor: string;
|
|
59
|
+
integrations: readonly Integration[];
|
|
60
|
+
authMode?: (provider: ProviderId) => AuthMode;
|
|
61
|
+
now: Date;
|
|
62
|
+
}): Capabilities;
|
|
63
|
+
export declare function needsCapabilities(name: string, args: Record<string, unknown>, channel: string | undefined): boolean;
|
|
64
|
+
export declare const CAPABILITIES_UNREAD = "This depends on an agent, a worker or an integration. Read get_capabilities first, then propose or promise in a later step; if something it needs can't work, say so plainly with its next step and show_control link instead.";
|