@timqi/pier 0.1.0 → 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 (158) hide show
  1. package/README.md +58 -125
  2. package/dist/agent/config.js +6 -15
  3. package/dist/agent/credentials.js +11 -23
  4. package/dist/agent/events.js +42 -64
  5. package/dist/agent/listing.js +39 -92
  6. package/dist/agent/pi.js +103 -252
  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 +7 -23
  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 +12 -34
  45. package/dist/core/queue.js +3 -5
  46. package/dist/core/reply.js +41 -142
  47. package/dist/core/router.js +209 -264
  48. package/dist/core/types.js +4 -0
  49. package/dist/db.js +88 -272
  50. package/dist/drain.js +57 -50
  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 +4 -12
  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 +84 -183
  63. package/dist/paths.js +10 -26
  64. package/dist/secrets.js +18 -45
  65. package/dist/service.js +31 -73
  66. package/dist/settings.js +19 -63
  67. package/dist/tasks/agent.js +24 -45
  68. package/dist/tasks/callbacks.js +8 -16
  69. package/dist/tasks/command.js +2 -6
  70. package/dist/tasks/definitions.js +39 -62
  71. package/dist/tasks/execution.js +41 -39
  72. package/dist/tasks/groups.js +8 -11
  73. package/dist/tasks/messages.js +88 -155
  74. package/dist/tasks/outbox.js +33 -54
  75. package/dist/tasks/routes.js +4 -7
  76. package/dist/tasks/runs.js +4 -9
  77. package/dist/tasks/service.js +29 -42
  78. package/dist/tasks/store.js +36 -27
  79. package/dist/tasks/tool.js +59 -60
  80. package/dist/tools-task.js +20 -60
  81. package/dist/tools.js +98 -325
  82. package/dist/update.js +20 -43
  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 +35 -82
  89. package/dist/web/providers.js +5 -11
  90. package/dist/web/public/assets/{activity-Bl3vZukb.js → activity-B89_hH7q.js} +1 -1
  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-DYuf4Mlj.js → boards-BeKW0ZXK.js} +1 -1
  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-BLJu7EXN.js → runs-Cwy0mN8i.js} +1 -1
  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-BrdVh-Zi.js → settings-DzZLmujq.js} +1 -1
  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-CeQS1rxa.js → task-runs-BCakxFk8.js} +1 -1
  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 +30 -16
  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/sw.js +14 -2
  121. package/dist/web/public/sw.js.br +0 -0
  122. package/dist/web/public/sw.js.gz +0 -0
  123. package/dist/web/push.js +55 -77
  124. package/dist/web/route.js +3 -7
  125. package/dist/web/server.js +109 -190
  126. package/dist/web/session-state.js +13 -53
  127. package/dist/web/types.js +2 -4
  128. package/dist/web/webpush.js +10 -25
  129. package/docs/deploy.md +115 -330
  130. package/package.json +1 -1
  131. package/skills/pier-boards/SKILL.md +81 -160
  132. package/skills/pier-help/SKILL.md +23 -20
  133. package/skills/pier-slack/SKILL.md +2 -2
  134. package/skills/pier-tasks/SKILL.md +23 -15
  135. package/dist/config-sync-fetch.js +0 -84
  136. package/dist/limits.js +0 -14
  137. package/dist/web/public/assets/activity-Bl3vZukb.js.br +0 -0
  138. package/dist/web/public/assets/activity-Bl3vZukb.js.gz +0 -0
  139. package/dist/web/public/assets/boards-DYuf4Mlj.js.br +0 -0
  140. package/dist/web/public/assets/boards-DYuf4Mlj.js.gz +0 -0
  141. package/dist/web/public/assets/explorer-qJH_9nTE.js +0 -4
  142. package/dist/web/public/assets/explorer-qJH_9nTE.js.br +0 -0
  143. package/dist/web/public/assets/explorer-qJH_9nTE.js.gz +0 -0
  144. package/dist/web/public/assets/index-Dqdb-Eqt.js +0 -85
  145. package/dist/web/public/assets/index-Dqdb-Eqt.js.br +0 -0
  146. package/dist/web/public/assets/index-Dqdb-Eqt.js.gz +0 -0
  147. package/dist/web/public/assets/index-DzmMzvi_.css +0 -2
  148. package/dist/web/public/assets/index-DzmMzvi_.css.br +0 -0
  149. package/dist/web/public/assets/index-DzmMzvi_.css.gz +0 -0
  150. package/dist/web/public/assets/runs-BLJu7EXN.js.br +0 -0
  151. package/dist/web/public/assets/runs-BLJu7EXN.js.gz +0 -0
  152. package/dist/web/public/assets/settings-BrdVh-Zi.js.br +0 -0
  153. package/dist/web/public/assets/settings-BrdVh-Zi.js.gz +0 -0
  154. package/dist/web/public/assets/task-runs-CeQS1rxa.js.br +0 -0
  155. package/dist/web/public/assets/task-runs-CeQS1rxa.js.gz +0 -0
  156. package/dist/web/public/assets/tasks-bcb3fYdK.js +0 -4
  157. package/dist/web/public/assets/tasks-bcb3fYdK.js.br +0 -0
  158. package/dist/web/public/assets/tasks-bcb3fYdK.js.gz +0 -0
