isorite 0.2.0 → 0.3.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 (71) hide show
  1. package/README.md +6 -3
  2. package/dist/core.js +207 -426
  3. package/dist/embed-worker.js +9 -4
  4. package/dist/main.js +7917 -6864
  5. package/docs/setup-for-agents.md +166 -0
  6. package/docs/setup.md +133 -0
  7. package/docs/terminal.md +119 -0
  8. package/package.json +2 -1
  9. package/plugin/isorite.ts +3 -3
  10. package/plugin/notepage.tsx +1 -9
  11. package/plugin/tui.tsx +9 -10
  12. package/web/_app/immutable/assets/0.IOi79DXH.css +1 -0
  13. package/web/_app/immutable/assets/{4.PVZR2_N5.css → 4.CUdclsPi.css} +1 -1
  14. package/web/_app/immutable/assets/5.TsqJFEFF.css +1 -0
  15. package/web/_app/immutable/assets/{NoteEditor.ChlpmyHO.css → NoteEditor.BCjOff8L.css} +1 -1
  16. package/web/_app/immutable/chunks/{B_nIecMW.js → 8IM6BNk6.js} +36 -37
  17. package/web/_app/immutable/chunks/{BEVz4Dgg.js → BHPhFydX.js} +1 -1
  18. package/web/_app/immutable/chunks/B_AYGp9o.js +1 -0
  19. package/web/_app/immutable/chunks/Bze-M_kr.js +3 -0
  20. package/web/_app/immutable/chunks/CFA70_0c.js +20 -0
  21. package/web/_app/immutable/chunks/CGgq5NWd.js +1 -0
  22. package/web/_app/immutable/chunks/CHlFgDNB.js +1 -0
  23. package/web/_app/immutable/chunks/CMp5T_-1.js +1 -0
  24. package/web/_app/immutable/chunks/D89YN-Dr.js +1 -0
  25. package/web/_app/immutable/chunks/DF3lgcLW.js +1 -0
  26. package/web/_app/immutable/chunks/DGVcYHaJ.js +43 -0
  27. package/web/_app/immutable/chunks/DHMLhlug.js +1 -0
  28. package/web/_app/immutable/chunks/DmgpLvxp.js +1 -0
  29. package/web/_app/immutable/chunks/DrsVvzvg.js +1 -0
  30. package/web/_app/immutable/entry/app.hlk6OrE8.js +2 -0
  31. package/web/_app/immutable/entry/start.BE1F0HZV.js +1 -0
  32. package/web/_app/immutable/nodes/0.DMGdYhkH.js +15 -0
  33. package/web/_app/immutable/nodes/1.Dn_0fgt7.js +1 -0
  34. package/web/_app/immutable/nodes/2.Y1Dd5PBI.js +1 -0
  35. package/web/_app/immutable/nodes/3.BlxuRbLf.js +1 -0
  36. package/web/_app/immutable/nodes/4.Bt_H0MbN.js +2 -0
  37. package/web/_app/immutable/nodes/5.BJ-EVWbz.js +5 -0
  38. package/web/_app/immutable/nodes/6.DDIiJnwi.js +1 -0
  39. package/web/_app/immutable/nodes/7.DmPPLDvb.js +1 -0
  40. package/web/_app/immutable/nodes/{9.NF5w8YC2.js → 8.CwMTBog4.js} +2 -2
  41. package/web/_app/immutable/workers/worker-CWkpeSVG.js +1 -0
  42. package/web/_app/version.json +1 -1
  43. package/web/index.html +25 -19
  44. package/web/_app/immutable/assets/0.s5xqevrq.css +0 -1
  45. package/web/_app/immutable/assets/5.DCxNIgyW.css +0 -1
  46. package/web/_app/immutable/assets/8.C0NYkyNY.css +0 -1
  47. package/web/_app/immutable/chunks/B0NIb88Q.js +0 -1
  48. package/web/_app/immutable/chunks/B7ZjnqOt.js +0 -20
  49. package/web/_app/immutable/chunks/BEk5idXo.js +0 -1
  50. package/web/_app/immutable/chunks/C8UdwlAg.js +0 -3
  51. package/web/_app/immutable/chunks/CcQ3P3p1.js +0 -13
  52. package/web/_app/immutable/chunks/CcRs965g.js +0 -1
  53. package/web/_app/immutable/chunks/Cw2GjAJR.js +0 -1
  54. package/web/_app/immutable/chunks/DjBHskLm.js +0 -1
  55. package/web/_app/immutable/chunks/Dm229u8R.js +0 -1
  56. package/web/_app/immutable/chunks/UM_L7-f7.js +0 -1
  57. package/web/_app/immutable/chunks/b-AOhhy1.js +0 -1
  58. package/web/_app/immutable/chunks/k55859zq.js +0 -1
  59. package/web/_app/immutable/entry/app.B3QXxjjH.js +0 -2
  60. package/web/_app/immutable/entry/start.BauJm-6k.js +0 -1
  61. package/web/_app/immutable/nodes/0.DP4c84Wn.js +0 -14
  62. package/web/_app/immutable/nodes/1.BcwcVDJZ.js +0 -1
  63. package/web/_app/immutable/nodes/2.DgGnQZ7P.js +0 -1
  64. package/web/_app/immutable/nodes/3.BudzO7We.js +0 -1
  65. package/web/_app/immutable/nodes/4.DmswyrSt.js +0 -2
  66. package/web/_app/immutable/nodes/5.DjXsfjL5.js +0 -5
  67. package/web/_app/immutable/nodes/6.uBh-sdQx.js +0 -1
  68. package/web/_app/immutable/nodes/7.BYBNXM1A.js +0 -1
  69. package/web/_app/immutable/nodes/8.Tbz9Rh77.js +0 -1
  70. package/web/_app/immutable/workers/worker-rKDNrEKQ.js +0 -1
  71. /package/web/_app/immutable/assets/{9.CFA9_nYZ.css → 8.CFA9_nYZ.css} +0 -0
