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.
- package/README.md +6 -3
- package/dist/core.js +207 -426
- package/dist/embed-worker.js +9 -4
- package/dist/main.js +7917 -6864
- package/docs/setup-for-agents.md +166 -0
- package/docs/setup.md +133 -0
- package/docs/terminal.md +119 -0
- package/package.json +2 -1
- package/plugin/isorite.ts +3 -3
- package/plugin/notepage.tsx +1 -9
- package/plugin/tui.tsx +9 -10
- package/web/_app/immutable/assets/0.IOi79DXH.css +1 -0
- package/web/_app/immutable/assets/{4.PVZR2_N5.css → 4.CUdclsPi.css} +1 -1
- package/web/_app/immutable/assets/5.TsqJFEFF.css +1 -0
- package/web/_app/immutable/assets/{NoteEditor.ChlpmyHO.css → NoteEditor.BCjOff8L.css} +1 -1
- package/web/_app/immutable/chunks/{B_nIecMW.js → 8IM6BNk6.js} +36 -37
- package/web/_app/immutable/chunks/{BEVz4Dgg.js → BHPhFydX.js} +1 -1
- package/web/_app/immutable/chunks/B_AYGp9o.js +1 -0
- package/web/_app/immutable/chunks/Bze-M_kr.js +3 -0
- package/web/_app/immutable/chunks/CFA70_0c.js +20 -0
- package/web/_app/immutable/chunks/CGgq5NWd.js +1 -0
- package/web/_app/immutable/chunks/CHlFgDNB.js +1 -0
- package/web/_app/immutable/chunks/CMp5T_-1.js +1 -0
- package/web/_app/immutable/chunks/D89YN-Dr.js +1 -0
- package/web/_app/immutable/chunks/DF3lgcLW.js +1 -0
- package/web/_app/immutable/chunks/DGVcYHaJ.js +43 -0
- package/web/_app/immutable/chunks/DHMLhlug.js +1 -0
- package/web/_app/immutable/chunks/DmgpLvxp.js +1 -0
- package/web/_app/immutable/chunks/DrsVvzvg.js +1 -0
- package/web/_app/immutable/entry/app.hlk6OrE8.js +2 -0
- package/web/_app/immutable/entry/start.BE1F0HZV.js +1 -0
- package/web/_app/immutable/nodes/0.DMGdYhkH.js +15 -0
- package/web/_app/immutable/nodes/1.Dn_0fgt7.js +1 -0
- package/web/_app/immutable/nodes/2.Y1Dd5PBI.js +1 -0
- package/web/_app/immutable/nodes/3.BlxuRbLf.js +1 -0
- package/web/_app/immutable/nodes/4.Bt_H0MbN.js +2 -0
- package/web/_app/immutable/nodes/5.BJ-EVWbz.js +5 -0
- package/web/_app/immutable/nodes/6.DDIiJnwi.js +1 -0
- package/web/_app/immutable/nodes/7.DmPPLDvb.js +1 -0
- package/web/_app/immutable/nodes/{9.NF5w8YC2.js → 8.CwMTBog4.js} +2 -2
- package/web/_app/immutable/workers/worker-CWkpeSVG.js +1 -0
- package/web/_app/version.json +1 -1
- package/web/index.html +25 -19
- package/web/_app/immutable/assets/0.s5xqevrq.css +0 -1
- package/web/_app/immutable/assets/5.DCxNIgyW.css +0 -1
- package/web/_app/immutable/assets/8.C0NYkyNY.css +0 -1
- package/web/_app/immutable/chunks/B0NIb88Q.js +0 -1
- package/web/_app/immutable/chunks/B7ZjnqOt.js +0 -20
- package/web/_app/immutable/chunks/BEk5idXo.js +0 -1
- package/web/_app/immutable/chunks/C8UdwlAg.js +0 -3
- package/web/_app/immutable/chunks/CcQ3P3p1.js +0 -13
- package/web/_app/immutable/chunks/CcRs965g.js +0 -1
- package/web/_app/immutable/chunks/Cw2GjAJR.js +0 -1
- package/web/_app/immutable/chunks/DjBHskLm.js +0 -1
- package/web/_app/immutable/chunks/Dm229u8R.js +0 -1
- package/web/_app/immutable/chunks/UM_L7-f7.js +0 -1
- package/web/_app/immutable/chunks/b-AOhhy1.js +0 -1
- package/web/_app/immutable/chunks/k55859zq.js +0 -1
- package/web/_app/immutable/entry/app.B3QXxjjH.js +0 -2
- package/web/_app/immutable/entry/start.BauJm-6k.js +0 -1
- package/web/_app/immutable/nodes/0.DP4c84Wn.js +0 -14
- package/web/_app/immutable/nodes/1.BcwcVDJZ.js +0 -1
- package/web/_app/immutable/nodes/2.DgGnQZ7P.js +0 -1
- package/web/_app/immutable/nodes/3.BudzO7We.js +0 -1
- package/web/_app/immutable/nodes/4.DmswyrSt.js +0 -2
- package/web/_app/immutable/nodes/5.DjXsfjL5.js +0 -5
- package/web/_app/immutable/nodes/6.uBh-sdQx.js +0 -1
- package/web/_app/immutable/nodes/7.BYBNXM1A.js +0 -1
- package/web/_app/immutable/nodes/8.Tbz9Rh77.js +0 -1
- package/web/_app/immutable/workers/worker-rKDNrEKQ.js +0 -1
- /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.
|
package/docs/terminal.md
ADDED
|
@@ -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.
|
|
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
|
|
39
|
-
export async function base(): Promise<{ name: string; notes: number
|
|
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
|
|
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. */
|
package/plugin/notepage.tsx
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
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"}
|
|
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
|
-
|
|
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
|
},
|