@timqi/pier 0.0.29 → 0.1.1
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 +58 -125
- package/dist/agent/config.js +24 -11
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +43 -62
- package/dist/agent/listing.js +113 -68
- package/dist/agent/pi.js +204 -211
- package/dist/boards/boards.js +19 -29
- package/dist/channels/attach.js +14 -42
- package/dist/channels/chains.js +33 -37
- package/dist/channels/chunk.js +8 -28
- package/dist/channels/commands.js +3 -14
- package/dist/channels/config.js +33 -52
- package/dist/channels/control.js +4 -13
- package/dist/channels/conversations.js +8 -25
- package/dist/channels/dedup.js +8 -17
- package/dist/channels/gatekeeper.js +13 -23
- package/dist/channels/lark-api.js +23 -63
- package/dist/channels/lark-outbound.js +12 -44
- package/dist/channels/lark-panel.js +8 -24
- package/dist/channels/lark-render.js +18 -62
- package/dist/channels/lark.js +52 -141
- package/dist/channels/lines.js +13 -15
- package/dist/channels/panel.js +16 -36
- package/dist/channels/receipts.js +29 -52
- package/dist/channels/routes.js +3 -9
- package/dist/channels/runtime.js +12 -23
- package/dist/channels/slack-api.js +34 -86
- package/dist/channels/slack-directory.js +7 -23
- package/dist/channels/slack-outbound.js +12 -56
- package/dist/channels/slack-panel.js +4 -13
- package/dist/channels/slack-render.js +23 -91
- package/dist/channels/slack-tool.js +48 -171
- package/dist/channels/slack.js +73 -239
- package/dist/channels/telegram-api.js +8 -20
- package/dist/channels/telegram-panel.js +5 -21
- package/dist/channels/telegram-render.js +13 -40
- package/dist/channels/telegram.js +54 -146
- package/dist/channels/types.js +5 -16
- package/dist/cli.js +17 -41
- package/dist/config-sync.js +87 -4
- package/dist/core/hub.js +7 -20
- package/dist/core/identity.js +20 -59
- package/dist/core/inbound-file.js +15 -49
- package/dist/core/inbox.js +13 -35
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -260
- package/dist/core/types.js +17 -1
- package/dist/db.js +98 -252
- package/dist/drain.js +58 -51
- package/dist/extensions/index.js +3 -11
- package/dist/extensions/web/anthropic.js +3 -9
- package/dist/extensions/web/artifacts.js +2 -5
- package/dist/extensions/web/content.js +6 -14
- package/dist/extensions/web/http.js +2 -6
- package/dist/extensions/web/language.js +8 -18
- package/dist/extensions/web/openai.js +1 -1
- package/dist/extensions/web/provider.js +5 -18
- package/dist/extensions/web/tools.js +19 -63
- package/dist/lock.js +98 -0
- package/dist/log.js +9 -26
- package/dist/main.js +87 -179
- package/dist/paths.js +10 -26
- package/dist/secrets.js +19 -46
- package/dist/service.js +33 -75
- package/dist/settings.js +42 -65
- package/dist/tasks/agent.js +129 -114
- package/dist/tasks/callbacks.js +9 -19
- package/dist/tasks/command.js +29 -14
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +46 -42
- package/dist/tasks/groups.js +41 -35
- package/dist/tasks/messages.js +121 -182
- package/dist/tasks/outbox.js +61 -55
- package/dist/tasks/routes.js +5 -11
- package/dist/tasks/runs.js +14 -13
- package/dist/tasks/service.js +53 -54
- package/dist/tasks/store.js +53 -30
- package/dist/tasks/tool.js +132 -61
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +100 -327
- package/dist/update.js +21 -44
- package/dist/web/auth.js +118 -179
- package/dist/web/config-sync.js +2 -2
- package/dist/web/config.js +3 -7
- package/dist/web/explorer.js +10 -21
- package/dist/web/fs.js +20 -42
- package/dist/web/instance.js +45 -85
- package/dist/web/providers.js +14 -13
- package/dist/web/public/assets/{activity-D3m4L2IL.js → activity-B89_hH7q.js} +2 -2
- package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
- package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js +1 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js +85 -0
- package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js +1 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js +5 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js +3 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
- package/dist/web/public/index.html +100 -130
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +2 -2
- package/dist/web/public/manifest.webmanifest.br +0 -0
- package/dist/web/public/manifest.webmanifest.gz +0 -0
- package/dist/web/public/sw.js +14 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/push.js +55 -77
- package/dist/web/route.js +3 -7
- package/dist/web/server.js +131 -180
- package/dist/web/session-state.js +14 -54
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +81 -160
- package/skills/pier-help/SKILL.md +23 -20
- package/skills/pier-slack/SKILL.md +2 -2
- package/skills/pier-tasks/SKILL.md +153 -160
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- package/dist/web/public/assets/activity-D3m4L2IL.js.br +0 -0
- package/dist/web/public/assets/activity-D3m4L2IL.js.gz +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js +0 -1
- package/dist/web/public/assets/boards-BIObcQeX.js.br +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js.gz +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js +0 -4
- package/dist/web/public/assets/explorer-C_rSWPNB.js.br +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js.gz +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css +0 -2
- package/dist/web/public/assets/index-CX3fYZY5.css.br +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css.gz +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js +0 -85
- package/dist/web/public/assets/index-uFsZkKOQ.js.br +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js.gz +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js +0 -1
- package/dist/web/public/assets/runs-Ch6DZq6O.js.br +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js.gz +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js +0 -5
- package/dist/web/public/assets/settings-BWcEIEcv.js.br +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js.gz +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js +0 -3
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.br +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.gz +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js +0 -4
- package/dist/web/public/assets/tasks-DTiCi2mH.js.br +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js.gz +0 -0
|
@@ -1,36 +1,20 @@
|
|
|
1
|
-
// The agent-facing Slack tool: the model states an intent, Pier performs it
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// instead of documenting the Slack API in a skill.
|
|
7
|
-
//
|
|
8
|
-
// Every read goes to Slack. Slack is the source of truth and it is the only
|
|
9
|
-
// party that knows about an edit or a deletion, so a stored copy can only be a
|
|
10
|
-
// copy that is wrong later — and Pier is a workspace-internal app, where
|
|
11
|
-
// `conversations.history`/`replies` are Tier 3 (~50+ req/min) and a read costs
|
|
12
|
-
// one or two calls. If that assumption changes (a distributed non-Marketplace
|
|
13
|
-
// app is capped at 1 req/min), this is the decision to revisit.
|
|
1
|
+
// The agent-facing Slack tool: the model states an intent, Pier performs it,
|
|
2
|
+
// and the token never leaves Pier. Every read goes to Slack — only it knows
|
|
3
|
+
// about an edit or a deletion. Affordable because a workspace-internal app gets
|
|
4
|
+
// Tier 3 (~50+ req/min) on history/replies; a distributed non-Marketplace app
|
|
5
|
+
// is capped at 1 req/min, and that is the decision to revisit.
|
|
14
6
|
import { Type } from "typebox";
|
|
15
7
|
import { saveInboundAll } from "../core/inbox.js";
|
|
16
8
|
import { MAX_INBOUND_BYTES } from "../core/inbound-file.js";
|
|
17
9
|
import { MARKDOWN_MAX } from "./slack-render.js";
|
|
18
|
-
/**
|
|
10
|
+
/** So a wide range cannot blow up the model's context. */
|
|
19
11
|
const MAX_MESSAGES = 400;
|
|
20
|
-
/** Pages to walk before giving up on a very wide window. */
|
|
21
12
|
const MAX_PAGES = 10;
|
|
22
|
-
/** Where fetched bytes land: the adapter's own channel id, so a file the agent
|
|
23
|
-
* asked for sits beside the ones people uploaded to Pier. */
|
|
24
13
|
const INBOX_CHANNEL = "slack";
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
* number but *not* as a string once the integer part changes width. Ordering
|
|
28
|
-
* goes through this; the string itself is never rewritten, because it is the
|
|
29
|
-
* id a reply or a reaction has to match exactly.
|
|
30
|
-
*/
|
|
14
|
+
/** A `ts` is `<epoch seconds>.<microseconds>`: sorts as a number, not as a
|
|
15
|
+
* string. Never rewritten — it is the id a reply must match exactly. */
|
|
31
16
|
const tsToNumber = (ts) => Number(ts);
|
|
32
17
|
const tsToIso = (ts) => new Date(Math.floor(tsToNumber(ts) * 1000)).toISOString();
|
|
33
|
-
/** Minute precision in the transcript: the exact time is in the `ts` beside it. */
|
|
34
18
|
const tsToMinute = (ts) => `${tsToIso(ts).slice(0, 16)}Z`;
|
|
35
19
|
/** Epoch seconds at the year 2100: past this, the caller meant milliseconds. */
|
|
36
20
|
const MAX_SECONDS = 4_102_444_800;
|
|
@@ -39,26 +23,19 @@ export function toTs(value) {
|
|
|
39
23
|
if (value === undefined || value === "")
|
|
40
24
|
return undefined;
|
|
41
25
|
const raw = typeof value === "number" ? String(value) : value.trim();
|
|
42
|
-
// A ts is passed through untouched — it is an id, not a number to reformat.
|
|
43
26
|
const numeric = /^\d+(\.\d+)?$/.test(raw);
|
|
44
27
|
const seconds = numeric ? Number(raw) : Date.parse(raw) / 1000;
|
|
45
28
|
if (Number.isNaN(seconds))
|
|
46
29
|
throw new Error(`not a time: ${value}`);
|
|
47
|
-
// Milliseconds
|
|
48
|
-
//
|
|
49
|
-
// read that is indistinguishable from a channel where nobody spoke.
|
|
30
|
+
// Milliseconds: Slack takes the window without complaint and answers an empty
|
|
31
|
+
// read indistinguishable from a channel where nobody spoke.
|
|
50
32
|
if (seconds > MAX_SECONDS) {
|
|
51
33
|
throw new Error(`${value} is past the year 2100 — Slack times are epoch seconds, not milliseconds`);
|
|
52
34
|
}
|
|
53
35
|
return numeric ? raw : String(seconds);
|
|
54
36
|
}
|
|
55
|
-
/**
|
|
56
|
-
*
|
|
57
|
-
* `handleSlackTool` checks, asked before the description is paid for. The call
|
|
58
|
-
* keeps its own checks and its own two messages — a session opened while Slack
|
|
59
|
-
* was configured outlives the operator switching it off, and that turn has to
|
|
60
|
-
* say so rather than find the tool quietly gone.
|
|
61
|
-
*/
|
|
37
|
+
/** Asked before the description is paid for. `handleSlackTool` keeps its own
|
|
38
|
+
* checks: a session opened while Slack was configured outlives the switch. */
|
|
62
39
|
export function slackToolAvailable(store) {
|
|
63
40
|
const config = store.get("slack");
|
|
64
41
|
return config.enabled && !!config.token && config.agentTool;
|
|
@@ -67,9 +44,6 @@ export function slackToolSpec(execute, available) {
|
|
|
67
44
|
return {
|
|
68
45
|
name: "slack",
|
|
69
46
|
label: "Slack",
|
|
70
|
-
// One screen of contract; the paragraph this once was lives in the
|
|
71
|
-
// pier-slack skill, which the description sends the model to before it
|
|
72
|
-
// posts — the part that goes wrong without instructions.
|
|
73
47
|
description: "Read and write Slack through Pier, which holds the bot token. Operations: context (which Slack conversation this session is in), read_channel (transcript for a time range), read_thread (one thread; only what is new since a message via after), read_message (the one at ts), fetch_file (save a file a transcript names, by its F… id), post, edit/delete (Pier's own messages only), channels (what Pier can reach). Omit channel and thread_ts to act on the conversation you are in. since/until/after accept ISO 8601, epoch seconds or a ts. Every read fetches live; nothing is kept between calls. @mentions, #channels and links need Slack's own syntax — read the pier-slack skill before posting.",
|
|
74
48
|
parameters: Type.Object({
|
|
75
49
|
// A JSON-Schema enum emits far fewer tokens than typebox's anyOf-of-consts.
|
|
@@ -87,25 +61,17 @@ export function slackToolSpec(execute, available) {
|
|
|
87
61
|
"channels",
|
|
88
62
|
],
|
|
89
63
|
}),
|
|
90
|
-
/**
|
|
91
|
-
* Channel id (`C…`/`D…`/`G…`) or the `#name` shown by `channels`. Omit to
|
|
92
|
-
* use the conversation this session is answering.
|
|
93
|
-
*/
|
|
94
64
|
channel: Type.Optional(Type.String()),
|
|
95
65
|
since: Type.Optional(Type.String()),
|
|
96
66
|
until: Type.Optional(Type.String()),
|
|
97
|
-
/** Strictly newer than this — "what changed since I last looked". */
|
|
98
67
|
after: Type.Optional(Type.String()),
|
|
99
|
-
/** The one message `read_message`, `edit` or `delete` is about. */
|
|
100
68
|
ts: Type.Optional(Type.String()),
|
|
101
|
-
/** `fetch_file`: the `F…` id a transcript line carries. */
|
|
102
69
|
file: Type.Optional(Type.String()),
|
|
103
70
|
limit: Type.Optional(Type.Number()),
|
|
104
71
|
thread_ts: Type.Optional(Type.String()),
|
|
105
72
|
text: Type.Optional(Type.String()),
|
|
106
73
|
}),
|
|
107
74
|
available,
|
|
108
|
-
// Stands down with the tool: without Slack, its manual is a route to nowhere.
|
|
109
75
|
skill: "pier-slack",
|
|
110
76
|
execute,
|
|
111
77
|
};
|
|
@@ -115,10 +81,8 @@ const required = (value, field) => {
|
|
|
115
81
|
throw new Error(`${field} is required`);
|
|
116
82
|
return value.trim();
|
|
117
83
|
};
|
|
118
|
-
/**
|
|
119
|
-
*
|
|
120
|
-
* a refusal the agent can act on beats a post that silently never happened.
|
|
121
|
-
*/
|
|
84
|
+
/** Slack rejects an oversized message outright; a refusal the agent can act
|
|
85
|
+
* on beats a post that silently never happened. */
|
|
122
86
|
const messageText = (raw) => {
|
|
123
87
|
const text = required(raw, "text");
|
|
124
88
|
if (text.length > MARKDOWN_MAX) {
|
|
@@ -132,8 +96,8 @@ export async function handleSlackTool(deps, raw, callerSessionId = "") {
|
|
|
132
96
|
if (!input)
|
|
133
97
|
throw new Error("slack tool parameters required");
|
|
134
98
|
const config = deps.store.get("slack");
|
|
135
|
-
//
|
|
136
|
-
//
|
|
99
|
+
// The error says which switch: "it does nothing" is the most expensive
|
|
100
|
+
// failure for a model to diagnose.
|
|
137
101
|
if (!config.enabled || !config.token)
|
|
138
102
|
throw new Error("the Slack channel is not configured in Pier");
|
|
139
103
|
if (!config.agentTool)
|
|
@@ -142,8 +106,7 @@ export async function handleSlackTool(deps, raw, callerSessionId = "") {
|
|
|
142
106
|
if (!client)
|
|
143
107
|
throw new Error("the Slack client is unavailable");
|
|
144
108
|
if (input.operation === "channels") {
|
|
145
|
-
// Only what Pier has
|
|
146
|
-
// channels", and a name the agent cannot address is worse than no name.
|
|
109
|
+
// Only what Pier has seen: Slack has no reliable "list my channels".
|
|
147
110
|
return config.chats.map((chat) => ({
|
|
148
111
|
id: chat.id,
|
|
149
112
|
name: chat.name,
|
|
@@ -169,26 +132,18 @@ export async function handleSlackTool(deps, raw, callerSessionId = "") {
|
|
|
169
132
|
note: "Omit channel and thread_ts to read or post here. Speaker ids for mentions come from read_thread.",
|
|
170
133
|
};
|
|
171
134
|
}
|
|
172
|
-
// Before the channel default
|
|
173
|
-
// asking a session that never touched Slack for a channel it has no way to
|
|
174
|
-
// name would refuse a call that needs none.
|
|
135
|
+
// Before the channel default: a file id is unique workspace-wide.
|
|
175
136
|
if (input.operation === "fetch_file") {
|
|
176
137
|
return fetchFile(deps, client, required(input.file, "file"));
|
|
177
138
|
}
|
|
178
|
-
// "Here" is the default target: an agent reached through a Slack thread
|
|
179
|
-
// should not have to be told which thread it is standing in.
|
|
180
139
|
const channel = input.channel === undefined || input.channel === ""
|
|
181
140
|
? at?.channel ??
|
|
182
141
|
(() => {
|
|
183
142
|
throw new Error("channel is required: this session was not reached through Slack, so there is no current conversation");
|
|
184
143
|
})()
|
|
185
144
|
: resolveChannel(deps, required(input.channel, "channel"));
|
|
186
|
-
//
|
|
187
|
-
// own bounds are inclusive-ish, so the boundary message is dropped here
|
|
188
|
-
// rather than trusted to the API.
|
|
145
|
+
// Slack's bounds are inclusive-ish, so the boundary message is dropped here.
|
|
189
146
|
const after = toTs(input.after);
|
|
190
|
-
// Both reads take it, so it is read once: a cap that worked on a channel and
|
|
191
|
-
// was ignored on a thread would be the more expensive kind of surprise.
|
|
192
147
|
const limit = Math.min(Number(input.limit) || MAX_MESSAGES, MAX_MESSAGES);
|
|
193
148
|
if (input.operation === "read_channel") {
|
|
194
149
|
const since = after ?? toTs(input.since);
|
|
@@ -209,8 +164,7 @@ export async function handleSlackTool(deps, raw, callerSessionId = "") {
|
|
|
209
164
|
}
|
|
210
165
|
if (input.operation === "post") {
|
|
211
166
|
const text = messageText(input.text);
|
|
212
|
-
//
|
|
213
|
-
// way to start a new top-level message instead.
|
|
167
|
+
// `thread_ts: "none"` is the explicit way to start a top-level message.
|
|
214
168
|
const asked = typeof input.thread_ts === "string" ? input.thread_ts.trim() : "";
|
|
215
169
|
const threadTs = asked === "none"
|
|
216
170
|
? undefined
|
|
@@ -226,24 +180,18 @@ export async function handleSlackTool(deps, raw, callerSessionId = "") {
|
|
|
226
180
|
channel,
|
|
227
181
|
ts: sent.ts,
|
|
228
182
|
at: sent.ts ? tsToIso(sent.ts) : null,
|
|
229
|
-
// Returned so a follow-up can reply under what was just posted.
|
|
230
183
|
threadTs: threadTs ?? sent.ts,
|
|
231
184
|
...inertMention(text),
|
|
232
185
|
};
|
|
233
186
|
}
|
|
234
187
|
if (input.operation === "edit") {
|
|
235
|
-
// Explicit `ts`, for delete's reason: an edit replaces the text outright,
|
|
236
|
-
// and Slack keeps no visible record of what it said before.
|
|
237
188
|
const ts = required(input.ts, "ts");
|
|
238
189
|
const text = messageText(input.text);
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
// see inside a thread — without this the read below always comes up empty.
|
|
190
|
+
// `conversations.history` cannot see inside a thread, so the read below
|
|
191
|
+
// needs the thread `post` would default to.
|
|
242
192
|
const asked = typeof input.thread_ts === "string" ? input.thread_ts.trim() : "";
|
|
243
193
|
const inThread = asked || (channel === at?.channel ? at?.threadTs : undefined);
|
|
244
|
-
//
|
|
245
|
-
// Slack keeps no version, so this log is the only record of what was
|
|
246
|
-
// replaced. Best-effort — a failed read must not block the correction.
|
|
194
|
+
// Slack keeps no version, so this log is the only record of what was replaced.
|
|
247
195
|
const was = await previousText(client, channel, ts, inThread);
|
|
248
196
|
try {
|
|
249
197
|
await client.updateMessage({ channel, ts, text, blocks: [{ type: "markdown", text }] });
|
|
@@ -255,8 +203,7 @@ export async function handleSlackTool(deps, raw, callerSessionId = "") {
|
|
|
255
203
|
return { channel, ts, edited: true, ...inertMention(text) };
|
|
256
204
|
}
|
|
257
205
|
if (input.operation === "delete") {
|
|
258
|
-
// Never defaulted from `here`:
|
|
259
|
-
// "delete" with an implied target is the one mistake with no undo.
|
|
206
|
+
// Never defaulted from `here`: an implied target is the one mistake with no undo.
|
|
260
207
|
const ts = required(input.ts, "ts");
|
|
261
208
|
try {
|
|
262
209
|
await client.deleteMessage(channel, ts);
|
|
@@ -264,42 +211,31 @@ export async function handleSlackTool(deps, raw, callerSessionId = "") {
|
|
|
264
211
|
catch (err) {
|
|
265
212
|
throw new Error(explain(err));
|
|
266
213
|
}
|
|
267
|
-
// A removal leaves nothing behind to read, so the log is the only record
|
|
268
|
-
// that it happened at all.
|
|
269
214
|
deps.log(`slack tool deleted ${ts} in ${channel}`);
|
|
270
215
|
return { channel, ts, deleted: true };
|
|
271
216
|
}
|
|
272
217
|
throw new Error(`unknown slack operation: ${String(input.operation)}`);
|
|
273
218
|
}
|
|
274
|
-
/**
|
|
275
|
-
*
|
|
276
|
-
* `conversations.history` never returns what was posted inside a thread — so a
|
|
277
|
-
* reply has to be asked for through its thread, and only the caller knows.
|
|
278
|
-
*/
|
|
219
|
+
/** `conversations.history` never returns what was posted inside a thread, so
|
|
220
|
+
* a reply has to be asked for through its thread. */
|
|
279
221
|
async function oneMessage(client, channel, ts, threadTs) {
|
|
280
222
|
const page = threadTs
|
|
281
223
|
? await client.replies(channel, threadTs, { oldest: ts, limit: 20 })
|
|
282
224
|
: await client.history(channel, { oldest: ts, latest: ts, limit: 1 });
|
|
283
225
|
return page.messages.find((msg) => msg.ts === ts);
|
|
284
226
|
}
|
|
285
|
-
/** One line of what a message said, for the log an edit leaves behind. */
|
|
286
227
|
async function previousText(client, channel, ts, threadTs) {
|
|
287
228
|
try {
|
|
288
229
|
const text = (await oneMessage(client, channel, ts, threadTs))?.text;
|
|
289
230
|
return text === undefined ? undefined : text.slice(0, 200).replace(/\s+/g, " ");
|
|
290
231
|
}
|
|
291
232
|
catch {
|
|
292
|
-
//
|
|
293
|
-
// which is the fact that matters. Refusing the edit over it would be worse.
|
|
233
|
+
// The caller logs "not captured"; refusing the edit over it would be worse.
|
|
294
234
|
return undefined;
|
|
295
235
|
}
|
|
296
236
|
}
|
|
297
|
-
/**
|
|
298
|
-
*
|
|
299
|
-
* message goes up, renders as text, and notifies nobody. Reported after the
|
|
300
|
-
* fact rather than refused — a name in prose is legitimate, an unping is not
|
|
301
|
-
* worth losing the message over.
|
|
302
|
-
*/
|
|
237
|
+
/** A plain `@alice` looks like it worked and notifies nobody. Reported, not
|
|
238
|
+
* refused: a name in prose is legitimate. */
|
|
303
239
|
function inertMention(text) {
|
|
304
240
|
const prose = text
|
|
305
241
|
.replace(/```[\s\S]*?```|`[^`]*`/g, "") // code says @ and # for other reasons
|
|
@@ -315,7 +251,6 @@ function inertMention(text) {
|
|
|
315
251
|
`Edit this ts if it was meant to reach someone.`,
|
|
316
252
|
};
|
|
317
253
|
}
|
|
318
|
-
/** Accept a `#name` or a bare name as well as an id — models prefer names. */
|
|
319
254
|
function resolveChannel(deps, given) {
|
|
320
255
|
if (/^[CDG][A-Z0-9]+$/.test(given))
|
|
321
256
|
return given;
|
|
@@ -329,9 +264,7 @@ function resolveChannel(deps, given) {
|
|
|
329
264
|
async function readChannel(deps, client, channel, since, until, after, limit) {
|
|
330
265
|
const fetched = await fetchPages(deps, (cursor) => client.history(channel, { oldest: since, latest: until, cursor }), `history for ${channel}`);
|
|
331
266
|
const all = newerThan(transcript(fetched.messages), after);
|
|
332
|
-
//
|
|
333
|
-
// truncated at its newest end — the oldest `limit` messages are the ones
|
|
334
|
-
// that read as a transcript.
|
|
267
|
+
// Truncated at the newest end: the oldest `limit` messages read as a transcript.
|
|
335
268
|
const window = all.slice(0, limit);
|
|
336
269
|
return {
|
|
337
270
|
channel,
|
|
@@ -343,21 +276,13 @@ async function readChannel(deps, client, channel, since, until, after, limit) {
|
|
|
343
276
|
messages: await lines(deps, client, window),
|
|
344
277
|
};
|
|
345
278
|
}
|
|
346
|
-
/**
|
|
347
|
-
* Exported for one caller: the adapter inlines a small shared thread, and a
|
|
348
|
-
* second `conversations.replies` walker — its paging, seam dedup, ordering and
|
|
349
|
-
* error translation — is exactly the copy this file exists to prevent.
|
|
350
|
-
*/
|
|
279
|
+
/** Exported for the adapter, which inlines a small shared thread. */
|
|
351
280
|
export async function readThread(deps, client, channel, threadTs, after, limit) {
|
|
352
281
|
const fetched = await fetchPages(deps, (cursor) => client.replies(channel, threadTs, { oldest: after, cursor }), `thread ${threadTs} in ${channel}`);
|
|
353
282
|
const all = newerThan(transcript(fetched.messages), after);
|
|
354
|
-
// Oldest first, as in a channel read: a thread cut at its newest end still
|
|
355
|
-
// reads as a thread, and the cut has to be said either way — a long thread
|
|
356
|
-
// that answers as if it were complete is the read nobody double-checks.
|
|
357
283
|
const messages = all.slice(0, limit);
|
|
358
284
|
return {
|
|
359
285
|
channel,
|
|
360
|
-
// Hoisted: every line in a thread carries the same one.
|
|
361
286
|
threadTs,
|
|
362
287
|
count: messages.length,
|
|
363
288
|
...(fetched.truncated || all.length > messages.length ? { truncated: true } : {}),
|
|
@@ -366,7 +291,6 @@ export async function readThread(deps, client, channel, threadTs, after, limit)
|
|
|
366
291
|
messages: await lines(deps, client, messages),
|
|
367
292
|
};
|
|
368
293
|
}
|
|
369
|
-
/** One message, because that is sometimes the whole question. */
|
|
370
294
|
async function readMessage(deps, client, channel, ts, threadTs) {
|
|
371
295
|
const found = await oneMessage(client, channel, ts, threadTs);
|
|
372
296
|
if (!found) {
|
|
@@ -382,35 +306,23 @@ async function readMessage(deps, client, channel, ts, threadTs) {
|
|
|
382
306
|
message: line,
|
|
383
307
|
};
|
|
384
308
|
}
|
|
385
|
-
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
* Pier — knows which of them the question is about. Even then the reply is only
|
|
389
|
-
* the marker line, so the bytes enter its context if it opens the file.
|
|
390
|
-
*
|
|
391
|
-
* `files.info` every call. A `SlackFile` carries a signed url that expires and
|
|
392
|
-
* a name its owner can change, so a kept copy is the same wrong-later copy this
|
|
393
|
-
* file's header refuses for messages.
|
|
394
|
-
*/
|
|
309
|
+
/** Explicit, not automatic: one read can name a hundred uploads, and the agent
|
|
310
|
+
* knows which one the question is about. `files.info` every call: the signed
|
|
311
|
+
* url expires. */
|
|
395
312
|
async function fetchFile(deps, client, id) {
|
|
396
313
|
let file;
|
|
397
314
|
try {
|
|
398
315
|
file = await client.filesInfo(id);
|
|
399
316
|
}
|
|
400
317
|
catch (err) {
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
// agent can guess. Name the fix, as uploadFile does for files:write.
|
|
318
|
+
// Manifest scopes apply only at app creation, so an older install refuses
|
|
319
|
+
// for a reason no agent can guess.
|
|
404
320
|
if (/missing_scope/.test(String(err))) {
|
|
405
321
|
throw new Error("Pier's Slack app cannot read files — add the files:read scope to the Slack app " +
|
|
406
322
|
"under OAuth & Permissions and reinstall it");
|
|
407
323
|
}
|
|
408
324
|
throw new Error(explain(err));
|
|
409
325
|
}
|
|
410
|
-
// The shared save loop owns the size gate, the marker and the lost-marker
|
|
411
|
-
// wording (core/inbox.ts), so a file over the cap or a refused download
|
|
412
|
-
// answers in the words every other inbound failure uses — never a stack, and
|
|
413
|
-
// never an empty reply.
|
|
414
326
|
const [marker] = await saveInboundAll(INBOX_CHANNEL, [{
|
|
415
327
|
label: file.name ?? id,
|
|
416
328
|
name: file.name,
|
|
@@ -421,14 +333,8 @@ async function fetchFile(deps, client, id) {
|
|
|
421
333
|
}], deps.log);
|
|
422
334
|
return marker;
|
|
423
335
|
}
|
|
424
|
-
/**
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
* A page that fails mid-walk does not throw away the pages before it: an
|
|
428
|
-
* agent that asked for a day of history and got an exception cannot tell a
|
|
429
|
-
* broken read from a quiet channel. It gets what there was, plus why the walk
|
|
430
|
-
* stopped, and decides for itself whether to retry or work with it.
|
|
431
|
-
*/
|
|
336
|
+
/** A page that fails mid-walk keeps the pages before it, plus why it stopped:
|
|
337
|
+
* an exception alone cannot be told from a quiet channel. */
|
|
432
338
|
async function fetchPages(deps, page, what) {
|
|
433
339
|
const messages = [];
|
|
434
340
|
let cursor;
|
|
@@ -452,11 +358,8 @@ async function fetchPages(deps, page, what) {
|
|
|
452
358
|
deps.log(`${what} truncated at ${messages.length} messages`);
|
|
453
359
|
return { messages, truncated: cursor !== undefined };
|
|
454
360
|
}
|
|
455
|
-
/**
|
|
456
|
-
*
|
|
457
|
-
* into the action; anything else keeps its raw code, which is at least
|
|
458
|
-
* searchable.
|
|
459
|
-
*/
|
|
361
|
+
/** Error codes an agent can act on become the action; the rest keep their
|
|
362
|
+
* searchable raw code. */
|
|
460
363
|
function explain(err) {
|
|
461
364
|
const code = /slack [\w.]+: (\w+)/.exec(String(err))?.[1] ?? "";
|
|
462
365
|
return {
|
|
@@ -474,11 +377,8 @@ function explain(err) {
|
|
|
474
377
|
}
|
|
475
378
|
/** Strictly newer, so `after: <last ts I saw>` never repeats that message. */
|
|
476
379
|
const newerThan = (messages, after) => after === undefined ? messages : messages.filter((m) => tsToNumber(m.ts) > tsToNumber(after));
|
|
477
|
-
/**
|
|
478
|
-
*
|
|
479
|
-
* bounds are inclusive-ish, so a paged read can repeat the message on the
|
|
480
|
-
* seam; the SQL `PRIMARY KEY` used to absorb that.
|
|
481
|
-
*/
|
|
380
|
+
/** Oldest first, one per ts: page bounds are inclusive-ish, so a paged read
|
|
381
|
+
* can repeat the message on the seam. */
|
|
482
382
|
function transcript(messages) {
|
|
483
383
|
const byTs = new Map();
|
|
484
384
|
for (const msg of messages)
|
|
@@ -486,51 +386,28 @@ function transcript(messages) {
|
|
|
486
386
|
byTs.set(msg.ts, msg);
|
|
487
387
|
return [...byTs.values()].sort((a, b) => tsToNumber(a.ts) - tsToNumber(b.ts));
|
|
488
388
|
}
|
|
489
|
-
/**
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
* transcript as lines costs a fraction, and a model reads it more easily than
|
|
493
|
-
* it reads JSON. The shape is declared in the reply's `format` so nothing has
|
|
494
|
-
* to be guessed.
|
|
495
|
-
*
|
|
496
|
-
* The name makes it readable, the id is the only thing `<@…>` can be built
|
|
497
|
-
* from, and Slack's own `ts` string is passed through untouched — it is what
|
|
498
|
-
* a reply, a reaction or `after` has to match exactly.
|
|
499
|
-
*
|
|
500
|
-
* The two suffixes are what a message *has* rather than what it said, so they
|
|
501
|
-
* are declared here and appended only when there is one: a message with
|
|
502
|
-
* neither reads exactly as it always did. Kept terse — the adapter puts this
|
|
503
|
-
* string in a prompt (`slack.ts`, an inlined shared thread), so every word is
|
|
504
|
-
* paid for per read.
|
|
505
|
-
*/
|
|
389
|
+
/** Lines, not objects: four hundred six-key objects spend most of their tokens
|
|
390
|
+
* on key names. The id is the only thing `<@…>` can be built from. Terse: the
|
|
391
|
+
* adapter puts this string in a prompt, so every word is paid for per read. */
|
|
506
392
|
const LINE_FORMAT = "<ts> | <time, UTC> | <name>[<id>] | <text>, then — when there are any —"
|
|
507
393
|
+ " [thread: <n> replies] and one [file: <name> <F… id> <size>] per upload";
|
|
508
|
-
/** Enough to judge a fetch against the 32 MB cap without arithmetic
|
|
509
|
-
* entirely when Slack sent no size, because a guessed one would be worse. */
|
|
394
|
+
/** Enough to judge a fetch against the 32 MB cap without arithmetic. */
|
|
510
395
|
const sizeLabel = (bytes) => bytes < 1024
|
|
511
396
|
? `${bytes}B`
|
|
512
397
|
: bytes < 1024 * 1024
|
|
513
398
|
? `${Math.round(bytes / 1024)}KB`
|
|
514
399
|
: `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
515
|
-
/**
|
|
516
|
-
* A message's uploads, named with the one handle that can fetch them. Without
|
|
517
|
-
* this the transcript dropped `files` entirely: a PDF somebody posted read as
|
|
518
|
-
* an empty message, and nothing said there was anything to open.
|
|
519
|
-
*/
|
|
400
|
+
/** Without this a PDF somebody posted reads as an empty message. */
|
|
520
401
|
const uploads = (files) => (files ?? []).map((file) => ` [file: ${file.name ?? file.mimetype ?? "file"} ${file.id}${file.size === undefined ? "" : ` ${sizeLabel(file.size)}`}]`).join("");
|
|
521
402
|
const speaker = (msg) => msg.user ?? msg.bot_id ?? null;
|
|
522
403
|
async function lines(deps, client, messages) {
|
|
523
|
-
//
|
|
524
|
-
// thread costs no lookups; the store is not consulted because a member need
|
|
525
|
-
// not be bound to have spoken.
|
|
404
|
+
// The store is not consulted: a member need not be bound to have spoken.
|
|
526
405
|
const ids = messages.map(speaker).filter((id) => !!id);
|
|
527
406
|
const names = await deps.directory.names(client, ids);
|
|
528
407
|
return messages.map((msg) => {
|
|
529
408
|
const id = speaker(msg);
|
|
530
409
|
const known = id ? names.get(id) : undefined;
|
|
531
410
|
const who = id ? (known && known !== id ? `${known}[${id}]` : `[${id}]`) : "[unknown]";
|
|
532
|
-
// A parent's reply count, so the agent can decide whether the thread is
|
|
533
|
-
// worth opening instead of spending a read to find out.
|
|
534
411
|
const replies = msg.reply_count && (msg.thread_ts ?? msg.ts) === msg.ts
|
|
535
412
|
? ` [thread: ${msg.reply_count} replies]`
|
|
536
413
|
: "";
|