@@ -0,0 +1,166 @@
1
+ # Setting isorite up: for an AI helping someone
2
+
3
+ You're helping someone set up isorite: a local web app over a folder of Markdown notes (the browser), an agent in it
4
+ called Iso, and `isorite term`, which runs OpenCode 2 beside the notes. This is everything isorite reads, writes and
5
+ runs for the setup, and how each failure shows. `isorite help setup` prints this file.
6
+
7
+ **Start here:** ask them to run `isorite doctor --report` (add the notes folder if they have one) and paste what it
8
+ prints. It shows isorite's and Node's versions, the platform, the configuration with secrets replaced by their
9
+ lengths, the environment variables isorite reads, and a line for each check, ✓ passed, ✗ failed, ! a warning, –
10
+ skipped. Most failures say what to change. Never ask them to paste a key or passphrase, and don't ask them to
11
+ turn off certificate checking (`NODE_TLS_REJECT_UNAUTHORIZED=0`).
12
+
13
+ ## Commands
14
+
15
+ | Command | What it does |
16
+ |---|---|
17
+ | `isorite new <folder>` | Makes the folder (and its parents), empty but for `.isorite/`. Refuses one with files in it already: that is opened as it is. |
18
+ | `isorite doctor [<notes>] [--report] [--offline]` | Checks the setup a step at a time (below). Exit 1 if anything failed. `--offline`: asks the provider nothing. |
19
+ | `isorite serve <notes> [--port <n>]` | The browser app at `http://localhost:4767/`, on 127.0.0.1 only. |
20
+ | `isorite term [<notes>] [--port <n>]` | OpenCode 2 in this terminal, in the current folder (the code), with the notes beside it. Starts the isorite server if none answers on the port, and stops it on exit. |
21
+ | `isorite check <notes>` | Lints the notes: frontmatter, links, cycles. |
22
+ | `isorite --tutorial` | A tour on an example base with a scripted model: no key or network needed. Good to check the install alone. |
23
+ | `isorite model [--from <folder>]` | Optional: the model for search by meaning. Says which is installed and where, else fetches it (53 MB) from npm, a GitHub release, then Hugging Face, saying why each failed. `--from`: granite's files, or any compatible model (below). |
24
+ | `isorite help setup` | Prints this. |
25
+
26
+ ## Files
27
+
28
+ | Where | What |
29
+ |---|---|
30
+ | `~/.config/isorite/config.toml` (or `$XDG_CONFIG_HOME/isorite/`) | isorite's settings for this machine: `device`, `[provider]`, `[tiers]`, `[prices]`. Mode 600. Holds the key. |
31
+ | `~/.config/isorite/opencode/` | OpenCode's settings as isorite writes them at each `isorite term` start (`opencode.json`, theme, plugin link). Rewritten; don't edit. |
32
+ | `~/.config/isorite/opencode/local.json` | The person's own OpenCode settings, laid over isorite's. Never rewritten. |
33
+ | `<notes>/.isorite/` | isorite's own folder in the notes: journals of every change, `settings.toml` (`repository = "<path>"`, where code links point). Made on first open; commit it with the notes. |
34
+ | `~/.local/share/isorite/models/` (or `$XDG_DATA_HOME/isorite/models/`) | The models for search by meaning, a folder each: `granite-embedding-small-english-r2/`, and any other installed with `--from`. `use` names the one used, if not granite. |
35
+ | `~/.cache/isorite/vectors-<hash>.json` | Each base's vectors, made by the model; made again when the model changes. |
36
+ | `$TMPDIR/isorite-term-<port>.log` | The isorite server's output, when `isorite term` started it. Look here when "the isorite server did not start". |
37
+ | `$TMPDIR/isorite-opencode-<port>.log` | OpenCode's server's output, under `isorite term`. |
38
+
39
+ isorite never reads or changes the person's own OpenCode settings (`~/.config/opencode/`).
40
+
41
+ ## config.toml
42
+
43
+ ```toml
44
+ [provider]
45
+ name = "work" # optional; what isorite calls the provider in messages
46
+ url = "https://…/v1" # OpenAI-compatible; /chat/completions and /models are added. Absent: OpenRouter
47
+ key = "…" # the key; also settable in the browser (behind the logo)
48
+ key_header = "api-key" # send the key in this header, as it is, instead of Authorization: Bearer
49
+ stream_usage = false # don't send stream_options (some gateways answer 400 to it)
50
+ max_tokens = 16000 # the least output budget any call gets, for models that think at length
51
+ cache_marks = true # send Anthropic cache marks (OpenRouter-style gateways only)
52
+ pfx = "~/certs/me.pfx" # client certificate (mTLS), PKCS#12; or cert + cert_key (PEM)
53
+ pfx_passphrase = "$PFX_PASS" # "$NAME": read from that environment variable
54
+ cert = "me.pem" # relative paths are from config.toml's folder
55
+ cert_key = "me.key"
56
+ ca = "company-root.pem" # extra authority for the gateway's certificate (added to the trusted ones)
57
+
58
+ [provider.headers] # sent with every request; values may be "$NAME"
59
+ X-Team = "…"
60
+
61
+ [provider.query] # added to every request's URL
62
+ api-version = "…"
63
+
64
+ [tiers] # the provider's model names
65
+ low = "…" # quick
66
+ mid = "…" # Iso's default; the terminal's default
67
+ max = "…" # most able
68
+
69
+ [prices] # optional: US$ per million tokens in and out, for providers that don't say
70
+ "model-name" = [3.0, 15.0]
71
+ ```
72
+
73
+ Rules:
74
+
75
+ - With `url` set and no `[tiers]`, OpenRouter's names are used (`anthropic/claude-…`). A gateway usually answers
76
+ 404 to them. Doctor warns: name the gateway's models under `[tiers]`.
77
+ - `pfx` and `cert` can't both be named. `cert` and `cert_key` go together.
78
+ - A `"$NAME"` value whose variable isn't set is an error naming both.
79
+ - Certificates are used only over https, and only for the provider's own address.
80
+
81
+ ## Environment variables
82
+
83
+ | Variable | Effect |
84
+ |---|---|
85
+ | `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY` | Used by the `isorite` command: it re-runs itself once with `NODE_USE_ENV_PROXY=1`, and adds `localhost,127.0.0.1,::1` to `NO_PROXY`. OpenCode (Bun) reads them itself. |
86
+ | `ISORITE_NODE_DEFAULTS=0` | Leave Node as it is: no system authorities, no proxy unless Node is told. |
87
+ | `NODE_EXTRA_CA_CERTS` | A .pem of extra authorities, read by Node at start. |
88
+ | `ISORITE_GATEWAY_URL`, `ISORITE_GATEWAY_MODELS`, `ISORITE_GATEWAY_KEY` | The terminal only: OpenCode asks this OpenAI-compatible address directly, with these models (comma-separated) and key, instead of `config.toml`'s provider. No certificate or headers apply. |
89
+ | `ISORITE_EMBED_DIR` | The model for search by meaning is in this folder (and nowhere else is looked). |
90
+ | `ISORITE_EMBED=off` | No search by meaning, whatever is installed. |
91
+ | `ISORITE_OPENCODE` | The OpenCode 2 binary to run. Otherwise `opencode2`, then `opencode`, must be version 2. |
92
+ | `XDG_CONFIG_HOME` | Where `isorite/config.toml` is looked for. |
93
+ | `SSH_CONNECTION` | Set over SSH: `term`, `serve` and `doctor` print the `ssh -L` command for the browser. |
94
+
95
+ ## How requests reach the provider
96
+
97
+ - **Iso** (in the isorite server) posts to `<url>/chat/completions?<query>`, streaming, with the key (in
98
+ `Authorization: Bearer` or `key_header`), `headers`, and `stream_options` unless `stream_usage = false`. With a
99
+ certificate or `ca`, it goes by `node:https` with them, through `HTTPS_PROXY` if set. Otherwise it uses Node's
100
+ fetch.
101
+ - **The terminal**: OpenCode is configured with the provider at `http://127.0.0.1:<port>/api/gateway`, the isorite
102
+ server's relay, and with the server's token as its key. The relay adds the real key, certificate, headers and
103
+ query, and passes `/chat/completions` and `/models` on. So whatever works for Iso works for the terminal. The
104
+ relay answers only on 127.0.0.1, and only that token.
105
+ - **Retries.** A 502, 503 or 504, a dropped connection or a timeout is asked again up to 3 times (after about 1, 2
106
+ and 4 s), and a 429 once (after its `Retry-After`, if 10 s or less), but only before any of the reply has come.
107
+ The relay does the same before it passes anything to OpenCode (which retries on its own as well). 4xx answers
108
+ and names not found are the settings', and are not asked again.
109
+ - **An empty reply.** A reply cut off at its length limit with nothing written (a reasoning model that spent the
110
+ budget thinking) is asked once more with three times the room.
111
+ - With no key and no certificate in `config.toml`, and no `ISORITE_GATEWAY_*`, OpenCode uses its own models.
112
+ `isorite term` prints which models it uses, and from where.
113
+
114
+ ## What working looks like
115
+
116
+ `isorite doctor` ends `nothing failed`, with lines like:
117
+
118
+ ```
119
+ ✓ config: /home/me/.config/isorite/config.toml: work at https://llm-gateway.corp.example/v1
120
+ ✓ key: <48 characters>, sent in api-key
121
+ ✓ certificate: /home/me/certs/me.pfx
122
+ ✓ models: low small, mid medium, max large
123
+ ✓ proxy: http://proxy.corp:8080, used
124
+ ✓ authorities: the system's (146), as well as Node's own
125
+ ✓ reached: https://llm-gateway.corp.example/v1/models answered with 12 models
126
+ ✓ reply from medium: "ready", 14 tokens in and 2 out
127
+ ✓ tools: medium called ping
128
+ ✓ opencode: opencode2 2.0.32
129
+ ✓ terminal: work's models, through isorite's server at http://127.0.0.1:4767/api/gateway, …
130
+ ```
131
+
132
+ `isorite term` prints `isorite: http://localhost:4767/` and the terminal's models before OpenCode opens.
133
+
134
+ ## Failures, and what to change
135
+
136
+ | Seen | Cause | Change |
137
+ |---|---|---|
138
+ | `could not be reached: … SELF_SIGNED_CERT_IN_CHAIN` or `UNABLE_TO_VERIFY_LEAF_SIGNATURE` | The gateway's certificate is signed by an authority not trusted, usually the company's. | Install the company's root on the machine (isorite trusts the system's), or name its .pem as `ca`, or in `NODE_EXTRA_CA_CERTS`. Check `ISORITE_NODE_DEFAULTS` isn't 0. |
139
+ | `… CERTIFICATE_REQUIRED`, `… other side closed (UND_ERR_SOCKET)` on https, `HANDSHAKE_FAILURE`, `BAD_CERTIFICATE` | The gateway wants a client certificate (mTLS), and got none it accepts. | `pfx` + `pfx_passphrase`, or `cert` + `cert_key`. Ask the person where theirs is: often a .pfx or .p12 file. If it's only in the Windows certificate store or the macOS keychain, it must be exported to a file. |
140
+ | `pfx_passphrase does not open …` | Wrong passphrase, or the variable it names holds the wrong one. | Fix the passphrase. |
141
+ | `ENOTFOUND` | The host name isn't found. | Check `url`. The name may resolve only on the company's network or VPN. |
142
+ | `ECONNREFUSED` | Nothing listens there. | Check the URL's port. |
143
+ | `ETIMEDOUT`, `UND_ERR_CONNECT_TIMEOUT` | No answer: often a proxy is needed. | Set `HTTPS_PROXY` (and `NO_PROXY` for internal hosts reached directly). |
144
+ | `said 401` / `said 403` | The key, or where it's sent, isn't accepted. | Check the key, `key_header`, and any header the gateway wants. The gateway's own message usually names it. |
145
+ | `said 404 … no model called X, which is OpenRouter's name for it` | No `[tiers]`. | Name the gateway's models under `[tiers]`. `doctor` lists the models `/models` offers. |
146
+ | `… is unavailable (503 …), tried 4 times over N s: it is likely …, not your settings` | The gateway, or the model behind it, is down. | Wait, and send it again (↻ under the turn). If it lasts, the gateway's status page or owner. |
147
+ | `… used its whole output budget (N tokens) thinking, and wrote nothing` | A reasoning model ran out of room before it wrote. | A lower tier, or `max_tokens` under `[provider]`. |
148
+ | `! search by meaning: no model …` | Not installed (optional). | `isorite model`. Its failures name each source; offline, `isorite model --from <folder>` with a copy. |
149
+ | `The model could not be fetched:` and a line for npm, GitHub, Hugging Face | None reachable from here. | Copy the model's folder from a machine that has it (`isorite model` there says where), then `isorite model --from <folder>`; or any compatible model. |
150
+ | `said 400: … stream_options` | The gateway refuses `stream_options`. | `stream_usage = false`. |
151
+ | `✗ tools: … answered without calling the tool` | The model or gateway drops tools. | Choose a model that supports tool calls. Iso and the terminal need them. |
152
+ | `isorite term needs OpenCode 2` | OpenCode 1, or none. | `npm i -g @opencode/cli`, or `ISORITE_OPENCODE=…`. |
153
+ | `port 4767 is serving <other>, not <notes>` | Another base is served there. | Stop it, or `--port`. |
154
+ | The link doesn't open, over SSH | `localhost` is the remote machine. | `ssh -L 4767:localhost:4767 <host>` from the person's own machine. |
155
+ | The terminal works but uses an unexpected model | Check the line `isorite term` prints: `config.toml`, `ISORITE_GATEWAY_URL`, or OpenCode's own (no key or certificate). | Fix what it names. |
156
+
157
+ ## Another model for search by meaning
158
+
159
+ Any ONNX model that gives 384 numbers for a text, with its `tokenizer.json`, can stand in for granite:
160
+ `isorite model --from <folder>`. It's checked by use (loaded, and asked for one vector), copied into the models
161
+ folder, and used from then on; each base's vectors are made again with it. An optional `isorite-model.json` beside
162
+ its files gives `{"name": "…", "pooling": "cls" | "mean"}`: `cls` (the default) takes the first token's state, as
163
+ granite is used; `mean` averages them all, as sentence-transformers models such as all-MiniLM-L6-v2 are used.
164
+
165
+ If the gateway isn't OpenAI-compatible (Bedrock's or Vertex's own APIs, for example), isorite can't use it directly.
166
+ An OpenAI-compatible proxy in front of it, such as LiteLLM, can.
package/docs/setup.md ADDED
@@ -0,0 +1,133 @@
1
+ # Setting isorite up
2
+
3
+ From nothing to Iso answering in the browser and OpenCode working in the terminal. If anything fails on the way,
4
+ run `isorite doctor`: it checks each step and says what to change.
5
+
6
+ ## 1. Install
7
+
8
+ - **Node 24** or later (`node --version`).
9
+ - **isorite**: `npm i -g isorite`.
10
+ - For the terminal, **OpenCode 2**: `npm i -g @opencode/cli`.
11
+
12
+ `isorite --tutorial` now shows you around, on an example base, with a scripted model: no key, nothing sent anywhere.
13
+
14
+ ## 2. A model provider
15
+
16
+ Iso and the terminal need an OpenAI-compatible provider. isorite's settings are in `~/.config/isorite/config.toml`
17
+ (or `$XDG_CONFIG_HOME/isorite/config.toml`), a file only you can read.
18
+
19
+ **OpenRouter** is the default. Open isorite in the browser, and set the key behind the logo (or sign in there with
20
+ OpenRouter, which makes one). Nothing else is needed.
21
+
22
+ **Any other provider or gateway** is named under `[provider]`, with its models under `[tiers]`:
23
+
24
+ ```toml
25
+ [provider]
26
+ name = "work" # what isorite calls it
27
+ url = "https://llm.example.com/v1" # /chat/completions is added to it
28
+ key = "…" # or set it behind the logo in the browser
29
+
30
+ [tiers] # the provider's own names for its models
31
+ low = "small-model" # quick replies
32
+ mid = "medium-model" # Iso's default, and the terminal's
33
+ max = "large-model" # the most able
34
+ ```
35
+
36
+ Without `[tiers]`, OpenRouter's names are used, and another provider usually answers 404 to them.
37
+
38
+ ### A company's gateway
39
+
40
+ Gateways inside companies often want more than a key. Each of these is optional:
41
+
42
+ ```toml
43
+ [provider]
44
+ url = "https://llm-gateway.corp.example/v1"
45
+ key = "…"
46
+ key_header = "api-key" # the header the key goes in, as it is, instead of Authorization: Bearer
47
+ stream_usage = false # for a gateway that refuses stream_options; the gauge then counts nothing
48
+ max_tokens = 16000 # the least room a reply is given, for models that think at length
49
+
50
+ # A client certificate (mTLS), for a gateway that asks for one: a .pfx (or .p12) ...
51
+ pfx = "~/certs/me.pfx"
52
+ pfx_passphrase = "$MY_PFX_PASSPHRASE" # "$NAME" is read from the environment, so it isn't written here
53
+ # ... or a certificate and its key, as .pem files:
54
+ # cert = "~/certs/me.pem"
55
+ # cert_key = "~/certs/me.key"
56
+
57
+ ca = "~/certs/company-root.pem" # the authority the gateway's own certificate is signed by, if needed
58
+
59
+ [provider.headers] # sent with every request
60
+ X-Team = "research"
61
+ X-Project = "$PROJECT_ID"
62
+
63
+ [provider.query] # added to every request's address
64
+ api-version = "2024-10-21"
65
+ ```
66
+
67
+ Paths may start with `~`, or are taken from `config.toml`'s folder. Any value written `"$NAME"` is read from that
68
+ environment variable.
69
+
70
+ ### Proxies and the company's certificates
71
+
72
+ The `isorite` command uses `HTTPS_PROXY` (and `NO_PROXY`) when they're set, and trusts the system's certificate
73
+ authorities as well as Node's own, so a company's authority installed on the machine is trusted. Node does neither
74
+ by itself. Set `ISORITE_NODE_DEFAULTS=0` to leave Node as it is.
75
+
76
+ If the company's authority isn't installed on the machine, name its certificate as `ca` above, or in
77
+ `NODE_EXTRA_CA_CERTS`.
78
+
79
+ ## 3. Your notes
80
+
81
+ A folder of Markdown files. To start from nothing:
82
+
83
+ ```sh
84
+ isorite new ~/notes/my-project
85
+ isorite serve ~/notes/my-project
86
+ ```
87
+
88
+ `isorite new` makes the folder, empty but for isorite's own `.isorite/`. A folder of notes you have already is
89
+ opened as it is, with `isorite serve`.
90
+
91
+ It prints a link to click: `http://localhost:4767/`. isorite keeps what it needs in the folder's `.isorite/`.
92
+
93
+ ## 4. The terminal
94
+
95
+ ```sh
96
+ cd ~/code/my-project
97
+ isorite term ~/notes/my-project
98
+ ```
99
+
100
+ It prints the same link, and which models the terminal uses. OpenCode uses the provider above, through isorite. More
101
+ is in [terminal.md](terminal.md).
102
+
103
+ ## Search by meaning (optional)
104
+
105
+ ```sh
106
+ isorite model
107
+ ```
108
+
109
+ It says which model is installed and where, or fetches it (53 MB, once): from npm, else from a release on GitHub,
110
+ else from Hugging Face. Without it, Iso searches by words and follows links. Where none of those can be reached,
111
+ copy the model's folder from a machine that has it (`isorite model` there says where it is) and run
112
+ `isorite model --from <folder>`. Any compatible model works the same way: an ONNX model that gives 384 numbers,
113
+ with its `tokenizer.json` (for example all-MiniLM-L6-v2, with `{"pooling": "mean"}` in an `isorite-model.json`
114
+ beside it).
115
+
116
+ ## When something fails
117
+
118
+ If a gateway is down for a moment (it answers 502, 503 or 504, or drops the connection), Iso and the terminal ask
119
+ it again, up to three times. When it stays down, Iso says it's likely the gateway, not your settings, and the ↻
120
+ under the turn sends it again.
121
+
122
+ ```sh
123
+ isorite doctor ~/notes/my-project
124
+ ```
125
+
126
+ It checks, in turn: the configuration, how Node reaches the network (the proxy, the authorities), the provider
127
+ (its list of models, a reply of a few words from each model, and a tool call: a few hundred tokens in all), the
128
+ terminal (OpenCode 2, the port, the models it will use), and the notes. Each line says what failed, and what to
129
+ change. `--offline` asks the provider nothing.
130
+
131
+ `isorite doctor --report` gives all of it as one block, with the configuration and the environment it read. The
132
+ key, passphrases and any other secrets are shown only by their length. Paste it to whoever is helping, or to an AI
133
+ helping you: [setup-for-agents.md](setup-for-agents.md) is written for it, and `isorite help setup` prints it.
@@ -0,0 +1,119 @@
1
+ # isorite beside your code: the terminal
2
+
3
+ How to use isorite on a project of your own. OpenCode 2 is the coding agent in your terminal; isorite gives it your
4
+ notes, and shows its session in Iso's pane in the browser. From there you can ask it things, give it notes and
5
+ passages, stop it, and answer what it asks before it acts.
6
+
7
+ Setting isorite up the first time, or at work behind a company's gateway, is in [setup.md](setup.md).
8
+
9
+ ## What you need
10
+
11
+ - **Node 24** or later.
12
+ - **isorite**: `npm i -g isorite`.
13
+ - **OpenCode 2**: `npm i -g @opencode/cli`. It installs as `opencode2` and `opencode`. If an older OpenCode is your
14
+ `opencode`, isorite finds `opencode2`; or name one with `ISORITE_OPENCODE=/path/to/opencode`.
15
+ - A model provider, set once in `~/.config/isorite/config.toml`: see [setup.md](setup.md). `isorite doctor` checks
16
+ all of this.
17
+
18
+ ## Your notes for the project
19
+
20
+ Keep the project's notes in a folder of their own, for example `~/notes/<project>`. It can start empty:
21
+ `isorite new ~/notes/<project>` makes it. isorite
22
+ keeps what it needs in the folder's `.isorite/` (the journals of every change, and its settings), made the first
23
+ time the folder is opened. Commit `.isorite/` with the notes, if they're in git.
24
+
25
+ ## Starting it
26
+
27
+ ```sh
28
+ cd ~/code/my-project
29
+ isorite term ~/notes/my-project
30
+ ```
31
+
32
+ This starts the isorite server for the notes (unless one is running on port 4767; `--port <n>` for another), and
33
+ OpenCode's own server, on this machine, with a password of its own. Then it opens the terminal on that server.
34
+
35
+ Before OpenCode takes the screen, `isorite term` prints:
36
+
37
+ ```
38
+ isorite: http://localhost:4767/
39
+ the terminal's models: work-large, work-small, as config.toml names them, through the isorite server
40
+ ```
41
+
42
+ Most terminals let you click the link. It prints the same again when OpenCode exits. Closing the terminal stops
43
+ both servers, if `isorite term` started them.
44
+
45
+ The first time, the folder you started in is named as the notes' repository in `.isorite/settings.toml`, so that
46
+ code links in the notes point into it.
47
+
48
+ ### Over SSH
49
+
50
+ `localhost` is the machine `isorite term` runs on. To open isorite from your own machine, forward the port:
51
+
52
+ ```sh
53
+ ssh -L 4767:localhost:4767 <the machine>
54
+ ```
55
+
56
+ Then open `http://localhost:4767/` on your own. Over SSH, `isorite term` prints this command too. Any local port
57
+ works (`-L 9000:localhost:4767`, then `http://localhost:9000/`).
58
+
59
+ ## In the browser
60
+
61
+ Open the link. While the terminal runs, Iso's pane header has two more switches beside *Iso may edit the notes*:
62
+ *Iso may edit the project's files* (Alt+F) and *Iso may run commands* (Alt+R). With either on, the pane is the
63
+ terminal's session:
64
+
65
+ - **The session:** each prompt (asked in the terminal, or from here), what the agent says, and a line for each thing
66
+ it does. A command opens on its output, a file on its diff, a look-up in your notes on the note.
67
+ - **Asking from here:** what you type goes to the terminal's session, with what you gave Iso (hold a note on the
68
+ Atlas, or select a passage and send it to Iso): notes by name, for the agent to read with isorite's tools, and
69
+ passages as they are. Escape, or the square, stops it.
70
+ - **Permissions:** when OpenCode asks before doing something, the same question is in the pane: allow once, always
71
+ allow, or don't allow. Answered in either place, it's gone from both.
72
+ - **Notes the agent changes** are written at once, under its name. Turn on provenance in the Note pane's header to
73
+ see the agent's words marked, and the timeline to put a note back.
74
+
75
+ With both off, the pane is Iso's own again.
76
+
77
+ ## The models
78
+
79
+ The terminal uses Iso's provider, as `config.toml` names it ([setup.md](setup.md)): the `[tiers]` models, `mid` its
80
+ default. OpenCode asks the isorite server, which passes each request on to the provider with the key, the client
81
+ certificate, the headers and the query `config.toml` names. OpenCode is given none of them, only the isorite
82
+ server's token.
83
+
84
+ `ISORITE_GATEWAY_URL`, `ISORITE_GATEWAY_MODELS` (comma-separated) and `ISORITE_GATEWAY_KEY`, when set, are used
85
+ instead, and OpenCode asks that address itself. With no key and no certificate in `config.toml`, OpenCode uses its
86
+ own models; `isorite term` says which.
87
+
88
+ OpenCode doesn't know a gateway's prices, so Iso's pane shows the session's tokens rather than its cost.
89
+
90
+ ## OpenCode's settings
91
+
92
+ isorite keeps OpenCode's settings in `~/.config/isorite/opencode/`, rewritten at each start, so your own OpenCode
93
+ settings are never read or changed. Anything of your own goes in `~/.config/isorite/opencode/local.json`, which is
94
+ never rewritten, and is laid over what isorite writes. For example, to have OpenCode ask before every command (by
95
+ default it runs them):
96
+
97
+ ```json
98
+ { "permissions": [{ "action": "shell", "resource": "*", "effect": "ask" }] }
99
+ ```
100
+
101
+ ## Without the internet
102
+
103
+ Once installed, isorite needs no network, as long as the model's address in `config.toml` is one you can reach (a
104
+ gateway, or a model served on your own machine). What it fetches, it fetches once:
105
+
106
+ - isorite and OpenCode, from npm: through your company's npm mirror, if it has one.
107
+ - The model for search by meaning (optional): `isorite model` gets it from npm too (`isorite-model-granite`), so the
108
+ same mirror serves it; else from its release on GitHub, else from Hugging Face. On a machine with none of them,
109
+ copy its folder from one that has it (`isorite model` there says where), or download the package from
110
+ `https://github.com/lachlanjs/isorite/releases/tag/model-granite-1.0.0` and unpack it, then
111
+ `isorite model --from <folder>`. Granite's files are checked against their SHA-256; any other compatible model is
112
+ checked by use ([setup-for-agents.md](setup-for-agents.md) says which).
113
+
114
+ ## Who can reach it
115
+
116
+ OpenCode's server listens on this machine only (127.0.0.1), and only the isorite server talks to it. The isorite
117
+ server listens on this machine only too, unless you put it on your tailnet (`tailscale serve`). Then anyone on your
118
+ tailnet who opens the page can ask the agent to act in your project, as they could already edit your notes. The
119
+ provider is passed on to OpenCode only on this machine, never over the tailnet.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isorite",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "An agentic harness for understanding: your notes as a knowledge base, an Atlas of them, an agent that reads them, and study.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,6 +28,7 @@
28
28
  "web",
