@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.
Files changed (165) hide show
  1. package/README.md +58 -125
  2. package/dist/agent/config.js +24 -11
  3. package/dist/agent/credentials.js +11 -23
  4. package/dist/agent/events.js +43 -62
  5. package/dist/agent/listing.js +113 -68
  6. package/dist/agent/pi.js +204 -211
  7. package/dist/boards/boards.js +19 -29
  8. package/dist/channels/attach.js +14 -42
  9. package/dist/channels/chains.js +33 -37
  10. package/dist/channels/chunk.js +8 -28
  11. package/dist/channels/commands.js +3 -14
  12. package/dist/channels/config.js +33 -52
  13. package/dist/channels/control.js +4 -13
  14. package/dist/channels/conversations.js +8 -25
  15. package/dist/channels/dedup.js +8 -17
  16. package/dist/channels/gatekeeper.js +13 -23
  17. package/dist/channels/lark-api.js +23 -63
  18. package/dist/channels/lark-outbound.js +12 -44
  19. package/dist/channels/lark-panel.js +8 -24
  20. package/dist/channels/lark-render.js +18 -62
  21. package/dist/channels/lark.js +52 -141
  22. package/dist/channels/lines.js +13 -15
  23. package/dist/channels/panel.js +16 -36
  24. package/dist/channels/receipts.js +29 -52
  25. package/dist/channels/routes.js +3 -9
  26. package/dist/channels/runtime.js +12 -23
  27. package/dist/channels/slack-api.js +34 -86
  28. package/dist/channels/slack-directory.js +7 -23
  29. package/dist/channels/slack-outbound.js +12 -56
  30. package/dist/channels/slack-panel.js +4 -13
  31. package/dist/channels/slack-render.js +23 -91
  32. package/dist/channels/slack-tool.js +48 -171
  33. package/dist/channels/slack.js +73 -239
  34. package/dist/channels/telegram-api.js +8 -20
  35. package/dist/channels/telegram-panel.js +5 -21
  36. package/dist/channels/telegram-render.js +13 -40
  37. package/dist/channels/telegram.js +54 -146
  38. package/dist/channels/types.js +5 -16
  39. package/dist/cli.js +17 -41
  40. package/dist/config-sync.js +87 -4
  41. package/dist/core/hub.js +7 -20
  42. package/dist/core/identity.js +20 -59
  43. package/dist/core/inbound-file.js +15 -49
  44. package/dist/core/inbox.js +13 -35
  45. package/dist/core/queue.js +3 -5
  46. package/dist/core/reply.js +41 -142
  47. package/dist/core/router.js +209 -260
  48. package/dist/core/types.js +17 -1
  49. package/dist/db.js +98 -252
  50. package/dist/drain.js +58 -51
  51. package/dist/extensions/index.js +3 -11
  52. package/dist/extensions/web/anthropic.js +3 -9
  53. package/dist/extensions/web/artifacts.js +2 -5
  54. package/dist/extensions/web/content.js +6 -14
  55. package/dist/extensions/web/http.js +2 -6
  56. package/dist/extensions/web/language.js +8 -18
  57. package/dist/extensions/web/openai.js +1 -1
  58. package/dist/extensions/web/provider.js +5 -18
  59. package/dist/extensions/web/tools.js +19 -63
  60. package/dist/lock.js +98 -0
  61. package/dist/log.js +9 -26
  62. package/dist/main.js +87 -179
  63. package/dist/paths.js +10 -26
  64. package/dist/secrets.js +19 -46
  65. package/dist/service.js +33 -75
  66. package/dist/settings.js +42 -65
  67. package/dist/tasks/agent.js +129 -114
  68. package/dist/tasks/callbacks.js +9 -19
  69. package/dist/tasks/command.js +29 -14
  70. package/dist/tasks/definitions.js +39 -62
  71. package/dist/tasks/execution.js +46 -42
  72. package/dist/tasks/groups.js +41 -35
  73. package/dist/tasks/messages.js +121 -182
  74. package/dist/tasks/outbox.js +61 -55
  75. package/dist/tasks/routes.js +5 -11
  76. package/dist/tasks/runs.js +14 -13
  77. package/dist/tasks/service.js +53 -54
  78. package/dist/tasks/store.js +53 -30
  79. package/dist/tasks/tool.js +132 -61
  80. package/dist/tools-task.js +20 -60
  81. package/dist/tools.js +100 -327
  82. package/dist/update.js +21 -44
  83. package/dist/web/auth.js +118 -179
  84. package/dist/web/config-sync.js +2 -2
  85. package/dist/web/config.js +3 -7
  86. package/dist/web/explorer.js +10 -21
  87. package/dist/web/fs.js +20 -42
  88. package/dist/web/instance.js +45 -85
  89. package/dist/web/providers.js +14 -13
  90. package/dist/web/public/assets/{activity-D3m4L2IL.js → activity-B89_hH7q.js} +2 -2
  91. package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
  92. package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
  93. package/dist/web/public/assets/boards-BeKW0ZXK.js +1 -0
  94. package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
  95. package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
  96. package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
  97. package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
  98. package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
  99. package/dist/web/public/assets/index-DzXDXra_.js +85 -0
  100. package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
  101. package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
  102. package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
  103. package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
  104. package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
  105. package/dist/web/public/assets/runs-Cwy0mN8i.js +1 -0
  106. package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
  107. package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
  108. package/dist/web/public/assets/settings-DzZLmujq.js +5 -0
  109. package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
  110. package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
  111. package/dist/web/public/assets/task-runs-BCakxFk8.js +3 -0
  112. package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
  113. package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
  114. package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
  115. package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
  116. package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
  117. package/dist/web/public/index.html +100 -130
  118. package/dist/web/public/index.html.br +0 -0
  119. package/dist/web/public/index.html.gz +0 -0
  120. package/dist/web/public/manifest.webmanifest +2 -2
  121. package/dist/web/public/manifest.webmanifest.br +0 -0
  122. package/dist/web/public/manifest.webmanifest.gz +0 -0
  123. package/dist/web/public/sw.js +14 -2
  124. package/dist/web/public/sw.js.br +0 -0
  125. package/dist/web/public/sw.js.gz +0 -0
  126. package/dist/web/push.js +55 -77
  127. package/dist/web/route.js +3 -7
  128. package/dist/web/server.js +131 -180
  129. package/dist/web/session-state.js +14 -54
  130. package/dist/web/types.js +2 -4
  131. package/dist/web/webpush.js +10 -25
  132. package/docs/deploy.md +115 -330
  133. package/package.json +2 -1
  134. package/skills/pier-boards/SKILL.md +81 -160
  135. package/skills/pier-help/SKILL.md +23 -20
  136. package/skills/pier-slack/SKILL.md +2 -2
  137. package/skills/pier-tasks/SKILL.md +153 -160
  138. package/dist/config-sync-fetch.js +0 -84
  139. package/dist/limits.js +0 -14
  140. package/dist/web/public/assets/activity-D3m4L2IL.js.br +0 -0
  141. package/dist/web/public/assets/activity-D3m4L2IL.js.gz +0 -0
  142. package/dist/web/public/assets/boards-BIObcQeX.js +0 -1
  143. package/dist/web/public/assets/boards-BIObcQeX.js.br +0 -0
  144. package/dist/web/public/assets/boards-BIObcQeX.js.gz +0 -0
  145. package/dist/web/public/assets/explorer-C_rSWPNB.js +0 -4
  146. package/dist/web/public/assets/explorer-C_rSWPNB.js.br +0 -0
  147. package/dist/web/public/assets/explorer-C_rSWPNB.js.gz +0 -0
  148. package/dist/web/public/assets/index-CX3fYZY5.css +0 -2
  149. package/dist/web/public/assets/index-CX3fYZY5.css.br +0 -0
  150. package/dist/web/public/assets/index-CX3fYZY5.css.gz +0 -0
  151. package/dist/web/public/assets/index-uFsZkKOQ.js +0 -85
  152. package/dist/web/public/assets/index-uFsZkKOQ.js.br +0 -0
  153. package/dist/web/public/assets/index-uFsZkKOQ.js.gz +0 -0
  154. package/dist/web/public/assets/runs-Ch6DZq6O.js +0 -1
  155. package/dist/web/public/assets/runs-Ch6DZq6O.js.br +0 -0
  156. package/dist/web/public/assets/runs-Ch6DZq6O.js.gz +0 -0
  157. package/dist/web/public/assets/settings-BWcEIEcv.js +0 -5
  158. package/dist/web/public/assets/settings-BWcEIEcv.js.br +0 -0
  159. package/dist/web/public/assets/settings-BWcEIEcv.js.gz +0 -0
  160. package/dist/web/public/assets/task-runs-DPkwv2UE.js +0 -3
  161. package/dist/web/public/assets/task-runs-DPkwv2UE.js.br +0 -0
  162. package/dist/web/public/assets/task-runs-DPkwv2UE.js.gz +0 -0
  163. package/dist/web/public/assets/tasks-DTiCi2mH.js +0 -4
  164. package/dist/web/public/assets/tasks-DTiCi2mH.js.br +0 -0
  165. package/dist/web/public/assets/tasks-DTiCi2mH.js.gz +0 -0