package/docs/deploy.md CHANGED
@@ -1,15 +1,7 @@
1
1
  # Running Pier as a service (Linux, systemd)
2
2
 
3
- Pier is one Node process that binds the loopback. Running it in a terminal
4
- (`pier serve`) is a complete installation — nothing below is required to *use* Pier. What systemd
5
- adds is the thing a terminal cannot: scheduled tasks fire and IM channels stay
6
- connected while nobody is logged in.
7
-
8
- That is also why this document is Linux-only. A laptop closes its lid; the
9
- machine you want always-on is a server, and on that machine systemd is already
10
- there.
11
-
12
- ## The short version
3
+ `pier serve` in a terminal is a complete installation; systemd keeps tasks
4
+ firing and channels connected while nobody is logged in. Linux only.
13
5
 
14
6
  ```sh
15
7
  npm install -g @timqi/pier
@@ -17,225 +9,97 @@ pier service install
17
9
  journalctl --user -u pier -e # the password, printed once
18
10
  ```
19
11
 
20
- That writes the unit below, enables linger, and starts the service. The rest of
21
- this page is what it wrote and why — read it before widening the bind, and when
22
- you want the unit to say something different.
12
+ That writes the units, enables linger, and starts the service. `systemctl
13
+ --user cat pier` shows the units with their per-line comments; this page is
14
+ what they do not say.
23
15
 
24
16
  ## Prerequisites
25
17
 
26
18
  - Node 24 or newer (`node:sqlite` is used unflagged).
27
- - A user-writable global npm prefix. The updater runs as you, so an initial
28
- install that needed `sudo npm install -g` cannot later update itself.
29
- - The `sqlite3` CLI is optional, for the off-machine backup and password steps
30
- below. Pier itself and its automatic update backup do not need it.
31
- - Pier installed globally: `npm install -g @timqi/pier`. The unit runs that
32
- installed entry point, so a deploy is `pier update`. A checkout
33
- (`git clone` + `npm ci && npm run build`) is the *develop* path; point the
34
- unit's `ExecStart` at its `dist/main.js` if you run one as the service, and
35
- update it with the "From a checkout" steps under Updating.
19
+ - A user-writable global npm prefix: the updater runs as you.
20
+ - `sqlite3` CLI: optional, for the off-machine backup and password steps below.
21
+ - Pier installed globally. A checkout (`git clone` + `npm ci && npm run build`)
22
+ is the *develop* path; point the unit's `ExecStart` at its `dist/main.js` if
23
+ you run one as the service, and update it with the checkout steps under
24
+ Updating.
36
25
 
37
26
  ## The unit
38
27
 
