@fastagent-sh/fastagent 0.14.0 → 0.16.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 +16 -6
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/agentcore-limits.d.ts +7 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-state.d.ts +83 -0
- package/dist/channels/agentcore-state.js +258 -0
- package/dist/channels/agentcore.d.ts +98 -0
- package/dist/channels/agentcore.js +260 -0
- package/dist/channels/busy.d.ts +23 -0
- package/dist/channels/busy.js +53 -0
- package/dist/channels/context-buffer.d.ts +35 -0
- package/dist/channels/context-buffer.js +103 -0
- package/dist/channels/control.d.ts +28 -0
- package/dist/channels/control.js +214 -0
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +15 -13
- package/dist/channels/feishu/context-buffer.js +62 -69
- package/dist/channels/feishu/feishu-api.d.ts +11 -3
- package/dist/channels/feishu/feishu-api.js +24 -3
- package/dist/channels/feishu/feishu.d.ts +27 -27
- package/dist/channels/feishu/feishu.js +318 -207
- package/dist/channels/feishu/invoke-turn.d.ts +7 -20
- package/dist/channels/feishu/invoke-turn.js +44 -79
- package/dist/channels/feishu/model.d.ts +2 -2
- package/dist/channels/feishu/normalize.d.ts +2 -1
- package/dist/channels/feishu/normalize.js +0 -1
- package/dist/channels/feishu/parse.d.ts +22 -8
- package/dist/channels/feishu/parse.js +24 -7
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +27 -142
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/scaffold/channel.ts +9 -8
- package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
- package/dist/channels/feishu/setup-mode.d.ts +37 -0
- package/dist/channels/feishu/setup-mode.js +28 -0
- package/dist/channels/feishu/ws-ingress.d.ts +28 -0
- package/dist/channels/feishu/ws-ingress.js +136 -0
- package/dist/channels/github/github.js +16 -7
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +36 -3
- package/dist/channels/invoke-turn-kit.d.ts +65 -0
- package/dist/channels/invoke-turn-kit.js +87 -0
- package/dist/channels/lark/lark.d.ts +4 -2
- package/dist/channels/lark/lark.js +4 -1
- package/dist/channels/lark/onboard.d.ts +8 -4
- package/dist/channels/lark/onboard.js +8 -0
- package/dist/channels/lark/scaffold/channel.ts +9 -8
- package/dist/channels/lark/scaffold/lark-send.ts +6 -4
- package/dist/channels/preview-kit.d.ts +115 -0
- package/dist/channels/preview-kit.js +184 -0
- package/dist/channels/seen.d.ts +5 -0
- package/dist/channels/seen.js +35 -0
- package/dist/channels/slack/bot-auth.d.ts +15 -0
- package/dist/channels/slack/bot-auth.js +146 -0
- package/dist/channels/slack/config-api.d.ts +60 -0
- package/dist/channels/slack/config-api.js +149 -0
- package/dist/channels/slack/context-buffer.d.ts +24 -0
- package/dist/channels/slack/context-buffer.js +37 -0
- package/dist/channels/slack/invoke-turn.d.ts +19 -0
- package/dist/channels/slack/invoke-turn.js +63 -0
- package/dist/channels/slack/manifest.d.ts +49 -0
- package/dist/channels/slack/manifest.js +69 -0
- package/dist/channels/slack/model.d.ts +67 -0
- package/dist/channels/slack/model.js +2 -0
- package/dist/channels/slack/onboard.d.ts +41 -0
- package/dist/channels/slack/onboard.js +120 -0
- package/dist/channels/slack/onboarding-state.d.ts +31 -0
- package/dist/channels/slack/onboarding-state.js +69 -0
- package/dist/channels/slack/parse.d.ts +38 -0
- package/dist/channels/slack/parse.js +124 -0
- package/dist/channels/slack/preview.d.ts +23 -0
- package/dist/channels/slack/preview.js +403 -0
- package/dist/channels/slack/reaction.d.ts +24 -0
- package/dist/channels/slack/reaction.js +62 -0
- package/dist/channels/slack/register-webhook.d.ts +10 -0
- package/dist/channels/slack/register-webhook.js +49 -0
- package/dist/channels/slack/scaffold/channel.ts +33 -0
- package/dist/channels/slack/scaffold/slack-send.ts +171 -0
- package/dist/channels/slack/setup-server.d.ts +17 -0
- package/dist/channels/slack/setup-server.js +103 -0
- package/dist/channels/slack/slack-api.d.ts +57 -0
- package/dist/channels/slack/slack-api.js +399 -0
- package/dist/channels/slack/slack.d.ts +51 -0
- package/dist/channels/slack/slack.js +496 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.d.ts +11 -4
- package/dist/channels/state.js +22 -15
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +7 -0
- package/dist/channels/tasks.js +25 -0
- package/dist/channels/telegram/context-buffer.d.ts +8 -17
- package/dist/channels/telegram/context-buffer.js +6 -85
- package/dist/channels/telegram/invoke-turn.d.ts +5 -22
- package/dist/channels/telegram/invoke-turn.js +11 -58
- package/dist/channels/telegram/parse.d.ts +0 -7
- package/dist/channels/telegram/parse.js +4 -2
- package/dist/channels/telegram/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
- package/dist/channels/telegram/telegram.js +24 -10
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
- package/dist/channels/{feishu/text.js → text.js} +15 -1
- package/dist/channels/thread-participants.d.ts +21 -0
- package/dist/channels/thread-participants.js +132 -0
- package/dist/channels/turn-queue.js +8 -1
- package/dist/channels/turn-store.d.ts +1 -1
- package/dist/channels/turn-store.js +2 -3
- package/dist/cli/add-feishu.d.ts +30 -0
- package/dist/cli/add-feishu.js +366 -0
- package/dist/cli/add-slack.d.ts +11 -0
- package/dist/cli/add-slack.js +199 -0
- package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
- package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
- package/dist/cli/commands/add.d.ts +4 -0
- package/dist/cli/commands/add.js +191 -60
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +563 -0
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +12 -7
- package/dist/cli/commands/deploy.d.ts +3 -1
- package/dist/cli/commands/deploy.js +384 -82
- package/dist/cli/commands/dev.js +34 -26
- package/dist/cli/commands/fire.js +17 -18
- package/dist/cli/commands/info.js +36 -29
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +65 -53
- package/dist/cli/commands/invoke.js +11 -9
- package/dist/cli/commands/login.js +48 -45
- package/dist/cli/commands/models.js +1 -1
- package/dist/cli/commands/schedule.js +6 -8
- package/dist/cli/commands/start.js +102 -39
- package/dist/cli/commands/tool.js +23 -18
- package/dist/cli/fail.d.ts +17 -2
- package/dist/cli/fail.js +25 -1
- package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
- package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
- package/dist/cli/kernel.d.ts +3 -2
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.js +135 -56
- package/dist/cli/serve.d.ts +60 -20
- package/dist/cli/serve.js +252 -47
- package/dist/cli/shared.d.ts +40 -7
- package/dist/cli/shared.js +180 -38
- package/dist/collect.d.ts +14 -3
- package/dist/collect.js +24 -0
- package/dist/core.d.ts +3 -1
- package/dist/core.js +2 -0
- package/dist/deploy/agentcore/plan.d.ts +117 -0
- package/dist/deploy/agentcore/plan.js +721 -0
- package/dist/deploy/agentcore/run.d.ts +73 -0
- package/dist/deploy/agentcore/run.js +412 -0
- package/dist/deploy/agentcore/zip.d.ts +17 -0
- package/dist/deploy/agentcore/zip.js +68 -0
- package/dist/deploy/container.d.ts +26 -25
- package/dist/deploy/container.js +93 -89
- package/dist/deploy/docker/plan.d.ts +3 -3
- package/dist/deploy/docker/plan.js +20 -23
- package/dist/deploy/fly/plan.d.ts +5 -1
- package/dist/deploy/fly/plan.js +39 -26
- package/dist/deploy/fly/run.d.ts +17 -5
- package/dist/deploy/fly/run.js +52 -7
- package/dist/deploy/preflight.d.ts +20 -8
- package/dist/deploy/preflight.js +267 -71
- package/dist/deploy/railway/plan.d.ts +10 -1
- package/dist/deploy/railway/plan.js +53 -20
- package/dist/deploy/railway/run.d.ts +13 -5
- package/dist/deploy/railway/run.js +22 -7
- package/dist/deploy/runner.d.ts +7 -3
- package/dist/deploy/runner.js +9 -3
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +11 -10
- package/dist/dev-supervisor.js +56 -50
- package/dist/engines/pi/auth.d.ts +8 -7
- package/dist/engines/pi/auth.js +12 -10
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +92 -62
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -261
- package/dist/engines/pi/config.d.ts +20 -55
- package/dist/engines/pi/config.js +49 -121
- package/dist/engines/pi/create.d.ts +30 -10
- package/dist/engines/pi/create.js +33 -26
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- package/dist/engines/pi/harness.d.ts +49 -0
- package/dist/engines/pi/harness.js +90 -2
- package/dist/engines/pi/invoke.d.ts +55 -2
- package/dist/engines/pi/invoke.js +304 -19
- package/dist/engines/pi/login.d.ts +12 -1
- package/dist/engines/pi/login.js +16 -4
- package/dist/engines/pi/models.d.ts +59 -13
- package/dist/engines/pi/models.js +62 -24
- package/dist/engines/pi/open.d.ts +114 -0
- package/dist/engines/pi/open.js +127 -0
- package/dist/engines/pi/session-builder.d.ts +16 -0
- package/dist/engines/pi/session-builder.js +308 -0
- package/dist/engines/pi/session-control.d.ts +50 -0
- package/dist/engines/pi/session-control.js +604 -0
- package/dist/engines/pi/sessions.d.ts +17 -2
- package/dist/engines/pi/sessions.js +9 -0
- package/dist/engines/pi/tool-context.d.ts +18 -11
- package/dist/engines/pi/tool-context.js +3 -9
- package/dist/engines/pi/tool.d.ts +6 -8
- package/dist/engines/pi/tool.js +10 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +25 -8
- package/dist/host/node.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/loader.d.ts +2 -2
- package/dist/loader.js +3 -3
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/observe.js +3 -0
- package/dist/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +6 -5
- package/dist/pi.js +4 -3
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +2 -2
- package/dist/scaffold/add-channel.d.ts +26 -9
- package/dist/scaffold/add-channel.js +175 -40
- package/dist/scaffold/init.d.ts +32 -41
- package/dist/scaffold/init.js +161 -185
- package/dist/scaffold/templates/env.example +15 -6
- package/dist/scaffold/templates/fastagent.config.mjs +5 -4
- package/dist/scaffold/templates/gitignore +14 -6
- package/dist/scaffold/templates/persona.md +4 -2
- package/dist/scaffold/templates/secrets.gitignore +5 -0
- package/dist/scaffold/templates.d.ts +1 -7
- package/dist/scaffold/templates.js +3 -25
- package/dist/scaffold/vendor-skill.d.ts +2 -2
- package/dist/scaffold/vendor-skill.js +13 -13
- package/dist/schedule/discover.js +4 -4
- package/dist/schedule/scheduler.d.ts +40 -1
- package/dist/schedule/scheduler.js +89 -56
- package/dist/schedule/state.js +1 -1
- package/dist/schedule/wake-alarm.d.ts +47 -0
- package/dist/schedule/wake-alarm.js +136 -0
- package/dist/schedule/wakeups.d.ts +1 -3
- package/dist/schedule/wakeups.js +19 -1
- package/dist/session-remote.d.ts +53 -0
- package/dist/session-remote.js +336 -0
- package/dist/session.d.ts +265 -0
- package/dist/session.js +37 -0
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +2 -0
- package/dist/tunnel.d.ts +7 -4
- package/dist/tunnel.js +23 -12
- package/package.json +22 -7
- package/dist/channels/feishu/owned-threads.d.ts +0 -11
- package/dist/channels/feishu/owned-threads.js +0 -47
- package/dist/channels/feishu/seen.d.ts +0 -5
- package/dist/channels/feishu/seen.js +0 -47
- package/dist/cli-add-feishu.d.ts +0 -8
- package/dist/cli-add-feishu.js +0 -223
- package/dist/cli-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
- package/dist/engines/pi/workspace.d.ts +0 -56
- package/dist/engines/pi/workspace.js +0 -72
- package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
- package/dist/scaffold/templates/gitignore.kit +0 -2
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.js +0 -45
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createSlackApp, exchangeSlackOAuthCode, SlackConfigApiError, updateSlackAppManifest } from "./config-api.js";
|
|
3
|
+
import { buildSlackManifest, slackBotScopes } from "./manifest.js";
|
|
4
|
+
import { currentSlackConfigToken, writeSlackOnboardingState } from "./onboarding-state.js";
|
|
5
|
+
/** Create/resume one internal Slack app and complete its workspace OAuth installation. */
|
|
6
|
+
export async function onboardSlackApp(input, io, deps = {}) {
|
|
7
|
+
let state = input.state;
|
|
8
|
+
const current = await currentSlackConfigToken(input.stateRoot, state);
|
|
9
|
+
state = current.state;
|
|
10
|
+
const manifest = buildSlackManifest({
|
|
11
|
+
name: state.appName,
|
|
12
|
+
groupBehavior: state.groupBehavior,
|
|
13
|
+
requestUrl: input.requestUrl,
|
|
14
|
+
redirectUrl: input.redirectUrl,
|
|
15
|
+
});
|
|
16
|
+
if (!state.appId) {
|
|
17
|
+
if (state.createAttemptedAt) {
|
|
18
|
+
throw new Error(`a Slack app creation request started at ${state.createAttemptedAt}, but no app ID was returned. ` +
|
|
19
|
+
"Inspect https://api.slack.com/apps before retrying; if an app exists, delete that incomplete app first, then remove the local Slack onboarding state and re-run");
|
|
20
|
+
}
|
|
21
|
+
io.note("Creating the internal Slack app from its FastAgent manifest…");
|
|
22
|
+
// Record BEFORE the non-idempotent API call. A transport/internal failure may have created the app;
|
|
23
|
+
// refusing a blind retry is safer than silently producing duplicates.
|
|
24
|
+
state = { ...state, createAttemptedAt: new Date().toISOString() };
|
|
25
|
+
await writeSlackOnboardingState(input.stateRoot, state);
|
|
26
|
+
let created;
|
|
27
|
+
try {
|
|
28
|
+
created = await (deps.createApp ?? createSlackApp)(current.token, manifest);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const ambiguous = !(error instanceof SlackConfigApiError) ||
|
|
32
|
+
["fatal_error", "internal_error", "request_timeout", "service_unavailable", "failed_creating_app"].includes(error.code);
|
|
33
|
+
if (!ambiguous) {
|
|
34
|
+
state = { ...state, createAttemptedAt: undefined };
|
|
35
|
+
await writeSlackOnboardingState(input.stateRoot, state);
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
state = {
|
|
40
|
+
...state,
|
|
41
|
+
appId: created.appId,
|
|
42
|
+
createAttemptedAt: undefined,
|
|
43
|
+
clientId: created.clientId,
|
|
44
|
+
clientSecret: created.clientSecret,
|
|
45
|
+
signingSecret: created.signingSecret,
|
|
46
|
+
};
|
|
47
|
+
// Irreversible boundary first: a cancellation or .env write failure can resume without creating a duplicate.
|
|
48
|
+
await writeSlackOnboardingState(input.stateRoot, state);
|
|
49
|
+
io.note(`Created Slack app ${created.appId}; credentials captured locally.`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
if (!state.clientId || !state.clientSecret) {
|
|
53
|
+
throw new Error(`Slack app ${state.appId} exists but OAuth client credentials are no longer available — ` +
|
|
54
|
+
"the app appears already installed; set SLACK_BOT_TOKEN/SLACK_SIGNING_SECRET in .env or remove the app and onboarding state to start over");
|
|
55
|
+
}
|
|
56
|
+
io.note(`Resuming Slack app ${state.appId}; refreshing its temporary setup URLs…`);
|
|
57
|
+
await (deps.updateManifest ?? updateSlackAppManifest)(current.token, state.appId, manifest);
|
|
58
|
+
}
|
|
59
|
+
if (state.signingSecret) {
|
|
60
|
+
await io.writeRuntimeSecrets({ signingSecret: state.signingSecret });
|
|
61
|
+
state = { ...state, signingSecret: undefined };
|
|
62
|
+
await writeSlackOnboardingState(input.stateRoot, state);
|
|
63
|
+
}
|
|
64
|
+
if (!state.appId || !state.clientId || !state.clientSecret) {
|
|
65
|
+
throw new Error("Slack onboarding state lost app OAuth credentials before installation");
|
|
66
|
+
}
|
|
67
|
+
const oauthState = randomBytes(24).toString("hex");
|
|
68
|
+
const authorize = new URL("https://slack.com/oauth/v2/authorize");
|
|
69
|
+
authorize.searchParams.set("client_id", state.clientId);
|
|
70
|
+
authorize.searchParams.set("scope", slackBotScopes(state.groupBehavior).join(","));
|
|
71
|
+
authorize.searchParams.set("redirect_uri", input.redirectUrl);
|
|
72
|
+
authorize.searchParams.set("state", oauthState);
|
|
73
|
+
io.note(`Approve the internal app installation in Slack: ${authorize}`);
|
|
74
|
+
io.openUrl(authorize.toString());
|
|
75
|
+
const callback = await io.waitForOAuth();
|
|
76
|
+
if (callback.error)
|
|
77
|
+
throw new Error("Slack OAuth installation was not approved");
|
|
78
|
+
if (!callback.code)
|
|
79
|
+
throw new Error("Slack OAuth callback carried no authorization code");
|
|
80
|
+
if (!callback.state || callback.state !== oauthState)
|
|
81
|
+
throw new Error("Slack OAuth callback state mismatch");
|
|
82
|
+
const oauth = await (deps.exchangeCode ?? exchangeSlackOAuthCode)({
|
|
83
|
+
clientId: state.clientId,
|
|
84
|
+
clientSecret: state.clientSecret,
|
|
85
|
+
code: callback.code,
|
|
86
|
+
redirectUrl: input.redirectUrl,
|
|
87
|
+
});
|
|
88
|
+
if (oauth.appId !== state.appId)
|
|
89
|
+
throw new Error("Slack OAuth installed a different app than the manifest app");
|
|
90
|
+
if (slackBotScopes(state.groupBehavior).some((scope) => !oauth.scopes.includes(scope))) {
|
|
91
|
+
throw new Error("Slack OAuth completed without all required bot scopes; re-run fastagent add slack to reinstall");
|
|
92
|
+
}
|
|
93
|
+
await io.writeRuntimeSecrets({
|
|
94
|
+
botToken: oauth.botToken,
|
|
95
|
+
botRefreshToken: oauth.botRefreshToken,
|
|
96
|
+
botTokenExpiresAt: oauth.botTokenExpiresAt,
|
|
97
|
+
clientId: state.clientId,
|
|
98
|
+
clientSecret: state.clientSecret,
|
|
99
|
+
});
|
|
100
|
+
state = {
|
|
101
|
+
...state,
|
|
102
|
+
clientSecret: undefined,
|
|
103
|
+
teamId: oauth.teamId,
|
|
104
|
+
teamName: oauth.teamName,
|
|
105
|
+
installedAt: new Date().toISOString(),
|
|
106
|
+
};
|
|
107
|
+
await writeSlackOnboardingState(input.stateRoot, state);
|
|
108
|
+
return state;
|
|
109
|
+
}
|
|
110
|
+
export function newSlackOnboardingState(input) {
|
|
111
|
+
return {
|
|
112
|
+
version: 1,
|
|
113
|
+
appName: input.appName,
|
|
114
|
+
groupBehavior: input.groupBehavior,
|
|
115
|
+
configToken: input.configToken,
|
|
116
|
+
configRefreshToken: input.configRefreshToken,
|
|
117
|
+
// Slack config access tokens expire in 12 hours; use 11h so registration rotates before the edge.
|
|
118
|
+
configTokenExpiresAt: (input.now ?? Date.now()) + 11 * 60 * 60_000,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SlackGroupBehavior } from "./manifest.ts";
|
|
2
|
+
export interface SlackOnboardingState {
|
|
3
|
+
version: 1;
|
|
4
|
+
appName: string;
|
|
5
|
+
groupBehavior: SlackGroupBehavior;
|
|
6
|
+
appId?: string;
|
|
7
|
+
/** Set before apps.manifest.create; without an appId it blocks blind duplicate-creation retries. */
|
|
8
|
+
createAttemptedAt?: string;
|
|
9
|
+
clientId?: string;
|
|
10
|
+
/** Kept only while OAuth installation is unfinished, then deleted. */
|
|
11
|
+
clientSecret?: string;
|
|
12
|
+
/** Kept only until it has been staged into .env, then deleted. */
|
|
13
|
+
signingSecret?: string;
|
|
14
|
+
configToken: string;
|
|
15
|
+
configRefreshToken: string;
|
|
16
|
+
configTokenExpiresAt: number;
|
|
17
|
+
teamId?: string;
|
|
18
|
+
teamName?: string;
|
|
19
|
+
installedAt?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function readSlackOnboardingState(stateRoot: string): Promise<SlackOnboardingState | undefined>;
|
|
22
|
+
/** Atomic replacement with owner-only permissions: this file carries a workspace-wide config refresh token. */
|
|
23
|
+
export declare function writeSlackOnboardingState(stateRoot: string, state: SlackOnboardingState): Promise<void>;
|
|
24
|
+
export declare function currentSlackConfigToken(stateRoot: string, state: SlackOnboardingState, options?: {
|
|
25
|
+
now?: number;
|
|
26
|
+
apiBaseUrl?: string;
|
|
27
|
+
fetch?: typeof fetch;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
token: string;
|
|
30
|
+
state: SlackOnboardingState;
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { rotateSlackConfigToken } from "./config-api.js";
|
|
4
|
+
function slackOnboardingStatePath(stateRoot) {
|
|
5
|
+
return join(stateRoot, "channels", "slack", "onboarding.json");
|
|
6
|
+
}
|
|
7
|
+
function validState(value) {
|
|
8
|
+
const state = value;
|
|
9
|
+
return (typeof state === "object" &&
|
|
10
|
+
state !== null &&
|
|
11
|
+
state.version === 1 &&
|
|
12
|
+
typeof state.appName === "string" &&
|
|
13
|
+
(state.groupBehavior === "context" || state.groupBehavior === "mentions") &&
|
|
14
|
+
typeof state.configToken === "string" &&
|
|
15
|
+
typeof state.configRefreshToken === "string" &&
|
|
16
|
+
typeof state.configTokenExpiresAt === "number");
|
|
17
|
+
}
|
|
18
|
+
export async function readSlackOnboardingState(stateRoot) {
|
|
19
|
+
const file = slackOnboardingStatePath(stateRoot);
|
|
20
|
+
let raw;
|
|
21
|
+
try {
|
|
22
|
+
raw = await readFile(file, "utf8");
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code === "ENOENT")
|
|
26
|
+
return undefined;
|
|
27
|
+
throw new Error(`cannot read Slack onboarding state ${file}: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const value = JSON.parse(raw);
|
|
31
|
+
if (!validState(value))
|
|
32
|
+
throw new Error("unexpected shape/version");
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`invalid Slack onboarding state ${file}: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Atomic replacement with owner-only permissions: this file carries a workspace-wide config refresh token. */
|
|
40
|
+
export async function writeSlackOnboardingState(stateRoot, state) {
|
|
41
|
+
const file = slackOnboardingStatePath(stateRoot);
|
|
42
|
+
await mkdir(dirname(file), { recursive: true });
|
|
43
|
+
const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
44
|
+
try {
|
|
45
|
+
await writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
46
|
+
await chmod(temp, 0o600);
|
|
47
|
+
await rename(temp, file);
|
|
48
|
+
await chmod(file, 0o600);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
await rm(temp, { force: true }).catch(() => { });
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function currentSlackConfigToken(stateRoot, state, options = {}) {
|
|
56
|
+
const now = options.now ?? Date.now();
|
|
57
|
+
if (state.configTokenExpiresAt > now + 5 * 60_000)
|
|
58
|
+
return { token: state.configToken, state };
|
|
59
|
+
const rotated = await rotateSlackConfigToken(state.configRefreshToken, options);
|
|
60
|
+
const next = {
|
|
61
|
+
...state,
|
|
62
|
+
configToken: rotated.token,
|
|
63
|
+
configRefreshToken: rotated.refreshToken,
|
|
64
|
+
configTokenExpiresAt: rotated.expiresAt,
|
|
65
|
+
teamId: state.teamId ?? rotated.teamId,
|
|
66
|
+
};
|
|
67
|
+
await writeSlackOnboardingState(stateRoot, next);
|
|
68
|
+
return { token: next.configToken, state: next };
|
|
69
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { SlackEventEnvelope, SlackMessageEvent, SlackRoute } from "./model.ts";
|
|
2
|
+
export type { SlackEventEnvelope, SlackFile, SlackMessageEvent, SlackRoute } from "./model.ts";
|
|
3
|
+
/** Does this text address ANYONE — a user, a broadcast, or a user group? The §3 discussion guard. */
|
|
4
|
+
export declare function hasSlackMention(text: string): boolean;
|
|
5
|
+
/** Does this text mention a USER? Distinct from the above because only a user mention can be the bot:
|
|
6
|
+
* a broadcast never is, so it must not make a message look like a possible summon. */
|
|
7
|
+
export declare function hasSlackUserMention(text: string): boolean;
|
|
8
|
+
/** Does this text mention this specific user (either form)? The id is MATCHED, not interpreted. */
|
|
9
|
+
export declare function mentionsSlackUser(text: string, userId: string): boolean;
|
|
10
|
+
/** Strip every mention, e.g. before matching a bare command word or building a title. */
|
|
11
|
+
export declare function stripSlackMentions(text: string, replacement?: string): string;
|
|
12
|
+
/** Slack message events whose content represents a new human message rather than a mutation/service event. */
|
|
13
|
+
export declare function isSlackHumanMessage(event: SlackMessageEvent | undefined): event is SlackMessageEvent & {
|
|
14
|
+
channel: string;
|
|
15
|
+
ts: string;
|
|
16
|
+
user: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function isSlackDirectMessage(event: SlackMessageEvent): boolean;
|
|
19
|
+
export declare function isSlackGroupMessage(event: SlackMessageEvent): boolean;
|
|
20
|
+
/** Stable install identity. Events API normally supplies team_id; the fallbacks cover Grid envelopes. */
|
|
21
|
+
export declare function slackTeamId(envelope: SlackEventEnvelope): string | undefined;
|
|
22
|
+
export declare function slackFileIds(event: SlackMessageEvent): string[];
|
|
23
|
+
/** Text plus a structural marker for file-only or captioned file shares. */
|
|
24
|
+
export declare function slackMessageText(event: SlackMessageEvent): string;
|
|
25
|
+
export declare function slackSenderLabel(event: SlackMessageEvent): string;
|
|
26
|
+
/** Main-channel discussion and each concrete thread are independent context buckets.
|
|
27
|
+
*
|
|
28
|
+
* The `:root:` segment carries `thread_ts`, which in Slack IS the thread's parent message and is
|
|
29
|
+
* stable for the life of the thread — unrelated to the `<chat>:root:<root_id>` shape Feishu retired
|
|
30
|
+
* (its `root_id` moves with the reply chain, so it could not identify a side conversation at all).
|
|
31
|
+
* Same token, different platform meaning; the key is left as-is because renaming it would discard
|
|
32
|
+
* live context buckets for no semantic gain. */
|
|
33
|
+
export declare function slackPlaceKey(teamId: string, event: Pick<SlackMessageEvent, "channel" | "thread_ts">): string;
|
|
34
|
+
export declare function slackBufferText(text: string): string;
|
|
35
|
+
/** Canonical prompt envelope. The channel/thread ids also give slack-send an explicit delivery target. */
|
|
36
|
+
export declare function slackEnvelope(envelope: SlackEventEnvelope): string;
|
|
37
|
+
/** Default explicit-summon policy: DMs and app_mention events only. */
|
|
38
|
+
export declare function defaultSlackRoute(envelope: SlackEventEnvelope): SlackRoute | null;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { BUFFER_LINE_MAX_CHARS } from "../context-buffer.js";
|
|
2
|
+
import { codePointPrefix, truncateCodePointPrefix } from "../text.js";
|
|
3
|
+
const HUMAN_MESSAGE_SUBTYPES = new Set(["file_share", "thread_broadcast"]);
|
|
4
|
+
/**
|
|
5
|
+
* Both forms Slack writes a mention in: `<@U123>` and the labelled `<@U123|name>`. ONE definition,
|
|
6
|
+
* because the forms have to agree across every site that reads them — the "@-mentions only other
|
|
7
|
+
* people is discussion" guard, the structural bot-summon check, the stop-command strip, and the
|
|
8
|
+
* assistant-thread title. They drifted once: widening the first two while the strip still matched only
|
|
9
|
+
* the bare form turned `<@bot|name> stop` into an ordinary turn, queued behind the run it meant to stop.
|
|
10
|
+
*/
|
|
11
|
+
const mentionSource = (idPattern) => String.raw `<@${idPattern}(?:\|[^>]*)?>`;
|
|
12
|
+
const USER_MENTION = mentionSource("[A-Z0-9]+");
|
|
13
|
+
/** Broadcasts and user groups address people too: `<!here>`, `<!channel>`, `<!everyone>`,
|
|
14
|
+
* `<!subteam^S123|@team>`. Feishu's twin guard counts every mention, and the summon rule is supposed
|
|
15
|
+
* to be the same on both — without these, "@here can someone look at this" in a thread the agent takes
|
|
16
|
+
* part in reads as a bare message addressed to IT, and gets answered. */
|
|
17
|
+
const ANY_MENTION = String.raw `(?:${USER_MENTION}|<!(?:here|channel|everyone)(?:\|[^>]*)?>|<!subteam\^[^>]*>)`;
|
|
18
|
+
/** Does this text address ANYONE — a user, a broadcast, or a user group? The §3 discussion guard. */
|
|
19
|
+
export function hasSlackMention(text) {
|
|
20
|
+
return new RegExp(ANY_MENTION, "i").test(text);
|
|
21
|
+
}
|
|
22
|
+
/** Does this text mention a USER? Distinct from the above because only a user mention can be the bot:
|
|
23
|
+
* a broadcast never is, so it must not make a message look like a possible summon. */
|
|
24
|
+
export function hasSlackUserMention(text) {
|
|
25
|
+
return new RegExp(USER_MENTION, "i").test(text);
|
|
26
|
+
}
|
|
27
|
+
/** Escape a platform-supplied id before it becomes part of a pattern. Slack ids are alphanumeric in
|
|
28
|
+
* practice, but `auth.test`'s value is not validated here, and this runs on every group message: an
|
|
29
|
+
* unescaped metacharacter would either mis-answer the summon question or throw on the acceptance
|
|
30
|
+
* path, which Slack answers with an endless redelivery. */
|
|
31
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
32
|
+
/** Does this text mention this specific user (either form)? The id is MATCHED, not interpreted. */
|
|
33
|
+
export function mentionsSlackUser(text, userId) {
|
|
34
|
+
return new RegExp(mentionSource(escapeRegExp(userId)), "i").test(text);
|
|
35
|
+
}
|
|
36
|
+
/** Strip every mention, e.g. before matching a bare command word or building a title. */
|
|
37
|
+
export function stripSlackMentions(text, replacement = " ") {
|
|
38
|
+
return text.replace(new RegExp(ANY_MENTION, "gi"), replacement);
|
|
39
|
+
}
|
|
40
|
+
/** Slack message events whose content represents a new human message rather than a mutation/service event. */
|
|
41
|
+
export function isSlackHumanMessage(event) {
|
|
42
|
+
if (!event || (event.type !== "message" && event.type !== "app_mention"))
|
|
43
|
+
return false;
|
|
44
|
+
if (!event.channel || !event.ts || !event.user || event.bot_id || event.hidden)
|
|
45
|
+
return false;
|
|
46
|
+
return event.subtype === undefined || HUMAN_MESSAGE_SUBTYPES.has(event.subtype);
|
|
47
|
+
}
|
|
48
|
+
export function isSlackDirectMessage(event) {
|
|
49
|
+
return event.channel_type === "im";
|
|
50
|
+
}
|
|
51
|
+
export function isSlackGroupMessage(event) {
|
|
52
|
+
return !isSlackDirectMessage(event) && (event.type === "app_mention" || event.channel_type !== undefined);
|
|
53
|
+
}
|
|
54
|
+
/** Stable install identity. Events API normally supplies team_id; the fallbacks cover Grid envelopes. */
|
|
55
|
+
export function slackTeamId(envelope) {
|
|
56
|
+
return (envelope.team_id ??
|
|
57
|
+
envelope.event?.team ??
|
|
58
|
+
envelope.authorizations?.find((authorization) => authorization.team_id)?.team_id ??
|
|
59
|
+
envelope.context_team_id ??
|
|
60
|
+
envelope.enterprise_id ??
|
|
61
|
+
envelope.authorizations?.find((authorization) => authorization.enterprise_id)?.enterprise_id);
|
|
62
|
+
}
|
|
63
|
+
export function slackFileIds(event) {
|
|
64
|
+
return [...new Set((event.files ?? []).flatMap((file) => (typeof file.id === "string" ? [file.id] : [])))];
|
|
65
|
+
}
|
|
66
|
+
/** Text plus a structural marker for file-only or captioned file shares. */
|
|
67
|
+
export function slackMessageText(event) {
|
|
68
|
+
const text = event.text?.trim() ?? "";
|
|
69
|
+
const files = slackFileIds(event).length;
|
|
70
|
+
if (files === 0)
|
|
71
|
+
return text;
|
|
72
|
+
const marker = `[${files} attached file${files === 1 ? "" : "s"}]`;
|
|
73
|
+
return text ? `${text}\n${marker}` : marker;
|
|
74
|
+
}
|
|
75
|
+
export function slackSenderLabel(event) {
|
|
76
|
+
return `user ${event.user ?? "unknown"}`;
|
|
77
|
+
}
|
|
78
|
+
/** Main-channel discussion and each concrete thread are independent context buckets.
|
|
79
|
+
*
|
|
80
|
+
* The `:root:` segment carries `thread_ts`, which in Slack IS the thread's parent message and is
|
|
81
|
+
* stable for the life of the thread — unrelated to the `<chat>:root:<root_id>` shape Feishu retired
|
|
82
|
+
* (its `root_id` moves with the reply chain, so it could not identify a side conversation at all).
|
|
83
|
+
* Same token, different platform meaning; the key is left as-is because renaming it would discard
|
|
84
|
+
* live context buckets for no semantic gain. */
|
|
85
|
+
export function slackPlaceKey(teamId, event) {
|
|
86
|
+
const base = `${teamId}:${event.channel ?? "unknown-channel"}`;
|
|
87
|
+
return event.thread_ts ? `${base}:root:${event.thread_ts}` : base;
|
|
88
|
+
}
|
|
89
|
+
export function slackBufferText(text) {
|
|
90
|
+
return truncateCodePointPrefix(text.replace(/\s+/g, " ").trim(), BUFFER_LINE_MAX_CHARS);
|
|
91
|
+
}
|
|
92
|
+
/** Canonical prompt envelope. The channel/thread ids also give slack-send an explicit delivery target. */
|
|
93
|
+
export function slackEnvelope(envelope) {
|
|
94
|
+
const event = envelope.event;
|
|
95
|
+
if (!event?.channel)
|
|
96
|
+
return "";
|
|
97
|
+
const team = slackTeamId(envelope) ?? "unknown-team";
|
|
98
|
+
const direct = isSlackDirectMessage(event);
|
|
99
|
+
const meta = [
|
|
100
|
+
`team ${team}`,
|
|
101
|
+
`channel ${event.channel} (${direct ? "direct" : "group"})`,
|
|
102
|
+
event.thread_ts ? `thread ${event.thread_ts}` : undefined,
|
|
103
|
+
event.ts ? `msg ${event.ts}` : undefined,
|
|
104
|
+
event.user ? `from user ${event.user}` : undefined,
|
|
105
|
+
]
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.join(", ");
|
|
108
|
+
const activeContext = (event.app_context?.entities ?? [])
|
|
109
|
+
.filter((entity) => entity.type && entity.value)
|
|
110
|
+
.slice(0, 5)
|
|
111
|
+
.map((entity) => `${codePointPrefix(String(entity.type).replace(/\s+/g, " "), 80)}=` +
|
|
112
|
+
codePointPrefix(String(entity.value).replace(/\s+/g, " "), 200))
|
|
113
|
+
.join(", ");
|
|
114
|
+
const scope = direct ? "" : "\n[group chat — multiple people; recent discussion is sender-prefixed]";
|
|
115
|
+
const context = activeContext ? `\n[Slack context currently open for the user: ${activeContext}]` : "";
|
|
116
|
+
return `[slack: ${meta}]${scope}${context}\n${slackMessageText(event)}`;
|
|
117
|
+
}
|
|
118
|
+
/** Default explicit-summon policy: DMs and app_mention events only. */
|
|
119
|
+
export function defaultSlackRoute(envelope) {
|
|
120
|
+
const event = envelope.event;
|
|
121
|
+
if (!isSlackHumanMessage(event))
|
|
122
|
+
return null;
|
|
123
|
+
return isSlackDirectMessage(event) || event.type === "app_mention" ? {} : null;
|
|
124
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Slack reply rendering: native Agent streams first, rate-safe edited-message compatibility second. */
|
|
2
|
+
import type { AgentEvent } from "../../agent.ts";
|
|
3
|
+
import { type ChannelFailure, defaultErrorMessage } from "../preview-kit.ts";
|
|
4
|
+
import { type SlackApi, type SlackTarget } from "./slack-api.ts";
|
|
5
|
+
export type SlackFailure = ChannelFailure;
|
|
6
|
+
export type SlackRendering = "native" | "classic";
|
|
7
|
+
export { defaultErrorMessage };
|
|
8
|
+
/** LLM output uses standard Markdown, never Slack's notification control syntax. Keep explicit sends in
|
|
9
|
+
* the slack-send tool, where the Agent has to choose a side-effecting delivery action deliberately.
|
|
10
|
+
* The inner class excludes BOTH `<` and `>` on purpose: bounding each run by the next delimiter keeps
|
|
11
|
+
* the scan linear even on adversarial input like `(<!)^n` (a `>`-only bound would still be polynomial
|
|
12
|
+
* across the many `<` start positions), while still neutralizing every `<@…>` / `<!…>` control sequence
|
|
13
|
+
* (real Slack controls never contain a `<`). */
|
|
14
|
+
export declare function sanitizeSlackMarkdown(markdown: string): string;
|
|
15
|
+
/** Settle a queue/drop/defer notice. These are authored plain strings, so the basic text API is enough. */
|
|
16
|
+
export declare function settleSlackPreview(api: SlackApi, target: SlackTarget, previewTs: string | undefined, text: string): Promise<void>;
|
|
17
|
+
export declare function streamSlackReply(events: AsyncIterable<AgentEvent>, api: SlackApi, target: SlackTarget, formatError: (failure: SlackFailure) => string | undefined, options?: {
|
|
18
|
+
rendering?: SlackRendering;
|
|
19
|
+
initialPreviewTs?: string;
|
|
20
|
+
threadTitle?: string;
|
|
21
|
+
disclaimer?: string | false;
|
|
22
|
+
label?: string;
|
|
23
|
+
}): Promise<void>;
|