@@ -1,10 +1,6 @@
1
- // Thin Telegram Bot API client: HTTP and payload shapes only, no policy.
2
- // The one file in channels/ that talks to api.telegram.org, so the adapter
3
- // stays testable against this interface. Long polling, not webhooks — Pier is
4
- // one local process with no inbound HTTP requirement.
5
- //
6
- // Behind a proxy, run node with NODE_USE_ENV_PROXY=1 and HTTPS_PROXY set;
7
- // nothing here needs to know.
1
+ // Thin Telegram Bot API client: HTTP and payload shapes only, no policy; the
2
+ // one file that talks to api.telegram.org. Long polling, so Pier needs no
3
+ // inbound HTTP. Behind a proxy: NODE_USE_ENV_PROXY=1 and HTTPS_PROXY.
8
4
  import { readCapped } from "../core/inbox.js";
9
5
  const BASE = "https://api.telegram.org";
10
6
  export class TelegramApi {
@@ -13,9 +9,7 @@ export class TelegramApi {
13
9
  this.token = token;
14
10
  }
15
11
  async call(method, payload, timeoutMs = 30_000, retry = true) {
16
- // An upload is the one call that is not JSON: FormData carries the bytes,
17
- // and fetch sets its own multipart boundary. It is re-sendable, so the
18
- // flood retry below still works on it.
12
+ // FormData is re-sendable, so the flood retry below still works on it.
19
13
  const multipart = payload instanceof FormData;
20
14
  const res = await fetch(`${BASE}/bot${this.token}/${method}`, {
21
15
  method: "POST",
@@ -26,9 +20,8 @@ export class TelegramApi {
26
20
  const body = (await res.json());
27
21
  if (body.ok)
28
22
  return body.result;
29
- // Telegram answers a flood (a long turn split into chunks hits ~1 msg/s per
30
- // chat) with the exact wait. Obeying it once turns a dropped reply into a
31
- // late one; a second 429 is a real problem and surfaces as an error.
23
+ // A long turn split into chunks hits ~1 msg/s per chat; `retry_after` is
24
+ // the exact wait. Obeyed once; a second 429 throws.
32
25
  const after = body.parameters?.retry_after;
33
26
  if (retry && after !== undefined && after <= 60) {
34
27
  await new Promise((r) => setTimeout(r, (after + 1) * 1000));
@@ -45,19 +38,15 @@ export class TelegramApi {
45
38
  sendMessage(payload) {
46
39
  return this.call("sendMessage", payload);
47
40
  }
48
- /** Bytes, not a file_id or a URL: the file is local to this machine, which
49
- * is the whole reason the agent could not just link it. */
50
41
  async sendFile({ chat_id, message_thread_id, file }) {
51
42
  const form = new FormData();
52
43
  form.set("chat_id", String(chat_id));
53
44
  if (message_thread_id !== undefined)
54
45
  form.set("message_thread_id", String(message_thread_id));
55
46
  const field = file.image ? "photo" : "document";
56
- // Copied into a fresh view: a Blob part must be backed by an ArrayBuffer,
57
- // and a Buffer read off disk carries the wider ArrayBufferLike type.
47
+ // A Blob part must be backed by an ArrayBuffer, not a Buffer's ArrayBufferLike.
58
48
  form.set(field, new Blob([new Uint8Array(file.bytes)]), file.name);
59
- // A long upload on a slow link is not a hung request; 30s is the budget
60
- // for a JSON call, not for megabytes.
49
+ // 30s is the budget for a JSON call, not for megabytes on a slow link.
61
50
  await this.call(file.image ? "sendPhoto" : "sendDocument", form, 120_000);
62
51
  }
63
52
  async editMessage(payload) {
@@ -82,7 +71,6 @@ export class TelegramApi {
82
71
  async answerCallbackQuery(id, text) {
83
72
  await this.call("answerCallbackQuery", { callback_query_id: id, text });
84
73
  }
85
- /** Bounded mid-stream: metadata size is the platform's word, not a cap. */
86
74
  async downloadFile(fileId, maxBytes) {
87
75
  const file = await this.call("getFile", { file_id: fileId });
88
76
  if (!file.file_path)
@@ -1,10 +1,5 @@
1
- // Telegram's half of the settings panel: HTML markup, an inline keyboard, and
2
- // a forced reply for the one typed answer. The panel itself lives in
3
- // `panel.ts`.
4
- //
5
- // Asking for a working directory costs a Map here: a forced reply arrives as
6
- // an ordinary message, so the prompt's message id has to be remembered to
7
- // recognize the answer. Slack's modal carries that context itself.
1
+ // Telegram's half of the settings panel (panel.ts has the rest). A forced reply
2
+ // arrives as an ordinary message, so the prompt's id is remembered to recognize it.
8
3
  import { ChatPanel, CWD_PLACEHOLDER, CWD_TAIL, PANEL_PREFIX, } from "./panel.js";
9
4
  import { escapeHtml as esc } from "./telegram-render.js";
10
5
  const button = (b) => ({ text: b.label, callback_data: `${PANEL_PREFIX}${b.action}` });
@@ -12,7 +7,6 @@ export class TelegramPanel extends ChatPanel {
12
7
  deps;
13
8
  platform = "telegram";
14
9
  fence = ["<code>", "</code>"];
15
- /** Conversations waiting for a typed working directory (ForceReply). */
16
10
  cwdPrompts = new Map();
17
11
  constructor(deps) {
18
12
  super(deps);
@@ -21,7 +15,6 @@ export class TelegramPanel extends ChatPanel {
21
15
  esc(text) {
22
16
  return esc(text);
23
17
  }
24
- /** Topics are a Telegram-only gate, and only on a forum. */
25
18
  gateExtras(chat, policy) {
26
19
  return chat.kind === "forum" ? ` · topics ${policy.topicMode ? "on" : "off"}` : "";
27
20
  }
@@ -41,7 +34,6 @@ export class TelegramPanel extends ChatPanel {
41
34
  ],
42
35
  };
43
36
  }
44
- /** Open a fresh panel, replacing whichever one this conversation had. */
45
37
  async open(key, chatId, topicId) {
46
38
  const view = await this.view(key, chatId);
47
39
  const sent = await this.deps.api.sendMessage({
@@ -67,10 +59,7 @@ export class TelegramPanel extends ChatPanel {
67
59
  .catch((err) => this.deps.log(`panel close failed: ${String(err)}`));
68
60
  }
69
61
  // --- actions -----------------------------------------------------------------
70
- /**
71
- * Handle a `cfg:` button. Returns false when the payload is not ours, so the
72
- * caller can treat it as one of the agent's next-step labels instead.
73
- */
62
+ /** Returns false when the payload is not ours. */
74
63
  async onCallback(query, key) {
75
64
  return this.dispatch(key, query.data ?? "", undefined, async () => {
76
65
  const message = query.message;
@@ -83,16 +72,12 @@ export class TelegramPanel extends ChatPanel {
83
72
  const sent = await this.deps.api.sendMessage({
84
73
  chat_id: state.chatId,
85
74
  message_thread_id: state.topicId,
86
- // Said plainly: this is not an edit, it is a new session.
87
75
  text: `Reply with an absolute path. ${CWD_TAIL}`,
88
76
  reply_markup: { force_reply: true, input_field_placeholder: CWD_PLACEHOLDER },
89
77
  });
90
78
  this.cwdPrompts.set(key.conversationId, sent.message_id);
91
79
  }
92
- /**
93
- * Consume a reply to the working-directory prompt. Returns true when this
94
- * message was that answer and must not reach the agent.
95
- */
80
+ /** True when this message was the answer and must not reach the agent. */
96
81
  async consumeCwdReply(msg, key) {
97
82
  const pending = this.cwdPrompts.get(key.conversationId);
98
83
  if (!pending || msg.reply_to_message?.message_id !== pending)
@@ -100,8 +85,7 @@ export class TelegramPanel extends ChatPanel {
100
85
  this.cwdPrompts.delete(key.conversationId);
101
86
  const path = (msg.text ?? "").trim();
102
87
  const started = await this.startSessionIn(key, path);
103
- // The answer was typed in the chat, so the outcome is said in the chat:
104
- // a panel note alone would be easy to miss under one's own message.
88
+ // The answer was typed in the chat, so the outcome is said there too.
105
89
  await this.deps.api.sendMessage({
106
90
  chat_id: msg.chat.id,
107
91
  message_thread_id: msg.message_thread_id,
@@ -1,13 +1,8 @@
1
1
  import { chunkText } from "./chunk.js";
2
- // How a reply looks on Telegram: text and buttons.
3
- //
4
- // Markdown → Telegram Bot API HTML. Telegram's parser accepts a tiny tag set
5
- // and rejects the whole message on anything else, so we escape first and
6
- // reintroduce exactly the tags it documents: b, i, s, code, pre, a.
7
- // Unsupported markdown (tables, images, nested lists) degrades to plain text
8
- // rather than losing the message.
2
+ // How a reply looks on Telegram: markdown → Bot API HTML. Telegram's parser
3
+ // rejects the whole message on any tag outside its documented set (b, i, s,
4
+ // code, pre, a), so text is escaped first and only those are reintroduced.
9
5
  const MAX_CHARS = 3800; // Telegram's hard limit is 4096; leave room for tags.
10
- /** Shared: the adapter and the panel escape plain text with this too. */
11
6
  export const escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
12
7
  /** Inline emphasis, applied after escaping so `<b>` can only come from us. */
13
8
  function inline(text) {
@@ -17,17 +12,11 @@ function inline(text) {
17
12
  .replace(/\*\*([^\n*]+)\*\*/g, "<b>$1</b>")
18
13
  .replace(/~~([^\n~]+)~~/g, "<s>$1</s>")
19
14
  .replace(/(^|[\s(])[*_]([^\n*_]+)[*_](?=[\s).,!?:;]|$)/g, "$1<i>$2</i>")
20
- // Headings carry no size in Telegram; bold is the closest honest render.
21
15
  .replace(/^#{1,6}[ \t]+(.+)$/gm, "<b>$1</b>");
22
16
  }
23
- /**
24
- * Render one assistant turn. Code spans and fences are extracted before
25
- * escaping so emphasis inside them stays literal.
26
- */
17
+ /** Code is stashed before escaping so emphasis inside it stays literal. */
27
18
  export function toTelegramHtml(markdown) {
28
19
  const stash = [];
29
- // Private-use sentinels: markdown can't contain them, so a stashed block
30
- // can't be re-matched by the escaping and emphasis passes that follow.
31
20
  const keep = (html) => `\uE000${stash.push(html) - 1}\uE001`;
32
21
  let out = markdown.replace(/```([\w.+-]*)\n?([\s\S]*?)```/g, (_m, lang, code) => keep(lang
33
22
  ? `<pre><code class="language-${escapeHtml(lang)}">${escapeHtml(code.replace(/\n+$/, ""))}</code></pre>`
@@ -36,31 +25,20 @@ export function toTelegramHtml(markdown) {
36
25
  out = inline(escapeHtml(out));
37
26
  return out.replace(/\uE000(\d+)\uE001/g, (_m, i) => stash[Number(i)] ?? "");
38
27
  }
39
- /**
40
- * Split rendered HTML into sendable chunks. A turn long enough to need this can
41
- * still be cut inside a <pre> block; Telegram closes the tag on its own and the
42
- * text survives, which beats dropping the turn. (Slack cannot do that, which is
43
- * why its renderer re-balances fences after the cut.)
44
- */
28
+ /** A cut inside a <pre> block is fine: Telegram closes the tag on its own. */
45
29
  export const chunk = (html) => chunkText(html, MAX_CHARS);
46
30
  // --- next-step buttons -------------------------------------------------------
47
- /**
48
- * Telegram caps `callback_data` at 64 *bytes* — about 21 CJK characters, far
49
- * too little to carry a label. Buttons send an index instead, and the label is
50
- * read back off the message's own keyboard.
51
- */
31
+ /** `callback_data` caps at 64 bytes (~21 CJK characters), so buttons send an
32
+ * index and the label is read back off the message's own keyboard. */
52
33
  export const OFFER_PREFIX = "sg:";
53
- // Buttons in one row share the row's width, so packing is budgeted by display
54
- // width rather than count: a CJK glyph takes about twice an ASCII one.
34
+ // Buttons in one row split its width evenly, so packing is budgeted by display
35
+ // width, not count.
55
36
  const ROW_WIDTH = 26;
56
37
  const ROW_BUTTONS = 3;
57
- /** Rough rendered width: CJK, fullwidth punctuation and emoji take two cells. */
38
+ /** CJK, fullwidth punctuation and emoji take two cells. */
58
39
  const displayWidth = (label) => [...label].reduce((n, ch) => n + ((ch.codePointAt(0) ?? 0) > 0x2e7f ? 2 : 1), 0);
59
- /**
60
- * Pack short labels onto shared rows, keeping the offered order. A row that is
61
- * already wide stops accepting: buttons in one Telegram row split the width
62
- * evenly, so squeezing a long label in truncates everything beside it.
63
- */
40
+ /** A row that is already wide stops accepting: a long label squeezed in
41
+ * truncates everything beside it. */
64
42
  export function keyboard(labels) {
65
43
  if (!labels.length)
66
44
  return undefined;
@@ -80,12 +58,7 @@ export function keyboard(labels) {
80
58
  });
81
59
  return { inline_keyboard: rows };
82
60
  }
83
- /**
84
- * The label a next-step payload stands for. Read off the tapped message's own
85
- * keyboard, which Telegram echoes back — so a button keeps working across a
86
- * restart or a config reload, where an in-memory offer list would not.
87
- * A payload we never wrote (an older label-as-payload button) passes through.
88
- */
61
+ /** A payload we never wrote (an older label-as-payload button) passes through. */
89
62
  export function offeredLabel(msg, data) {
90
63
  if (!data.startsWith(OFFER_PREFIX))
91
64
  return data;