Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Working on Jeview

Jeview is a local gateway for Jev (TypeSafe System One) with a live viewer. To *use* a running Jeview, read
[llms.txt](llms.txt), or `/llms.txt` on the Jeview itself. This file is for changing the code.

## The lie of the land

- `src/jeview.ts`: the whole server. The proxy, the SQLite store, the read API, and the text of llms.txt.
- `jeview.ts`: the command line. `launch.sh` finds a Node and runs it.
- `ui/`: the viewer, plain JavaScript with no build step, served from disk on every request.
- `demo/`: Pixel Knight and Support Desk, each a small server and a page. They play through a running Jeview.
- `test/jeview.test.ts`: every promise the server makes, against a stand-in for Jev.

## Check your work

```sh
npm install # only the type checker
npm run typecheck
npm run check:pages # the browser scripts parse
npm test
```

Tests never call TypeSafe. The demos do: every call is real and costs money, so do not run them in CI or leave them
running. They play only while a page is open.

## Rules of the house

- **No dependencies at run time.** Node 24 and its standard library are all Jeview needs. Keep it so.
- **Node runs the TypeScript as it is**, by stripping the types. Use only syntax that can be stripped: no enums, no
namespaces, no parameter properties. Imports name the file, `.ts` included.
- **The body goes to Jev byte for byte**, and Jev's answer comes back as sent, plus `events`. Jeview's own headers
(`Jeview-*`) never reach Jev.
- **The key is never shown**, logged or returned: only whether it is set, and its last four characters.
- **Loopback only.** Jeview answers to local host names, refuses requests from web pages on other sites, and has no
login. Each of these has a test; a change that needs one of them loosened is probably the wrong change.
- **Nothing leaves the machine but calls to Jev.** No telemetry, no fonts or scripts from elsewhere: the viewer's
content security policy allows only its own origin.
- **Pages are built from nodes and text**, never from HTML strings that carry data: recorded calls hold whatever a
caller sent. See `h()` in `ui/app.js`.
- **The viewer must work with an older server**, and the server with an older viewer's requests: the pages are read
from disk while an older process may still be running.
- After changing `llmsText`, run `npm run llms` to write `llms.txt` again. A test compares the two.

## Writing

Comments say why, in plain words, and only where the code cannot. Commit messages are one sentence about what
changed for the person using Jeview, then the details. Match the code around you: it is dense on purpose.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Jeview

> Agents: [llms.txt](llms.txt) says how to use a running Jeview, and [AGENTS.md](AGENTS.md) how to work on this repository.

