@pasko70/pibo 1.7.2 → 1.7.4

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 (185) hide show
  1. package/README.md +183 -0
  2. package/context/codex-base-prompt.md +0 -0
  3. package/context/compute-worker.md +0 -0
  4. package/context/pibo-compaction-prompt.md +0 -0
  5. package/context/pibo-native-tooling.md +0 -0
  6. package/context/pibo-system-prompt.md +0 -0
  7. package/dist/apps/chat/chat-trace-helpers.js +3 -1
  8. package/dist/apps/chat/data/session-query-service.js +66 -3
  9. package/dist/apps/chat/data/timeline-query-service.js +8 -2
  10. package/dist/apps/chat/trace-v2.js +9 -4
  11. package/dist/apps/chat/trace.js +189 -4
  12. package/dist/apps/chat/web-app.js +164 -34
  13. package/dist/apps/chat-ui/assets/{dist-Ddn6n4ge.js → dist-B8jyBeFH.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-eRtG3vUM.js → dist-BS-A0VYR.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{dist-Jc0njXt3.js → dist-BZoXuF9y.js} +1 -1
  16. package/dist/apps/chat-ui/assets/{dist-CCHtiwhr.js → dist-Bl9cf-yT.js} +1 -1
  17. package/dist/apps/chat-ui/assets/{dist-CxWQqjbA.js → dist-BzmsjE6I.js} +1 -1
  18. package/dist/apps/chat-ui/assets/{dist-Cfyo-Bgw.js → dist-C6kgrKy2.js} +1 -1
  19. package/dist/apps/chat-ui/assets/{dist-8eLx-6HY.js → dist-CSKVaxJI.js} +1 -1
  20. package/dist/apps/chat-ui/assets/{dist-BXT6rjJn.js → dist-ChDQZXGN.js} +1 -1
  21. package/dist/apps/chat-ui/assets/{dist-DjmyW251.js → dist-D6v32djn.js} +1 -1
  22. package/dist/apps/chat-ui/assets/{dist-cyStkSiF.js → dist-MCSKIQ04.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-CCt9U3Or.js → dist-OTFZZTiY.js} +1 -1
  24. package/dist/apps/chat-ui/assets/index-CXXHizfD.js +166 -0
  25. package/dist/apps/chat-ui/assets/{index-Dz6asKNQ.css → index-DgVHk3gL.css} +1 -1
  26. package/dist/apps/chat-ui/assets/pwa-images/android/launchericon-144x144.png +0 -0
  27. package/dist/apps/chat-ui/assets/pwa-images/android/launchericon-192x192.png +0 -0
  28. package/dist/apps/chat-ui/assets/pwa-images/android/launchericon-48x48.png +0 -0
  29. package/dist/apps/chat-ui/assets/pwa-images/android/launchericon-512x512.png +0 -0
  30. package/dist/apps/chat-ui/assets/pwa-images/android/launchericon-72x72.png +0 -0
  31. package/dist/apps/chat-ui/assets/pwa-images/android/launchericon-96x96.png +0 -0
  32. package/dist/apps/chat-ui/assets/pwa-images/ios/100.png +0 -0
  33. package/dist/apps/chat-ui/assets/pwa-images/ios/1024.png +0 -0
  34. package/dist/apps/chat-ui/assets/pwa-images/ios/114.png +0 -0
  35. package/dist/apps/chat-ui/assets/pwa-images/ios/120.png +0 -0
  36. package/dist/apps/chat-ui/assets/pwa-images/ios/128.png +0 -0
  37. package/dist/apps/chat-ui/assets/pwa-images/ios/144.png +0 -0
  38. package/dist/apps/chat-ui/assets/pwa-images/ios/152.png +0 -0
  39. package/dist/apps/chat-ui/assets/pwa-images/ios/16.png +0 -0
  40. package/dist/apps/chat-ui/assets/pwa-images/ios/167.png +0 -0
  41. package/dist/apps/chat-ui/assets/pwa-images/ios/180.png +0 -0
  42. package/dist/apps/chat-ui/assets/pwa-images/ios/192.png +0 -0
  43. package/dist/apps/chat-ui/assets/pwa-images/ios/20.png +0 -0
  44. package/dist/apps/chat-ui/assets/pwa-images/ios/256.png +0 -0
  45. package/dist/apps/chat-ui/assets/pwa-images/ios/29.png +0 -0
  46. package/dist/apps/chat-ui/assets/pwa-images/ios/32.png +0 -0
  47. package/dist/apps/chat-ui/assets/pwa-images/ios/40.png +0 -0
  48. package/dist/apps/chat-ui/assets/pwa-images/ios/50.png +0 -0
  49. package/dist/apps/chat-ui/assets/pwa-images/ios/512.png +0 -0
  50. package/dist/apps/chat-ui/assets/pwa-images/ios/57.png +0 -0
  51. package/dist/apps/chat-ui/assets/pwa-images/ios/58.png +0 -0
  52. package/dist/apps/chat-ui/assets/pwa-images/ios/60.png +0 -0
  53. package/dist/apps/chat-ui/assets/pwa-images/ios/64.png +0 -0
  54. package/dist/apps/chat-ui/assets/pwa-images/ios/72.png +0 -0
  55. package/dist/apps/chat-ui/assets/pwa-images/ios/76.png +0 -0
  56. package/dist/apps/chat-ui/assets/pwa-images/ios/80.png +0 -0
  57. package/dist/apps/chat-ui/assets/pwa-images/ios/87.png +0 -0
  58. package/dist/apps/chat-ui/assets/pwa-images/windows/LargeTile.scale-100.png +0 -0
  59. package/dist/apps/chat-ui/assets/pwa-images/windows/LargeTile.scale-125.png +0 -0
  60. package/dist/apps/chat-ui/assets/pwa-images/windows/LargeTile.scale-150.png +0 -0
  61. package/dist/apps/chat-ui/assets/pwa-images/windows/LargeTile.scale-200.png +0 -0
  62. package/dist/apps/chat-ui/assets/pwa-images/windows/LargeTile.scale-400.png +0 -0
  63. package/dist/apps/chat-ui/assets/pwa-images/windows/SmallTile.scale-100.png +0 -0
  64. package/dist/apps/chat-ui/assets/pwa-images/windows/SmallTile.scale-125.png +0 -0
  65. package/dist/apps/chat-ui/assets/pwa-images/windows/SmallTile.scale-150.png +0 -0
  66. package/dist/apps/chat-ui/assets/pwa-images/windows/SmallTile.scale-200.png +0 -0
  67. package/dist/apps/chat-ui/assets/pwa-images/windows/SmallTile.scale-400.png +0 -0
  68. package/dist/apps/chat-ui/assets/pwa-images/windows/SplashScreen.scale-100.png +0 -0
  69. package/dist/apps/chat-ui/assets/pwa-images/windows/SplashScreen.scale-125.png +0 -0
  70. package/dist/apps/chat-ui/assets/pwa-images/windows/SplashScreen.scale-150.png +0 -0
  71. package/dist/apps/chat-ui/assets/pwa-images/windows/SplashScreen.scale-200.png +0 -0
  72. package/dist/apps/chat-ui/assets/pwa-images/windows/SplashScreen.scale-400.png +0 -0
  73. package/dist/apps/chat-ui/assets/pwa-images/windows/Square150x150Logo.scale-100.png +0 -0
  74. package/dist/apps/chat-ui/assets/pwa-images/windows/Square150x150Logo.scale-125.png +0 -0
  75. package/dist/apps/chat-ui/assets/pwa-images/windows/Square150x150Logo.scale-150.png +0 -0
  76. package/dist/apps/chat-ui/assets/pwa-images/windows/Square150x150Logo.scale-200.png +0 -0
  77. package/dist/apps/chat-ui/assets/pwa-images/windows/Square150x150Logo.scale-400.png +0 -0
  78. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-16.png +0 -0
  79. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-20.png +0 -0
  80. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-24.png +0 -0
  81. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-256.png +0 -0
  82. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-30.png +0 -0
  83. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-32.png +0 -0
  84. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-36.png +0 -0
  85. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-40.png +0 -0
  86. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-44.png +0 -0
  87. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-48.png +0 -0
  88. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-60.png +0 -0
  89. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-64.png +0 -0
  90. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-72.png +0 -0
  91. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-80.png +0 -0
  92. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-lightunplated_targetsize-96.png +0 -0
  93. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-16.png +0 -0
  94. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-20.png +0 -0
  95. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-24.png +0 -0
  96. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-256.png +0 -0
  97. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-30.png +0 -0
  98. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-32.png +0 -0
  99. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-36.png +0 -0
  100. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-40.png +0 -0
  101. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-44.png +0 -0
  102. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-48.png +0 -0
  103. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-60.png +0 -0
  104. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-64.png +0 -0
  105. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-72.png +0 -0
  106. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-80.png +0 -0
  107. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.altform-unplated_targetsize-96.png +0 -0
  108. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.scale-100.png +0 -0
  109. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.scale-125.png +0 -0
  110. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.scale-150.png +0 -0
  111. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.scale-200.png +0 -0
  112. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.scale-400.png +0 -0
  113. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-16.png +0 -0
  114. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-20.png +0 -0
  115. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-24.png +0 -0
  116. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-256.png +0 -0
  117. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-30.png +0 -0
  118. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-32.png +0 -0
  119. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-36.png +0 -0
  120. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-40.png +0 -0
  121. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-44.png +0 -0
  122. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-48.png +0 -0
  123. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-60.png +0 -0
  124. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-64.png +0 -0
  125. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-72.png +0 -0
  126. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-80.png +0 -0
  127. package/dist/apps/chat-ui/assets/pwa-images/windows/Square44x44Logo.targetsize-96.png +0 -0
  128. package/dist/apps/chat-ui/assets/pwa-images/windows/StoreLogo.scale-100.png +0 -0
  129. package/dist/apps/chat-ui/assets/pwa-images/windows/StoreLogo.scale-125.png +0 -0
  130. package/dist/apps/chat-ui/assets/pwa-images/windows/StoreLogo.scale-150.png +0 -0
  131. package/dist/apps/chat-ui/assets/pwa-images/windows/StoreLogo.scale-200.png +0 -0
  132. package/dist/apps/chat-ui/assets/pwa-images/windows/StoreLogo.scale-400.png +0 -0
  133. package/dist/apps/chat-ui/assets/pwa-images/windows/Wide310x150Logo.scale-100.png +0 -0
  134. package/dist/apps/chat-ui/assets/pwa-images/windows/Wide310x150Logo.scale-125.png +0 -0
  135. package/dist/apps/chat-ui/assets/pwa-images/windows/Wide310x150Logo.scale-150.png +0 -0
  136. package/dist/apps/chat-ui/assets/pwa-images/windows/Wide310x150Logo.scale-200.png +0 -0
  137. package/dist/apps/chat-ui/assets/pwa-images/windows/Wide310x150Logo.scale-400.png +0 -0
  138. package/dist/apps/chat-ui/index.html +2 -2
  139. package/dist/apps/chat-ui/manifest.webmanifest +0 -0
  140. package/dist/apps/chat-ui/sw.js +0 -0
  141. package/dist/apps/chat-vscode-web/assets/index-B4Jk2P5o.js +41 -0
  142. package/dist/apps/chat-vscode-web/index.html +1 -1
  143. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  144. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.7.4.vsix +0 -0
  145. package/dist/bin/pibo.js +0 -0
  146. package/dist/bin/rg.js +0 -0
  147. package/dist/data/schema.js +2 -0
  148. package/dist/shared/trace-page-merge.js +40 -0
  149. package/dist/web/http.js +7 -1
  150. package/docs/README.md +23 -0
  151. package/docs/ops/install-developer-host.md +112 -0
  152. package/docs/ops/install-user-host.md +96 -0
  153. package/docs/ops/upgrade-user-to-developer-host.md +69 -0
  154. package/docs/ops/vscode-extension-release.md +160 -0
  155. package/package.json +93 -93
  156. package/skills/builtin/graphify/SKILL.md +0 -0
  157. package/skills/builtin/pi-agent-harness/SKILL.md +0 -0
  158. package/skills/builtin/pi-agent-harness/agents/openai.yaml +0 -0
  159. package/skills/builtin/pibo-docker-system/SKILL.md +0 -0
  160. package/skills/builtin/pibo-spec-writing/SKILL.md +0 -0
  161. package/skills/builtin/prd/SKILL.md +0 -0
  162. package/skills/builtin/ralph-loop/SKILL.md +0 -0
  163. package/skills/builtin/ralph-prd-json/SKILL.md +0 -0
  164. package/skills/builtin/skill-creator/LICENSE.txt +0 -0
  165. package/skills/builtin/skill-creator/SKILL.md +0 -0
  166. package/skills/builtin/skill-creator/agents/analyzer.md +0 -0
  167. package/skills/builtin/skill-creator/agents/comparator.md +0 -0
  168. package/skills/builtin/skill-creator/agents/grader.md +0 -0
  169. package/skills/builtin/skill-creator/assets/eval_review.html +0 -0
  170. package/skills/builtin/skill-creator/eval-viewer/generate_review.py +0 -0
  171. package/skills/builtin/skill-creator/eval-viewer/viewer.html +0 -0
  172. package/skills/builtin/skill-creator/references/schemas.md +0 -0
  173. package/skills/builtin/skill-creator/scripts/__init__.py +0 -0
  174. package/skills/builtin/skill-creator/scripts/aggregate_benchmark.py +0 -0
  175. package/skills/builtin/skill-creator/scripts/generate_report.py +0 -0
  176. package/skills/builtin/skill-creator/scripts/improve_description.py +0 -0
  177. package/skills/builtin/skill-creator/scripts/package_skill.py +0 -0
  178. package/skills/builtin/skill-creator/scripts/quick_validate.py +0 -0
  179. package/skills/builtin/skill-creator/scripts/run_eval.py +0 -0
  180. package/skills/builtin/skill-creator/scripts/run_loop.py +0 -0
  181. package/skills/builtin/skill-creator/scripts/utils.py +0 -0
  182. package/skills/builtin/web-annotations/SKILL.md +0 -0
  183. package/src/mcp/LICENSE.mcp-cli +0 -0
  184. package/dist/apps/chat-ui/assets/index-COp5Hme0.js +0 -166
  185. package/dist/apps/chat-vscode-web/assets/index-CfeNUaRh.js +0 -41
package/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # pibo
2
+
3
+ Pibo is a thin product boundary around Pi Coding Agent. Pi remains the inner engine for model turns, tools, streaming, sessions, and compaction. Pibo adds profiles, plugins, channels, local gateways, web auth, and operator tooling.
4
+
5
+ The default posture is intentionally powerful: Pibo is built for agentic coding and server operation, not for a restrictive sandbox. Install and run it as the Linux user that should own its state and credentials.
6
+
7
+ ## Supported platforms
8
+
9
+ Pibo is Linux-first. The supported hosts are:
10
+
11
+ - **Linux** (any modern distribution) — primary target
12
+ - **macOS** — supported, same code path as Linux
13
+ - **Windows via WSL2** — fully supported; see [docs/guides/pibo-on-windows-via-wsl.md](./docs/guides/pibo-on-windows-via-wsl.md)
14
+ - **Native Windows** — not supported; `pibo setup doctor` and `pibo setup user-host` will print a clear error pointing to the WSL guide
15
+
16
+ ## Install
17
+
18
+ Pibo is distributed as an npm package:
19
+
20
+ ```bash
21
+ npm install -g @pasko70/pibo
22
+ pibo --help
23
+ ```
24
+
25
+ The current package declares Node.js 24+. Some Linux distributions still ship Node 22 through `apt`, so install a recent Node release first when needed. A user-local Node/npm setup through `nvm`, `fnm`, or a user-writable npm prefix works well.
26
+
27
+ For a user-local global npm prefix:
28
+
29
+ ```bash
30
+ mkdir -p ~/.local
31
+ npm config set prefix ~/.local
32
+ export PATH="$HOME/.local/bin:$PATH"
33
+ npm install -g @pasko70/pibo
34
+ ```
35
+
36
+ ## State and user paths
37
+
38
+ Pibo stores product-wide state under:
39
+
40
+ ```text
41
+ ${PIBO_HOME:-~/.pibo}
42
+ ```
43
+
44
+ That means the executing Linux user owns the installation state:
45
+
46
+ ```text
47
+ alice -> /home/alice/.pibo
48
+ pibo -> /home/pibo/.pibo
49
+ root -> /root/.pibo
50
+ ```
51
+
52
+ There is no requirement to run as `root`. If you SSH as root and run Pibo, state naturally lands in `/root/.pibo`. If you create or use a dedicated user, state lands in that user's home. This is the preferred way to keep SSH keys, config, logs, and sessions separated.
53
+
54
+ Workspace-scoped files are separate and live under the active workspace:
55
+
56
+ ```text
57
+ <workspace>/.pibo
58
+ ```
59
+
60
+ Examples include custom prompts and Pi package registrations.
61
+
62
+ ## Server notes
63
+
64
+ A fresh server install usually needs only three decisions:
65
+
66
+ 1. Choose the Linux user that should own Pibo.
67
+ 2. Install Node.js 24+ for that user or system-wide.
68
+ 3. Install Pibo through npm.
69
+
70
+ Use the setup planner to keep the first run simple:
71
+
72
+ ```bash
73
+ pibo setup doctor --domain pibo.example.com --expected-ip <server-ip>
74
+ pibo setup user-host --domain pibo.example.com --print-files
75
+ pibo setup user-host --domain pibo.example.com --write-to /tmp/pibo-setup
76
+ ```
77
+
78
+ This is the normal user path: one gateway, one `PIBO_HOME`, no required Docker, no dev gateway, and no GitHub App setup. After reviewing staged files, use `--apply --yes` to write the generated systemd/Caddy files.
79
+
80
+ Developer hosts are opt-in and add production/dev separation plus Docker compute workers:
81
+
82
+ ```bash
83
+ pibo setup developer-host \
84
+ --origin git@github.com:<your-fork>/pibo.git \
85
+ --prod-domain pibo.example.com \
86
+ --dev-domain dev.pibo.example.com \
87
+ --print-files
88
+
89
+ pibo setup developer-host \
90
+ --origin git@github.com:<your-fork>/pibo.git \
91
+ --prod-domain pibo.example.com \
92
+ --dev-domain dev.pibo.example.com \
93
+ --write-to /tmp/pibo-setup
94
+ ```
95
+
96
+ See `docs/ops/install-user-host.md`, `docs/ops/install-developer-host.md`, and `docs/ops/upgrade-user-to-developer-host.md`. Developer-host services are source-pinned so production and dev do not fight over one global `pibo` symlink. Docker and swap are developer-host prerequisites only; verify them with `pibo setup doctor --require-docker --min-swap-gb 8`.
97
+
98
+ If the agent should be able to perform server administration, give that Linux user the required sudo or Docker permissions explicitly. Pibo does not need a special onboarding user to work correctly; normal Unix ownership is enough.
99
+
100
+ ## Docker notes
101
+
102
+ Docker is optional. If you run Pibo in a container, avoid hard-coding `/root` unless the container is intentionally root-owned. Prefer mapping the host user's state into the container user's home:
103
+
104
+ ```text
105
+ $HOME/.pibo -> /home/pibo/.pibo
106
+ $HOME/code -> /workspace/code
107
+ ```
108
+
109
+ Set `HOME` and `PIBO_HOME` consistently inside the container so the same path rule applies:
110
+
111
+ ```text
112
+ HOME=/home/pibo
113
+ PIBO_HOME=/home/pibo/.pibo
114
+ ```
115
+
116
+ ## Web gateway auth
117
+
118
+ `pibo gateway:web` starts the authenticated web runtime. It requires Better Auth configuration before production use. This step is intentionally not automated because you must create/select the Google OAuth client and allowed user list:
119
+
120
+ ```bash
121
+ pibo config set auth.baseURL https://your-host.example
122
+ pibo config set auth.secret <at-least-32-characters>
123
+ pibo config set auth.googleClientId <google-client-id>
124
+ pibo config set auth.googleClientSecret <google-client-secret>
125
+ pibo config set auth.allowedEmails you@example.com
126
+ ```
127
+
128
+ For local loopback testing, `auth.baseURL` can be `http://localhost:4788`.
129
+
130
+ ## Development from source
131
+
132
+ Use source installs for development, not normal usage:
133
+
134
+ ```bash
135
+ git clone git@github.com:Pascapone-server/pibo.git
136
+ cd pibo
137
+ npm install
138
+ npm run build
139
+ npm run start -- --help
140
+ ```
141
+
142
+ Useful scripts:
143
+
144
+ ```bash
145
+ npm run dev -- --help
146
+ npm run profile -- codex
147
+ npm run tui:routed -- codex
148
+ npm run gateway
149
+ npm run gateway:web
150
+ npm test
151
+ npm run typecheck
152
+ ```
153
+
154
+ ## Main CLI areas
155
+
156
+ ```bash
157
+ pibo config # local config under ${PIBO_HOME:-~/.pibo}/config.json
158
+ pibo mcp # discover and call configured MCP servers
159
+ pibo tools # install and inspect curated external CLI tools
160
+ pibo pi-packages # register Pi Coding Agent packages
161
+ pibo debug # inspect local Pibo data stores
162
+ pibo setup # plan user-host installs and developer-host upgrades
163
+ pibo profile # inspect runtime profiles
164
+ pibo tui # start the direct Pi TUI
165
+ pibo tui:routed # start the routed Pibo TUI
166
+ pibo gateway # local gateway runtime
167
+ pibo gateway:web # authenticated web gateway runtime
168
+ ```
169
+
170
+ ## Further docs
171
+
172
+ - `docs/architecture.md` describes runtime architecture and boundaries.
173
+ - `docs/mcp.md` documents MCP configuration and commands.
174
+ - `docs/tools.md` documents curated external CLI tools.
175
+ - `docs/pi-packages.md` documents Pi package registration.
176
+ - `docs/chat-rooms-event-log.md` documents Chat Web rooms and durable event storage.
177
+ - `docs/progress.md` is the short implementation status snapshot.
178
+
179
+ ## Philosophy
180
+
181
+ Keep Pibo thin. Pi Coding Agent should remain the inner engine; Pibo owns only the product boundary: profiles, plugins, channels, routing, auth, policy, and operator tooling.
182
+
183
+ Optional integrations should stay outside the core package until installed. MCP servers, Python virtual environments, external CLIs, and user skills are configured on demand rather than bundled into every runtime.
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -75,10 +75,12 @@ export function annotateTracePage(trace, events, input) {
75
75
  eventLimit: input.pageSize,
76
76
  pageSize: input.pageSize,
77
77
  beforeSequence: input.beforeSequence,
78
+ beforeCursor: input.beforeCursor,
78
79
  firstEventSequence,
79
80
  lastEventSequence,
80
81
  nextBeforeSequence: firstEventSequence,
81
- hasOlderEvents: firstEventSequence !== undefined ? firstEventSequence > 1 : false,
82
+ nextBeforeCursor: input.nextBeforeCursor ?? (firstEventSequence !== undefined ? String(firstEventSequence) : undefined),
83
+ hasOlderEvents: input.hasOlderEvents ?? (firstEventSequence !== undefined ? firstEventSequence > 1 : false),
82
84
  };
83
85
  }