39
- A **user** unit, not a system one: Pier runs as you, reads your Pi
40
- configuration, and drives sessions in your own directories. Running it as root
41
- or as a dedicated system user means an agent that cannot touch the files you
42
- wanted it to work on.
43
-
44
- `~/.config/systemd/user/pier.service` — what `pier service install` generates,
45
- with your absolute Node and package paths filled in:
46
-
47
- ```ini
48
- [Unit]
49
- Description=Pier — agent workspace
50
- Documentation=https://github.com/timqi/pier
51
- After=network-online.target
52
- Wants=network-online.target
53
-
54
- [Service]
55
- Type=simple
56
- WorkingDirectory=%h
57
- # Absolute paths on purpose: systemd starts with a minimal PATH, so a node
58
- # installed by nvm/fnm/asdf is not on it — the installer fills in the node
59
- # that installed Pier and the globally installed entry point.
60
- ExecStart="/absolute/path/to/node" "/absolute/npm/prefix/lib/node_modules/@timqi/pier/dist/main.js"
61
- # Inherited by every command a turn runs: on systemd's bare PATH an agent asked
62
- # to run npm or node would be told they do not exist. The installer records the
63
- # PATH of the shell that ran it — that shell is your login one — with the node
64
- # above first and the standard directories as a floor. Installed a new tool
65
- # since? Re-run install --force, or add your own drop-in.
66
- Environment="PATH=/absolute/path/to/node/bin:/your/shell/PATH:/usr/local/bin:/usr/bin:/bin"
67
- # Loopback by default. Put a reverse proxy in front before widening this —
68
- # whoever reaches this port can drive an agent that runs a shell.
69
- Environment="HOST=127.0.0.1"
70
- Environment="PORT=3141"
71
- Restart=always
72
- RestartSec=2
73
- StandardOutput=journal
74
- StandardError=journal
75
- SyslogIdentifier=pier
76
-
77
- [Install]
78
- WantedBy=default.target
79
- ```
80
-
81
- `--pier-home` adds a quoted `PIER_HOME` environment line. Paths with spaces and
82
- literal systemd `%` specifiers are escaped. `pier service install` also writes an
83
- updater unit containing the exact npm executable currently on `PATH`. Re-run with
84
- `--force` after changing service settings or the Node/npm installation; it
85
- rewrites both units and restarts the running service. The limits drop-in remains
86
- operator-owned and is never overwritten.
87
-
88
- Enable it, and tell logind to keep your user manager alive after you log out —
89
- without lingering, every scheduled task stops when your SSH session ends:
90
-
91
- ```sh
92
- loginctl enable-linger "$USER"
93
- systemctl --user daemon-reload
94
- systemctl --user enable --now pier
95
- ```
28
+ A **user** unit. `~/.config/systemd/user/pier.service` records the absolute
29
+ node and entry point, the installing shell's `PATH`, and a loopback bind;
30
+ `--pier-home` adds `PIER_HOME`. `pier service install --force` rewrites both
31
+ units and restarts (after a new tool, or a moved Node); the limits drop-in is
32
+ never overwritten. Linger is enabled by the installer.
96
33
 
97
34
  ## Memory limits
98
35
 
99
- Worth setting: an agent runs whatever a turn decided to run, so the failure to
100
- plan for is a build or a test suite eating the machine, not Pier itself
101
- leaking. A cgroup limit turns "the box OOMs and sshd dies with it" into "the
102
- heaviest process inside this unit is killed".
103
-
104
- User units can do this without root as long as the memory controller is
105
- delegated to your user manager, which it is by default on current systemd:
36
+ Requires the memory controller delegated to your user manager (default on
37
+ current systemd):
106
38
 
107
39
  ```sh
108
40
  systemctl show "user@$(id -u).service" -p DelegateControllers
109
41
  # DelegateControllers=cpu memory pids ← memory listed means these work
110
42
  ```
111
43
 
112
- `~/.config/systemd/user/pier.service.d/limits.conf` — a drop-in, so the unit
113
- above stays about what Pier is and this file is about what it may consume:
114
-
115
- The limit is on the **whole unit**: `node`, every Pi subagent, and every
116
- command a turn ran, added together — plus the page cache those processes
117
- touched, which is why the soft ceiling should be the one that bites. So size it
118
- as a share of the machine, not as "how much should Pier need". Percentages are
119
- relative to installed physical memory, which also keeps this file portable:
120
-
121
- ```ini
122
- [Service]
123
- # Soft ceiling: past this the kernel reclaims hard and lets the unit crawl
124
- # instead of killing anything. This is the one that should bite first.
125
- MemoryHigh=60%
126
- # Hard ceiling: the kernel OOM-kills *inside this cgroup*. The number exists to
127
- # protect everything outside it — the OS, sshd, your other services — so what
128
- # it should leave behind is a few GB for them, not a small share for Pier.
129
- MemoryMax=75%
130
- # Swapping an agent is worse than failing it — the machine stops responding
131
- # long before the limit is reached.
132
- MemorySwapMax=0
133
- # A runaway command an agent ran can fork as well as allocate.
134
- TasksMax=512
135
- # The unit's own processes are the preferred victims if the *machine* still
136
- # runs out, e.g. before these limits are tuned. Works with no cgroup limit at
137
- # all, which makes it the cheapest half of this file.
138
- OOMScoreAdjust=200
139
- # A child being OOM-killed must not take the service with it: the turn that
140
- # ran it fails, Pier keeps serving. (Delegated units default to this; set
141
- # explicitly because the default depends on system configuration.)
142
- OOMPolicy=continue
143
- ```
144
-
145
- On a dedicated 4–8 GB VPS those percentages land around 2.5–6 GB, which is
146
- roughly what one agent doing ordinary work needs. On a big shared box, prefer
147
- absolute values sized to what you are willing to lose to a runaway build —
148
- `MemoryHigh=8G` / `MemoryMax=12G` on 32 GB, say — because 75% of a large
149
- machine is no longer a meaningful ceiling.
150
-
151
- Pick the numbers by measuring, not by guessing — Pier idles in the hundreds of
152
- MB, and what varies is what the agent runs:
44
+ The installer writes `~/.config/systemd/user/pier.service.d/limits.conf` once
45
+ (`MemoryHigh=60%`, `MemoryMax=75%`, no swap, `TasksMax=512`, `OOMPolicy=continue`,
46
+ each line commented). The limit covers the **whole unit** — `node`, Pi
47
+ subagents, every command a turn ran, their page cache. Dedicated 4–8 GB VPS:
48
+ the defaults land around 2.5–6 GB. Big shared box: absolute values
49
+ (`MemoryHigh=8G` / `MemoryMax=12G` on 32 GB). Pier idles in the hundreds of MB:
153
50
 
