pikibun 0.0.0-stage → 0.19.0

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 (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -2
  3. package/dist/chunks/chunk-5QG5ACQ3.js +382 -0
  4. package/dist/chunks/chunk-B3B75QDQ.js +15735 -0
  5. package/dist/chunks/chunk-LMXDTJAS.js +13 -0
  6. package/dist/chunks/cli-M3XHZYKE.js +15413 -0
  7. package/dist/chunks/postgres-3KR5M5D5.js +140 -0
  8. package/dist/chunks/postgres-PE662ND4.js +456 -0
  9. package/dist/cli.js +133 -0
  10. package/dist/migrations/postgres/0001_schema.sql +404 -0
  11. package/dist/migrations/postgres/0002_chat_agents.sql +3 -0
  12. package/dist/migrations/postgres/0003_tool_grants.sql +12 -0
  13. package/dist/migrations/postgres/0004_owner_host_push.sql +9 -0
  14. package/dist/migrations/postgres/0005_session_archive.sql +3 -0
  15. package/dist/migrations/postgres/0006_session_activity.sql +3 -0
  16. package/dist/migrations/postgres/0006_workspace_databases.sql +26 -0
  17. package/dist/migrations/postgres/0007_session_links.sql +9 -0
  18. package/dist/migrations/postgres/0008_session_repins.sql +10 -0
  19. package/dist/migrations/postgres/0009_native_sessions.sql +18 -0
  20. package/dist/migrations/postgres/0010_host_migrations.sql +41 -0
  21. package/dist/migrations/postgres/0011_removed_hosts.sql +2 -0
  22. package/dist/migrations/postgres/0012_network_policies.sql +9 -0
  23. package/dist/migrations/postgres/0013_clone_sessions.sql +4 -0
  24. package/dist/migrations/postgres/0014_user_suspension.sql +2 -0
  25. package/dist/migrations/postgres/0015_automatic_backups.sql +32 -0
  26. package/dist/migrations/postgres/0016_member_roles.sql +4 -0
  27. package/dist/migrations/postgres/0017_repository_lfs.sql +8 -0
  28. package/dist/migrations/postgres/0017_session_branches.sql +5 -0
  29. package/dist/migrations/postgres/0018_host_sharing_automation.sql +41 -0
  30. package/dist/migrations/postgres/0019_conversation_only_backups.sql +4 -0
  31. package/dist/migrations/postgres/0020_owner_hosts.sql +18 -0
  32. package/dist/migrations/postgres/0021_workspace_invites.sql +22 -0
  33. package/dist/migrations/postgres/0022_pull_request_states.sql +6 -0
  34. package/dist/migrations/postgres/0023_disabled_host_models.sql +13 -0
  35. package/dist/migrations/postgres/0024_host_configuration.sql +31 -0
  36. package/dist/migrations/postgres/0025_transcript_display.sql +14 -0
  37. package/dist/migrations/postgres/0026_scheduled_prompts.sql +32 -0
  38. package/dist/migrations/postgres/0027_host_compaction.sql +6 -0
  39. package/dist/migrations/postgres/0028_workspace_tool_output.sql +5 -0
  40. package/dist/migrations/postgres/0029_host_session_grants.sql +6 -0
  41. package/dist/migrations/postgres/0029_session_branches.sql +24 -0
  42. package/dist/migrations/postgres/0030_cross_session_messages.sql +42 -0
  43. package/dist/migrations/postgres/0031_password_sign_in.sql +14 -0
  44. package/dist/migrations/postgres/0032_web_search.sql +6 -0
  45. package/dist/migrations/postgres/0033_workspace_sources.sql +115 -0
  46. package/dist/migrations/postgres/0034_git_servers.sql +28 -0
  47. package/dist/migrations/postgres/0035_workspace_connections.sql +57 -0
  48. package/dist/migrations/postgres/0036_session_packing.sql +6 -0
  49. package/dist/migrations/sqlite/0001_control_plane.sql +135 -0
  50. package/dist/migrations/sqlite/0002_connectors.sql +70 -0
  51. package/dist/migrations/sqlite/0003_restores.sql +35 -0
  52. package/dist/migrations/sqlite/0004_approval_reviews.sql +38 -0
  53. package/dist/migrations/sqlite/0005_repositories.sql +27 -0
  54. package/dist/migrations/sqlite/0006_sharing_defaults.sql +16 -0
  55. package/dist/migrations/sqlite/0007_git_providers.sql +46 -0
  56. package/dist/migrations/sqlite/0008_git_push.sql +75 -0
  57. package/dist/migrations/sqlite/0009_chat_agents.sql +3 -0
  58. package/dist/migrations/sqlite/0010_tool_grants.sql +12 -0
  59. package/dist/migrations/sqlite/0011_owner_host_push.sql +46 -0
  60. package/dist/migrations/sqlite/0012_session_archive.sql +3 -0
  61. package/dist/migrations/sqlite/0013_session_activity.sql +3 -0
  62. package/dist/migrations/sqlite/0013_workspace_databases.sql +25 -0
  63. package/dist/migrations/sqlite/0014_session_links.sql +9 -0
  64. package/dist/migrations/sqlite/0015_session_repins.sql +10 -0
  65. package/dist/migrations/sqlite/0016_native_sessions.sql +18 -0
  66. package/dist/migrations/sqlite/0017_host_migrations.sql +45 -0
  67. package/dist/migrations/sqlite/0018_removed_hosts.sql +2 -0
  68. package/dist/migrations/sqlite/0019_network_policies.sql +10 -0
  69. package/dist/migrations/sqlite/0020_clone_sessions.sql +7 -0
  70. package/dist/migrations/sqlite/0021_user_suspension.sql +3 -0
  71. package/dist/migrations/sqlite/0022_automatic_backups.sql +31 -0
  72. package/dist/migrations/sqlite/0023_member_roles.sql +17 -0
  73. package/dist/migrations/sqlite/0024_repository_lfs.sql +9 -0
  74. package/dist/migrations/sqlite/0024_session_branches.sql +9 -0
  75. package/dist/migrations/sqlite/0025_host_sharing_automation.sql +41 -0
  76. package/dist/migrations/sqlite/0026_conversation_only_backups.sql +3 -0
  77. package/dist/migrations/sqlite/0027_owner_hosts.sql +16 -0
  78. package/dist/migrations/sqlite/0028_workspace_invites.sql +21 -0
  79. package/dist/migrations/sqlite/0029_pull_request_states.sql +7 -0
  80. package/dist/migrations/sqlite/0030_disabled_host_models.sql +11 -0
  81. package/dist/migrations/sqlite/0031_host_configuration.sql +29 -0
  82. package/dist/migrations/sqlite/0032_transcript_display.sql +12 -0
  83. package/dist/migrations/sqlite/0033_scheduled_prompts.sql +30 -0
  84. package/dist/migrations/sqlite/0034_host_compaction.sql +4 -0
  85. package/dist/migrations/sqlite/0035_workspace_tool_output.sql +3 -0
  86. package/dist/migrations/sqlite/0036_host_session_grants.sql +4 -0
  87. package/dist/migrations/sqlite/0036_session_branches.sql +22 -0
  88. package/dist/migrations/sqlite/0037_cross_session_messages.sql +40 -0
  89. package/dist/migrations/sqlite/0038_password_sign_in.sql +36 -0
  90. package/dist/migrations/sqlite/0039_web_search.sql +4 -0
  91. package/dist/migrations/sqlite/0040_workspace_sources.sql +90 -0
  92. package/dist/migrations/sqlite/0041_git_servers.sql +94 -0
  93. package/dist/migrations/sqlite/0042_workspace_connections.sql +46 -0
  94. package/dist/migrations/sqlite/0043_session_packing.sql +4 -0
  95. package/dist/pikibun.js +9 -0
  96. package/dist/web/assets/index--HHEIJzn.js +53 -0
  97. package/dist/web/assets/index-CFSPzPNw.css +1 -0
  98. package/dist/web/assets/index-ZSxhzT-_.js +5 -0
  99. package/dist/web/favicon.svg +1 -0
  100. package/dist/web/index.html +17 -0
  101. package/npm-shrinkwrap.json +2426 -0
  102. package/package.json +32 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenCara
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,162 @@
1
- # Temporary Holding Version
1
+ # pikibun
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Connect your Linux or WSL2 Pi host to a [Pikibun](https://pikibun.com) workspace. The connector uses outbound WebSocket only; no inbound port or tunnel is needed.
4
+
5
+ Or run Pikibun itself on your own computer, for yourself: `npx pikibun@latest serve` (see [Running Pikibun on your own computer](#running-pikibun-on-your-own-computer)).
6
+
7
+ ## Requirements
8
+
9
+ - Linux or WSL2 (native Windows is unsupported)
10
+ - Node.js 24 or newer
11
+ - Pi configured locally with the providers and models you want to offer
12
+ - `bubblewrap`, Python 3, Git and `flock` (util-linux)
13
+
14
+ ## Usage
15
+
16
+ In Pikibun, open **Settings → Hosts → Pair a host**, choose a workspace and create a pairing code. Then run, on the Pi host:
17
+
18
+ ```sh
19
+ npx pikibun@latest connect --server https://pikibun.com --pairing-id PAIRING_ID
20
+ ```
21
+
22
+ Enter the single-use code when prompted; it is never accepted as an argument and expires after 10 minutes. Later runs reuse the private saved connector token, kept in `~/.local/share/pikibun/connector` (`--state-dir` chooses another directory):
23
+
24
+ ```sh
25
+ npx pikibun@latest connect
26
+ ```
27
+
28
+ To pair the machine again, for example after revoking its host in Pikibun or to move it to another workspace or server, create a new pairing code and add `--force` to its command:
29
+
30
+ ```sh
31
+ npx pikibun@latest connect --server https://pikibun.com --pairing-id PAIRING_ID --force
32
+ ```
33
+
34
+ The saved token is replaced only once the new code is accepted. The old host stays in **Settings → Hosts**, offline, until you revoke it there; its sessions are not moved to the new host.
35
+
36
+ Connector 0.8.0 moved the default state directory from `~/.local/share/opencara/connector` to `~/.local/share/pikibun/connector` without moving anything: to keep a pairing made by an older connector, run `npx pikibun@latest connect --state-dir ~/.local/share/opencara/connector`, or pair the machine again.
37
+
38
+ Run `npx pikibun connect --help` for all options. The connector takes only what pairing and this machine's own setup need (`--state-dir`, `--pi-agent-dir`, `--model`, `--resources`, `--unsafe-no-sandbox`, `--experimental-sandbox`).
39
+
40
+ ## Host configuration
41
+
42
+ Everything else about how the host runs sessions is set in the web app, on the host's page under **Settings → Hosts → the host → Configuration**, and reaches running sessions within a minute:
43
+
44
+ - **Command time limit**: how long a shell command may run, from 30 seconds (the default) to an hour. A longer limit also lifts the per-process limits of short commands (25 seconds of CPU, 64 MiB per file, 256 open files) so builds fit. Set by the host's owner or its admins.
45
+ - **GPU**: sandboxed commands get the host's GPU render device for offscreen rendering and compute: `/dev/dxg` on WSL2 (install Mesa's `vulkan-dzn` there for Vulkan), or Linux DRM render nodes. Never the screen, windows or input. Offered only where the connector found such a device. Set by the host's owner or its admins.
46
+ - **Git credentials** and **Repository cache**: see below. Only the host's owner changes them.
47
+ - **Sessions on the host**: the host's owner may start sessions that run directly on this machine, without the sandbox, by choosing **Where it runs → On this host** when starting one. Members' sessions run in the sandbox unless the owner's host command grant to them includes **Sessions on the host**. Only the owner changes it.
48
+
49
+ `--git-credentials`, `--repo-cache` and `--repo-cache-scope` are still accepted, so existing service units keep starting, but ignored: a host that used them keeps those settings in its configuration.
50
+
51
+ Workspace repositories can come from GitHub, GitHub Enterprise Server, GitLab (gitlab.com or self-managed), Bitbucket Cloud, Azure DevOps, Gitea/Forgejo and any Git server over HTTPS (smart HTTP). Connect the non-GitHub accounts in the web app under **Settings → Profile → Connected accounts** with personal access tokens, or a user name and password for a Git server. This connector accepts Git server repositories in its sessions; older connectors are sent the workspace's other repositories only. Sessions read the repositories through the Pikibun server, which uses those tokens; the connector never receives them.
52
+
53
+ Model credentials stay in Pi on your host. Pairing keeps the host private to you; sharing it with a workspace is a separate, explicit action in the web app.
54
+
55
+ The connector offers the models matched by `enabledModels` in Pi's `settings.json` (the same patterns Pi uses for model cycling, e.g. `"anthropic/*"`, `"*/claude-opus-5-5*"`), or every authenticated Pi model when that list is unset. `--model PROVIDER/MODEL` (repeatable) overrides both. In the web app, you and the host's admins can also disable any of these models, for everyone who uses the host, without restarting the connector (**Settings → Hosts →** the host **→ Models**).
56
+
57
+ ## Host commands from chat
58
+
59
+ Ask the agent to use `host_bash` when you need a host-installed SDK or network access, for example `dotnet --info`. Ordinary `bash` remains in the offline sandbox; a missing SDK does not silently turn a sandbox command into a host command. Update the connector to a version advertising `hostCommands` to expose this tool.
60
+
61
+ **Host commands run outside the sandbox as the connector's OS user.** They can read or change host files and credentials, access the network, and launch other processes. They have no CPU, memory, process-count or disk quotas. Pikibun passes the host's `PATH` and `HOME`, not the connector's entire environment, and starts Bash without shell profiles, in the session worktree. Use an absolute SDK path or configure the connector's `PATH` if a tool is only available in your interactive shell.
62
+
63
+ Commands run in the session's worktree, whose files and Git configuration the agent can change: builds, package scripts, Git hooks and settings such as `core.fsmonitor` there can run the agent's code on the host, even for a command that looks harmless like `git status`. Approve a host command only if you would run it yourself in that directory.
64
+
65
+ Each exact command and timeout needs the approval of the **host owner**, or of a member they (or one of the host's admins) granted host commands, in the session. On a shared host, the approver must also have approve access to the session. The session owner's **Auto-approve** skips this decision only when they own the host too, or hold an automatic host command grant on it; without one it never authorizes commands on someone else's host. Commands default to a five-minute timeout (`timeout_seconds`, 1-3600); timeout, cancellation and output limits terminate the process group, but cannot contain an unrestricted command that deliberately detaches processes.
66
+
67
+ ## macOS, Windows and hosts without bubblewrap
68
+
69
+ The isolated sandbox needs Linux (or WSL2 on Windows). macOS and native Windows can try their own experimental sandboxes with `--experimental-sandbox` (see the main README, "Experimental sandboxes on macOS and Windows"). On macOS, native Windows, or Linux without bubblewrap, you can also opt out of isolation:
70
+
71
+ ```sh
72
+ npx pikibun@latest connect --unsafe-no-sandbox
73
+ ```
74
+
75
+ This needs only Git on your `PATH` (on macOS: `xcode-select --install` or `brew install git`). The connector prints the risks and asks you to type `unsandboxed` before starting:
76
+
77
+ - Approved shell commands run directly as your user, with access to your files, credentials (`~/.ssh`, cloud and Git config, Pi's auth) and network, and no CPU, memory or disk limits.
78
+ - File tools are confined to each session folder by path checks only.
79
+ - Anyone you share the Pi with, and anyone granted approve permission on a session that runs on it, can cause approved commands to run on your machine.
80
+
81
+ Every command needs an exact human approval unless the session's owner turns on **Auto-approve** in the chat box; then that owner's commands, pushes, GitHub comments, merges and issues run immediately. Pikibun labels the host **Unsandboxed** to everyone who can see it.
82
+
83
+ ## Repository cache (preview)
84
+
85
+ Every session starts with an empty worktree, and its agent clones the workspace repositories it needs. With **Repository cache** set to **Shared** (one cache for every session on this host) or **Per owner** (a separate cache for each session owner) in the host's configuration, the connector keeps a full mirror of each repository on the host, so clones borrow its objects instead of downloading everything again. A change applies when the connector next connects.
86
+
87
+ - `git clone` of a workspace repository in a sandboxed session borrows the cached objects (`--reference-if-able`) after bringing the mirror up to date through Pikibun, with the session owner's own account. The host uses no Git credential of its own for this. A session sees a mirror, read-only, only while Pikibun lets its owner read the repository.
88
+ - `npx pikibun@latest cache list` shows the cached repositories and their sizes; `npx pikibun@latest cache prune --older-than 30` forgets clone sessions idle for longer and collects what nothing reaches any more (their clones lose the objects they borrowed). Pins of sessions from before every session cloned are kept.
89
+ - It needs a Pikibun server that supports the repository cache; older servers refuse the connector's manifest.
90
+
91
+ ## Pushing commits
92
+
93
+ In a session that clones on demand, agents push their work to the session's own branch, named for its work (`pikibun/<title-slug>-<short id>`, for example `pikibun/add-dark-mode-1a2b3c4d5e6f`), with plain `git push` and no approval; Pikibun pushes it with the workspace's push identity below. Agents with the `git_push` tool push a clone's checked-out commit, as it is, to any other branch of the workspace repository it was cloned from. A push needs an approval that shows the repository, the branch and the exact commit, unless the session owner chose **Auto-approve** for that message. Pushes are fast-forward only and never forced, and send only the objects the branch doesn't have. On a host without the sandbox, the agent pushes with the host's own Git instead, each command with approval. The agent can also ask for a pull request; Pikibun opens it as the session owner with the owner's connected account (non-GitHub tokens then need write scopes: GitHub Enterprise pull requests write, GitLab `api`, Bitbucket `write:pullrequest:bitbucket`, Azure DevOps Code (Read & write), Gitea `write:repository`).
94
+
95
+ For each push, Pikibun checks with the session owner's own account that the owner may push to the repository and, where the Git host reports it, to that branch (GitLab, Gitea/Forgejo and Azure DevOps branch permissions; GitHub protected branches are refused). Bitbucket owners need an Atlassian API token connected with their account email for this check. Pikibun then issues a credential for that push from the push identity that a workspace owner or admin sets up for the provider under **Settings → Workspaces → Push access**: usually a short-lived token minted from an app or bot, as below. The connector keeps the credential in memory only and talks to the Git host directly, so large files never pass through the Pikibun server, and the agent's shell and sandbox never see the credential.
96
+
97
+ | Provider | Push identity | Token |
98
+ |---|---|---|
99
+ | GitHub | The Pikibun GitHub App installed on the repository, or your own GitHub App | 1 hour, this repository only, revoked after the push |
100
+ | GitHub Enterprise Server | A GitHub App registered on your server | 1 hour, this repository only, revoked after the push |
101
+ | GitLab | An OAuth application authorized once by a bot account (`write_repository read_user`) | Up to 2 hours, the bot's projects |
102
+ | Bitbucket Cloud | An OAuth consumer of the Bitbucket workspace with Repositories: Write | 1–2 hours as Bitbucket issues it, the workspace's repositories |
103
+ | Azure DevOps | A Microsoft Entra app with a federated credential that trusts Pikibun, its service principal added to the organization | About 1 hour, what you grant the service principal |
104
+ | Forgejo v16 or newer | A Generic JWT Authorized Integration that trusts Pikibun | 10 minutes, what the integration allows |
105
+ | Gitea 1.23 or newer | An OAuth2 application authorized once by a bot account (`write:repository read:user`) | Up to 1 hour, the bot's repositories |
106
+
107
+ Except for GitHub, a push token carries everything its identity can reach until it expires, and the host that runs the session receives it. Give each bot, consumer, service principal or integration access to the workspace's repositories only; a Forgejo integration can also require the `repository` and `ref` claims Pikibun adds to each token. Workspace owners and admins can forbid agent pushes to a repository's default branch, and removing a repository from the workspace stops pushes from sessions pinned to it.
108
+
109
+ Instead of an app or bot, a provider's push identity can be one of these. Both push only from sessions running on the session owner's own hosts, so no one's credentials reach anyone else's machine; elsewhere the push is refused and needs an app or bot identity:
110
+
111
+ - **Session owner's account**: the push uses the account the session owner connected to Pikibun, the GitHub sign-in or the token under **Settings → Profile → Connected accounts**, which then needs write scopes (GitHub Enterprise Contents write, GitLab `write_repository`, Bitbucket `write:repository:bitbucket`, Azure DevOps Code (Read & write), Gitea `write:repository`). Your account tokens otherwise never reach the connector: with this identity your own host receives yours for each push in your sessions. Pikibun resets a GitHub token through its OAuth app when the push ends, so the host's copy stops working while you stay connected; if the host never reports the push, the reset follows about an hour later. Tokens for the other providers can't be reset and stay valid after the push.
112
+ - **Host's Git credentials** (connector 0.6.0 or newer): the connector reads the HTTPS credentials Git on the host would use for the repository, through its credential helpers (for example after `gh auth setup-git`, or from the macOS keychain or Git Credential Manager), without ever prompting. Pikibun sends no credential. The host's owner turns on **Git credentials** in its configuration; then only their own sessions on that host push this way.
113
+
114
+ Pull requests are still opened as the session owner, with the owner's connected account.
115
+
116
+ ## GitHub issues and pull requests
117
+
118
+ The sandbox has no network, so `gh` and `curl` inside it can't reach GitHub. Agents read and act on GitHub through Pikibun instead, as the session owner and with the owner's GitHub sign-in, which stays on the Pikibun server: the host and the sandbox never see it. These tools need connector 0.7.0 or newer; an agent whose tools predate them asks the session owner the first time, with the permission card described below. GitHub Enterprise and other providers are not covered.
119
+
120
+ - `github_read` reads an issue or pull request of one of the workspace's github.com repositories: its description and comments, and for a pull request its merge state, head commit, reviews, review comments with file, line and diff hunk, and checks. Long threads keep their newest items. Like any repository content, what it returns is untrusted input. A read needs no approval, but only runs during a write message (until an hour after it was sent), and each read is recorded in the session's audit log.
121
+ - `github_merge` merges a pull request of one of those repositories at the exact head commit the agent read, with a merge commit, squash or rebase. It waits for a human approval unless the session owner's message chose **Auto-approve** (in older Pikibun apps, with their **Auto-merge** option on too). If the head moved or GitHub refuses (failing checks, missing reviews, conflicts, a method the repository doesn't allow), nothing is merged and the agent gets GitHub's reason.
122
+ - `github_comment` posts a comment. It waits for a human approval unless the session owner's message chose **Auto-approve**.
123
+ - `github_issue` opens an issue with exactly the approved title and body in one of those repositories, and gives the agent its number and URL (connector newer than 0.9.0). It waits for a human approval unless the session owner's message chose **Auto-approve**. If GitHub refuses (issues turned off, no access), no issue is opened and the agent gets GitHub's reason.
124
+
125
+ ## Web pages and search
126
+
127
+ Agents read web pages with `web_fetch` and search the web with `web_search` (connector newer than 0.9.0).
128
+
129
+ - `web_fetch` reads one `https://` page from this host, as far as the workspace's network access lets the session's sandbox reach, and never at an address in this host's own networks. It sends no cookies or credentials and only reads; HTML comes back as text, images as images. It needs no approval, during a shared message. On a host running without the sandbox it still follows the workspace's network access.
130
+ - `web_search` asks Pikibun, which searches with the provider its operator configured, where the workspace turned web search on. The provider's key never reaches this host.
131
+
132
+ What they return is untrusted input, like any repository content.
133
+
134
+ ## Chat options
135
+
136
+ In a session's chat box you can pick any model this host's Pi offers that isn't disabled in the web app, its reasoning level, and the access mode for each message: **Read only**, **Ask before acting** (the default) or **Auto-approve** (session owner only). Hosts running a connector older than 0.3.0 keep each session's pinned model; restart the connector with `npx pikibun@latest connect` to enable the options.
137
+
138
+ A model starts at the reasoning level Pi on the host gives it: the level in its `enabledModels` pattern (such as `"anthropic/*:high"`), else its entry in `modelThinkingLevels`, else `defaultThinkingLevel`, else Pi's default (medium), moved to the nearest level the model supports. The chat box shows that level when you pick the model (connector 0.5.1 or newer). The levels on offer are the ones Pi reports for the model. If the host's `models.json` redefines a model without its `thinkingLevelMap`, Pi offers only Off to High for it.
139
+
140
+ Agents change files with `edit_file`, which replaces exact text in place, or with `write_file`, which writes a whole file. `edit_file` needs connector 0.5.1 or newer.
141
+
142
+ A new session starts from a host and one of its models, and gets every tool that host supports with no extra instructions. The access mode still decides what runs without asking.
143
+
144
+ When a call needs more than the session allows, the agent asks instead of failing (connector 0.6.0 or newer): a tool its agent doesn't include, a change in a **Read only** message, or a shell command on a sandboxed host without resource limits. The session owner gets a card with **Allow once**, **Allow for this session** and **Deny**. The call can wait for the answer until an hour after the message was sent, and the session's queue waits with it. Allowing a tool doesn't approve what it does: commands, commits, pushes, GitHub comments, merges and issues still need their own approval unless the message auto-approves. A message the session owner sends with **Auto-approve** asks none of this (connector 0.7.1 or newer): its calls go ahead at once, for that message only. Only the host's owner can let commands run without resource limits in their own sessions, and so can members they granted host commands there, in theirs (connectors advertising `unboundedShellGrants`), whose commands are then decided like host commands. Either way they still run in the sandbox, offline and for at most the host's command time limit, but with no cap on CPU, memory, processes or disk. A session restored from a backup asks again.
145
+
146
+ ## Session status
147
+
148
+ The session list shows what each session's agent is doing, as its host reports it: **In progress** while a message is queued or running, **Needs approval** while a permission card holds the work or, once the work stops, an operation awaits its approval, then **Done** or **Failed** for how the latest shared message ended. Side asks don't change done or failed. Hosts report this with connectors newer than 0.7.1; restart an older one with `npx pikibun@latest connect` to show it.
149
+
150
+ ## License
151
+
152
+ MIT
153
+
154
+ ## Running Pikibun on your own computer
155
+
156
+ ```sh
157
+ npx pikibun@latest serve
158
+ ```
159
+
160
+ runs the Pikibun server and its web app on `http://127.0.0.1:7878`, reachable from this computer only, with this computer as the host that runs your sessions. You sign in with GitHub through an OAuth app of your own: register one once at <https://github.com/settings/applications/new> with the homepage `http://127.0.0.1` and the callback URL `http://127.0.0.1/api/auth/github/callback`, and the first run asks for its client ID and secret (hidden). It then prints a sign-in link: the GitHub account that signs in through it becomes the owner, and the only account that can sign in. Once you have, this computer pairs itself as your host, sandboxed as with `connect` (or with `--unsafe-no-sandbox`; `--no-host` serves without it).
161
+
162
+ Everything is kept in `~/.local/share/pikibun/server` (`--state-dir`), apart from `connect`'s own state, so later runs need no options. Run `npx pikibun serve --help` for the rest.
@@ -0,0 +1,382 @@
1
+ // ../../apps/server/src/db/types.ts
2
+ var ConstraintError = class extends Error {
3
+ constructor(message, constraint, options) {
4
+ super(message, options);
5
+ this.constraint = constraint;
6
+ this.name = "ConstraintError";
7
+ }
8
+ constraint;
9
+ };
10
+
11
+ // ../../apps/server/src/db/migrations.ts
12
+ import { existsSync } from "node:fs";
13
+ import { readdir, readFile } from "node:fs/promises";
14
+ import { dirname, join, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ function migrationsDirectory(provider) {
17
+ if (process.env.MIGRATIONS_DIR) return resolve(process.env.MIGRATIONS_DIR, provider);
18
+ let directory = dirname(fileURLToPath(import.meta.url));
19
+ for (let depth = 0; depth < 3; depth++) {
20
+ const candidate = join(directory, "migrations", provider);
21
+ if (existsSync(candidate)) return candidate;
22
+ directory = dirname(directory);
23
+ }
24
+ throw new Error(`No migrations directory for ${provider} near ${fileURLToPath(import.meta.url)}; set MIGRATIONS_DIR.`);
25
+ }
26
+ async function readMigrations(directory) {
27
+ const names = (await readdir(directory)).filter((name) => name.endsWith(".sql")).sort();
28
+ return Promise.all(names.map(async (name) => ({ name, sql: await readFile(join(directory, name), "utf8") })));
29
+ }
30
+
31
+ // ../../apps/server/src/db/sql.ts
32
+ var identStart = /[A-Za-z_]/;
33
+ var identChar = /[A-Za-z0-9_$]/;
34
+ var operators = ["->>", "<>", "!=", "<=", ">=", "||", "->"];
35
+ function tokenize(sql) {
36
+ const tokens = [];
37
+ let i = 0;
38
+ while (i < sql.length) {
39
+ const ch = sql[i];
40
+ if (/\s/.test(ch)) {
41
+ let end = i + 1;
42
+ while (end < sql.length && /\s/.test(sql[end])) end++;
43
+ tokens.push({ kind: "space", text: sql.slice(i, end) });
44
+ i = end;
45
+ } else if (ch === "-" && sql[i + 1] === "-") {
46
+ let end = sql.indexOf("\n", i);
47
+ if (end === -1) end = sql.length;
48
+ tokens.push({ kind: "space", text: sql.slice(i, end) });
49
+ i = end;
50
+ } else if (ch === "/" && sql[i + 1] === "*") {
51
+ let end = sql.indexOf("*/", i + 2);
52
+ if (end === -1) throw new Error("Unterminated comment in SQL statement.");
53
+ end += 2;
54
+ tokens.push({ kind: "space", text: sql.slice(i, end) });
55
+ i = end;
56
+ } else if (ch === "'" || ch === '"') {
57
+ let end = i + 1;
58
+ for (; ; ) {
59
+ const close = sql.indexOf(ch, end);
60
+ if (close === -1) throw new Error("Unterminated string literal in SQL statement.");
61
+ if (sql[close + 1] === ch) {
62
+ end = close + 2;
63
+ continue;
64
+ }
65
+ end = close + 1;
66
+ break;
67
+ }
68
+ tokens.push({ kind: ch === "'" ? "string" : "quoted", text: sql.slice(i, end) });
69
+ i = end;
70
+ } else if (ch === "?") {
71
+ tokens.push({ kind: "placeholder", text: "?" });
72
+ i++;
73
+ } else if (identStart.test(ch)) {
74
+ let end = i + 1;
75
+ while (end < sql.length && identChar.test(sql[end])) end++;
76
+ tokens.push({ kind: "ident", text: sql.slice(i, end) });
77
+ i = end;
78
+ } else if (/[0-9]/.test(ch)) {
79
+ let end = i + 1;
80
+ while (end < sql.length && /[0-9.]/.test(sql[end])) end++;
81
+ tokens.push({ kind: "number", text: sql.slice(i, end) });
82
+ i = end;
83
+ } else {
84
+ const operator = operators.find((candidate) => sql.startsWith(candidate, i));
85
+ tokens.push({ kind: "punct", text: operator ?? ch });
86
+ i += operator ? operator.length : 1;
87
+ }
88
+ }
89
+ return tokens;
90
+ }
91
+ var keywordList = [
92
+ "SELECT",
93
+ "FROM",
94
+ "WHERE",
95
+ "AND",
96
+ "OR",
97
+ "NOT",
98
+ "NULL",
99
+ "IS",
100
+ "IN",
101
+ "AS",
102
+ "ON",
103
+ "JOIN",
104
+ "LEFT",
105
+ "RIGHT",
106
+ "INNER",
107
+ "CROSS",
108
+ "OUTER",
109
+ "FULL",
110
+ "NATURAL",
111
+ "USING",
112
+ "GROUP",
113
+ "ORDER",
114
+ "BY",
115
+ "HAVING",
116
+ "LIMIT",
117
+ "OFFSET",
118
+ "UNION",
119
+ "ALL",
120
+ "EXCEPT",
121
+ "INTERSECT",
122
+ "INSERT",
123
+ "INTO",
124
+ "VALUES",
125
+ "UPDATE",
126
+ "SET",
127
+ "DELETE",
128
+ "RETURNING",
129
+ "CONFLICT",
130
+ "DO",
131
+ "NOTHING",
132
+ "WITH",
133
+ "RECURSIVE",
134
+ "CASE",
135
+ "WHEN",
136
+ "THEN",
137
+ "ELSE",
138
+ "END",
139
+ "EXISTS",
140
+ "BETWEEN",
141
+ "LIKE",
142
+ "GLOB",
143
+ "DISTINCT",
144
+ "ASC",
145
+ "DESC",
146
+ "COLLATE",
147
+ "CAST",
148
+ "LATERAL",
149
+ "EXCLUDED",
150
+ "TRUE",
151
+ "FALSE",
152
+ "REPLACE",
153
+ "DEFAULT",
154
+ "ESCAPE"
155
+ ];
156
+ var keywords = Object.fromEntries(keywordList.map((word) => [word, true]));
157
+ var joinQualifiers = { CROSS: true, INNER: true, LEFT: true, RIGHT: true, FULL: true, OUTER: true, NATURAL: true };
158
+ var isKeyword = (token) => token?.kind === "ident" && keywords[token.text.toUpperCase()] === true;
159
+ var isIdent = (token, name) => token?.kind === "ident" && token.text.toUpperCase() === name;
160
+ var isPunct = (token, text) => token?.kind === "punct" && token.text === text;
161
+ function next(tokens, from) {
162
+ let i = from;
163
+ while (i < tokens.length && tokens[i].kind === "space") i++;
164
+ return i;
165
+ }
166
+ function previous(tokens, from) {
167
+ let i = from;
168
+ while (i >= 0 && tokens[i].kind === "space") i--;
169
+ return i;
170
+ }
171
+ function closing(tokens, open) {
172
+ let depth = 0;
173
+ for (let i = open; i < tokens.length; i++) {
174
+ if (isPunct(tokens[i], "(")) depth++;
175
+ else if (isPunct(tokens[i], ")") && --depth === 0) return i;
176
+ }
177
+ throw new Error("Unbalanced parentheses in SQL statement.");
178
+ }
179
+ function callArguments(tokens, open, close) {
180
+ const ranges = [];
181
+ let start = open + 1, depth = 0;
182
+ for (let i = open + 1; i < close; i++) {
183
+ if (isPunct(tokens[i], "(")) depth++;
184
+ else if (isPunct(tokens[i], ")")) depth--;
185
+ else if (depth === 0 && isPunct(tokens[i], ",")) {
186
+ ranges.push([start, i]);
187
+ start = i + 1;
188
+ }
189
+ }
190
+ if (next(tokens, start) < close) ranges.push([start, close]);
191
+ return ranges;
192
+ }
193
+ function jsonPath(tokens, range) {
194
+ const token = range && tokens[next(tokens, range[0])];
195
+ const literal = token?.kind === "string" ? token.text.slice(1, -1) : null;
196
+ if (literal === null || !/^\$(\.[A-Za-z_][A-Za-z0-9_]*)+$/.test(literal) || next(tokens, next(tokens, range[0]) + 1) < range[1]) {
197
+ throw new Error("Only literal JSON paths of the form '$.a.b' can be translated.");
198
+ }
199
+ return literal.slice(2).split(".");
200
+ }
201
+ var pathOperator = (segments) => segments.length === 1 ? `->> '${segments[0]}'` : `#>> '{${segments.join(",")}}'`;
202
+ var pathArray = (segments) => `'{${segments.join(",")}}'`;
203
+ var CHANGES = "\0changes\0";
204
+ var Translator = class {
205
+ constructor(tokens) {
206
+ this.tokens = tokens;
207
+ }
208
+ tokens;
209
+ placeholders = 0;
210
+ changes = false;
211
+ subqueries = 0;
212
+ /** Indices of `)` closing an unaliased FROM/JOIN subquery that needs an alias appended. */
213
+ aliasAfter = /* @__PURE__ */ new Set();
214
+ range(from, to) {
215
+ const { tokens } = this;
216
+ let out = "";
217
+ for (let i = from; i < to; ) {
218
+ const token = tokens[i];
219
+ if (token.kind === "placeholder") {
220
+ out += `$${++this.placeholders}`;
221
+ const is = next(tokens, i + 1);
222
+ if (isIdent(tokens[is], "IS")) {
223
+ let operand = next(tokens, is + 1);
224
+ if (isIdent(tokens[operand], "NOT")) operand = next(tokens, operand + 1);
225
+ if (isIdent(tokens[operand], "NULL")) out += "::text";
226
+ }
227
+ i++;
228
+ continue;
229
+ }
230
+ if (token.kind === "ident") {
231
+ const call = next(tokens, i + 1);
232
+ if (isPunct(tokens[call], "(")) {
233
+ const translated = this.call(token.text.toLowerCase(), i, call);
234
+ if (translated) {
235
+ out += translated.text;
236
+ i = translated.end;
237
+ continue;
238
+ }
239
+ }
240
+ const upper = token.text.toUpperCase();
241
+ if (upper === "IS") {
242
+ const not = next(tokens, i + 1);
243
+ const negated = isIdent(tokens[not], "NOT");
244
+ const literal = tokens[negated ? next(tokens, not + 1) : not];
245
+ if (!isIdent(literal, "NULL") && !isIdent(literal, "DISTINCT") && !isIdent(literal, "TRUE") && !isIdent(literal, "FALSE")) {
246
+ out += negated ? "IS DISTINCT FROM" : "IS NOT DISTINCT FROM";
247
+ i = negated ? not + 1 : i + 1;
248
+ continue;
249
+ }
250
+ } else if (upper === "COLLATE" && isIdent(tokens[next(tokens, i + 1)], "NOCASE")) {
251
+ i = next(tokens, i + 1) + 1;
252
+ continue;
253
+ } else if (upper === "JOIN" && joinQualifiers[tokens[previous(tokens, i - 1)]?.text.toUpperCase() ?? ""] !== true) {
254
+ const fn = next(tokens, i + 1);
255
+ const open = next(tokens, fn + 1);
256
+ if (isIdent(tokens[fn], "JSON_EACH") && isPunct(tokens[open], "(")) {
257
+ let after = next(tokens, closing(tokens, open) + 1);
258
+ if (isIdent(tokens[after], "AS")) after = next(tokens, after + 1);
259
+ if (tokens[after]?.kind === "ident" && !isKeyword(tokens[after])) after = next(tokens, after + 1);
260
+ if (!isIdent(tokens[after], "ON") && !isIdent(tokens[after], "USING")) {
261
+ out += "CROSS JOIN";
262
+ i++;
263
+ continue;
264
+ }
265
+ }
266
+ } else if (!isKeyword(token) && /[A-Z]/.test(token.text) && /[a-z]/.test(token.text)) {
267
+ out += `"${token.text}"`;
268
+ i++;
269
+ continue;
270
+ }
271
+ out += token.text;
272
+ i++;
273
+ continue;
274
+ }
275
+ if (isPunct(token, "(")) {
276
+ const before = tokens[previous(tokens, i - 1)];
277
+ if ((isIdent(before, "FROM") || isIdent(before, "JOIN")) && isIdent(tokens[next(tokens, i + 1)], "SELECT")) {
278
+ const close = closing(tokens, i);
279
+ const after = tokens[next(tokens, close + 1)];
280
+ if (!isIdent(after, "AS") && !(after?.kind === "ident" && !isKeyword(after))) this.aliasAfter.add(close);
281
+ }
282
+ } else if (isPunct(token, ")") && this.aliasAfter.has(i)) {
283
+ out += `) AS sq${++this.subqueries}`;
284
+ i++;
285
+ continue;
286
+ }
287
+ out += token.text;
288
+ i++;
289
+ }
290
+ return out;
291
+ }
292
+ call(name, at, open) {
293
+ const { tokens } = this;
294
+ const close = closing(tokens, open);
295
+ const args = callArguments(tokens, open, close);
296
+ const end = close + 1;
297
+ const argument = (index) => {
298
+ const range = args[index];
299
+ if (!range) throw new Error(`${name}() needs ${index + 1} arguments.`);
300
+ return this.range(range[0], range[1]).trim();
301
+ };
302
+ switch (name) {
303
+ case "json_extract":
304
+ return { text: `((${argument(0)})::jsonb ${pathOperator(jsonPath(tokens, args[1]))})`, end };
305
+ case "json_each":
306
+ return { text: `jsonb_array_elements_text((${argument(0)})::jsonb)`, end };
307
+ case "json_group_array":
308
+ return { text: `COALESCE(jsonb_agg(${this.range(open + 1, close).trim()}), '[]'::jsonb)::text`, end };
309
+ case "json_array":
310
+ return { text: `jsonb_build_array(${this.range(open + 1, close).trim()})::text`, end };
311
+ case "json":
312
+ return { text: `((${argument(0)})::jsonb::text)`, end };
313
+ case "json_type": {
314
+ const value = args.length > 1 ? `((${argument(0)})::jsonb #> ${pathArray(jsonPath(tokens, args[1]))})` : `((${argument(0)})::jsonb)`;
315
+ return { text: `(CASE jsonb_typeof(${value}) WHEN 'boolean' THEN (${value} #>> '{}') WHEN 'string' THEN 'text' WHEN 'number' THEN (CASE WHEN (${value} #>> '{}') ~ '^-?[0-9]+$' THEN 'integer' ELSE 'real' END) ELSE jsonb_typeof(${value}) END)`, end };
316
+ }
317
+ case "json_set": {
318
+ const path = pathArray(jsonPath(tokens, args[1]));
319
+ const valueRange = args[2];
320
+ if (!valueRange) throw new Error("json_set() needs 3 arguments.");
321
+ const bare = next(tokens, valueRange[0]);
322
+ const value = tokens[bare]?.kind === "placeholder" && next(tokens, bare + 1) >= valueRange[1] ? `${argument(2)}::text` : argument(2);
323
+ return { text: `jsonb_set((${argument(0)})::jsonb, ${path}, to_jsonb(${value}))::text`, end };
324
+ }
325
+ case "changes":
326
+ if (args.length) return null;
327
+ this.changes = true;
328
+ return { text: CHANGES, end };
329
+ case "lower": {
330
+ const hex = next(tokens, open + 1), hexOpen = next(tokens, hex + 1), blob = next(tokens, hexOpen + 1);
331
+ const blobOpen = next(tokens, blob + 1), size = next(tokens, blobOpen + 1);
332
+ if (isIdent(tokens[hex], "HEX") && isPunct(tokens[hexOpen], "(") && isIdent(tokens[blob], "RANDOMBLOB") && isPunct(tokens[blobOpen], "(") && tokens[size]?.text === "16" && closing(tokens, blobOpen) === next(tokens, size + 1) && closing(tokens, hexOpen) === next(tokens, closing(tokens, blobOpen) + 1) && close === next(tokens, closing(tokens, hexOpen) + 1)) {
333
+ return { text: "replace(gen_random_uuid()::text, '-', '')", end };
334
+ }
335
+ return null;
336
+ }
337
+ default:
338
+ return null;
339
+ }
340
+ }
341
+ };
342
+ var postgresCache = /* @__PURE__ */ new Map();
343
+ function translateToPostgres(sql) {
344
+ const cached = postgresCache.get(sql);
345
+ if (cached) return cached;
346
+ const tokens = tokenize(sql);
347
+ const translator = new Translator(tokens);
348
+ let text = translator.range(0, tokens.length);
349
+ if (translator.changes) text = text.replaceAll(CHANGES, `$${translator.placeholders + 1}`);
350
+ const statement = { text, placeholders: translator.placeholders, changes: translator.changes };
351
+ if (postgresCache.size >= 2048) postgresCache.clear();
352
+ postgresCache.set(sql, statement);
353
+ return statement;
354
+ }
355
+ var shapeCache = /* @__PURE__ */ new Map();
356
+ function statementShape(sql) {
357
+ const cached = shapeCache.get(sql);
358
+ if (cached) return cached;
359
+ const tokens = tokenize(sql);
360
+ let depth = 0, verb = null, returning = false;
361
+ for (const token of tokens) {
362
+ if (isPunct(token, "(")) depth++;
363
+ else if (isPunct(token, ")")) depth--;
364
+ else if (depth === 0 && token.kind === "ident") {
365
+ const upper = token.text.toUpperCase();
366
+ if (!verb && ["SELECT", "INSERT", "UPDATE", "DELETE", "REPLACE"].includes(upper)) verb = upper;
367
+ if (upper === "RETURNING") returning = true;
368
+ }
369
+ }
370
+ const shape = { write: verb !== null && verb !== "SELECT", returning };
371
+ if (shapeCache.size >= 2048) shapeCache.clear();
372
+ shapeCache.set(sql, shape);
373
+ return shape;
374
+ }
375
+
376
+ export {
377
+ ConstraintError,
378
+ migrationsDirectory,
379
+ readMigrations,
380
+ translateToPostgres,
381
+ statementShape
382
+ };