84
86
  export function setTraceCache(cache, key, trace, maxEntries, maxBytes = TRACE_CACHE_MAX_BYTES) {
@@ -1,5 +1,6 @@
1
1
  import { chatRoomIdFromMetadata } from "../types/rooms.js";
2
2
  import { sessionFromRow, statusFromOutputEvent } from "./chat-data-mappers.js";
3
+ import { rootSessionId } from "../../../data/session-store.js";
3
4
  export class ChatSessionQueryService {
4
5
  store;
5
6
  constructor(store) {
@@ -11,9 +12,17 @@ export class ChatSessionQueryService {
11
12
  this.upsertNavigation(session, roomId, status, session.updatedAt);
12
13
  }
13
14
  upsertSessionsIfChanged(sessions) {
14
- for (const session of sessions)
15
+ let written = 0;
16
+ let skipped = 0;
17
+ for (const session of sessions) {
18
+ if (this.sessionIndexMatches(session)) {
19
+ skipped++;
20
+ continue;
21
+ }
15
22
  this.upsertSession(session);
16
- return { checked: sessions.length, written: sessions.length, skipped: 0 };
23
+ written++;
24
+ }
25
+ return { checked: sessions.length, written, skipped };
17
26
  }
18
27
  recordEvent(event, session) {
19
28
  if (session)
@@ -44,11 +53,65 @@ export class ChatSessionQueryService {
44
53
  return Number(result.changes ?? 0);
45
54
  }
46
55
  close() { }
56
+ sessionIndexMatches(session, status = "idle") {
57
+ const roomId = chatRoomIdFromMetadata(session.metadata) ?? "room_default";
58
+ const title = session.title || "Untitled Session";
59
+ const metadataJson = JSON.stringify(session.metadata ?? {});
60
+ const activeModelJson = session.activeModel ? JSON.stringify(session.activeModel) : null;
61
+ const row = this.store.db.prepare(`
62
+ SELECT
63
+ s.pi_session_id,
64
+ s.room_id,
65
+ s.root_session_id,
66
+ s.parent_id,
67
+ s.origin_id,
68
+ s.channel,
69
+ s.kind,
70
+ s.profile,
71
+ s.active_model_json,
72
+ s.workspace,
73
+ s.title,
74
+ s.status,
75
+ s.metadata_json,
76
+ s.created_at,
77
+ s.updated_at,
78
+ s.last_activity_at,
79
+ n.status AS navigation_status,
80
+ n.title AS navigation_title,
81
+ n.sort_key AS navigation_sort_key,
82
+ n.updated_at AS navigation_updated_at
83
+ FROM sessions s
84
+ LEFT JOIN session_navigation n ON n.session_id = s.id
85
+ WHERE s.id = ? AND s.deleted_at IS NULL
86
+ `).get(session.id);
87
+ if (!row)
88
+ return false;
89
+ return row.pi_session_id === session.piSessionId
90
+ && row.room_id === roomId
91
+ && row.root_session_id === rootSessionId(session)
92
+ && row.parent_id === (session.parentId ?? null)
93
+ && row.origin_id === (session.originId ?? null)
94
+ && row.channel === session.channel
95
+ && row.kind === session.kind
96
+ && row.profile === session.profile
97
+ && row.active_model_json === activeModelJson
98
+ && row.workspace === (session.workspace ?? null)
99
+ && row.title === title
100
+ && row.status === status
101
+ && row.metadata_json === metadataJson
102
+ && row.created_at === session.createdAt
103
+ && row.updated_at === session.updatedAt
104
+ && row.last_activity_at === session.updatedAt
105
+ && row.navigation_status === status
106
+ && row.navigation_title === title
107
+ && row.navigation_sort_key === session.updatedAt
108
+ && row.navigation_updated_at === session.updatedAt;
109
+ }
47
110
  upsertNavigation(session, roomId, status, now) {
48
111
  this.store.navigation.upsertSession({
49
112
  roomId,
50
113
  sessionId: session.id,
51
- rootSessionId: session.parentId ? (typeof session.metadata?.rootSessionId === "string" ? session.metadata.rootSessionId : session.parentId) : session.id,
114
+ rootSessionId: rootSessionId(session),
52
115
  parentId: session.parentId,
53
116
  originId: session.originId,
54
117
  title: session.title || "Untitled Session",
@@ -75,8 +75,14 @@ export class ChatTimelineQueryService {
75
75
  return rows.map((row) => ({ eventType: row.type, count: Number(row.count) }));
76
76
  }
77
77
  getLatestEventSequence(piboSessionId) {
78
- const row = this.store.db.prepare("SELECT COALESCE(MAX(session_sequence), 0) AS latest_sequence FROM event_log WHERE session_id = ?").get(piboSessionId);
79
- return row.latest_sequence;
78
+ const row = this.store.db.prepare(`
79
+ SELECT session_sequence AS latest_sequence
80
+ FROM event_log
81
+ WHERE session_id = ?
82
+ ORDER BY session_sequence DESC, stream_id DESC
83
+ LIMIT 1
84
+ `).get(piboSessionId);
85
+ return row?.latest_sequence ?? 0;
80
86
  }
81
87
  getLatestStreamId(input = {}) {
82
88
  const clauses = [];
@@ -20,6 +20,10 @@ export function traceTimelinePageFromView(input) {
20
20
  limit: Math.max(1, Math.min(input.limit, TRACE_V2_MAX_TIMELINE_LIMIT)),
21
21
  fromTail: input.fromTail,
22
22
  });
23
+ const transcriptTailCursor = input.fromTail ? input.transcriptTailCursor : undefined;
24
+ const hasOlder = input.trace.hasOlderEvents === true || transcriptTailCursor !== undefined;
25
+ const nextBeforeCursor = transcriptTailCursor ?? (hasOlder ? input.trace.nextBeforeCursor : undefined);
26
+ const nextBeforeSequence = transcriptTailCursor ? undefined : (hasOlder ? input.trace.nextBeforeSequence : undefined);
23
27
  let page = {
24
28
  piboSessionId: input.trace.piboSessionId,
25
29
  piSessionId: input.trace.piSessionId,
@@ -28,9 +32,9 @@ export function traceTimelinePageFromView(input) {
28
32
  latestStreamId: input.trace.latestStreamId,
29
33
  projectionStatus: "ready",
30
34
  cursor: {
31
- before: input.trace.nextBeforeSequence !== undefined ? String(input.trace.nextBeforeSequence) : undefined,
35
+ before: nextBeforeCursor ?? (nextBeforeSequence !== undefined ? String(nextBeforeSequence) : undefined),
32
36
  after: input.trace.lastEventSequence !== undefined ? String(input.trace.lastEventSequence) : undefined,
33
- hasOlder: input.trace.hasOlderEvents === true,
37
+ hasOlder,
34
38
  hasNewer: false,
35
39
  },
36
40
  nodes,
@@ -43,8 +47,9 @@ export function traceTimelinePageFromView(input) {
43
47
  pageSize: input.trace.pageSize,
44
48
  firstEventSequence: input.trace.firstEventSequence,
45
49
  lastEventSequence: input.trace.lastEventSequence,
46
- nextBeforeSequence: input.trace.nextBeforeSequence,
47
- hasOlderEvents: input.trace.hasOlderEvents,
50
+ nextBeforeSequence,
51
+ nextBeforeCursor,
52
+ hasOlderEvents: hasOlder,
48
53
  };
49
54
  while (Buffer.byteLength(JSON.stringify(page), "utf8") > byteLimit && page.nodes.length > 1) {
50
55
  page = {
@@ -8,10 +8,17 @@ import { isChatWebSessionArchived } from "./session-metadata.js";
8
8
  import { workflowSessionKindFromMetadata } from "../../sessions/workflow-session-kind.js";
9
9
  export { compareTraceNodes, sortTraceNodes, nestTraceNodes, flattenTraceNodes, mapTraceNodesById, buildTraceViewFromEvents, traceNodesFromEntries, } from "../../shared/trace-engine.js";
10
10
  export const TRACE_TRANSCRIPT_TAIL_MAX_BYTES = 2 * 1024 * 1024;
11
+ export const TRACE_TRANSCRIPT_HISTORY_PAGE_MAX_BYTES = 512 * 1024;
12
+ export const TRACE_TRANSCRIPT_HISTORY_SCAN_MAX_BYTES = 16 * 1024 * 1024;
11
13
  export async function loadPiSessionMetadata(session, cwd = process.cwd()) {
12
14
  const piSession = await findPiSession(session, cwd);
13
15
  return metadataFromPiSession(piSession);
14
16
  }
17
+ export async function loadPiSessionFastMetadata(session, cwd = process.cwd()) {
18
+ const piSession = findPiSessionDirectFast(session.piSessionId, cwd)
19
+ ?? findPiSessionDirect(session.piSessionId, cwd, { fast: true });
20
+ return metadataFromPiSession(piSession);
21
+ }
15
22
  function metadataFromPiSession(piSession) {
16
23
  if (!piSession)
17
24
  return {};
@@ -133,6 +140,9 @@ export async function buildTraceView(input) {
133
140
  includeRawEvents: input.includeRawEvents,
134
141
  rawEventsLimit: input.rawEventsLimit,
135
142
  });
143
+ if (input.transcriptOrderOffset && input.transcriptOrderOffset > 0) {
144
+ offsetTranscriptTraceOrder(view.nodes, input.transcriptOrderOffset);
145
+ }
136
146
  annotateForkableUserMessageNodes(view.nodes, allEntries);
137
147
  return {
138
148
  ...view,
@@ -191,7 +201,10 @@ export function createTraceViewVersion(input) {
191
201
  .digest("hex");
192
202
  }
193
203
  const PI_SESSION_LIST_CACHE_TTL_MS = 5_000;
204
+ const PI_SESSION_DIRECT_CACHE_TTL_MS = 5_000;
205
+ const PI_SESSION_FAST_HEAD_BYTES = 64 * 1024;
194
206
  const piSessionListCache = new Map();
207
+ const piSessionDirectCache = new Map();
195
208
  export async function listPiSessions(cwd = process.cwd()) {
196
209
  const now = Date.now();
197
210
  const cached = piSessionListCache.get(cwd);
@@ -229,7 +242,31 @@ async function findPiSession(piboSession, cwd) {
229
242
  return (findPiSessionDirect(piboSession.piSessionId, cwd) ??
230
243
  (await listPiSessions(cwd)).find((session) => session.id === piboSession.piSessionId));
231
244
  }
232
- function findPiSessionDirect(piSessionId, cwd) {
245
+ function findPiSessionDirectFast(piSessionId, cwd) {
246
+ const cacheKey = `${cwd}\0${piSessionId}`;
247
+ const cached = piSessionDirectCache.get(cacheKey);
248
+ if (!cached || cached.expiresAt <= Date.now())
249
+ return undefined;
250
+ try {
251
+ const stats = statSync(cached.path);
252
+ if (stats.size === cached.size && stats.mtimeMs === cached.mtimeMs)
253
+ return cached.item;
254
+ }
255
+ catch {
256
+ piSessionDirectCache.delete(cacheKey);
257
+ }
258
+ return undefined;
259
+ }
260
+ function setPiSessionDirectFastCache(piSessionId, cwd, item, stats) {
261
+ piSessionDirectCache.set(`${cwd}\0${piSessionId}`, {
262
+ expiresAt: Date.now() + PI_SESSION_DIRECT_CACHE_TTL_MS,
263
+ path: item.path,
264
+ size: stats.size,
265
+ mtimeMs: stats.mtimeMs,
266
+ item,
267
+ });
268
+ }
269
+ function findPiSessionDirect(piSessionId, cwd, options = {}) {
233
270
  const sessionDir = defaultPiSessionDir(cwd);
234
271
  if (!existsSync(sessionDir))
235
272
  return undefined;
@@ -239,7 +276,9 @@ function findPiSessionDirect(piSessionId, cwd) {
239
276
  return undefined;
240
277
  const sessionPath = join(sessionDir, file);
241
278
  const stats = statSync(sessionPath);
242
- const entries = parseSessionEntries(readFileSync(sessionPath, "utf8"));
279
+ const entries = options.fast
280
+ ? readHeadEntries(sessionPath, stats.size, PI_SESSION_FAST_HEAD_BYTES)
281
+ : parseSessionEntries(readFileSync(sessionPath, "utf8"));
243
282
  const header = entries.find((entry) => entry.type === "session");
244
283
  if (header?.id !== piSessionId)
245
284
  return undefined;
@@ -256,17 +295,22 @@ function findPiSessionDirect(piSessionId, cwd) {
256
295
  continue;
257
296
  firstMessage = extractMessageText(messageContent(entry));
258
297
  }
259
- return {
298
+ const item = {
260
299
  path: sessionPath,
261
300
  id: piSessionId,
262
301
  cwd: stringValue(header.cwd) ?? cwd,
263
302
  name,
264
303
  parentSessionPath: stringValue(header.parentSession),
265
304
  created: stringValue(header.timestamp) ?? stats.birthtime.toISOString(),
266
- modified: sessionModifiedIso(entries.filter((entry) => entry.type !== "session"), stringValue(header.timestamp), stats.mtime),
305
+ modified: options.fast
306
+ ? stats.mtime.toISOString()
307
+ : sessionModifiedIso(entries.filter((entry) => entry.type !== "session"), stringValue(header.timestamp), stats.mtime),
267
308
  messageCount,
268
309
  firstMessage: firstMessage || "(no messages)",
269
310
  };
311
+ if (options.fast)
312
+ setPiSessionDirectFastCache(piSessionId, cwd, item, stats);
313
+ return item;
270
314
  }
271
315
  catch {
272
316
  return undefined;
@@ -302,6 +346,26 @@ function readEntries(path) {
302
346
  const content = readFileSync(path, "utf8");
303
347
  return parseSessionEntries(content).filter((entry) => entry.type !== "session");
304
348
  }
349
+ function readHeadEntries(path, fileSize, maxBytes) {
350
+ const length = Math.max(1, Math.min(maxBytes, fileSize));
351
+ const buffer = Buffer.alloc(length);
352
+ const fd = openSync(path, "r");
353
+ try {
354
+ const bytesRead = readSync(fd, buffer, 0, length, 0);
355
+ let content = buffer.subarray(0, bytesRead).toString("utf8");
356
+ if (bytesRead < fileSize && !content.endsWith("\n")) {
357
+ const lastNewline = content.lastIndexOf("\n");
358
+ if (lastNewline >= 0)
359
+ content = content.slice(0, lastNewline + 1);
360
+ }
361
+ if (!content.trim())
362
+ return [];
363
+ return parseSessionEntries(content.endsWith("\n") ? content : `${content}\n`);
364
+ }
365
+ finally {
366
+ closeSync(fd);
367
+ }
368
+ }
305
369
  export function readTailEntries(path, maxBytes = TRACE_TRANSCRIPT_TAIL_MAX_BYTES) {
306
370
  if (!existsSync(path))
307
371
  return [];
@@ -324,6 +388,127 @@ export function readTailEntries(path, maxBytes = TRACE_TRANSCRIPT_TAIL_MAX_BYTES
324
388
  closeSync(fd);
325
389
  }
326
390
  }
391
+ export function readTranscriptHistoryPage(path, input = {}) {
392
+ if (!existsSync(path))
393
+ return { entries: [], hasOlder: false, scannedBytes: 0, startByte: 0, endByte: 0 };
394
+ const stats = statSync(path);
395
+ const fileSize = Math.max(0, stats.size);
396
+ const initialEnd = input.beforeByte === undefined
397
+ ? fileSize
398
+ : Math.max(0, Math.min(input.beforeByte, fileSize));
399
+ const limit = Math.max(1, Math.min(input.limit ?? 50, 500));
400
+ const pageBytes = Math.max(1024, Math.min(input.pageBytes ?? TRACE_TRANSCRIPT_HISTORY_PAGE_MAX_BYTES, TRACE_TRANSCRIPT_HISTORY_SCAN_MAX_BYTES));
401
+ const maxScanBytes = Math.max(pageBytes, Math.min(input.maxScanBytes ?? TRACE_TRANSCRIPT_HISTORY_SCAN_MAX_BYTES, 32 * 1024 * 1024));
402
+ const beforeTime = input.beforeTimestamp ? Date.parse(input.beforeTimestamp) : undefined;
403
+ const entries = [];
404
+ let cursorEnd = initialEnd;
405
+ let scannedBytes = 0;
406
+ let nextBeforeByte;
407
+ while (cursorEnd > 0 && entries.length < limit && scannedBytes < maxScanBytes) {
408
+ const chunkStart = Math.max(0, cursorEnd - pageBytes);
409
+ const chunk = readUtf8Range(path, chunkStart, cursorEnd);
410
+ scannedBytes += cursorEnd - chunkStart;
411
+ const records = transcriptLineRecords(chunk, chunkStart, cursorEnd);
412
+ for (let index = records.length - 1; index >= 0; index -= 1) {
413
+ const record = records[index];
414
+ const parsed = parseTranscriptLine(record.text);
415
+ for (let entryIndex = parsed.length - 1; entryIndex >= 0; entryIndex -= 1) {
416
+ const entry = parsed[entryIndex];
417
+ if (beforeTime !== undefined && entryTimestampMs(entry) >= beforeTime)
418
+ continue;
419
+ entries.push({ entry, startByte: record.startByte });
420
+ if (entries.length >= limit) {
421
+ nextBeforeByte = record.startByte;
422
+ break;
423
+ }
424
+ }
425
+ if (entries.length >= limit)
426
+ break;
427
+ }
428
+ cursorEnd = chunkStart;
429
+ }
430
+ if (nextBeforeByte === undefined)
431
+ nextBeforeByte = cursorEnd > 0 ? cursorEnd : undefined;
432
+ return {
433
+ entries: entries.reverse().map((item) => item.entry),
434
+ nextBeforeByte,
435
+ hasOlder: nextBeforeByte !== undefined && nextBeforeByte > 0,
436
+ scannedBytes,
437
+ startByte: nextBeforeByte ?? 0,
438
+ endByte: initialEnd,
439
+ };
440
+ }
441
+ function readUtf8Range(path, start, end) {
442
+ const length = Math.max(0, end - start);
443
+ if (length === 0)
444
+ return "";
445
+ const buffer = Buffer.alloc(length);
446
+ const fd = openSync(path, "r");
447
+ try {
448
+ const bytesRead = readSync(fd, buffer, 0, length, start);
449
+ return buffer.subarray(0, bytesRead).toString("utf8");
450
+ }
451
+ finally {
452
+ closeSync(fd);
453
+ }
454
+ }
455
+ function transcriptLineRecords(content, startByte, endByte) {
456
+ let text = content;
457
+ let byteOffset = startByte;
458
+ if (startByte > 0) {
459
+ const firstNewline = text.indexOf("\n");
460
+ if (firstNewline < 0)
461
+ return [];
462
+ const skipped = text.slice(0, firstNewline + 1);
463
+ byteOffset += Buffer.byteLength(skipped, "utf8");
464
+ text = text.slice(firstNewline + 1);
465
+ }
466
+ if (endByte > startByte && text && !text.endsWith("\n")) {
467
+ const lastNewline = text.lastIndexOf("\n");
468
+ if (lastNewline >= 0)
469
+ text = text.slice(0, lastNewline + 1);
470
+ }
471
+ const records = [];
472
+ const segments = text.match(/[^\n]*(?:\n|$)/g) ?? [];
473
+ for (const segment of segments) {
474
+ if (!segment)
475
+ continue;
476
+ const lineStart = byteOffset;
477
+ byteOffset += Buffer.byteLength(segment, "utf8");
478
+ const line = segment.replace(/\r?\n$/, "");
479
+ if (line.trim())
480
+ records.push({ text: line, startByte: lineStart });
481
+ }
482
+ return records;
483
+ }
484
+ function parseTranscriptLine(line) {
485
+ try {
486
+ return parseSessionEntries(`${line}\n`).filter((entry) => entry.type !== "session");
487
+ }
488
+ catch {
489
+ return [];
490
+ }
491
+ }
492
+ function entryTimestampMs(entry) {
493
+ const timestamp = entry.timestamp;
494
+ if (typeof timestamp !== "string")
495
+ return Number.POSITIVE_INFINITY;
496
+ const parsed = Date.parse(timestamp);
497
+ return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
498
+ }
499
+ function offsetTranscriptTraceOrder(nodes, offset) {
500
+ for (const node of nodes) {
501
+ if (node.source === "transcript" && node.orderKey) {
502
+ const transcriptIndex = node.orderKey.transcriptIndex;
503
+ node.orderKey = {
504
+ ...node.orderKey,
505
+ turnSeq: node.orderKey.turnSeq + offset,
506
+ transcriptIndex: transcriptIndex === undefined ? undefined : transcriptIndex + offset,
507
+ };
508
+ }
509
+ offsetTranscriptTraceOrder(node.children, offset);
510
+ }
511
+ }
327
512
  function annotateForkableUserMessageNodes(nodes, entries) {
328
513
  const candidates = userMessageForkCandidates(entries);
329
514
  if (!candidates.length)