154
51
  ```sh
155
52
  systemctl --user show pier -p MemoryCurrent -p MemoryPeak
156
53
  systemd-cgtop --depth=3 "user.slice/user-$(id -u).slice"
157
54
  ```
158
55
 
159
- A limit that fires during ordinary work is worse than none: the turn dies with
160
- a signal and the cause is a kernel message nobody reads. Set it to protect the
161
- machine, then raise it the first time it kills something legitimate.
162
-
163
56
  ## Logs
164
57
 
165
- Pier writes to stdout and stderr and to nothing else — no log file, no
166
- rotation, no log configuration. Under the unit above that *is* the log:
167
- journald stamps the time, keeps the history and rotates it, which is why
168
- nothing in Pier reimplements any of that.
58
+ stdout and stderr only; journald stamps, keeps and rotates.
169
59
 
170
60
  ```sh
171
61
  journalctl --user -u pier -f # follow
172
62
  journalctl --user -u pier -p warning # only what went wrong
173
63
  journalctl --user -u pier --since -1h | grep 'tasks:' # one area
64
+ journalctl --user -u pier | grep 'client:' # browser-side errors
174
65
  ```
175
66
 
176
67
  Every line is `area: message` — `core`, `agent`, `tasks`, `slack`, `telegram`,
177
68
  `lark`, `channels`, `slack.tool`, `auth`, `boards`, `client`, `db`, `drain`,
178
- `secrets`, `settings`, `credentials`, `update`, `tools`, `push`,
179
- `web`, `web.providers`, `pier` — so an area is a grep
180
- and a level is a `-p`. The level reaches journald as a syslog priority prefix, which Pier
181
- emits only when systemd says the output is a journal (`$JOURNAL_STREAM`); run
182
- in a terminal, the same lines carry a timestamp and a level word instead.
183
-
184
- What is logged by default: process start and shutdown, sessions opening,
185
- conversation → session routing, every turn ending, every task run queued and
186
- settled, callback and message delivery failures with their retries, adapters
187
- starting (and failing to start or stop), dropped inbound messages, failed
188
- logins, and any request that threw. A watch probe that matched nothing is the
189
- one routine event kept at `debug` — it fires on every interval.
190
-
191
- `client:` is the browser's half, posted back by the workbench (`ui/report.ts`)
192
- and written into this same stream: script errors, unhandled rejections and a
193
- dead SSE stream, each with the view and the user agent. It comes from signed-in
194
- tabs only — the route sits behind the password like the rest of `/api`, so
195
- nobody who cannot already reach Pier can write into this journal. The person sees the
196
- same sentence in the chat pane, so "I clicked and nothing happened" has a line
197
- on both ends:
198
-
199
- ```sh
200
- journalctl --user -u pier | grep 'client:'
201
- ```
69
+ `secrets`, `settings`, `credentials`, `update`, `tools`, `push`, `web`,
70
+ `web.providers`, `pier`. Level: a syslog priority prefix under
71
+ `$JOURNAL_STREAM`, a level word in a terminal. `client:` is posted back by
72
+ signed-in workbench tabs (`ui/report.ts`): script errors, unhandled rejections,
73
+ a dead SSE stream, with view and user agent.
202
74
 
203
75
  ```sh
204
76
  systemctl --user set-environment PIER_LOG=debug # + per-message tracing
205
77
  systemctl --user restart pier
206
78
  ```
