isorite 0.2.0 → 0.3.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 (70) hide show
  1. package/README.md +5 -2
  2. package/dist/core.js +207 -426
  3. package/dist/main.js +3796 -3278
  4. package/docs/setup-for-agents.md +142 -0
  5. package/docs/setup.md +111 -0
  6. package/docs/terminal.md +115 -0
  7. package/package.json +2 -1
  8. package/plugin/isorite.ts +3 -3
  9. package/plugin/notepage.tsx +1 -9
  10. package/plugin/tui.tsx +9 -10
  11. package/web/_app/immutable/assets/0.IOi79DXH.css +1 -0
  12. package/web/_app/immutable/assets/{4.PVZR2_N5.css → 4.CUdclsPi.css} +1 -1
  13. package/web/_app/immutable/assets/5.TsqJFEFF.css +1 -0
  14. package/web/_app/immutable/assets/{NoteEditor.ChlpmyHO.css → NoteEditor.BCjOff8L.css} +1 -1
  15. package/web/_app/immutable/chunks/-XH9wLvF.js +1 -0
  16. package/web/_app/immutable/chunks/{B_nIecMW.js → BN1DRYj2.js} +36 -37
  17. package/web/_app/immutable/chunks/{BEVz4Dgg.js → BNCmWi9e.js} +1 -1
  18. package/web/_app/immutable/chunks/BZeALCyJ.js +1 -0
  19. package/web/_app/immutable/chunks/B_AYGp9o.js +1 -0
  20. package/web/_app/immutable/chunks/BavLcJN1.js +1 -0
  21. package/web/_app/immutable/chunks/BcrJkegU.js +1 -0
  22. package/web/_app/immutable/chunks/BqrPWk6N.js +20 -0
  23. package/web/_app/immutable/chunks/Bze-M_kr.js +3 -0
  24. package/web/_app/immutable/chunks/Cr4-_2Vw.js +1 -0
  25. package/web/_app/immutable/chunks/D25FgEb3.js +43 -0
  26. package/web/_app/immutable/chunks/D8jeeZa0.js +1 -0
  27. package/web/_app/immutable/chunks/DHMLhlug.js +1 -0
  28. package/web/_app/immutable/chunks/DsnHcTIO.js +1 -0
  29. package/web/_app/immutable/entry/app.B9MdwZzs.js +2 -0
  30. package/web/_app/immutable/entry/start.gtQcHKDR.js +1 -0
  31. package/web/_app/immutable/nodes/0.DSajjyn5.js +15 -0
  32. package/web/_app/immutable/nodes/1.hmQ_l8t-.js +1 -0
  33. package/web/_app/immutable/nodes/2.Bu6Zvx8F.js +1 -0
  34. package/web/_app/immutable/nodes/3.DcSG1nNk.js +1 -0
  35. package/web/_app/immutable/nodes/4.QUhLgH4i.js +2 -0
  36. package/web/_app/immutable/nodes/5.D3JSWVyR.js +5 -0
  37. package/web/_app/immutable/nodes/6.CWfUNDww.js +1 -0
  38. package/web/_app/immutable/nodes/7.DmPPLDvb.js +1 -0
  39. package/web/_app/immutable/nodes/{9.NF5w8YC2.js → 8.CwMTBog4.js} +2 -2
  40. package/web/_app/immutable/workers/worker-CWkpeSVG.js +1 -0
  41. package/web/_app/version.json +1 -1
  42. package/web/index.html +25 -19
  43. package/web/_app/immutable/assets/0.s5xqevrq.css +0 -1
  44. package/web/_app/immutable/assets/5.DCxNIgyW.css +0 -1
  45. package/web/_app/immutable/assets/8.C0NYkyNY.css +0 -1
  46. package/web/_app/immutable/chunks/B0NIb88Q.js +0 -1
  47. package/web/_app/immutable/chunks/B7ZjnqOt.js +0 -20
  48. package/web/_app/immutable/chunks/BEk5idXo.js +0 -1
  49. package/web/_app/immutable/chunks/C8UdwlAg.js +0 -3
  50. package/web/_app/immutable/chunks/CcQ3P3p1.js +0 -13
  51. package/web/_app/immutable/chunks/CcRs965g.js +0 -1
  52. package/web/_app/immutable/chunks/Cw2GjAJR.js +0 -1
  53. package/web/_app/immutable/chunks/DjBHskLm.js +0 -1
  54. package/web/_app/immutable/chunks/Dm229u8R.js +0 -1
  55. package/web/_app/immutable/chunks/UM_L7-f7.js +0 -1
  56. package/web/_app/immutable/chunks/b-AOhhy1.js +0 -1
  57. package/web/_app/immutable/chunks/k55859zq.js +0 -1
  58. package/web/_app/immutable/entry/app.B3QXxjjH.js +0 -2
  59. package/web/_app/immutable/entry/start.BauJm-6k.js +0 -1
  60. package/web/_app/immutable/nodes/0.DP4c84Wn.js +0 -14
  61. package/web/_app/immutable/nodes/1.BcwcVDJZ.js +0 -1
  62. package/web/_app/immutable/nodes/2.DgGnQZ7P.js +0 -1
  63. package/web/_app/immutable/nodes/3.BudzO7We.js +0 -1
  64. package/web/_app/immutable/nodes/4.DmswyrSt.js +0 -2
  65. package/web/_app/immutable/nodes/5.DjXsfjL5.js +0 -5
  66. package/web/_app/immutable/nodes/6.uBh-sdQx.js +0 -1
  67. package/web/_app/immutable/nodes/7.BYBNXM1A.js +0 -1
  68. package/web/_app/immutable/nodes/8.Tbz9Rh77.js +0 -1
  69. package/web/_app/immutable/workers/worker-rKDNrEKQ.js +0 -1
  70. /package/web/_app/immutable/assets/{9.CFA9_nYZ.css → 8.CFA9_nYZ.css} +0 -0
