@timqi/pier 0.0.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 (79) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +97 -0
  3. package/dist/agent/config.js +133 -0
  4. package/dist/agent/credentials.js +179 -0
  5. package/dist/agent/events.js +253 -0
  6. package/dist/agent/models.js +15 -0
  7. package/dist/agent/pi.js +296 -0
  8. package/dist/boards/boards.js +200 -0
  9. package/dist/boards/pier.css +445 -0
  10. package/dist/channels/chains.js +67 -0
  11. package/dist/channels/chunk.js +28 -0
  12. package/dist/channels/commands.js +28 -0
  13. package/dist/channels/config.js +172 -0
  14. package/dist/channels/control.js +71 -0
  15. package/dist/channels/conversations.js +65 -0
  16. package/dist/channels/gatekeeper.js +63 -0
  17. package/dist/channels/panel.js +233 -0
  18. package/dist/channels/receipts.js +104 -0
  19. package/dist/channels/routes.js +110 -0
  20. package/dist/channels/runtime.js +76 -0
  21. package/dist/channels/slack-api.js +296 -0
  22. package/dist/channels/slack-directory.js +77 -0
  23. package/dist/channels/slack-outbound.js +121 -0
  24. package/dist/channels/slack-panel.js +122 -0
  25. package/dist/channels/slack-render.js +214 -0
  26. package/dist/channels/slack-tool.js +334 -0
  27. package/dist/channels/slack.js +510 -0
  28. package/dist/channels/telegram-api.js +78 -0
  29. package/dist/channels/telegram-panel.js +113 -0
  30. package/dist/channels/telegram-render.js +96 -0
  31. package/dist/channels/telegram.js +473 -0
  32. package/dist/channels/types.js +27 -0
  33. package/dist/cli.js +101 -0
  34. package/dist/core/hub.js +53 -0
  35. package/dist/core/identity.js +66 -0
  36. package/dist/core/queue.js +11 -0
  37. package/dist/core/reply.js +202 -0
  38. package/dist/core/router.js +189 -0
  39. package/dist/core/types.js +7 -0
  40. package/dist/db.js +268 -0
  41. package/dist/log.js +55 -0
  42. package/dist/main.js +183 -0
  43. package/dist/paths.js +17 -0
  44. package/dist/secrets.js +191 -0
  45. package/dist/service.js +134 -0
  46. package/dist/settings.js +57 -0
  47. package/dist/tasks/agent.js +197 -0
  48. package/dist/tasks/callbacks.js +140 -0
  49. package/dist/tasks/command.js +74 -0
  50. package/dist/tasks/definitions.js +316 -0
  51. package/dist/tasks/execution.js +141 -0
  52. package/dist/tasks/groups.js +187 -0
  53. package/dist/tasks/messages.js +248 -0
  54. package/dist/tasks/routes.js +219 -0
  55. package/dist/tasks/runs.js +104 -0
  56. package/dist/tasks/service.js +282 -0
  57. package/dist/tasks/store.js +168 -0
  58. package/dist/tasks/tool.js +281 -0
  59. package/dist/tasks/types.js +5 -0
  60. package/dist/web/auth.js +280 -0
  61. package/dist/web/files.js +167 -0
  62. package/dist/web/public/assets/index-8CinH1uR.css +2 -0
  63. package/dist/web/public/assets/index-DAgP1Gq8.js +78 -0
  64. package/dist/web/public/icon-192.png +0 -0
  65. package/dist/web/public/icon-32.png +0 -0
  66. package/dist/web/public/icon-512.png +0 -0
  67. package/dist/web/public/icon-maskable-512.png +0 -0
  68. package/dist/web/public/icon-touch-192.png +0 -0
  69. package/dist/web/public/icon.svg +19 -0
  70. package/dist/web/public/index.html +251 -0
  71. package/dist/web/public/manifest.webmanifest +16 -0
  72. package/dist/web/public/sw.js +21 -0
  73. package/dist/web/server.js +366 -0
  74. package/dist/web/session-state.js +39 -0
  75. package/docs/deploy.md +307 -0
  76. package/package.json +55 -0
  77. package/skills/pier-boards/SKILL.md +210 -0
  78. package/skills/pier-slack/SKILL.md +135 -0
  79. package/skills/pier-tasks/SKILL.md +120 -0