An unofficial local middleman for Jev ([TypeSafe](https://typesafe.ai) System One), with a live view of every call.
Jeview is an independent project: it is not made or endorsed by TypeSafe.

Expand Down Expand Up @@ -45,7 +47,8 @@ requests and the same answers, with no key needed from the caller.
those answers, send its event id in a `Jeview-Trigger` header, and the map grows that request off the answer.
Jeview drops its own headers before calling Jev and sends the body on unchanged.

Agents can read `http://127.0.0.1:4777/llms.txt`.
Agents can read `http://127.0.0.1:4777/llms.txt`: a running Jeview serves it with its own address and whether a key is
set. [llms.txt](llms.txt) here is the same text, for the default address.

## Options

Expand Down
45 changes: 45 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Jeview

A local middleman for Jev (TypeSafe System One) at http://127.0.0.1:4777, with a live view of every call at http://127.0.0.1:4777/
It is a gateway, not a model: it sits between a Jev client and TypeSafe and answers nothing itself. Each request it
receives goes on to Jev with the Jev key set in the viewer, Jev's answer goes back to the caller, and the call is kept
whole (what was asked, what Jev saw, what it answered) in a SQLite database on this machine. Nothing is stored anywhere
else: Jeview runs locally, and TypeSafe is the only place it sends anything.

The Jev key: not set yet, so calls are refused. Set it in the viewer at http://127.0.0.1:4777/ (the key icon, top right).

## Send Jev requests here

Use http://127.0.0.1:4777/v1/systemone wherever you would use https://api.typesafe.ai/v1/systemone: the same body
{ model, state, questions }, the same answers. No key is needed from the caller. To group requests under a project or
label, add it to the path: http://127.0.0.1:4777/<label>/v1/systemone.

## Link a question to the answer that led to it

Every answer comes back with an event id, in "events" beside the answers: { "<question id>": "<call>:<question id>" }.
When a later request follows from one of those answers, send its event id in a header: Jeview-Trigger: <event id>.
The viewer then grows that request's questions as a branch off the answer that triggered them. Jeview drops its own
headers before calling Jev, and sends the body on exactly as it came.

## When a call is refused

Whatever Jev answers, a refusal included, comes back as Jev sent it. Jeview's own refusals are JSON, { "error": "..." }:

- 401: no Jev key is set.
- 400: a Jeview-Trigger that is empty or longer than 200 characters.
- 413: a body over 16 MB.
- 502: Jev could not be reached, or its answer was cut short.
- 403: the request came from a web page on another site. Jeview serves programs on this machine, not pages elsewhere.

What Jev answered or refused is recorded, and so are the 401s and 502s. The 400s, 413s and 403s are not.

## Read what was recorded (JSON, from this machine only)

- GET http://127.0.0.1:4777/_/api/records?since=<n>: summaries in the order calls finished, at most 5,000 at a time; "cursor" is
the next "since", and "more" says the next page is already there. ?latest=<n> starts at the latest n calls instead.
- GET http://127.0.0.1:4777/_/api/records?ids=<id>,<id>: the summaries of those calls, at most 1,000.
- GET http://127.0.0.1:4777/_/api/records/<id>: one call, the request sent to Jev and the response it returned.
- GET http://127.0.0.1:4777/_/api/search?q=<words>: the ids of the calls whose label, trigger, request or response contains every
word, newest first, at most 1,000.

A record's "key" is the sha256 of the exact body sent to Jev.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"demo:support": "node demo/support-desk.ts",
"test": "node --test test/*.test.ts",
"typecheck": "tsc --noEmit",
"check:pages": "node --check ui/app.js && node --check demo/game.js && node --check demo/support-desk.js"
"check:pages": "node --check ui/app.js && node --check demo/game.js && node --check demo/support-desk.js",
"llms": "node --input-type=module -e \"import { writeFileSync } from 'node:fs'; import { llmsText } from './src/jeview.ts'; writeFileSync('llms.txt', llmsText('http://127.0.0.1:4777', false));\""
},
"devDependencies": {
"@types/node": "^24.3.0",
Expand Down
7 changes: 6 additions & 1 deletion test/jeview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";

import { createJeview, DATABASE, JEV_USD_PER_INPUT_TOKEN, summarize, type JeviewSummary } from "../src/jeview.ts";
import { createJeview, DATABASE, JEV_USD_PER_INPUT_TOKEN, llmsText, summarize, type JeviewSummary } from "../src/jeview.ts";

type Seen = { method: string; url: string; headers: IncomingMessage["headers"]; body: string };
type Hooks = { after(fn: () => unknown): void };
Expand Down Expand Up @@ -254,6 +254,11 @@ test("two Jeviews sharing a folder never hand out the same call id, and each sho
assert.deepEqual([two.store.allocate(), three.store.allocate()], [21, 22]);
});

test("the llms.txt in the repository is the one a Jeview at the default address serves", () => {
// after changing llmsText, write it out again: see AGENTS.md
assert.equal(readFileSync(new URL("../llms.txt", import.meta.url), "utf8"), llmsText("http://127.0.0.1:4777", false));
});

test("the Jev key is set from the viewer only, never shown, and used by every call; llms.txt says how to connect", async (t) => {
const upstream = await jev(t, () => ({ body: jevAnswer }));
const { base } = await proxy(t, upstream.url, { key: null });
Expand Down
Loading