@fastagent-sh/fastagent 0.19.0 → 0.21.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/README.md +5 -1
- package/dist/atomic-write.d.ts +11 -7
- package/dist/atomic-write.js +16 -11
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +12 -15
- package/dist/channels/agentcore-service.js +15 -24
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +9 -83
- package/dist/channels/agentcore.js +101 -93
- package/dist/channels/control.d.ts +50 -12
- package/dist/channels/control.js +251 -157
- package/dist/channels/discover.d.ts +26 -5
- package/dist/channels/discover.js +13 -26
- package/dist/channels/feishu/context-buffer.d.ts +6 -0
- package/dist/channels/feishu/context-buffer.js +0 -38
- package/dist/channels/feishu/crypto.d.ts +0 -2
- package/dist/channels/feishu/crypto.js +3 -8
- package/dist/channels/feishu/feishu-api.js +7 -12
- package/dist/channels/feishu/feishu.js +86 -128
- package/dist/channels/feishu/parse.d.ts +4 -3
- package/dist/channels/feishu/parse.js +5 -4
- package/dist/channels/feishu/register-webhook.d.ts +5 -5
- package/dist/channels/feishu/register-webhook.js +43 -57
- package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
- package/dist/channels/feishu/setup-mode.d.ts +30 -0
- package/dist/channels/feishu/setup-mode.js +26 -0
- package/dist/channels/feishu/shared-api.d.ts +10 -0
- package/dist/channels/feishu/shared-api.js +38 -0
- package/dist/channels/http.d.ts +0 -8
- package/dist/channels/http.js +4 -56
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/kit/context-buffer.d.ts +9 -0
- package/dist/channels/kit/context-buffer.js +11 -0
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/kit/stop-command.js +3 -3
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +10 -4
- package/dist/channels/kit/turn-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- package/dist/channels/kit/turn-store.d.ts +19 -3
- package/dist/channels/kit/turn-store.js +29 -2
- package/dist/channels/lark/scaffold/lark-send.ts +12 -67
- package/dist/channels/registration.d.ts +36 -1
- package/dist/channels/registration.js +57 -1
- package/dist/channels/secret.d.ts +1 -0
- package/dist/channels/secret.js +16 -0
- package/dist/channels/slack/config-api.d.ts +35 -3
- package/dist/channels/slack/config-api.js +51 -9
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +5 -1
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +44 -23
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/register-webhook.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +35 -15
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -141
- package/dist/channels/slack/shared-api.d.ts +10 -0
- package/dist/channels/slack/shared-api.js +34 -0
- package/dist/channels/slack/slack-api.d.ts +20 -2
- package/dist/channels/slack/slack-api.js +100 -73
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +68 -107
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +65 -11
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +2 -2
- package/dist/channels/telegram/telegram.js +97 -204
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +13 -25
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +22 -35
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/chat.js +6 -12
- package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
- package/dist/cli/commands/deploy/agentcore.js +178 -0
- package/dist/cli/commands/deploy/docker.d.ts +2 -0
- package/dist/cli/commands/deploy/docker.js +119 -0
- package/dist/cli/commands/deploy/fly.d.ts +2 -0
- package/dist/cli/commands/deploy/fly.js +131 -0
- package/dist/cli/commands/deploy/railway.d.ts +2 -0
- package/dist/cli/commands/deploy/railway.js +71 -0
- package/dist/cli/commands/deploy/shared.d.ts +114 -0
- package/dist/cli/commands/deploy/shared.js +124 -0
- package/dist/cli/commands/deploy.d.ts +38 -25
- package/dist/cli/commands/deploy.js +101 -631
- package/dist/cli/commands/dev.js +20 -76
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +1 -1
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +41 -80
- package/dist/cli/commands/tool.js +3 -8
- package/dist/cli/kernel.d.ts +0 -2
- package/dist/cli/kernel.js +0 -2
- package/dist/cli/program.js +7 -12
- package/dist/cli/serve.d.ts +42 -34
- package/dist/cli/serve.js +107 -28
- package/dist/cli/shared.d.ts +52 -27
- package/dist/cli/shared.js +69 -6
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +34 -11
- package/dist/deploy/agentcore/plan.js +52 -259
- package/dist/deploy/agentcore/run.d.ts +9 -24
- package/dist/deploy/agentcore/run.js +35 -43
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +35 -4
- package/dist/deploy/docker/run.js +40 -8
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +14 -22
- package/dist/deploy/fly/run.d.ts +36 -23
- package/dist/deploy/fly/run.js +129 -83
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +9 -5
- package/dist/deploy/railway/plan.js +18 -26
- package/dist/deploy/railway/run.d.ts +11 -10
- package/dist/deploy/railway/run.js +44 -43
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +63 -28
- package/dist/dev-supervisor.js +3 -4
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +111 -106
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +2 -2
- package/dist/engines/pi/create.d.ts +22 -27
- package/dist/engines/pi/create.js +59 -74
- package/dist/engines/pi/definition.d.ts +1 -1
- package/dist/engines/pi/definition.js +11 -12
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/login.js +32 -16
- package/dist/engines/pi/open.d.ts +3 -4
- package/dist/engines/pi/open.js +32 -42
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +1 -1
- package/dist/engines/pi/service.js +8 -0
- package/dist/engines/pi/session-builder.js +26 -140
- package/dist/engines/pi/session-control.d.ts +27 -22
- package/dist/engines/pi/session-control.js +551 -490
- package/dist/engines/pi/session-inheritance.d.ts +8 -22
- package/dist/engines/pi/session-inheritance.js +95 -76
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +5 -5
- package/dist/engines/pi/session-settings.js +8 -5
- package/dist/engines/pi/session-store.d.ts +91 -26
- package/dist/engines/pi/session-store.js +413 -82
- package/dist/engines/pi/tool-context.d.ts +24 -11
- package/dist/engines/pi/tool-context.js +29 -4
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +3 -2
- package/dist/env.js +1 -2
- package/dist/feishu.d.ts +1 -0
- package/dist/feishu.js +1 -0
- package/dist/lark.d.ts +1 -0
- package/dist/lark.js +1 -0
- package/dist/loader.d.ts +51 -7
- package/dist/loader.js +84 -18
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +26 -3
- package/dist/paths.js +43 -5
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +50 -67
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/wake-alarm.d.ts +14 -19
- package/dist/schedule/wake-alarm.js +89 -48
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +10 -7
- package/dist/service.d.ts +24 -24
- package/dist/service.js +33 -77
- package/dist/session-remote.d.ts +12 -7
- package/dist/session-remote.js +185 -118
- package/dist/session.d.ts +227 -93
- package/dist/session.js +61 -23
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +34 -9
- package/dist/tunnel.js +83 -50
- package/package.json +5 -4
- package/dist/channels/slack/bot-auth.d.ts +0 -15
- package/dist/channels/slack/bot-auth.js +0 -135
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Every other concern lives in its own module, each owning its invariants:
|
|
10
10
|
* - parse.ts pure message parsing: field extraction, prompt envelope, summon/route policy
|
|
11
11
|
* - invoke-turn.ts run one turn: assemble inputs (resolve attachments) + stream `agent.invoke`
|
|
12
|
-
* - turn-
|
|
12
|
+
* - kit/turn-runner.ts the durable-turn lifecycle (accept → dequeue → execute → end) over the queue + store
|
|
13
13
|
* - turn-store.ts durable turn intent (L1): pre-ACK persist, replay a crash-surviving turn
|
|
14
14
|
* - context-buffer.ts un-summoned group discussion, folded into the next answered turn
|
|
15
15
|
* - preview.ts the live-preview pump ("💭 Thinking…" → edits → final answer) + terminal writes
|
|
@@ -22,40 +22,25 @@
|
|
|
22
22
|
* Authored against the public `@fastagent-sh/fastagent` surface only (the contract + the channel-authoring
|
|
23
23
|
* kit: readBodyCapped / text), so it is exactly what a third-party `fastagent-channel-*` package would write.
|
|
24
24
|
*/
|
|
25
|
-
import { timingSafeEqual } from "node:crypto";
|
|
26
25
|
import { isAbsolute, join } from "node:path";
|
|
27
26
|
import { log } from "../../log.js";
|
|
28
27
|
import { readBodyCapped } from "../body.js";
|
|
29
28
|
import { text } from "../respond.js";
|
|
29
|
+
import { secretEquals } from "../secret.js";
|
|
30
30
|
import { invokeTurn } from "./invoke-turn.js";
|
|
31
31
|
import { collectAttachments, createContextBuffer } from "./context-buffer.js";
|
|
32
|
-
import { attachmentSummary, defaultTelegramRoute, extractFiles, extractImages, fromLabel, messageText, ownFiles, ownImages, pickMessage, telegramEnvelope, } from "./parse.js";
|
|
32
|
+
import { attachmentSummary, defaultTelegramRoute, extractFiles, extractImages, fromLabel, messageText, ownFiles, ownImages, pickMessage, telegramEnvelope, telegramStop, } from "./parse.js";
|
|
33
33
|
import { defaultErrorMessage, streamReply } from "./preview.js";
|
|
34
34
|
import { ensureStateHome } from "../kit/state.js";
|
|
35
35
|
import { dispatchStop } from "../kit/stop-command.js";
|
|
36
36
|
import { callApi, editMessageText, sendMessage } from "./telegram-api.js";
|
|
37
|
-
import {
|
|
37
|
+
import { createTurnRunner } from "../kit/turn-runner.js";
|
|
38
|
+
import { discussionBlock } from "../kit/context-buffer.js";
|
|
38
39
|
import { createTurnStore } from "./turn-store.js";
|
|
39
40
|
// Re-export the public surface authored elsewhere, so `@fastagent-sh/fastagent/telegram` keeps one entry point.
|
|
40
|
-
export { defaultTelegramRoute, telegramEnvelope };
|
|
41
|
-
/** Execution ceiling: a turn that has STARTED running this many times without finishing is dropped
|
|
42
|
-
* rather than run again (a poison turn must not loop forever under a restart policy). Counted per turn
|
|
43
|
-
* at dequeue, so a never-run turn queued behind a poison one keeps its full budget.
|
|
44
|
-
*
|
|
45
|
-
* Known limitation: `startAttempt` cannot tell a self-inflicted process crash from an external SIGTERM
|
|
46
|
-
* (no graceful drain), so a legitimately LONG turn interrupted by this many successive deploys is
|
|
47
|
-
* dropped ("please ask again") as if it were poison. Accepted: catching SIGTERM to spare it would
|
|
48
|
-
* reintroduce the drain the design refuses, and a turn outliving this many deploy cycles is an outlier
|
|
49
|
-
* — raise this constant if such turns are expected. */
|
|
50
|
-
const MAX_TURN_ATTEMPTS = 3;
|
|
41
|
+
export { defaultTelegramRoute, telegramEnvelope, telegramStop };
|
|
51
42
|
/** Update body cap — Telegram updates are small JSON; 1 MiB is generous and guards a public endpoint. */
|
|
52
43
|
const MAX_UPDATE_BYTES = 1 << 20;
|
|
53
|
-
/** Constant-time compare so the secret-token check leaks no timing signal. */
|
|
54
|
-
function tokenMatches(header, secret) {
|
|
55
|
-
const a = Buffer.from(header);
|
|
56
|
-
const b = Buffer.from(secret);
|
|
57
|
-
return a.length === b.length && timingSafeEqual(a, b);
|
|
58
|
-
}
|
|
59
44
|
/**
|
|
60
45
|
* Build a Telegram bot channel: policy options in, a {@link ChannelModule} out. The framework (or an
|
|
61
46
|
* embedder) mounts it with the context — `telegramChannel(opts)` in `channels/telegram.ts` is the whole
|
|
@@ -77,9 +62,10 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
77
62
|
throw new Error("telegramChannel requires a non-empty botToken (used to send the agent's reply)");
|
|
78
63
|
}
|
|
79
64
|
const formatError = onError ?? defaultErrorMessage;
|
|
80
|
-
// One getMe at startup: the bot's @username (for the default route's group @mention summon
|
|
81
|
-
// not supplied) and the group-privacy flag — privacy
|
|
82
|
-
// group messages that feed the context buffer, so
|
|
65
|
+
// One getMe at startup: the bot's @username (for the default route's group @mention summon and for
|
|
66
|
+
// recognising an addressed `/stop`, only when not supplied) and the group-privacy flag — privacy
|
|
67
|
+
// mode off is required to receive the un-summoned group messages that feed the context buffer, so
|
|
68
|
+
// warn if it is on.
|
|
83
69
|
let mentionName = botUsername;
|
|
84
70
|
void callApi(apiBaseUrl, botToken, "getMe", {}).then((me) => {
|
|
85
71
|
if (mentionName === undefined)
|
|
@@ -88,7 +74,11 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
88
74
|
log.warn("[telegram] privacy mode is on: the bot only sees @mentions / replies / commands, so group " +
|
|
89
75
|
"context (un-summoned messages) won't be captured. Disable it via @BotFather → /setprivacy.");
|
|
90
76
|
}
|
|
91
|
-
},
|
|
77
|
+
},
|
|
78
|
+
// Name every capability the missing username costs, so "the bot ignores /stop@name" is
|
|
79
|
+
// diagnosable from this ONE line — the alternative, a warn per undecidable message, repeats a
|
|
80
|
+
// single startup fact on every update.
|
|
81
|
+
(e) => log.warn(`[telegram] getMe failed; @mention summon, addressed /stop, and the privacy check are skipped: ${String(e)}`));
|
|
92
82
|
// A bot token is "<bot_id>:<secret>" — the bot's own id is knowable synchronously, so reply-to-bot
|
|
93
83
|
// targeting is precise from the first update (no getMe race; getMe only resolves the @username).
|
|
94
84
|
// Every real token parses; one that doesn't (a mock/test token) degrades visibly: reply summon stays
|
|
@@ -114,159 +104,64 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
114
104
|
// Durable turn intent (L1): persist an accepted turn pre-ACK, remove it when the turn ends; a crash
|
|
115
105
|
// leaves it for replay on the next start. See turn-store.ts for the at-least-once semantics.
|
|
116
106
|
const store = createTurnStore(join(stateHome, "turns.json"));
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
107
|
+
const targetOf = (r) => ({ chatId: r.chatId, threadId: r.threadId, replyTo: r.replyTo });
|
|
108
|
+
// Tell the asker when a turn is dropped at the execution ceiling: the chain's end needs a signal, not
|
|
109
|
+
// just an operator log line. Take over the ⏳ "Queued" notice in place if the turn had one (else send
|
|
110
|
+
// fresh) — leaving it pinned at "Queued" while sending a separate failure would double-post. Best-
|
|
111
|
+
// effort, like the queue notices.
|
|
112
|
+
const notifyDropped = (r) => {
|
|
113
|
+
const body = "⚠️ I couldn’t complete an earlier request — please ask again.";
|
|
114
|
+
const sent = r.previewId !== undefined
|
|
115
|
+
? editMessageText(apiBaseUrl, botToken, targetOf(r), r.previewId, body, { html: false })
|
|
116
|
+
: sendMessage(apiBaseUrl, botToken, targetOf(r), body, { html: false }).then(() => { });
|
|
117
|
+
void sent.catch((e) => log.warn(`[telegram] could not notify a dropped turn (session=${r.session}): ${String(e)}`));
|
|
120
118
|
};
|
|
121
|
-
|
|
122
|
-
// the notice message over (rec.previewId) instead of racing it and orphaning a late-arriving notice.
|
|
123
|
-
const notices = new Map();
|
|
124
|
-
const queue = createTurnQueue({
|
|
119
|
+
const runner = createTurnRunner({
|
|
125
120
|
label: "[telegram]",
|
|
121
|
+
store,
|
|
122
|
+
buffer,
|
|
123
|
+
toStored: ({ previewId: _live, ...intent }) => ({ ...intent, attempts: 0 }),
|
|
124
|
+
fromStored: ({ attempts: _a, ...intent }) => ({ ...intent, previewId: undefined }),
|
|
125
|
+
bufferKey: (rec) => rec.placeKey,
|
|
126
|
+
where: (rec) => `chat=${rec.chatId}${rec.threadId !== undefined ? ` thread=${rec.threadId}` : ""}`,
|
|
126
127
|
// Queue feedback: when this session already has a turn running/queued, a silent wait reads as "the
|
|
127
128
|
// bot ignored me" once the current turn runs long — tell the asker NOW (reply-quoted, so it is
|
|
128
129
|
// clear whose ask is queued). Best-effort and post-ACK: a failed notice is a log line, never a
|
|
129
|
-
// failed update. The turn's live preview then edits this same message in place.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
threadId: rec.threadId,
|
|
134
|
-
replyTo: rec.replyTo,
|
|
135
|
-
};
|
|
136
|
-
notices.set(rec.id, sendMessage(apiBaseUrl, botToken, target, "⏳ Queued — I’ll start once the current task finishes.", {
|
|
130
|
+
// failed update. The turn's live preview then edits this same message in place. The runner holds
|
|
131
|
+
// this same `rec` object, so mutating it here (awaited at dequeue) hands the turn its preview id.
|
|
132
|
+
onQueuedBehind: (rec) => ({
|
|
133
|
+
done: sendMessage(apiBaseUrl, botToken, targetOf(rec), "⏳ Queued — I’ll start once the current task finishes.", {
|
|
137
134
|
html: false,
|
|
138
|
-
}).then(
|
|
139
|
-
// The runner holds this same `rec` object, so mutating it here (gated by the notices await at
|
|
140
|
-
// dequeue below) is what hands the turn its preview message id.
|
|
141
|
-
(id) => {
|
|
135
|
+
}).then((id) => {
|
|
142
136
|
if (id !== undefined)
|
|
143
137
|
rec.previewId = id;
|
|
144
|
-
}, (e) => log.warn(`[telegram] queue notice failed (the turn still runs): ${String(e)}`))
|
|
145
|
-
},
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
await notices.get(rec.id);
|
|
155
|
-
notices.delete(rec.id);
|
|
156
|
-
// Count this execution against the durable record (poison-turn ceiling) before running it again.
|
|
157
|
-
const decision = store.startAttempt(rec.id, MAX_TURN_ATTEMPTS);
|
|
158
|
-
if (decision === "exceeded") {
|
|
159
|
-
// Started MAX_TURN_ATTEMPTS times without finishing — tell the asker (reusing its ⏳ notice if
|
|
160
|
-
// any), drop it.
|
|
161
|
-
notifyDropped(rec);
|
|
162
|
-
return;
|
|
138
|
+
}, (e) => log.warn(`[telegram] queue notice failed (the turn still runs): ${String(e)}`)),
|
|
139
|
+
}),
|
|
140
|
+
// Its ⏳ notice (if any) now falsely reads "Queued": delete it best-effort — the eventual replay
|
|
141
|
+
// sends a fresh preview, and leaving it would orphan a stale message above that.
|
|
142
|
+
onDeferred: (rec) => {
|
|
143
|
+
if (rec.previewId !== undefined) {
|
|
144
|
+
void callApi(apiBaseUrl, botToken, "deleteMessage", {
|
|
145
|
+
chat_id: rec.chatId,
|
|
146
|
+
message_id: rec.previewId,
|
|
147
|
+
}).catch(() => { });
|
|
163
148
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// (if it was queued behind another turn) now falsely reads "Queued", so delete it best-effort — the
|
|
170
|
-
// eventual replay sends a fresh preview, and leaving it would orphan a stale message above that.
|
|
171
|
-
if (rec.previewId !== undefined) {
|
|
172
|
-
void callApi(apiBaseUrl, botToken, "deleteMessage", {
|
|
173
|
-
chat_id: rec.chatId,
|
|
174
|
-
message_id: rec.previewId,
|
|
175
|
-
}).catch(() => { });
|
|
176
|
-
}
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const startedAt = Date.now();
|
|
180
|
-
const where = `chat=${rec.chatId}${rec.threadId !== undefined ? ` thread=${rec.threadId}` : ""}`;
|
|
181
|
-
log.info(`[telegram] turn start: turn=${rec.id} session=${rec.session} ${where}`);
|
|
182
|
-
// Fold the un-summoned discussion since the last answered turn into the prompt; it is cleared
|
|
183
|
-
// only when the turn COMPLETES (then it lives in the session).
|
|
184
|
-
const { text: recent, consumed } = buffer.peek(rec.placeKey);
|
|
185
|
-
const prompt = recent ? `[recent group discussion:\n${recent}\n]\n\n${rec.baseText}` : rec.baseText;
|
|
186
|
-
const buffered = collectAttachments(consumed, {
|
|
149
|
+
},
|
|
150
|
+
notifyDropped,
|
|
151
|
+
execute: (rec, discussion, onCompleted) => streamReply(invokeTurn(agent, rec.session, `${discussionBlock(discussion.text)}${rec.baseText}`, { api: apiBaseUrl, botToken, chatId: rec.chatId, filesDir: join(stateHome, "files") }, {
|
|
152
|
+
primary: { imageFileIds: rec.imageFileIds, fileIds: rec.fileIds },
|
|
153
|
+
buffered: collectAttachments(discussion.consumed, {
|
|
187
154
|
files: new Set(rec.fileIds),
|
|
188
155
|
images: new Set(rec.imageFileIds),
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
chatId: rec.chatId,
|
|
192
|
-
threadId: rec.threadId,
|
|
193
|
-
replyTo: rec.replyTo,
|
|
194
|
-
};
|
|
195
|
-
try {
|
|
196
|
-
await streamReply(invokeTurn(agent, rec.session, prompt, {
|
|
197
|
-
api: apiBaseUrl,
|
|
198
|
-
botToken,
|
|
199
|
-
chatId: rec.chatId,
|
|
200
|
-
filesDir: join(stateHome, "files"),
|
|
201
|
-
}, {
|
|
202
|
-
primary: {
|
|
203
|
-
imageFileIds: rec.imageFileIds,
|
|
204
|
-
fileIds: rec.fileIds,
|
|
205
|
-
},
|
|
206
|
-
buffered,
|
|
207
|
-
}, () => {
|
|
208
|
-
// On completed, ORDER the two durable clears so a crash between them can't replay a
|
|
209
|
-
// context-stripped turn: drop the intent FIRST (a crash after this won't replay it), THEN
|
|
210
|
-
// commit the context buffer (a crash before this just re-folds the same context into the
|
|
211
|
-
// next summon — harmless and additive). The reverse order would leave intent+no-context.
|
|
212
|
-
store.remove(rec.id);
|
|
213
|
-
buffer.commit(rec.placeKey, consumed);
|
|
214
|
-
}), apiBaseUrl, botToken, target, formatError, rec.previewId);
|
|
215
|
-
log.info(`[telegram] turn done: turn=${rec.id} session=${rec.session} (${Date.now() - startedAt}ms)`);
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
log.error(`[telegram] turn failed: turn=${rec.id} session=${rec.session} (${Date.now() - startedAt}ms): ${String(error)}`);
|
|
219
|
-
}
|
|
220
|
-
finally {
|
|
221
|
-
// Fallback removal for the caught-error paths (a `failed` event or a transport throw): those
|
|
222
|
-
// never reach the completed hook above, which is where a completed turn removes its intent in
|
|
223
|
-
// order. Idempotent — a second remove after the completed hook is a no-op. Only an INTERRUPTED run
|
|
224
|
-
// (this finally never runs — a crash or a SIGTERM deploy, no graceful drain) leaves the record for
|
|
225
|
-
// replay; a transport throw is dropped, not retried (safe retry needs an L2 delivery key).
|
|
226
|
-
store.remove(rec.id);
|
|
227
|
-
}
|
|
228
|
-
},
|
|
156
|
+
}),
|
|
157
|
+
}, onCompleted), apiBaseUrl, botToken, targetOf(rec), formatError, rec.previewId),
|
|
229
158
|
});
|
|
230
|
-
|
|
231
|
-
// crash-surviving turn WITHOUT re-persisting (it is already on disk with a bumped attempt count).
|
|
232
|
-
const submit = (rec, persist) => {
|
|
233
|
-
if (persist)
|
|
234
|
-
store.add(toStored(rec)); // pre-ACK: a failed write throws → webhook 500 → redeliver
|
|
235
|
-
queue.accept(rec);
|
|
236
|
-
};
|
|
237
|
-
// Tell the asker when a turn is dropped at the execution ceiling: the chain's end needs a signal, not
|
|
238
|
-
// just an operator log line. Take over the ⏳ "Queued" notice in place if the turn had one (else send
|
|
239
|
-
// fresh) — leaving it pinned at "Queued" while sending a separate failure would double-post. Best-
|
|
240
|
-
// effort, like the queue notices.
|
|
241
|
-
const notifyDropped = (r) => {
|
|
242
|
-
const body = "⚠️ I couldn’t complete an earlier request — please ask again.";
|
|
243
|
-
const target = {
|
|
244
|
-
chatId: r.chatId,
|
|
245
|
-
threadId: r.threadId,
|
|
246
|
-
replyTo: r.replyTo,
|
|
247
|
-
};
|
|
248
|
-
const sent = r.previewId !== undefined
|
|
249
|
-
? editMessageText(apiBaseUrl, botToken, target, r.previewId, body, {
|
|
250
|
-
html: false,
|
|
251
|
-
})
|
|
252
|
-
: sendMessage(apiBaseUrl, botToken, target, body, {
|
|
253
|
-
html: false,
|
|
254
|
-
}).then(() => { });
|
|
255
|
-
void sent.catch((e) => log.warn(`[telegram] could not notify a dropped turn (session=${r.session}): ${String(e)}`));
|
|
256
|
-
};
|
|
257
|
-
// Re-enqueue turns a prior crash left mid-flight (ACKed but unfinished). Synchronous at construction:
|
|
258
|
-
// the queue runs them on the next tick, once this factory returns and the event loop turns. The
|
|
259
|
-
// execution ceiling is enforced per turn at dequeue (run), not here — a never-run turn keeps its budget.
|
|
260
|
-
const recovered = store.recover();
|
|
261
|
-
if (recovered.length > 0)
|
|
262
|
-
log.info(`[telegram] recovering ${recovered.length} unfinished turn(s) from a prior run`);
|
|
263
|
-
for (const { attempts: _a, ...intent } of recovered)
|
|
264
|
-
submit({ ...intent, previewId: undefined }, false);
|
|
159
|
+
runner.recover();
|
|
265
160
|
const handler = async (req) => {
|
|
266
161
|
if (req.method !== "POST")
|
|
267
162
|
return text("POST only\n", 405);
|
|
268
163
|
// Fail closed: a missing/wrong secret token is 401, never routed.
|
|
269
|
-
if (!
|
|
164
|
+
if (!secretEquals(req.headers.get("x-telegram-bot-api-secret-token"), secretToken)) {
|
|
270
165
|
return text("invalid secret token\n", 401);
|
|
271
166
|
}
|
|
272
167
|
const body = await readBodyCapped(req, MAX_UPDATE_BYTES);
|
|
@@ -313,54 +208,52 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
313
208
|
}
|
|
314
209
|
return new Response(null, { status: 200 });
|
|
315
210
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}, true);
|
|
356
|
-
}
|
|
211
|
+
const session = r.session ?? placeKey;
|
|
212
|
+
const chatId = r.chatId ?? m.chat.id;
|
|
213
|
+
// Reply to the summoning message in groups (threads the answer under the asker); a 1:1 DM needs no
|
|
214
|
+
// reply-quote. Only when the RESOLVED target is the message's own chat+thread: a route that
|
|
215
|
+
// redirects elsewhere must not carry a reply_parameters that resolves in the wrong place (fail, or
|
|
216
|
+
// quote a same-id message there). Compare VALUES, not whether the route touched the field — a route
|
|
217
|
+
// that explicitly returns the same chat/thread still quotes.
|
|
218
|
+
const threadId = r.threadId ?? m.message_thread_id;
|
|
219
|
+
const sameTarget = String(chatId) === String(m.chat.id) && threadId === m.message_thread_id;
|
|
220
|
+
const replyTo = m.chat.type !== "private" && sameTarget ? m.message_id : undefined;
|
|
221
|
+
// Explicit user stop (`/stop`): a control action, never a turn — it must not queue behind the
|
|
222
|
+
// run it stops. Awaited before the ACK: dispatch + one sendMessage is fast, and a delivery failure
|
|
223
|
+
// logs instead of failing the webhook. Read AFTER the route, on the route's own session: the route
|
|
224
|
+
// is both the gate (a route that ignores this chat must not have the bot abort or answer in it) and
|
|
225
|
+
// the session authority (a route that remaps `session` would otherwise have its stop abort a
|
|
226
|
+
// session nobody runs). The default route summons an ADDRESSED `/stop` in a group for this reason:
|
|
227
|
+
// the general slash-command refusal would leave it buffered as discussion while the run kept going.
|
|
228
|
+
// An unaddressed one is not a command at all (see telegramStop) and never reaches here.
|
|
229
|
+
if (telegramStop(update, { botUsername: mentionName, botId })) {
|
|
230
|
+
const feedback = await dispatchStop(control, session, "[telegram]");
|
|
231
|
+
await sendMessage(apiBaseUrl, botToken, { chatId, threadId, replyTo }, feedback, { html: false }).catch((e) => log.warn(`[telegram] stop feedback failed: ${String(e)}`));
|
|
232
|
+
return new Response(null, { status: 200 });
|
|
233
|
+
}
|
|
234
|
+
const baseText = r.text ?? telegramEnvelope(m);
|
|
235
|
+
const imageFileIds = extractImages(m);
|
|
236
|
+
const fileIds = extractFiles(m);
|
|
237
|
+
if (baseText.trim() !== "" || imageFileIds.length > 0 || fileIds.length > 0) {
|
|
238
|
+
// Everything the turn needs, as a plain record; persisted pre-ACK then run serially per session.
|
|
239
|
+
runner.submit({
|
|
240
|
+
id: `${update.update_id}`,
|
|
241
|
+
session,
|
|
242
|
+
placeKey,
|
|
243
|
+
baseText,
|
|
244
|
+
chatId,
|
|
245
|
+
threadId,
|
|
246
|
+
replyTo,
|
|
247
|
+
imageFileIds,
|
|
248
|
+
fileIds,
|
|
249
|
+
}, true);
|
|
357
250
|
}
|
|
358
251
|
return new Response(null, { status: 200 });
|
|
359
252
|
};
|
|
360
253
|
// Test/observability seam: await the fire-and-forget turns this handler enqueues. Inert in production
|
|
361
254
|
// (nothing reads it; the runtime never drains — see turn-queue), it lets a test await a turn
|
|
362
255
|
// deterministically instead of polling for side effects to settle.
|
|
363
|
-
handler.turnsIdle = () =>
|
|
256
|
+
handler.turnsIdle = () => runner.idle();
|
|
364
257
|
return { "POST /telegram": handler };
|
|
365
258
|
};
|
|
366
259
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Readiness probe
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Readiness probe for a server THIS process can reach directly — the local port `deploy docker --run`
|
|
3
|
+
* just published, and the live probes' own origins.
|
|
4
|
+
*
|
|
5
|
+
* NOT a probe for a public URL a platform must reach: a freshly minted hostname (a quick tunnel, a
|
|
6
|
+
* fresh deploy) is routinely unreachable from here for a minute or more while the platform reaches it
|
|
7
|
+
* fine, and polling it hard from t+0 makes that worse (#421). The webhook registrars therefore let the
|
|
8
|
+
* platform's own URL verification be the probe, retried, instead of gating on this.
|
|
6
9
|
*/
|
|
7
10
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
8
11
|
/**
|
package/dist/cli/add-feishu.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { readFile } from "node:fs/promises";
|
|
13
13
|
import { isCancel, log as clackLog, password, text as clackText } from "@clack/prompts";
|
|
14
14
|
import { bootstrapFeishuVerificationToken } from "../channels/feishu/bootstrap-token.js";
|
|
15
|
-
import { FEISHU_GROUP_CONTEXT_SCOPE, FEISHU_CONTEXT_ONBOARDING_SCOPES, FEISHU_MESSAGE_READ_SCOPE, scopeSatisfied, } from "../channels/feishu/setup-mode.js";
|
|
15
|
+
import { FEISHU_GROUP_CONTEXT_SCOPE, FEISHU_CONTEXT_ONBOARDING_SCOPES, FEISHU_MESSAGE_READ_SCOPE, feishuAppAddons, scopeSatisfied, } from "../channels/feishu/setup-mode.js";
|
|
16
16
|
import { cloudFor } from "../channels/feishu/cloud.js";
|
|
17
17
|
import { createFeishuApi, isFeishuConfigApiMissing, isTransientFeishuRegistrationError, } from "../channels/feishu/feishu-api.js";
|
|
18
18
|
import { registerFeishuApp } from "../channels/feishu/register-app.js";
|
|
@@ -233,16 +233,9 @@ async function createFeishuAppFlow(target, existing, ingress, groupBehavior) {
|
|
|
233
233
|
const app = await registerFeishuApp({
|
|
234
234
|
name: "{user}'s agent", // the platform expands {user} to the confirming user's name; editable on the page
|
|
235
235
|
desc: "Served by fastagent",
|
|
236
|
-
// The agent template alone is not enough to SERVE
|
|
237
|
-
// context-aware group scope setup) demand application:application:patch, and the app must subscribe
|
|
238
|
-
// the receive event. Addons merge those BASE capabilities onto the confirm page; sensitive group
|
|
236
|
+
// The agent template alone is not enough to SERVE (see feishuAppAddons). Sensitive group
|
|
239
237
|
// permission approval and version publishing remain explicit console work.
|
|
240
|
-
addons:
|
|
241
|
-
...(ingress === "webhook" || (groupBehavior.behavior === "context" && groupBehavior.explicit)
|
|
242
|
-
? { scopes: { tenant: ["application:application:patch"] } }
|
|
243
|
-
: {}),
|
|
244
|
-
events: { items: { tenant: ["im.message.receive_v1"] } },
|
|
245
|
-
},
|
|
238
|
+
addons: feishuAppAddons(),
|
|
246
239
|
onVerificationUrl: ({ url, expiresInS }) => {
|
|
247
240
|
console.error(`\n Opening the confirmation link in your browser (or open it in Feishu / render it as a QR code) — valid for ${Math.round(expiresInS / 60)} minutes:\n\n ${url}\n\n waiting for confirmation… (keep this running — the credentials are delivered here)`);
|
|
248
241
|
openExternalUrl(url); // best-effort, like `login` — the URL above is the fallback
|
package/dist/cli/add-slack.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
|
|
4
|
-
import { waitForHealth } from "../channels/wait-health.js";
|
|
5
4
|
import { dotEnvPath, parseEnvContent } from "../env.js";
|
|
6
5
|
import { openExternalUrl } from "../open-url.js";
|
|
7
6
|
import { installProxyFetch } from "../proxy.js";
|
|
@@ -27,7 +26,7 @@ export async function onboardSlackInternalApp(input) {
|
|
|
27
26
|
throw new Error("`add slack` needs an interactive terminal for internal-app creation and OAuth — " +
|
|
28
27
|
"re-run in a terminal, or pass --no-onboard to scaffold only");
|
|
29
28
|
}
|
|
30
|
-
let state =
|
|
29
|
+
let state = readSlackOnboardingState(input.stateRoot);
|
|
31
30
|
const resumed = state !== undefined;
|
|
32
31
|
if (input.replaceConfig && !state) {
|
|
33
32
|
throw new Error("--replace-config found no local Slack onboarding state on this machine — nothing to replace. " +
|
|
@@ -41,14 +40,7 @@ export async function onboardSlackInternalApp(input) {
|
|
|
41
40
|
return new Map();
|
|
42
41
|
throw error;
|
|
43
42
|
});
|
|
44
|
-
const missingRuntime = [
|
|
45
|
-
"SLACK_BOT_TOKEN",
|
|
46
|
-
"SLACK_BOT_REFRESH_TOKEN",
|
|
47
|
-
"SLACK_BOT_TOKEN_EXPIRES_AT",
|
|
48
|
-
"SLACK_CLIENT_ID",
|
|
49
|
-
"SLACK_CLIENT_SECRET",
|
|
50
|
-
"SLACK_SIGNING_SECRET",
|
|
51
|
-
].filter((name) => !((process.env[name] ?? env.get(name))?.trim() ?? ""));
|
|
43
|
+
const missingRuntime = ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"].filter((name) => !((process.env[name] ?? env.get(name))?.trim() ?? ""));
|
|
52
44
|
if (missingRuntime.length > 0) {
|
|
53
45
|
throw new Error(`Slack app ${state.appId ?? "(unknown)"} is installed but ${dotEnvPath(input.target)} is missing ` +
|
|
54
46
|
`${missingRuntime.join(", ")} — ` +
|
|
@@ -84,7 +76,7 @@ export async function onboardSlackInternalApp(input) {
|
|
|
84
76
|
if (!configToken.startsWith("xoxe.") || !configRefreshToken.startsWith("xoxe-")) {
|
|
85
77
|
throw new Error("invalid Slack configuration token prefix (expected xoxe. access + xoxe- refresh)");
|
|
86
78
|
}
|
|
87
|
-
|
|
79
|
+
writeSlackOnboardingState(input.stateRoot, {
|
|
88
80
|
...state,
|
|
89
81
|
configToken,
|
|
90
82
|
configRefreshToken,
|
|
@@ -116,11 +108,11 @@ export async function onboardSlackInternalApp(input) {
|
|
|
116
108
|
configToken,
|
|
117
109
|
configRefreshToken,
|
|
118
110
|
});
|
|
119
|
-
|
|
111
|
+
writeSlackOnboardingState(input.stateRoot, state);
|
|
120
112
|
}
|
|
121
113
|
else if (input.groupBehavior.explicit && !state.appId) {
|
|
122
114
|
state = { ...state, groupBehavior: input.groupBehavior.behavior };
|
|
123
|
-
|
|
115
|
+
writeSlackOnboardingState(input.stateRoot, state);
|
|
124
116
|
}
|
|
125
117
|
if (state.createAttemptedAt && !state.appId) {
|
|
126
118
|
throw new Error(`a prior Slack app creation attempt at ${state.createAttemptedAt} returned no app ID — ` +
|
|
@@ -144,6 +136,7 @@ export async function onboardSlackInternalApp(input) {
|
|
|
144
136
|
action = answer;
|
|
145
137
|
}
|
|
146
138
|
if (action === "replace-config") {
|
|
139
|
+
console.error(`[fastagent] generate a fresh App Configuration Token pair at ${CONFIG_TOKEN_URL}`);
|
|
147
140
|
openExternalUrl(CONFIG_TOKEN_URL);
|
|
148
141
|
state = {
|
|
149
142
|
...state,
|
|
@@ -154,7 +147,7 @@ export async function onboardSlackInternalApp(input) {
|
|
|
154
147
|
if (!state.configToken.startsWith("xoxe.") || !state.configRefreshToken.startsWith("xoxe-")) {
|
|
155
148
|
throw new Error("invalid Slack configuration token prefix (expected xoxe. access + xoxe- refresh)");
|
|
156
149
|
}
|
|
157
|
-
|
|
150
|
+
writeSlackOnboardingState(input.stateRoot, state);
|
|
158
151
|
}
|
|
159
152
|
}
|
|
160
153
|
const server = await startSlackSetupServer();
|
|
@@ -167,20 +160,16 @@ export async function onboardSlackInternalApp(input) {
|
|
|
167
160
|
const redirectUrl = `${tunnel.url}${server.redirectPath}`;
|
|
168
161
|
console.error(`[fastagent] temporary Slack setup tunnel ready → ${tunnel.url}`);
|
|
169
162
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
// No local readiness probe: Slack challenges requestUrl from ITS network during app creation, and
|
|
164
|
+
// that is the reachability that matters (#421) — this machine often cannot reach a fresh tunnel
|
|
165
|
+
// hostname for a minute. onboardSlackApp retries the create while Slack cannot verify it yet.
|
|
173
166
|
await onboardSlackApp({ stateRoot: input.stateRoot, state, requestUrl, redirectUrl }, {
|
|
174
167
|
note: (message) => clackLog.info(message),
|
|
175
168
|
openUrl: openExternalUrl,
|
|
176
169
|
waitForOAuth: () => server.waitForOAuth(),
|
|
177
|
-
writeRuntimeSecrets: async ({ botToken,
|
|
170
|
+
writeRuntimeSecrets: async ({ botToken, signingSecret }) => {
|
|
178
171
|
const values = {
|
|
179
172
|
...(botToken ? { SLACK_BOT_TOKEN: botToken } : {}),
|
|
180
|
-
...(botRefreshToken ? { SLACK_BOT_REFRESH_TOKEN: botRefreshToken } : {}),
|
|
181
|
-
...(botTokenExpiresAt ? { SLACK_BOT_TOKEN_EXPIRES_AT: String(botTokenExpiresAt) } : {}),
|
|
182
|
-
...(clientId ? { SLACK_CLIENT_ID: clientId } : {}),
|
|
183
|
-
...(clientSecret ? { SLACK_CLIENT_SECRET: clientSecret } : {}),
|
|
184
173
|
...(signingSecret ? { SLACK_SIGNING_SECRET: signingSecret } : {}),
|
|
185
174
|
};
|
|
186
175
|
if (Object.keys(values).length > 0) {
|
|
@@ -188,9 +177,8 @@ export async function onboardSlackInternalApp(input) {
|
|
|
188
177
|
}
|
|
189
178
|
},
|
|
190
179
|
});
|
|
191
|
-
|
|
192
|
-
console.error(`[fastagent]
|
|
193
|
-
"replace the temporary Events API URL automatically");
|
|
180
|
+
// What happened, once. What to do next is `add`'s "next steps" block.
|
|
181
|
+
console.error(`[fastagent] Slack app installed; credentials written to ${dotEnvPath(input.target)}`);
|
|
194
182
|
}
|
|
195
183
|
finally {
|
|
196
184
|
tunnel.close();
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type ChannelKind } from "../../scaffold/add-channel.ts";
|
|
2
2
|
/** `fastagent add <kind> [dir]`: scaffold `channels/<kind>.ts` — the adapter import plus a starter `on()`. */
|
|
3
3
|
export declare function runAddChannel(channelKind: ChannelKind, dirArg: string, opts: {
|
|
4
|
-
createApp?: boolean;
|
|
5
4
|
ingress?: string;
|
|
6
5
|
groupBehavior?: string;
|
|
7
6
|
onboard?: boolean;
|