207
79
 
208
- `PIER_LOG` takes `debug`, `info` (default), `warn`, `error` or `silent`. Prefer
209
- turning `debug` on for a session and back off: it logs one line per inbound
210
- message and per tool call.
80
+ `PIER_LOG`: `debug`, `info` (default), `warn`, `error`, `silent`. `debug` logs
81
+ one line per inbound message and per tool call.
211
82
 
212
83
  ## First login
213
84
 
214
- Pier generates a password on an empty database and prints it once, so the
215
- journal is where you read it:
85
+ A password is generated on an empty database and printed once:
216
86
 
217
87
  ```sh
218
88
  journalctl --user -u pier | grep -A2 'no password'
219
89
  ```
220
90
 
221
- Only its scrypt hash is stored. If you lose it, drop the row and restart — a
222
- new password is generated and printed:
91
+ Only its scrypt hash is stored. Lost it? Drop the row and restart:
223
92
 
224
93
  ```sh
225
94
  sqlite3 ~/.pier/db/pier.db 'DELETE FROM auth'
226
95
  pier restart
227
96
  ```
228
97
 
229
- Changing the password signs out every browser: the session rows behind the
230
- cookies are deleted. So does the recovery above — a new password does not leave
231
- the old one's browsers signed in. To end one browser without changing the
232
- password, use Settings → Instance → Signed-in devices. A session that is not
233
- used expires 7 days after its last request. However a session ends — signed
234
- out, expired, password changed, password recovered — its push subscription is
235
- deleted with it, so that browser stops being notified as well.
236
-
237
- The upgrade that introduced these rows signs everyone out once: a cookie issued
238
- before it names no row. Have the password to hand.
98
+ - Changing or recovering the password signs out every browser.
99
+ - One browser: Settings → Instance → Signed-in devices.
100
+ - A browser session expires 7 days after its last request, and 90 days after
101
+ it signed in however often it is used.
102
+ - However a session ends, its push subscription goes with it.
239
103
 
240
104
  ## Restarting and reloading
241
105
 
@@ -245,34 +109,26 @@ pier reload # apply channel config and recycle idle sessions in place
245
109
  pier tools sync # install/update the managed CLI tools
246
110
  ```
247
111
 
248
- Both commands signal the installed systemd service; they are not foreground
249
- process controls. `pier restart` refuses new messages and root Task runs, waits
250
- up to five minutes for active work, then exits for `Restart=always` to start the
251
- next process. At the deadline it records every aborted IM turn first, and the
252
- next process posts that note after its adapter starts. Cleanup after the deadline
253
- has one shared 10-second bound regardless of how many sessions are stuck.
254
-
255
- `pier tools sync` converges the command-line tools switched on in Console →
256
- Settings — they install into `~/.pier/tools/bin`, which the service puts first
257
- on the PATH every session and task inherits. Normally a switch runs
258
- it for you as a task; typing it is for a machine that was offline when one was
259
- flipped. One sync runs at a time per machine: an overlapping one waits for the
260
- lock (and converges on the switches as they stand when its turn comes) instead
261
- of racing the other's installs.
262
-
263
- `pier reload` does not stop active work. It reloads Slack and Telegram adapters
264
- and immediately evicts idle sessions nobody is watching, so their next message
265
- opens with current agent files and configuration. Streaming sessions and
266
- sessions held by an open workbench stay attached until their normal eviction.
267
-
268
- Console → Settings → Instance → **Reload** is the same thing from a browser, and
269
- needs no shell on the box. It also takes the session open in the tab that asked
270
- (only a turn in flight is exempt) and answers with how many were recycled and
271
- how many are still mid-turn — the second number is the only reason a change can
272
- still fail to show up.
273
-
274
- An ordinary `systemctl --user restart pier` and `pier update` remain fast,
275
- hard-stop paths. Let active work finish first when using either one.
112
+ All three signal the installed service.
113
+
114
+ - `pier restart`: refuses new messages and root Task runs, waits up to five
115
+ minutes, exits for `Restart=always`; aborted IM turns at the deadline are
116
+ recorded and posted by the next process; post-deadline cleanup has one shared
117
+ 10-second bound.
118
+ - `pier reload`: reloads channel adapters, evicts idle unwatched sessions;
119
+ streaming or watched sessions, and sessions still holding queued messages,
120
+ stay until normal eviction. Console → Settings → Instance → **Reload** is
121
+ the same, also takes the asking tab's session (unless mid-turn or holding a
122
+ queue), and answers `recycled` / `busy`.
123
+ - `pier tools sync`: converges the tools switched on in Console → Settings into
124
+ `~/.pier/tools/bin` (first on every session's PATH); one sync at a time per
125
+ machine.
126
+ - `systemctl --user restart pier` and `pier update` are hard stops.
127
+ - One Pier per `$PIER_HOME`: a start whose directory another live Pier holds
128
+ logs `another Pier (pid N) owns …` and exits before opening the database. Kept
129
+ under `Restart=always` on purpose — the service takes the directory back by
130
+ itself once the other process (usually a hand-typed `pier serve`) is gone, at
131
+ one refused start every `RestartSec=2` until then.
276
132
 
277
133
  ## Updating
278
134
 
@@ -281,12 +137,10 @@ pier update # installs the latest release, then hard-stops/restarts Pi
281
137
  pier update --check # only says whether one exists
282
138
  ```