@@ -0,0 +1,445 @@
1
+ /* pier.css — the one stylesheet every Board can link, served at
2
+ /boards/_assets/pier.css. Classless on purpose: plain semantic HTML must
3
+ look finished, so a board needs no build step and no framework. A handful of
4
+ named helpers cover the shapes a report actually asks for. */
5
+
6
+ :root {
7
+ --bg: #fbfbfa;
8
+ --fg: #1f2328;
9
+ --muted: #6a737d;
10
+ --line: #e4e4e2;
11
+ --card: #ffffff;
12
+ --accent: #4f46e5;
13
+ --code-bg: #f4f4f3;
14
+ /* Semantic palette: colour encodes meaning (good / attention / problem),
15
+ never decoration. Boards are presentations, so these are first-class. */
16
+ --good: #0f7b4f;
17
+ --warn: #a25b00;
18
+ --bad: #b42318;
19
+ color-scheme: light dark;
20
+ }
21
+
22
+ @media (prefers-color-scheme: dark) {
23
+ :root {
24
+ --bg: #16181c;
25
+ --fg: #e6e7e9;
26
+ --muted: #9aa0a6;
27
+ --line: #2c2f36;
28
+ --card: #1c1f24;
29
+ --accent: #8b87f7;
30
+ --code-bg: #22262c;
31
+ --good: #4ade80;
32
+ --warn: #fbbf24;
33
+ --bad: #f87171;
34
+ }
35
+ }
36
+
37
+ *,
38
+ *::before,
39
+ *::after {
40
+ box-sizing: border-box;
41
+ }
42
+
43
+ html {
44
+ -webkit-text-size-adjust: 100%;
45
+ }
46
+
47
+ /* The page fills a desktop window and reflows down to a phone: the canvas
48
+ widens with the viewport so tables, KPI rows and split layouts get room,
49
+ while running prose keeps a readable measure (--measure) instead of
50
+ stretching into 150-character lines. */
51
+ body {
52
+ --measure: 46rem;
53
+ margin: 0 auto;
54
+ max-width: 46rem;
55
+ padding: 2rem 1.15rem 5rem;
56
+ background: var(--bg);
57
+ color: var(--fg);
58
+ font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
59
+ "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
60
+ }
61
+
62
+ @media (min-width: 64rem) {
63
+ body {
64
+ max-width: 62rem;
65
+ padding: 2.5rem 2rem 5rem;
66
+ }
67
+ }
68
+
69
+ @media (min-width: 100rem) {
70
+ body {
71
+ max-width: 76rem;
72
+ }
73
+ }
74
+
75
+ /* Top-level prose only — inside a card, callout or column the box is the
76
+ measure already. */
77
+ body > p,
78
+ body > ul,
79
+ body > ol,
80
+ body > dl,
81
+ body > blockquote {
82
+ max-width: var(--measure);
83
+ }
84
+
85
+ .hero .lede,
86
+ .hero > p {
87
+ max-width: var(--measure);
88
+ }
89
+
90
+ h1,
91
+ h2,
92
+ h3,
93
+ h4 {
94
+ line-height: 1.25;
95
+ margin: 2.2rem 0 0.6rem;
96
+ font-weight: 640;
97
+ letter-spacing: -0.01em;
98
+ }
99
+
100
+ h1 {
101
+ margin-top: 0;
102
+ font-size: 1.75rem;
103
+ }
104
+ h2 {
105
+ font-size: 1.3rem;
106
+ }
107
+ h3 {
108
+ font-size: 1.08rem;
109
+ }
110
+ h4 {
111
+ font-size: 1rem;
112
+ color: var(--muted);
113
+ }
114
+
115
+ p,
116
+ ul,
117
+ ol,
118
+ dl,
119
+ pre,
120
+ table,
121
+ blockquote,
122
+ figure {
123
+ margin: 0 0 1rem;
124
+ }
125
+
126
+ ul,
127
+ ol {
128
+ padding-left: 1.35rem;
129
+ }
130
+ li + li {
131
+ margin-top: 0.25rem;
132
+ }
133
+
134
+ a {
135
+ color: var(--accent);
136
+ text-underline-offset: 2px;
137
+ }
138
+
139
+ strong {
140
+ font-weight: 640;
141
+ }
142
+
143
+ hr {
144
+ height: 0;
145
+ margin: 2rem 0;
146
+ border: 0;
147
+ border-top: 1px solid var(--line);
148
+ }
149
+
150
+ blockquote {
151
+ padding: 0.15rem 0 0.15rem 0.9rem;
152
+ border-left: 3px solid var(--line);
153
+ color: var(--muted);
154
+ }
155
+
156
+ code,
157
+ kbd,
158
+ samp {
159
+ padding: 0.1em 0.32em;
160
+ border-radius: 4px;
161
+ background: var(--code-bg);
162
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
163
+ font-size: 0.875em;
164
+ }
165
+
166
+ pre {
167
+ overflow-x: auto;
168
+ padding: 0.85rem 1rem;
169
+ border-radius: 8px;
170
+ background: var(--code-bg);
171
+ line-height: 1.5;
172
+ }
173
+
174
+ pre code {
175
+ padding: 0;
176
+ background: none;
177
+ font-size: 0.85rem;
178
+ }
179
+
180
+ /* Tables scroll inside themselves rather than widening the page on a phone. */
181
+ table {
182
+ display: block;
183
+ width: 100%;
184
+ overflow-x: auto;
185
+ border-collapse: collapse;
186
+ font-size: 0.94rem;
187
+ }
188
+
189
+ th,
190
+ td {
191
+ padding: 0.45rem 0.7rem;
192
+ border-bottom: 1px solid var(--line);
193
+ text-align: left;
194
+ vertical-align: top;
195
+ }
196
+
197
+ th {
198
+ font-size: 0.78rem;
199
+ font-weight: 620;
200
+ text-transform: uppercase;
201
+ letter-spacing: 0.04em;
202
+ color: var(--muted);
203
+ white-space: nowrap;
204
+ }
205
+
206
+ /* Zebra rows: a comparison table is the most common board content, and this
207
+ is what makes a long one scannable. */
208
+ tbody tr:nth-child(even) {
209
+ background: color-mix(in oklab, var(--fg) 3.5%, transparent);
210
+ }
211
+
212
+ tbody tr:last-child td {
213
+ border-bottom: 0;
214
+ }
215
+
216
+ td:has(+ td:last-child),
217
+ td:last-child {
218
+ font-variant-numeric: tabular-nums;
219
+ }
220
+
221
+ img,
222
+ svg,
223
+ video {
224
+ max-width: 100%;
225
+ height: auto;
226
+ }
227
+
228
+ details {
229
+ margin: 0 0 1rem;
230
+ padding: 0.7rem 0.9rem;
231
+ border: 1px solid var(--line);
232
+ border-radius: 8px;
233
+ background: var(--card);
234
+ }
235
+
236
+ summary {
237
+ cursor: pointer;
238
+ font-weight: 560;
239
+ }
240
+
241
+ details[open] summary {
242
+ margin-bottom: 0.6rem;
243
+ }
244
+
245
+ footer {
246
+ margin-top: 3rem;
247
+ padding-top: 1rem;
248
+ border-top: 1px solid var(--line);
249
+ color: var(--muted);
250
+ font-size: 0.85rem;
251
+ }
252
+
253
+ /* --- helpers: the shapes a report keeps needing ------------------------- */
254
+
255
+ .muted {
256
+ color: var(--muted);
257
+ }
258
+
259
+ /* One-sentence answer under the title, before any detail. */
260
+ .lede {
261
+ font-size: 1.08rem;
262
+ line-height: 1.5;
263
+ color: var(--muted);
264
+ }
265
+
266
+ .good {
267
+ color: var(--good);
268
+ }
269
+ .warn {
270
+ color: var(--warn);
271
+ }
272
+ .bad {
273
+ color: var(--bad);
274
+ }
275
+
276
+ /* Right-align and tabulate a numeric column: `<td class="num">`. */
277
+ .num {
278
+ text-align: right;
279
+ font-variant-numeric: tabular-nums;
280
+ }
281
+
282
+ .card {
283
+ padding: 0.9rem 1.1rem;
284
+ margin: 0 0 1rem;
285
+ border: 1px solid var(--line);
286
+ border-radius: 10px;
287
+ background: var(--card);
288
+ }
289
+
290
+ /* A status card reads at a glance from across the room. */
291
+ .card.good,
292
+ .card.warn,
293
+ .card.bad,
294
+ .card.info {
295
+ border-left: 3px solid var(--c);
296
+ background: color-mix(in oklab, var(--c) 7%, var(--card));
297
+ }
298
+ .card.good {
299
+ --c: var(--good);
300
+ }
301
+ .card.warn {
302
+ --c: var(--warn);
303
+ }
304
+ .card.bad {
305
+ --c: var(--bad);
306
+ }
307
+ .card.info {
308
+ --c: var(--accent);
309
+ }
310
+
311
+ /* Opening block: title, one-line answer, and the numbers that matter. */
312
+ .hero {
313
+ margin: 0 0 1.75rem;
314
+ padding: 1.4rem 1.5rem;
315
+ border-radius: 14px;
316
+ background: color-mix(in oklab, var(--accent) 9%, var(--card));
317
+ }
318
+ .hero h1 {
319
+ margin-bottom: 0.4rem;
320
+ }
321
+ .hero > :last-child {
322
+ margin-bottom: 0;
323
+ }
324
+
325
+ /* Two columns that become one on a phone — before/after, text + aside. */
326
+ .split {
327
+ display: grid;
328
+ gap: 1rem;
329
+ grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
330
+ margin: 0 0 1rem;
331
+ }
332
+
333
+ /* Auto-fitting so a KPI row reflows to one column on a phone with no query. */
334
+ .grid {
335
+ display: grid;
336
+ gap: 0.75rem;
337
+ grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
338
+ margin: 0 0 1rem;
339
+ }
340
+
341
+ .kpi {
342
+ display: block;
343
+ font-size: 1.65rem;
344
+ font-weight: 650;
345
+ line-height: 1.15;
346
+ letter-spacing: -0.02em;
347
+ font-variant-numeric: tabular-nums;
348
+ }
349
+
350
+ .tag {
351
+ display: inline-block;
352
+ padding: 0.08em 0.5em;
353
+ border: 1px solid var(--line);
354
+ border-radius: 999px;
355
+ background: var(--card);
356
+ font-size: 0.78rem;
357
+ color: var(--muted);
358
+ white-space: nowrap;
359
+ }
360
+
361
+ /* Status colour on tags, cards and callouts. Pair it with a word — colour
362
+ alone fails in print, in dark mode and for colour-blind readers. */
363
+ .tag.good,
364
+ .tag.warn,
365
+ .tag.bad,
366
+ .tag.info {
367
+ border-color: currentColor;
368
+ color: var(--c);
369
+ background: color-mix(in oklab, var(--c) 12%, transparent);
370
+ }
371
+ .tag.good {
372
+ --c: var(--good);
373
+ }
374
+ .tag.warn {
375
+ --c: var(--warn);
376
+ }
377
+ .tag.bad {
378
+ --c: var(--bad);
379
+ }
380
+ .tag.info {
381
+ --c: var(--accent);
382
+ }
383
+
384
+ /* The takeaway box: what the reader must not miss. */
385
+ .callout {
386
+ --c: var(--accent);
387
+ margin: 0 0 1rem;
388
+ padding: 0.75rem 1rem;
389
+ border-left: 3px solid var(--c);
390
+ border-radius: 0 8px 8px 0;
391
+ background: color-mix(in oklab, var(--c) 8%, transparent);
392
+ }
393
+ .callout > :last-child {
394
+ margin-bottom: 0;
395
+ }
396
+ .callout.good {
397
+ --c: var(--good);
398
+ }
399
+ .callout.warn {
400
+ --c: var(--warn);
401
+ }
402
+ .callout.bad {
403
+ --c: var(--bad);
404
+ }
405
+
406
+ /* Proportion bar — the honest chart for "share of total" inside a table,
407
+ with no library and no SVG: `<span class="bar" style="--v:62%"></span>`. */
408
+ .bar {
409
+ display: block;
410
+ min-width: 3rem;
411
+ height: 0.45rem;
412
+ border-radius: 999px;
413
+ background: color-mix(in oklab, var(--fg) 10%, transparent);
414
+ }
415
+ .bar::after {
416
+ content: "";
417
+ display: block;
418
+ width: var(--v, 0);
419
+ height: 100%;
420
+ border-radius: inherit;
421
+ background: var(--c, var(--accent));
422
+ }
423
+ .bar.good {
424
+ --c: var(--good);
425
+ }
426
+ .bar.warn {
427
+ --c: var(--warn);
428
+ }
429
+ .bar.bad {
430
+ --c: var(--bad);
431
+ }
432
+
433
+ @media print {
434
+ body {
435
+ max-width: none;
436
+ padding: 0;
437
+ background: #fff;
438
+ }
439
+ details {
440
+ break-inside: avoid;
441
+ }
442
+ details:not([open]) > *:not(summary) {
443
+ display: revert;
444
+ }
445
+ }
@@ -0,0 +1,67 @@
1
+ // Ordering: one promise chain per conversation.
2
+ //
3
+ // Messages from different chats are handled concurrently — a slow download must
4
+ // not stall another group — but messages within one chat strictly in arrival
5
+ // order, because a steer overtaking the message it interrupts reorders the
6
+ // conversation.
7
+ //
8
+ // Two rules live here because both adapters got to write them once and neither
9
+ // should write them twice:
10
+ //
11
+ // - Every link needs its own `catch`. One rejected handler otherwise poisons
12
+ // the chain and silences that chat for the life of the process — an ordering
13
+ // mechanism that fails closed, permanently.
14
+ // - `drain()` is bounded. `runtime.reload()` runs on the Console's save, so a
15
+ // stuck handler must not hold that request open.
16
+ export class Chains {
17
+ log;
18
+ maxActive;
19
+ active = new Map();
20
+ constructor(log,
21
+ /**
22
+ * How many conversations may be in flight before a new one queues behind an
23
+ * existing chain. Bounds concurrency (open sockets, downloads); a transport
24
+ * that can also slow its source down should do that separately.
25
+ */
26
+ maxActive = Infinity) {
27
+ this.log = log;
28
+ this.maxActive = maxActive;
29
+ }
30
+ get size() {
31
+ return this.active.size;
32
+ }
33
+ /** Append to a conversation's chain, dropping the entry once it drains. */
34
+ run(key, task) {
35
+ const mine = this.active.get(key);
36
+ // A conversation with a chain already waits on itself; only a new one has
37
+ // to wait for a slot. `active` is non-empty whenever the cap is hit, so the
38
+ // race always settles.
39
+ const start = mine ??
40
+ (this.active.size >= this.maxActive
41
+ ? Promise.race(this.active.values()).catch(() => { })
42
+ : Promise.resolve());
43
+ const next = start
44
+ .then(task)
45
+ .catch((err) => this.log(`handler failed in ${key}: ${String(err)}`));
46
+ this.active.set(key, next);
47
+ void next.then(() => {
48
+ if (this.active.get(key) === next)
49
+ this.active.delete(key);
50
+ });
51
+ }
52
+ /** Wait for the oldest in-flight chain — the backpressure primitive. */
53
+ oldest() {
54
+ return Promise.race(this.active.values()).catch(() => { });
55
+ }
56
+ /**
57
+ * Let in-flight work finish before a replacement adapter starts, but never
58
+ * wait forever: two adapters handling one message would prompt twice, and a
59
+ * hung handler holding up a config save is worse than either.
60
+ */
61
+ async drain(timeoutMs) {
62
+ await Promise.race([
63
+ Promise.allSettled(this.active.values()),
64
+ new Promise((resolve) => setTimeout(resolve, timeoutMs)),
65
+ ]);
66
+ }
67
+ }
@@ -0,0 +1,28 @@
1
+ // Splitting a long turn into sendable messages.
2
+ //
3
+ // Every platform caps a message, and the index arithmetic for "cut at the last
4
+ // blank line that fits, else the last newline, else mid-text" is fiddly enough
5
+ // that having it twice is having it wrong once. The cap and any per-platform
6
+ // repair (Slack re-balances code fences across the cut) stay with the renderer.
7
+ /**
8
+ * Cut `text` into pieces of at most `max` characters, preferring a blank line,
9
+ * then a newline, then the hard limit. A cut lands mid-text only when there is
10
+ * no break in the second half of the window — better a blunt split than a
11
+ * dropped turn.
12
+ */
13
+ export function chunkText(text, max) {
14
+ if (text.length <= max)
15
+ return [text];
16
+ const parts = [];
17
+ let rest = text;
18
+ while (rest.length > max) {
19
+ const window = rest.slice(0, max);
20
+ const cut = Math.max(window.lastIndexOf("\n\n"), window.lastIndexOf("\n"));
21
+ const at = cut > max / 2 ? cut : max;
22
+ parts.push(rest.slice(0, at).trimEnd());
23
+ rest = rest.slice(at).trimStart();
24
+ }
25
+ if (rest)
26
+ parts.push(rest);
27
+ return parts;
28
+ }
@@ -0,0 +1,28 @@
1
+ // Slash commands in IM text, parsed once for every platform.
2
+ //
3
+ // Every chat platform types commands the same way and mangles them slightly
4
+ // differently: Telegram appends `@botname` when several bots share a group,
5
+ // clients add stray whitespace, and users capitalise. One parser so an adapter
6
+ // never re-derives "is this a command" and the answer never drifts between
7
+ // platforms.
8
+ /**
9
+ * Parse a command out of one inbound message. Whitespace on both ends is
10
+ * dropped first, then a leading `/` is required: anything else is ordinary
11
+ * text and must reach the agent untouched.
12
+ */
13
+ export function parseCommand(text) {
14
+ const trimmed = text.trim();
15
+ if (!trimmed.startsWith("/"))
16
+ return null;
17
+ // Args are taken verbatim, not re-joined from split words: a path or a
18
+ // sentence must survive with its own spacing intact.
19
+ const match = /^\/(\S+)[ \t]*([\s\S]*)$/.exec(trimmed);
20
+ const [name = "", target] = (match?.[1] ?? "").split("@");
21
+ if (!name)
22
+ return null; // a bare "/" is not a command
23
+ return {
24
+ name: name.toLowerCase(),
25
+ args: match?.[2] ?? "",
26
+ ...(target ? { target } : {}),
27
+ };
28
+ }