@@ -0,0 +1,142 @@
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 doctor [<notes>] [--report] [--offline]` | Checks the setup a step at a time (below). Exit 1 if anything failed. `--offline`: asks the provider nothing. |
18
+ | `isorite serve <notes> [--port <n>]` | The browser app at `http://localhost:4767/`, on 127.0.0.1 only. |
19
+ | `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. |
20
+ | `isorite check <notes>` | Lints the notes: frontmatter, links, cycles. |
21
+ | `isorite --tutorial` | A tour on an example base with a scripted model: no key or network needed. Good to check the install alone. |
22
+ | `isorite model [--from <folder>]` | Optional: fetches the model for search by meaning (53 MB). |
23
+ | `isorite help setup` | Prints this. |
24
+
25
+ ## Files
26
+
27
+ | Where | What |
28
+ |---|---|
29
+ | `~/.config/isorite/config.toml` (or `$XDG_CONFIG_HOME/isorite/`) | isorite's settings for this machine: `device`, `[provider]`, `[tiers]`, `[prices]`. Mode 600. Holds the key. |
30
+ | `~/.config/isorite/opencode/` | OpenCode's settings as isorite writes them at each `isorite term` start (`opencode.json`, theme, plugin link). Rewritten; don't edit. |
31
+ | `~/.config/isorite/opencode/local.json` | The person's own OpenCode settings, laid over isorite's. Never rewritten. |
32
+ | `<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. |
33
+ | `$TMPDIR/isorite-term-<port>.log` | The isorite server's output, when `isorite term` started it. Look here when "the isorite server did not start". |
34
+ | `$TMPDIR/isorite-opencode-<port>.log` | OpenCode's server's output, under `isorite term`. |
35
+
36
+ isorite never reads or changes the person's own OpenCode settings (`~/.config/opencode/`).
37
+
38
+ ## config.toml
39
+
40
+ ```toml
41
+ [provider]
42
+ name = "work" # optional; what isorite calls the provider in messages
43
+ url = "https://…/v1" # OpenAI-compatible; /chat/completions and /models are added. Absent: OpenRouter
44
+ key = "…" # the key; also settable in the browser (behind the logo)
45
+ key_header = "api-key" # send the key in this header, as it is, instead of Authorization: Bearer
46
+ stream_usage = false # don't send stream_options (some gateways answer 400 to it)
47
+ cache_marks = true # send Anthropic cache marks (OpenRouter-style gateways only)
48
+ pfx = "~/certs/me.pfx" # client certificate (mTLS), PKCS#12; or cert + cert_key (PEM)
49
+ pfx_passphrase = "$PFX_PASS" # "$NAME": read from that environment variable
50
+ cert = "me.pem" # relative paths are from config.toml's folder
51
+ cert_key = "me.key"
52
+ ca = "company-root.pem" # extra authority for the gateway's certificate (added to the trusted ones)
53
+
54
+ [provider.headers] # sent with every request; values may be "$NAME"
55
+ X-Team = "…"
56
+
57
+ [provider.query] # added to every request's URL
58
+ api-version = "…"
59
+
60
+ [tiers] # the provider's model names
61
+ low = "…" # quick
62
+ mid = "…" # Iso's default; the terminal's default
63
+ max = "…" # most able
64
+
65
+ [prices] # optional: US$ per million tokens in and out, for providers that don't say
66
+ "model-name" = [3.0, 15.0]
67
+ ```
68
+
69
+ Rules:
70
+
71
+ - With `url` set and no `[tiers]`, OpenRouter's names are used (`anthropic/claude-…`). A gateway usually answers
72
+ 404 to them. Doctor warns: name the gateway's models under `[tiers]`.
73
+ - `pfx` and `cert` can't both be named. `cert` and `cert_key` go together.
74
+ - A `"$NAME"` value whose variable isn't set is an error naming both.
75
+ - Certificates are used only over https, and only for the provider's own address.
76
+
77
+ ## Environment variables
78
+
79
+ | Variable | Effect |
80
+ |---|---|
81
+ | `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. |
82
+ | `ISORITE_NODE_DEFAULTS=0` | Leave Node as it is: no system authorities, no proxy unless Node is told. |
83
+ | `NODE_EXTRA_CA_CERTS` | A .pem of extra authorities, read by Node at start. |
84
+ | `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. |
85
+ | `ISORITE_OPENCODE` | The OpenCode 2 binary to run. Otherwise `opencode2`, then `opencode`, must be version 2. |
86
+ | `XDG_CONFIG_HOME` | Where `isorite/config.toml` is looked for. |
87
+ | `SSH_CONNECTION` | Set over SSH: `term`, `serve` and `doctor` print the `ssh -L` command for the browser. |
88
+
89
+ ## How requests reach the provider
90
+
91
+ - **Iso** (in the isorite server) posts to `<url>/chat/completions?<query>`, streaming, with the key (in
92
+ `Authorization: Bearer` or `key_header`), `headers`, and `stream_options` unless `stream_usage = false`. With a
93
+ certificate or `ca`, it goes by `node:https` with them, through `HTTPS_PROXY` if set. Otherwise it uses Node's
94
+ fetch.
95
+ - **The terminal**: OpenCode is configured with the provider at `http://127.0.0.1:<port>/api/gateway`, the isorite
96
+ server's relay, and with the server's token as its key. The relay adds the real key, certificate, headers and
97
+ query, and passes `/chat/completions` and `/models` on. So whatever works for Iso works for the terminal. The
98
+ relay answers only on 127.0.0.1, and only that token.
99
+ - With no key and no certificate in `config.toml`, and no `ISORITE_GATEWAY_*`, OpenCode uses its own models.
100
+ `isorite term` prints which models it uses, and from where.
101
+
102
+ ## What working looks like
103
+
104
+ `isorite doctor` ends `nothing failed`, with lines like:
105
+
106
+ ```
107
+ ✓ config: /home/me/.config/isorite/config.toml: work at https://llm-gateway.corp.example/v1
108
+ ✓ key: <48 characters>, sent in api-key
109
+ ✓ certificate: /home/me/certs/me.pfx
110
+ ✓ models: low small, mid medium, max large
111
+ ✓ proxy: http://proxy.corp:8080, used
112
+ ✓ authorities: the system's (146), as well as Node's own
113
+ ✓ reached: https://llm-gateway.corp.example/v1/models answered with 12 models
114
+ ✓ reply from medium: "ready", 14 tokens in and 2 out
115
+ ✓ tools: medium called ping
116
+ ✓ opencode: opencode2 2.0.32
117
+ ✓ terminal: work's models, through isorite's server at http://127.0.0.1:4767/api/gateway, …
118
+ ```
119
+
120
+ `isorite term` prints `isorite: http://localhost:4767/` and the terminal's models before OpenCode opens.
121
+
122
+ ## Failures, and what to change
123
+
124
+ | Seen | Cause | Change |
125
+ |---|---|---|
126
+ | `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. |
127
+ | `… 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. |
128
+ | `pfx_passphrase does not open …` | Wrong passphrase, or the variable it names holds the wrong one. | Fix the passphrase. |
129
+ | `ENOTFOUND` | The host name isn't found. | Check `url`. The name may resolve only on the company's network or VPN. |
130
+ | `ECONNREFUSED` | Nothing listens there. | Check the URL's port. |
131
+ | `ETIMEDOUT`, `UND_ERR_CONNECT_TIMEOUT` | No answer: often a proxy is needed. | Set `HTTPS_PROXY` (and `NO_PROXY` for internal hosts reached directly). |
132
+ | `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. |
133
+ | `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. |
134
+ | `said 400: … stream_options` | The gateway refuses `stream_options`. | `stream_usage = false`. |
135
+ | `✗ 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. |
136
+ | `isorite term needs OpenCode 2` | OpenCode 1, or none. | `npm i -g @opencode/cli`, or `ISORITE_OPENCODE=…`. |
137
+ | `port 4767 is serving <other>, not <notes>` | Another base is served there. | Stop it, or `--port`. |
138
+ | The link doesn't open, over SSH | `localhost` is the remote machine. | `ssh -L 4767:localhost:4767 <host>` from the person's own machine. |
139
+ | 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. |
140
+
141
+ If the gateway isn't OpenAI-compatible (Bedrock's or Vertex's own APIs, for example), isorite can't use it directly.
142
+ An OpenAI-compatible proxy in front of it, such as LiteLLM, can.
package/docs/setup.md ADDED
@@ -0,0 +1,111 @@
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
+
49
+ # A client certificate (mTLS), for a gateway that asks for one: a .pfx (or .p12) ...
50
+ pfx = "~/certs/me.pfx"
51
+ pfx_passphrase = "$MY_PFX_PASSPHRASE" # "$NAME" is read from the environment, so it isn't written here
52
+ # ... or a certificate and its key, as .pem files:
53
+ # cert = "~/certs/me.pem"
54
+ # cert_key = "~/certs/me.key"
55
+
56
+ ca = "~/certs/company-root.pem" # the authority the gateway's own certificate is signed by, if needed
57
+
58
+ [provider.headers] # sent with every request
59
+ X-Team = "research"
60
+ X-Project = "$PROJECT_ID"
61
+
62
+ [provider.query] # added to every request's address
63
+ api-version = "2024-10-21"
64
+ ```
65
+
66
+ Paths may start with `~`, or are taken from `config.toml`'s folder. Any value written `"$NAME"` is read from that
67
+ environment variable.
68
+
69
+ ### Proxies and the company's certificates
70
+
71
+ The `isorite` command uses `HTTPS_PROXY` (and `NO_PROXY`) when they're set, and trusts the system's certificate
72
+ authorities as well as Node's own, so a company's authority installed on the machine is trusted. Node does neither
73
+ by itself. Set `ISORITE_NODE_DEFAULTS=0` to leave Node as it is.
74
+
75
+ If the company's authority isn't installed on the machine, name its certificate as `ca` above, or in
76
+ `NODE_EXTRA_CA_CERTS`.
77
+
78
+ ## 3. Your notes
79
+
80
+ A folder of Markdown files; empty is fine.
81
+
82
+ ```sh
83
+ isorite serve ~/notes/my-project
84
+ ```
85
+
86
+ It prints a link to click: `http://localhost:4767/`. isorite keeps what it needs in the folder's `.isorite/`.
87
+
88
+ ## 4. The terminal
89
+
90
+ ```sh
91
+ cd ~/code/my-project
92
+ isorite term ~/notes/my-project
93
+ ```
94
+
95
+ It prints the same link, and which models the terminal uses. OpenCode uses the provider above, through isorite. More
96
+ is in [terminal.md](terminal.md).
97
+
98
+ ## When something fails
99
+
100
+ ```sh
101
+ isorite doctor ~/notes/my-project
102
+ ```
103
+
104
+ It checks, in turn: the configuration, how Node reaches the network (the proxy, the authorities), the provider
105
+ (its list of models, a reply of a few words from each model, and a tool call: a few hundred tokens in all), the
106
+ terminal (OpenCode 2, the port, the models it will use), and the notes. Each line says what failed, and what to
107
+ change. `--offline` asks the provider nothing.
108
+
109
+ `isorite doctor --report` gives all of it as one block, with the configuration and the environment it read. The
110
+ key, passphrases and any other secrets are shown only by their length. Paste it to whoever is helping, or to an AI
111
+ helping you: [setup-for-agents.md](setup-for-agents.md) is written for it, and `isorite help setup` prints it.
@@ -0,0 +1,115 @@
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. isorite
21
+ keeps what it needs in the folder's `.isorite/` (the journals of every change, and its settings), made the first
22
+ time the folder is opened. Commit `.isorite/` with the notes, if they're in git.
23
+
24
+ ## Starting it
25
+
26
+ ```sh
27
+ cd ~/code/my-project
28
+ isorite term ~/notes/my-project
29
+ ```
30
+
31
+ This starts the isorite server for the notes (unless one is running on port 4767; `--port <n>` for another), and
32
+ OpenCode's own server, on this machine, with a password of its own. Then it opens the terminal on that server.
33
+
34
+ Before OpenCode takes the screen, `isorite term` prints:
35
+
36
+ ```
37
+ isorite: http://localhost:4767/
38
+ the terminal's models: work-large, work-small, as config.toml names them, through the isorite server
39
+ ```
40
+
41
+ Most terminals let you click the link. It prints the same again when OpenCode exits. Closing the terminal stops
42
+ both servers, if `isorite term` started them.
43
+
44
+ The first time, the folder you started in is named as the notes' repository in `.isorite/settings.toml`, so that
45
+ code links in the notes point into it.
46
+
47
+ ### Over SSH
48
+
49
+ `localhost` is the machine `isorite term` runs on. To open isorite from your own machine, forward the port:
50
+
51
+ ```sh
52
+ ssh -L 4767:localhost:4767 <the machine>
53
+ ```
54
+
55
+ Then open `http://localhost:4767/` on your own. Over SSH, `isorite term` prints this command too. Any local port
56
+ works (`-L 9000:localhost:4767`, then `http://localhost:9000/`).
57
+
58
+ ## In the browser
59
+
60
+ Open the link. While the terminal runs, Iso's pane header has two more switches beside *Iso may edit the notes*:
61
+ *Iso may edit the project's files* (Alt+F) and *Iso may run commands* (Alt+R). With either on, the pane is the
62
+ terminal's session:
63
+
64
+ - **The session:** each prompt (asked in the terminal, or from here), what the agent says, and a line for each thing
65
+ it does. A command opens on its output, a file on its diff, a look-up in your notes on the note.
66
+ - **Asking from here:** what you type goes to the terminal's session, with what you gave Iso (hold a note on the
67
+ Atlas, or select a passage and send it to Iso): notes by name, for the agent to read with isorite's tools, and
68
+ passages as they are. Escape, or the square, stops it.
69
+ - **Permissions:** when OpenCode asks before doing something, the same question is in the pane: allow once, always
70
+ allow, or don't allow. Answered in either place, it's gone from both.
71
+ - **Notes the agent changes** are written at once, under its name. Turn on provenance in the Note pane's header to
72
+ see the agent's words marked, and the timeline to put a note back.
73
+
74
+ With both off, the pane is Iso's own again.
75
+
76
+ ## The models
77
+
78
+ The terminal uses Iso's provider, as `config.toml` names it ([setup.md](setup.md)): the `[tiers]` models, `mid` its
79
+ default. OpenCode asks the isorite server, which passes each request on to the provider with the key, the client
80
+ certificate, the headers and the query `config.toml` names. OpenCode is given none of them, only the isorite
81
+ server's token.
82
+
83
+ `ISORITE_GATEWAY_URL`, `ISORITE_GATEWAY_MODELS` (comma-separated) and `ISORITE_GATEWAY_KEY`, when set, are used
84
+ instead, and OpenCode asks that address itself. With no key and no certificate in `config.toml`, OpenCode uses its
85
+ own models; `isorite term` says which.
86
+
87
+ OpenCode doesn't know a gateway's prices, so Iso's pane shows the session's tokens rather than its cost.
88
+
89
+ ## OpenCode's settings
90
+
91
+ isorite keeps OpenCode's settings in `~/.config/isorite/opencode/`, rewritten at each start, so your own OpenCode
92
+ settings are never read or changed. Anything of your own goes in `~/.config/isorite/opencode/local.json`, which is
93
+ never rewritten, and is laid over what isorite writes. For example, to have OpenCode ask before every command (by
94
+ default it runs them):
95
+
96
+ ```json
97
+ { "permissions": [{ "action": "shell", "resource": "*", "effect": "ask" }] }
98
+ ```
99
+
100
+ ## Without the internet
101
+
102
+ Once installed, isorite needs no network, as long as the model's address in `config.toml` is one you can reach (a
103
+ gateway, or a model served on your own machine). What it fetches, it fetches once:
104
+
105
+ - isorite and OpenCode, from npm: through your company's npm mirror, if it has one.
106
+ - The model for search by meaning (optional): `isorite model` gets it from npm too (`isorite-model-granite`), so the
107
+ same mirror serves it; else from Hugging Face. On a machine with neither, copy its files from one that has them:
108
+ `isorite model --from <folder>`. Every file is checked against its SHA-256.
109
+
110
+ ## Who can reach it
111
+
112
+ OpenCode's server listens on this machine only (127.0.0.1), and only the isorite server talks to it. The isorite
113
+ server listens on this machine only too, unless you put it on your tailnet (`tailscale serve`). Then anyone on your
114
+ tailnet who opens the page can ask the agent to act in your project, as they could already edit your notes. The
115
+ 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.0",
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
  },