283
139
 
284
- The workbench footer says the same thing without being asked: the server asks
285
- `registry.npmjs.org` at boot and at most every 30 minutes after that, and the
286
- version turns into `v0.0.1 → 0.0.2` when there is something newer. A failed
287
- check is silent by design — an offline box is not a broken one.
140
+ The footer asks `registry.npmjs.org` at boot and at most every 30 minutes and
141
+ shows `v0.0.1 → 0.0.2` when newer exists; a failed check is silent.
288
142
 
289
- From a checkout instead, stop and back up before replacing the build:
143
+ From a checkout:
290
144
 
291
145
  ```sh
292
146
  systemctl --user stop pier
@@ -297,54 +151,27 @@ npm ci && npm run build
297
151
  systemctl --user start pier
298
152
  ```
299
153
 
300
- For a service install, `pier update` hard-stops Pier; it does not use the
301
- graceful `pier restart` path. The Console's **Update now** and the automatic
302
- path do: both drain (new work refused, running turns finished, the rest
303
- ledgered for the next boot to report) before the updater unit is started.
304
-
305
- Either way the updater snapshots the database to
306
- `~/.pier/db/backups/pier.db.release-<version>.bak` before npm touches the
307
- package — `<version>` being the Pier that is being replaced, i.e. the release to
308
- reinstall if that copy is ever restored. This happens for every release,
309
- including releases with no schema change.
310
-
311
- Both of those steps run while Pier is still serving: the snapshot is taken
312
- through a read-only connection, so it is consistent on a live database, and npm
313
- writes into the global prefix rather than into the running process. Only the
314
- stop and the start that follow them are downtime — a second or two instead of
315
- the ten to twenty an install takes. A backup or install that fails therefore
316
- never stops anything; the failure is in the updater's journal and the running
317
- Pier keeps serving the version it already loaded. (For those seconds the live
318
- process is running code whose files on disk have already been replaced: a
319
- browser left open on the old page can see a lazily loaded asset 404 until it
320
- reloads. On the drained paths nothing else is running by then; `pier update`
321
- does not drain, which is the same reason to let active work finish first.)
322
-
323
- ### Automatic updates
324
-
325
- Off by default. Switched on from the version panel, it checks every 15 minutes
326
- and hands over only when all three hold: the switch is on, a newer release
327
- exists, and the instance is idle (nothing streaming, no task run in flight).
328
- systemd only — without the unit there is nothing to hand the install to.
154
+ `pier update` hard-stops; the Console's **Update now** and the automatic path
155
+ drain first (new work refused, running turns finished, the rest ledgered for
156
+ the next boot). Either way the updater snapshots
157
+ `~/.pier/db/backups/pier.db.release-<version>.bak` (`<version>` = the Pier being
158
+ replaced) before npm runs, every release. Only stop and start are downtime; a
159
+ failed backup or install stops nothing (`journalctl --user -u pier-update`).
329
160
 
330
- The unit records **absolute** paths to the node and npm that installed Pier,
331
- because systemd's PATH has neither. That pins it to one directory of one version
332
- manager: with fnm or nvm, `fnm uninstall v24` deletes the Node that `ExecStart`
333
- names while the running process survives (Linux keeps a deleted binary mapped).
334
- Pier checks those paths at boot and before every handover, and reports it in the
335
- journal and in the version panel rather than letting the next restart fail:
161
+ **Automatic updates**: off by default; switched on from the version panel;
162
+ checks every 15 minutes; hands over only when the switch is on, a newer release
163
+ exists, and the instance is idle (nothing streaming, no task run in flight).
164
+ systemd only.
336
165
 