29
29
  "notes",
30
30
  "plugin",
31
+ "docs",
31
32
  "README.md",
32
33
  "LICENSE"
33
34
  ],
package/plugin/isorite.ts CHANGED
@@ -35,10 +35,10 @@ export async function post<T>(path: string, body: unknown): Promise<T> {
35
35
  /** A tool the server offers an agent, as OpenAI's API describes one. */
36
36
  export interface ToolDef { type: "function"; function: { name: string; description: string; parameters: Record<string, unknown> } }
37
37
 
38
- /** What the splash says of the base: its name, how many notes, whether it has a study base. */
39
- export async function base(): Promise<{ name: string; notes: number; study: boolean }> {
38
+ /** What the splash says of the base: its name and how many notes. */
39
+ export async function base(): Promise<{ name: string; notes: number }> {
40
40
  const [session, listing] = await Promise.all([get<Session>("/api/session"), get<Listing>("/api/notes")]);
41
- return { name: session.name, notes: listing.notes.length, study: session.study };
41
+ return { name: session.name, notes: listing.notes.length };
42
42
  }
43
43
 
44
44
  /** The server's events, as the browser hears them, until the signal aborts; heard again after a dropped stream. */
@@ -1,7 +1,7 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  // A note in the terminal (`Terminal-Note`): its blocks behind a gutter of who wrote them, links underlined, maths as
3
3
  // source, code on its own ground, a figure offered in the browser; the changes waiting to be kept between dotted
4
- // rules, one of them current, with its keys; the step and squares at the top right.
4
+ // rules, one of them current, with its keys.
5
5
 
6
6
  import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
7
7
  import type { Entry } from "../dist/core.js";
@@ -20,12 +20,8 @@ export interface NoteData {
20
20
  /** At a moment of the timeline: what it was, and how long ago. Read only. */
21
21
  at?: { said: string; ago: string };
22
22
  entries: Entry[];
23
- /** What the study record says of it: 0 not reached … 3 understood, and its squares of 6. */
24
- step: number;
25
- squares: number;
26
23
  }
27
24
 
28
- export const STEPS = ["not reached", "opened", "worked through", "understood"];
29
25
  const DOTS = "· ".repeat(38).trim();
30
26
  /** The widest a note's column is: its gutter, rail and about 76 characters of text. */
31
27
  const MEASURE = 84;
@@ -112,10 +108,6 @@ export function NotePage(props: { note: Accessor<NoteData | undefined>; current:
112
108
  <text fg={g().text} wrapMode="none" flexShrink={1} minWidth={0}>{pathOf(note().id, note().title)}</text>
113
109
  <Show when={note().at}>{(at) => <text fg={g().growth} wrapMode="none" flexShrink={1} minWidth={0}>{` as it was ${at().ago}: ${at().said}`}</text>}</Show>
114
110
  <box flexGrow={1} />
115
- <text fg={g().soft} wrapMode="none" flexShrink={0}>{`${STEPS[note().step] ?? ""} `}</text>
116
- <text wrapMode="none" flexShrink={0}>
117
- <For each={[0, 1, 2, 3, 4, 5]}>{(k) => <span style={{ fg: k < note().squares ? g().mal : g().line }}>{k < note().squares ? "■" : "□"}</span>}</For>
118
- </text>
119
111
  </box>
120
112
  <scrollbox ref={props.scroll} flexGrow={1} paddingLeft={4} paddingRight={4} paddingTop={1} scrollbarOptions={{ visible: false }}>
121
113
  {/* As the board: the text in a column of reading width, not the terminal's. */}
package/plugin/tui.tsx CHANGED
@@ -7,7 +7,7 @@ import { BoxRenderable, fg, ImageRenderable, RGBA, StyledText, TextAttributes, T
7
7
  import { Plugin } from "@opencode/plugin/tui";
8
8
  import { createEffect, createResource, createSignal, For, onCleanup, Show, untrack } from "solid-js";
9
9
  import type { ScrollBoxRenderable } from "@opentui/core";
10
- import { ago, anchorOf, blocks, describe, isStudyId, momentKey, moments, reach, type Entry, type MapModel } from "../dist/core.js";
10
+ import { ago, anchorOf, blocks, describe, momentKey, moments, reach, type Entry, type MapModel } from "../dist/core.js";
11
11
  import { drawAtlas, runsOf, type Role } from "./oneatlas.ts";
12
12
  import { noteView, type ViewChange } from "./noteview.ts";
13
13
  import { NotePage, fromWhom, type NoteData } from "./notepage.tsx";
@@ -107,12 +107,9 @@ export default Plugin.define({
107
107
  };
108
108
  const loadNote = async (id: string, at: { key: string; said: string; ago: string } | null): Promise<NoteData> => {
109
109
  const q = `?id=${encodeURIComponent(id)}`;
110
- const [note, entries, study] = await Promise.all([
111
- Isorite.get<{ text: string }>(`/api/note${q}`), Isorite.get<Entry[]>(`/api/journal${q}`),
112
- Isorite.get<{ notes: Record<string, { step: number; squares: number }> }>("/api/study").catch(() => null),
113
- ]);
114
- const reading = study?.notes[id], title = titles().get(id) ?? id.split("/").at(-1)!;
115
- const common = { id, title, entries, step: reading?.step ?? 0, squares: reading?.squares ?? 0 };
110
+ const [note, entries] = await Promise.all([Isorite.get<{ text: string }>(`/api/note${q}`), Isorite.get<Entry[]>(`/api/journal${q}`)]);
111
+ const title = titles().get(id) ?? id.split("/").at(-1)!;
112
+ const common = { id, title, entries };
116
113
  if (!at) return { ...common, text: note.text, items: noteView(note.text, entries) };
117
114
  // At a moment of the timeline: the note as it was then, read only, its changes then waiting left out.
118
115
  const then = await Isorite.get<{ text: string }>(`/api/version${q}&entry=${encodeURIComponent(at.key)}`);
@@ -198,7 +195,7 @@ export default Plugin.define({
198
195
  const model = map(), id = focal();
199
196
  if (!model || !id) return null;
200
197
  const r = reach(model, id), direct = r.requires.filter((n) => r.depth[n] === 1);
201
- return { direct, further: r.requires.length - direct.length, builds: r.builds.filter((n) => !isStudyId(n)) };
198
+ return { direct, further: r.requires.length - direct.length, builds: r.builds };
202
199
  };
203
200
  const grid = () => {
204
201
  const n = near(), id = focal();
@@ -449,7 +446,7 @@ export default Plugin.define({
449
446
  const b = base();
450
447
  if (!b) return " ";
451
448
  if ("error" in b) return `isorite is not answering at ${Isorite.url}`;
452
- return `${b.name} · ${b.notes} ${b.notes === 1 ? "note" : "notes"} · ${b.study ? "study base found" : "no study base"}`;
449
+ return `${b.name} · ${b.notes} ${b.notes === 1 ? "note" : "notes"}`;
453
450
  };
454
451
  return (
455
452
  <box flexDirection="column" alignItems="center" justifyContent="center" width="100%" height="100%">
@@ -517,7 +514,9 @@ export default Plugin.define({
517
514
  id: "isorite.atlas.show", title: "One note's atlas, beside the chat", palette: true, slash: { name: "atlas", arguments: true },
518
515
  run: async (input) => {
519
516
  const words = input?.trim();
520
- const id = words ? (await Isorite.get<{ id: string }[]>(`/api/search?q=${encodeURIComponent(words)}`).catch(() => []))[0]?.id : "";
517
+ // A note titled with exactly these words comes before the search's first hit, which may only mention them.
518
+ const hits = words ? await Isorite.get<{ id: string; title: string }[]>(`/api/search?q=${encodeURIComponent(words)}`).catch(() => []) : [];
519
+ const id = words ? (hits.find((h) => h.title.toLowerCase() === words.toLowerCase()) ?? hits[0])?.id : "";
521
520
  if (words && !id) { ctx.ui.toast?.show?.({ message: `no note matches “${words}”`, variant: "warning" }); return; }
522
521
  openAtlas(id ?? "");
523
522
  },