337
- ```
338
- pier service install --force # re-records the current node and npm
339
- ```
166
+ **Node paths**: `fnm uninstall v24` (or nvm) deletes the Node `ExecStart`
167
+ names while the process survives; Pier checks at boot and before every handover
168
+ and reports in the journal and the version panel. Fix: `pier service install
169
+ --force`.
340
170
 
341
- A newer Pier brings its own schema up on the next start: the migrations run in
342
- one transaction before the port opens, and the version they leave behind is
343
- stamped in the database. It also snapshots the immediately preceding schema to
344
- `~/.pier/db/backups/pier.db.v<N>.bak` (`N` = the schema it was at). **Upgrades
345
- only.** Start an older Pier on a database a newer one has migrated and it
346
- refuses to run rather than write tables it does not understand — the way back
347
- down is either a release backup or that schema snapshot:
171
+ **Schema**: a newer Pier migrates on start, in one transaction before the port
172
+ opens, after snapshotting to `~/.pier/db/backups/pier.db.v<N>.bak` (`N` = the
173
+ schema it was at). Upgrades only: an older Pier refuses a newer database. Way
174
+ back:
348
175
 
349
176
  ```sh
350
177
  systemctl --user stop pier
@@ -354,86 +181,44 @@ cp backups/pier.db.release-0.0.4.bak pier.db # the version in the name
354
181
  # then reinstall that Pier release: npm install -g @timqi/pier@0.0.4
355
182
  ```
356
183
 
357
- Every copy lives in `~/.pier/db/backups/`, written under a temporary name and
358
- renamed into place, so a `.bak` name only ever refers to a finished copy. The
359
- three newest of **each kind** are kept — three release backups and three schema
360
- snapshots, counted separately so a run of releases cannot evict the copies taken
361
- before a migration. Backing up twice at the same version replaces that version's
362
- copy. Each is a full copy of the database, one directory below it, and therefore
363
- protects against a bad upgrade, not against a lost disk — an off-machine copy is
364
- still yours to take.
365
-
366
- ### Can it update itself?
367
-
368
- Yes, with one caveat that decides the shape: **the updater must not be a child
369
- of the service it restarts.** `systemctl --user restart pier` kills everything
370
- in `pier.service`'s cgroup, so an update script spawned by Pier dies halfway
371
- through — sometimes after unpacking and before restarting, which is the one
372
- outcome worse than not updating.
373
-
374
- So installation writes a second unit and `pier update` starts it after recording
375
- the running service's effective `PIER_HOME` in a runtime drop-in. That includes
376
- an operator environment override, so the updater cannot back up one database and
377
- migrate another. `~/.config/systemd/user/pier-update.service`:
378
-
379
- ```ini
380
- [Unit]
381
- Description=Update Pier to the latest published version
382
-
383
- [Service]
384
- Type=oneshot
385
- # npm's dependencies run postinstall scripts as `sh -c node …`, which needs a
386
- # node on PATH — the absolute one below only answers npm's own shebang. Same
387
- # recorded PATH as pier.service, and recorded rather than sourced from a login
388
- # shell at run time: a dotfile must not get to decide which node npm uses.
389
- Environment="PATH=/path/to/node/bin:/your/shell/PATH:/usr/local/bin:/usr/bin:/bin"
390
- ExecStart=/path/to/node /path/to/pier/dist/cli.js backup
391
- ExecStart=/path/to/node /recorded/path/to/npm install -g @timqi/pier@latest
392
- # Last: everything above it runs with Pier still up, so the stop is the downtime.
393
- ExecStart=systemctl --user stop pier.service
394
- ExecStopPost=systemctl --user start pier.service
395
- ```
396
-
397
- `pier update` triggers that unit with a call that survives Pier's restart because
398
- the work happens in a different cgroup. Starting the unit directly is unsupported:
399
- the command first records the effective database home used by the running service.
184
+ Copies are written under a temporary name and renamed into place; the three
185
+ newest of **each kind** (release, schema) are kept; a second backup at the same
186
+ version replaces it. They protect against a bad upgrade, not a lost disk.
400
187
 
401
- Deliberately **not** a `systemd.timer`. An unattended update is a machine that
402
- rewrites its own code from the network while holding your API keys, and its hard
403
- stop interrupts whatever session was mid-turn. Pier notices a newer release
404
- and says so in the workbench footer; starting the update stays a decision someone
405
- makes. The updater's `ExecStopPost` is what brings the service back: after a
406
- successful stop, and — as a no-op, since nothing was stopped — after a backup or
407
- install that failed.
188
+ **The updater unit** `pier-update.service` (backup, `npm install -g`, stop,
189
+ `ExecStopPost` start) runs outside `pier.service`'s cgroup, which a restart
190
+ kills. `pier update` records the service's effective `PIER_HOME` in a runtime
191
+ drop-in, then starts it; starting the unit directly is unsupported. No
192
+ `systemd.timer`: only Pier starts an update, so it can drain first.
408
193
 
409
194
  ## Remote access
410
195
 
411
- The unit binds the loopback, and that is the intended posture. To reach it from
412
- elsewhere, pick a tunnel rather than a wider bind:
413
-
414
- - `ssh -L 3141:localhost:3141 server` — nothing to configure, nothing exposed.
415
- - Tailscale, or Cloudflare Tunnel — no open port, and TLS terminates outside.
416
- - A reverse proxy (Caddy, nginx) if you want a real hostname. Terminate TLS
417
- there, preserve the external `Host` (or pass `X-Forwarded-Host`), and pass
418
- `X-Forwarded-For`; Pier uses the external host for write-origin checks and
419
- counts login failures per forwarded client. Its session cookie is marked
420
- `Secure` when a loopback proxy reports `X-Forwarded-Proto: https` (the header
421
- is ignored from anywhere else, where it is a header the client wrote).
196
+ Loopback bind; reach it over a tunnel, not a wider bind:
197
+
198
+ - `ssh -L 3141:localhost:3141 server`
199
+ - Tailscale, or Cloudflare Tunnel — no open port, TLS terminates outside.
200
+ - A reverse proxy (Caddy, nginx): terminate TLS there, preserve the external
201
+ `Host` (or pass `X-Forwarded-Host`), and **set** `X-Forwarded-For` to the
202
+ client — nginx `proxy_set_header X-Forwarded-For $remote_addr;`, Caddy by
203
+ default. Pier reads the rightmost hop, so appending the peer is safe too
204
+ (`$proxy_add_x_forwarded_for`); the unsafe case is a proxy that passes the
205
+ client's own header through without adding a hop of its own, which lets the
206
+ client name its throttle bucket.
207
+ Pier uses the external host for write-origin checks and counts login
208
+ failures per forwarded client. The cookie is `Secure` when a loopback proxy
209
+ reports `X-Forwarded-Proto: https` (ignored from anywhere else).
210
+ - `ssh -L` sends no such header, so every client through the tunnel shares one
211
+ throttle bucket.
422
212
 
423
213
  ## Backups
424
214
 
425
- Three paths hold everything: `~/.pier/db/pier.db` (tasks, channels, the chat →
426
- session map, workbench state, settings, the password hash, and the sealed
427
- provider credentials and channel tokens), `~/.pier/master.key` (the key that
428
- seals them — without it the database's sealed values are unreadable), and
429
- `~/.pier/boards/`. `~/.pier/db/backups/` holds the automatic pre-update and
430
- pre-migration copies, named for the release or schema they came from.
431
- For off-machine backups, use `sqlite3 ... "VACUUM INTO '…'"` rather than `cp`,
432
- which under WAL can miss the most recent commits. Pi's own session history lives
433
- under `~/.pier/pi` (Pier sets `PI_CODING_AGENT_DIR` there unless the environment
434
- already names another directory).
435
-
436
- Inbound chat attachments (photos, uploads from any surface) accumulate under
437
- `~/.pier/inbox/<channel>/` and are never deleted by Pier — a transcript may
438
- reference them indefinitely. Prune old files by hand (or a cron) when disk
439
- matters; a pruned file degrades to a broken attachment link, nothing else.
215
+ - `~/.pier/db/pier.db` — tasks, channels, chat → session map, workbench state,
216
+ settings, password hash, sealed credentials and tokens. Off-machine: `sqlite3
217
+ ... "VACUUM INTO '…'"`, not `cp` (WAL can miss the latest commits).
218
+ - `~/.pier/master.key` — seals the database's credentials.
219
+ - `~/.pier/boards/`.
220
+ - `~/.pier/db/backups/` — the automatic pre-update and pre-migration copies.
221
+ - `~/.pier/pi` — Pi's session history (unless `PI_CODING_AGENT_DIR` names
222
+ another directory).
223
+ - `~/.pier/inbox/<channel>/` — inbound chat attachments, never deleted by
224
+ Pier; prune by hand (a pruned file becomes a broken attachment link).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timqi/pier",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A self-hosted workspace for coding agents: web workbench and IM channels in front of Pi sessions",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": "github:timqi/pier",