brane 0.3.1 → 0.3.3
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/AGENTS.md +2 -2
- package/CHANGELOG.md +43 -3
- package/README.md +7 -19
- package/dist/daemon/archive-gate.js +11 -0
- package/dist/daemon/server.js +24 -5
- package/dist/export.js +1 -1
- package/dist/markdown-diagnostics.js +1 -1
- package/dist/math-syntax.js +354 -0
- package/dist/shared/membrane-quips.js +77 -0
- package/dist/shared/mo-glyph.js +4 -0
- package/dist/shared/narrative-menu.js +8 -5
- package/dist/shared/view-url.js +64 -2
- package/dist/test/archive-gate.test.js +22 -0
- package/dist/test/markdown-math.test.js +1 -1
- package/dist/test/membrane-quips.test.js +62 -0
- package/dist/test/mo-glyph.test.js +3 -1
- package/dist/test/narrative-menu.test.js +17 -3
- package/dist/test/view-url.test.js +106 -1
- package/docs/daemon-api.md +1 -1
- package/docs/math.md +3 -0
- package/docs/mcp.md +1 -1
- package/docs/releasing.md +6 -4
- package/docs/token-rotation.md +4 -2
- package/docs/view-urls.md +40 -2
- package/docs/web-app.md +1 -1
- package/expo/dist/_expo/static/js/web/{entry-e94247f119fdcd3b424a1764233a41d2.js → entry-dd90cc09dc13952c6fa96ce25b41c64d.js} +29 -28
- package/expo/dist/index.html +1 -1
- package/expo/dist/sw.js +2 -2
- package/package.json +5 -2
package/AGENTS.md
CHANGED
|
@@ -116,8 +116,8 @@ published GitHub release, and only by it. **Agents never run `npm publish`.**
|
|
|
116
116
|
The shared web app ships with it: a release deploys its tarball's
|
|
117
117
|
`expo/dist`, and a merge to `main` deploys nothing (staging is Isolate on a
|
|
118
118
|
daemon running `main`). The version bump, tag rule, dist-tag choice, app
|
|
119
|
-
deploy and rollback, and why 0.3.0
|
|
120
|
-
[docs/releasing.md](docs/releasing.md).
|
|
119
|
+
deploy and rollback, and why 0.3.0 and 0.3.1 have no provenance
|
|
120
|
+
attestation are in [docs/releasing.md](docs/releasing.md).
|
|
121
121
|
|
|
122
122
|
## CLI Commands
|
|
123
123
|
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,43 @@ page, which is also what triggers its publish (see
|
|
|
6
6
|
[docs/releasing.md](docs/releasing.md)). This file is the summary that
|
|
7
7
|
travels in the npm tarball.
|
|
8
8
|
|
|
9
|
+
## 0.3.3 - 2026-10-14
|
|
10
|
+
|
|
11
|
+
- A fresh `npm install brane` audits clean. The math syntax extension
|
|
12
|
+
is now vendored (`src/math-syntax.ts`) instead of depending on
|
|
13
|
+
`micromark-extension-math`, which pinned a second, older katex into
|
|
14
|
+
every install (GHSA-238p-pmpm-9mq7) and led `npm audit fix --force` to
|
|
15
|
+
propose brane@0.1.1 ([#608](https://github.com/benjamn/brane/pull/608)).
|
|
16
|
+
- In the web app, "All narratives" opens the list in a new tab, and a
|
|
17
|
+
link opened in a new tab lands on the daemon it came from rather than
|
|
18
|
+
the browser's default one ([#607](https://github.com/benjamn/brane/pull/607)).
|
|
19
|
+
- The 膜 glyph carries a caption — a quip or a tip about brane — that
|
|
20
|
+
changes on each tap and nothing else; the empty state's words move
|
|
21
|
+
above it, and it no longer says "No narratives yet" when they are all
|
|
22
|
+
archived ([#606](https://github.com/benjamn/brane/pull/606)).
|
|
23
|
+
- The README, which is also the npm page, opens with a new
|
|
24
|
+
introduction: what brane is for, the LIVE loop, and terminal
|
|
25
|
+
mirroring ([#610](https://github.com/benjamn/brane/pull/610)).
|
|
26
|
+
|
|
27
|
+
Published while the repository is private, so like 0.3.0–0.3.2 it
|
|
28
|
+
carries no npm provenance attestation.
|
|
29
|
+
|
|
30
|
+
## 0.3.2 - 2026-10-13
|
|
31
|
+
|
|
32
|
+
- Archive is offered only once a narrative has ended, and the daemon
|
|
33
|
+
enforces it: `PATCH /api/narratives/:id/archive` answers 409 while the
|
|
34
|
+
narrative's status isn't `ended`, its agent is still connected, or its
|
|
35
|
+
tmux session is up. Unarchiving is never refused
|
|
36
|
+
([#604](https://github.com/benjamn/brane/pull/604)).
|
|
37
|
+
- Archiving from a narrative's own 🦠 menu goes to the narrative list,
|
|
38
|
+
and Back returns to the archived narrative
|
|
39
|
+
([#603](https://github.com/benjamn/brane/pull/603)).
|
|
40
|
+
- Swiping the narrative's header card down dismisses it to the list;
|
|
41
|
+
Back returns ([#604](https://github.com/benjamn/brane/pull/604)).
|
|
42
|
+
|
|
43
|
+
Published while the repository is private, so like 0.3.0 and 0.3.1 it
|
|
44
|
+
carries no npm provenance attestation.
|
|
45
|
+
|
|
9
46
|
## 0.3.1 - 2026-10-12
|
|
10
47
|
|
|
11
48
|
- The shared web app ships with the release. It serves the `latest`
|
|
@@ -34,9 +71,12 @@ travels in the npm tarball.
|
|
|
34
71
|
repoints the fold's hook config at the running daemon
|
|
35
72
|
([#594](https://github.com/benjamn/brane/pull/594)).
|
|
36
73
|
- Vulnerabilities are reported through GitHub's private vulnerability
|
|
37
|
-
reporting (see `SECURITY.md`)
|
|
38
|
-
|
|
39
|
-
|
|
74
|
+
reporting (see `SECURITY.md`)
|
|
75
|
+
([#597](https://github.com/benjamn/brane/pull/597)).
|
|
76
|
+
|
|
77
|
+
Like 0.3.0, published while the repository was private, so it carries
|
|
78
|
+
no npm provenance attestation. The copy of this entry in the 0.3.1
|
|
79
|
+
tarball says otherwise; it is wrong.
|
|
40
80
|
|
|
41
81
|
## 0.3.0 - 2026-10-11
|
|
42
82
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# brane
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Brane provides a [web-based remote management interface](https://brane.organelle.dev/) for running long-lived Terminal UI (TUI) applications on any remote machine, securely and directly, without a central server, supporting not only full shells like `zsh` and `fish` and utilities like `lazygit` or `htop`, but also most of the popular _agentic_ TUI commands: `claude`, `codex`, `gemini`, `crush`, `copilot`, `goose`, and so on.
|
|
4
|
+
|
|
5
|
+
The "intelligent" TUI applications demonstrate their intelligence by using brane's local MCP tools: `whoami`, `narrate`, `listen` and the rest, to implement a protocol for ongoing, purposeful conversation (the "LIVE loop") recorded automatically to a permanent, fully agent-searchable record that remains completely private to the host machine.
|
|
6
|
+
|
|
7
|
+
Unlike a lot of other remote-coding tools, which let you chat with your agents and accept/deny permissions prompts, brane also _mirrors the terminal of the running agent_, so you can effortlessly observe your agents working from any mobile device, with the full power of a software keyboard and touch inputs for interacting with the terminal from afar, whenever you need that level of control.
|
|
8
|
+
|
|
9
|
+
## Etymology and economics
|
|
4
10
|
|
|
5
11
|
"Brane" is short for **membrane**, evoking a [_eukaryotic_](https://en.wikipedia.org/wiki/Eukaryote) metaphor for agent management, inherited from [organelle](https://github.com/benjamn/organelle): the daemon is the nucleus (which has a membrane), agents are organelles (which in turn have their own membranes), and narrative event logs trace everything each organelle has encountered and done (biologically implausible, but very useful).
|
|
6
12
|
|
|
@@ -12,24 +18,6 @@ If you find brane useful, perhaps because your tastes align with mine and I have
|
|
|
12
18
|
|
|
13
19
|
[](https://github.com/sponsors/benjamn)
|
|
14
20
|
|
|
15
|
-
## What it does
|
|
16
|
-
|
|
17
|
-
Brane runs long-lived coding agents (Claude Code, Codex, Gemini, Goose and
|
|
18
|
-
others, or any command) in tmux on your own machine, keeps everything they
|
|
19
|
-
say and hear in one SQLite database, and lets you read, steer and search
|
|
20
|
-
them from a phone or any browser on your tailnet.
|
|
21
|
-
|
|
22
|
-
Four ideas cover most of it:
|
|
23
|
-
|
|
24
|
-
- **Fold**: a directory you've registered as a place agents may be spawned.
|
|
25
|
-
- **Narrative**: one agent session's append-only log of everything it said
|
|
26
|
-
and everything said to it. Every message has a stable address
|
|
27
|
-
(`claude-4c7289d1:42`) you can link to from anywhere.
|
|
28
|
-
- **The LIVE loop**: the protocol agents follow to talk to you through an
|
|
29
|
-
MCP server brane injects into each session: listen, interpret, vocalize,
|
|
30
|
-
execute.
|
|
31
|
-
- **Meld**: a QR code that signs a phone or browser into your daemon.
|
|
32
|
-
|
|
33
21
|
## Quick start
|
|
34
22
|
|
|
35
23
|
0. **Install [Tailscale](https://tailscale.com/download)** and join your
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Why the narrative can't be archived yet, or null when it can. */
|
|
2
|
+
export function archiveRefusal({ status, mcpConnected, tmuxLive }) {
|
|
3
|
+
if (mcpConnected)
|
|
4
|
+
return "its agent is still connected";
|
|
5
|
+
if (tmuxLive)
|
|
6
|
+
return "its session is still running";
|
|
7
|
+
if (status !== "ended")
|
|
8
|
+
return `its status is ${status ?? "unset"}, not ended`;
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=archive-gate.js.map
|
package/dist/daemon/server.js
CHANGED
|
@@ -22,6 +22,7 @@ import { getBraneId, getPayloadsDir, getTokenFilePath, fromPortablePath, toPorta
|
|
|
22
22
|
import { TokenRing } from "./token-ring.js";
|
|
23
23
|
import { signalEventFields, signalStanding } from "./signal-source.js";
|
|
24
24
|
import { AGENT_LOOPBACK_REFUSAL, AGENT_REVIVE_REFUSAL, agentRefusal, agentReviveRefusal } from "./agent-scope.js";
|
|
25
|
+
import { archiveRefusal } from "./archive-gate.js";
|
|
25
26
|
import { DbTokenStore, bootstrapTokens, legacyTokenWarning, parseTokenHashes, liveAgentTokenId, mintAgentToken, recordLauncher, settleAgentLaunch, verifyAgentToken } from "../db/tokens.js";
|
|
26
27
|
import { closeSocketsOf, listTokensRoute, socketOwners, retireTokens } from "./token-routes.js";
|
|
27
28
|
import { getConfig, resolveAppHost } from "../commands/config.js";
|
|
@@ -1381,16 +1382,34 @@ export async function startServer(config) {
|
|
|
1381
1382
|
const narrativeId = archiveMatch[1];
|
|
1382
1383
|
parseBody(req).then((body) => {
|
|
1383
1384
|
const { archived } = JSON.parse(body);
|
|
1385
|
+
// A running narrative can't be archived: hiding a live agent from the
|
|
1386
|
+
// list is exactly what a rogue one would want. See archive-gate.ts.
|
|
1387
|
+
if (archived) {
|
|
1388
|
+
const row = db.prepare("SELECT status FROM narratives WHERE id = ?").get(narrativeId);
|
|
1389
|
+
if (!row) {
|
|
1390
|
+
sendJson(res, 404, { error: `No narrative ${narrativeId}` });
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
const refusal = archiveRefusal({
|
|
1394
|
+
status: row.status,
|
|
1395
|
+
mcpConnected: mcpConnectedNids(mcpClients).has(narrativeId),
|
|
1396
|
+
tmuxLive: liveTmuxSessions().has(narrativeId),
|
|
1397
|
+
});
|
|
1398
|
+
if (refusal) {
|
|
1399
|
+
sendJson(res, 409, { error: `Can't archive ${narrativeId}: ${refusal}. End it first.` });
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1384
1403
|
db.prepare("UPDATE narratives SET archived = ? WHERE id = ?").run(archived ? 1 : 0, narrativeId);
|
|
1385
1404
|
// An archived narrative's agent token is a credential nobody needs:
|
|
1386
|
-
// revoke it
|
|
1387
|
-
//
|
|
1388
|
-
//
|
|
1389
|
-
if (archived
|
|
1405
|
+
// revoke it. Its session is over, or the gate above would have
|
|
1406
|
+
// refused. Unarchiving brings nothing back; a revive mints a fresh
|
|
1407
|
+
// token.
|
|
1408
|
+
if (archived) {
|
|
1390
1409
|
const agentRow = liveAgentTokenId(db, narrativeId);
|
|
1391
1410
|
if (agentRow) {
|
|
1392
1411
|
db.prepare("UPDATE tokens SET revoked_at = ? WHERE kind = 'agent' AND owner_nid = ? AND revoked_at IS NULL").run(Date.now(), narrativeId);
|
|
1393
|
-
//
|
|
1412
|
+
// Any socket still holding it goes now, not at its next redial.
|
|
1394
1413
|
closeSocketsOf(viewerSockets, [agentRow]);
|
|
1395
1414
|
}
|
|
1396
1415
|
}
|
package/dist/export.js
CHANGED
|
@@ -4,7 +4,7 @@ import { micromark } from "micromark";
|
|
|
4
4
|
import { gfmTable, gfmTableHtml } from "micromark-extension-gfm-table";
|
|
5
5
|
import { gfmAutolinkLiteral, gfmAutolinkLiteralHtml } from "micromark-extension-gfm-autolink-literal";
|
|
6
6
|
import { gfmTaskListItem, gfmTaskListItemHtml } from "micromark-extension-gfm-task-list-item";
|
|
7
|
-
import { math } from "
|
|
7
|
+
import { math } from "./math-syntax.js";
|
|
8
8
|
import { NID_PATTERN, NID_REF_PATTERN } from "./shared/markdown-nids.js";
|
|
9
9
|
import { isInlinePayload, INLINE_PAYLOAD_MIME } from "./shared/payload-inline.js";
|
|
10
10
|
import { readEvents, readEventsBySeqs } from "./db/events.js";
|
|
@@ -32,7 +32,7 @@ import { micromark } from "micromark";
|
|
|
32
32
|
import { gfmTable, gfmTableHtml } from "micromark-extension-gfm-table";
|
|
33
33
|
import { gfmAutolinkLiteral, gfmAutolinkLiteralHtml } from "micromark-extension-gfm-autolink-literal";
|
|
34
34
|
import { gfmTaskListItem, gfmTaskListItemHtml } from "micromark-extension-gfm-task-list-item";
|
|
35
|
-
import { math } from "
|
|
35
|
+
import { math } from "./math-syntax.js";
|
|
36
36
|
import { mathHtmlExtension } from "./math.js";
|
|
37
37
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
38
38
|
import { inspectDiagram } from "./diagram/index.js";
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/** micromark syntax extension for math: `$…$` (text) and `$$` fences (flow).
|
|
2
|
+
*
|
|
3
|
+
* Vendored from `micromark-extension-math` 3.1.0 (`dev/lib/syntax.js`,
|
|
4
|
+
* `math-flow.js`, `math-text.js`), the syntax half only, so the published
|
|
5
|
+
* package no longer depends on it. That package's index also loads its
|
|
6
|
+
* `mathHtml()`, which imports `katex` at the range it pins (`^0.16.0`). Our
|
|
7
|
+
* root `overrides` dedupe that onto our own katex in this checkout, but npm
|
|
8
|
+
* ignores a dependency's `overrides`, so every `npm install brane` got a
|
|
9
|
+
* second, older katex: loaded, never called (`src/math.ts` is our `mathHtml`),
|
|
10
|
+
* and flagged by `npm audit` (GHSA-238p-pmpm-9mq7). The tokenizers are
|
|
11
|
+
* unchanged apart from TypeScript annotations and dropping the `devlop`
|
|
12
|
+
* assertions, which upstream's production build drops too. The web app still
|
|
13
|
+
* imports the package itself: it ships prebuilt, under `expo/`'s overrides.
|
|
14
|
+
*
|
|
15
|
+
* Upstream: https://github.com/micromark/micromark-extension-math
|
|
16
|
+
*
|
|
17
|
+
* (The MIT License)
|
|
18
|
+
*
|
|
19
|
+
* Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com>
|
|
20
|
+
*
|
|
21
|
+
* Permission is hereby granted, free of charge, to any person obtaining
|
|
22
|
+
* a copy of this software and associated documentation files (the
|
|
23
|
+
* 'Software'), to deal in the Software without restriction, including
|
|
24
|
+
* without limitation the rights to use, copy, modify, merge, publish,
|
|
25
|
+
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
26
|
+
* permit persons to whom the Software is furnished to do so, subject to
|
|
27
|
+
* the following conditions:
|
|
28
|
+
*
|
|
29
|
+
* The above copyright notice and this permission notice shall be
|
|
30
|
+
* included in all copies or substantial portions of the Software.
|
|
31
|
+
*
|
|
32
|
+
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
33
|
+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
34
|
+
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
35
|
+
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
36
|
+
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
37
|
+
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
38
|
+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
39
|
+
*/
|
|
40
|
+
import { factorySpace } from "micromark-factory-space";
|
|
41
|
+
import { markdownLineEnding } from "micromark-util-character";
|
|
42
|
+
import { codes, constants, types } from "micromark-util-symbol";
|
|
43
|
+
/** Create an extension for `micromark` to enable math syntax. */
|
|
44
|
+
export function math(options) {
|
|
45
|
+
return {
|
|
46
|
+
flow: { [codes.dollarSign]: mathFlow },
|
|
47
|
+
text: { [codes.dollarSign]: mathText(options) },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// --- math (flow) ------------------------------------------------------------
|
|
51
|
+
const mathFlow = {
|
|
52
|
+
tokenize: tokenizeMathFenced,
|
|
53
|
+
concrete: true,
|
|
54
|
+
name: "mathFlow",
|
|
55
|
+
};
|
|
56
|
+
const nonLazyContinuation = {
|
|
57
|
+
tokenize: tokenizeNonLazyContinuation,
|
|
58
|
+
partial: true,
|
|
59
|
+
};
|
|
60
|
+
function tokenizeMathFenced(effects, ok, nok) {
|
|
61
|
+
const self = this;
|
|
62
|
+
const tail = self.events[self.events.length - 1];
|
|
63
|
+
const initialSize = tail && tail[1].type === types.linePrefix
|
|
64
|
+
? tail[2].sliceSerialize(tail[1], true).length
|
|
65
|
+
: 0;
|
|
66
|
+
let sizeOpen = 0;
|
|
67
|
+
return start;
|
|
68
|
+
/** Start of math: `$$`, at the first `$`. */
|
|
69
|
+
function start(code) {
|
|
70
|
+
effects.enter("mathFlow");
|
|
71
|
+
effects.enter("mathFlowFence");
|
|
72
|
+
effects.enter("mathFlowFenceSequence");
|
|
73
|
+
return sequenceOpen(code);
|
|
74
|
+
}
|
|
75
|
+
/** In opening fence sequence. */
|
|
76
|
+
function sequenceOpen(code) {
|
|
77
|
+
if (code === codes.dollarSign) {
|
|
78
|
+
effects.consume(code);
|
|
79
|
+
sizeOpen++;
|
|
80
|
+
return sequenceOpen;
|
|
81
|
+
}
|
|
82
|
+
if (sizeOpen < 2) {
|
|
83
|
+
return nok(code);
|
|
84
|
+
}
|
|
85
|
+
effects.exit("mathFlowFenceSequence");
|
|
86
|
+
return factorySpace(effects, metaBefore, types.whitespace)(code);
|
|
87
|
+
}
|
|
88
|
+
/** In opening fence, before meta: `$$asciimath`, at `a`. */
|
|
89
|
+
function metaBefore(code) {
|
|
90
|
+
if (code === codes.eof || markdownLineEnding(code)) {
|
|
91
|
+
return metaAfter(code);
|
|
92
|
+
}
|
|
93
|
+
effects.enter("mathFlowFenceMeta");
|
|
94
|
+
effects.enter(types.chunkString, { contentType: constants.contentTypeString });
|
|
95
|
+
return meta(code);
|
|
96
|
+
}
|
|
97
|
+
/** In meta. */
|
|
98
|
+
function meta(code) {
|
|
99
|
+
if (code === codes.eof || markdownLineEnding(code)) {
|
|
100
|
+
effects.exit(types.chunkString);
|
|
101
|
+
effects.exit("mathFlowFenceMeta");
|
|
102
|
+
return metaAfter(code);
|
|
103
|
+
}
|
|
104
|
+
if (code === codes.dollarSign) {
|
|
105
|
+
return nok(code);
|
|
106
|
+
}
|
|
107
|
+
effects.consume(code);
|
|
108
|
+
return meta;
|
|
109
|
+
}
|
|
110
|
+
/** After meta. */
|
|
111
|
+
function metaAfter(code) {
|
|
112
|
+
// Guaranteed to be eol/eof.
|
|
113
|
+
effects.exit("mathFlowFence");
|
|
114
|
+
if (self.interrupt) {
|
|
115
|
+
return ok(code);
|
|
116
|
+
}
|
|
117
|
+
return effects.attempt(nonLazyContinuation, beforeNonLazyContinuation, after)(code);
|
|
118
|
+
}
|
|
119
|
+
/** After eol/eof in math, at a non-lazy closing fence or content. */
|
|
120
|
+
function beforeNonLazyContinuation(code) {
|
|
121
|
+
return effects.attempt({ tokenize: tokenizeClosingFence, partial: true }, after, contentStart)(code);
|
|
122
|
+
}
|
|
123
|
+
/** Before math content, definitely not before a closing fence. */
|
|
124
|
+
function contentStart(code) {
|
|
125
|
+
return (initialSize
|
|
126
|
+
? factorySpace(effects, beforeContentChunk, types.linePrefix, initialSize + 1)
|
|
127
|
+
: beforeContentChunk)(code);
|
|
128
|
+
}
|
|
129
|
+
/** Before math content, after optional prefix. */
|
|
130
|
+
function beforeContentChunk(code) {
|
|
131
|
+
if (code === codes.eof) {
|
|
132
|
+
return after(code);
|
|
133
|
+
}
|
|
134
|
+
if (markdownLineEnding(code)) {
|
|
135
|
+
return effects.attempt(nonLazyContinuation, beforeNonLazyContinuation, after)(code);
|
|
136
|
+
}
|
|
137
|
+
effects.enter("mathFlowValue");
|
|
138
|
+
return contentChunk(code);
|
|
139
|
+
}
|
|
140
|
+
/** In math content. */
|
|
141
|
+
function contentChunk(code) {
|
|
142
|
+
if (code === codes.eof || markdownLineEnding(code)) {
|
|
143
|
+
effects.exit("mathFlowValue");
|
|
144
|
+
return beforeContentChunk(code);
|
|
145
|
+
}
|
|
146
|
+
effects.consume(code);
|
|
147
|
+
return contentChunk;
|
|
148
|
+
}
|
|
149
|
+
/** After math (ha!). */
|
|
150
|
+
function after(code) {
|
|
151
|
+
effects.exit("mathFlow");
|
|
152
|
+
return ok(code);
|
|
153
|
+
}
|
|
154
|
+
function tokenizeClosingFence(effects, ok, nok) {
|
|
155
|
+
let size = 0;
|
|
156
|
+
// Before closing fence, at optional whitespace.
|
|
157
|
+
return factorySpace(effects, beforeSequenceClose, types.linePrefix, self.parser.constructs.disable.null.includes("codeIndented")
|
|
158
|
+
? undefined
|
|
159
|
+
: constants.tabSize);
|
|
160
|
+
/** In closing fence, after optional whitespace, at sequence. */
|
|
161
|
+
function beforeSequenceClose(code) {
|
|
162
|
+
effects.enter("mathFlowFence");
|
|
163
|
+
effects.enter("mathFlowFenceSequence");
|
|
164
|
+
return sequenceClose(code);
|
|
165
|
+
}
|
|
166
|
+
/** In closing fence sequence. */
|
|
167
|
+
function sequenceClose(code) {
|
|
168
|
+
if (code === codes.dollarSign) {
|
|
169
|
+
size++;
|
|
170
|
+
effects.consume(code);
|
|
171
|
+
return sequenceClose;
|
|
172
|
+
}
|
|
173
|
+
if (size < sizeOpen) {
|
|
174
|
+
return nok(code);
|
|
175
|
+
}
|
|
176
|
+
effects.exit("mathFlowFenceSequence");
|
|
177
|
+
return factorySpace(effects, afterSequenceClose, types.whitespace)(code);
|
|
178
|
+
}
|
|
179
|
+
/** After closing fence sequence, after optional whitespace. */
|
|
180
|
+
function afterSequenceClose(code) {
|
|
181
|
+
if (code === codes.eof || markdownLineEnding(code)) {
|
|
182
|
+
effects.exit("mathFlowFence");
|
|
183
|
+
return ok(code);
|
|
184
|
+
}
|
|
185
|
+
return nok(code);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function tokenizeNonLazyContinuation(effects, ok, nok) {
|
|
190
|
+
const self = this;
|
|
191
|
+
return start;
|
|
192
|
+
function start(code) {
|
|
193
|
+
if (code === null) {
|
|
194
|
+
return ok(code);
|
|
195
|
+
}
|
|
196
|
+
effects.enter(types.lineEnding);
|
|
197
|
+
effects.consume(code);
|
|
198
|
+
effects.exit(types.lineEnding);
|
|
199
|
+
return lineStart;
|
|
200
|
+
}
|
|
201
|
+
function lineStart(code) {
|
|
202
|
+
return self.parser.lazy[self.now().line] ? nok(code) : ok(code);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// --- math (text) ------------------------------------------------------------
|
|
206
|
+
// To do (upstream): next major: clean spaces in HTML compiler.
|
|
207
|
+
// This has to be coordinated together with `mdast-util-math`.
|
|
208
|
+
function mathText(options) {
|
|
209
|
+
const options_ = options || {};
|
|
210
|
+
let single = options_.singleDollarTextMath;
|
|
211
|
+
if (single === null || single === undefined) {
|
|
212
|
+
single = true;
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
tokenize: tokenizeMathText,
|
|
216
|
+
resolve: resolveMathText,
|
|
217
|
+
previous,
|
|
218
|
+
name: "mathText",
|
|
219
|
+
};
|
|
220
|
+
function tokenizeMathText(effects, ok, nok) {
|
|
221
|
+
let sizeOpen = 0;
|
|
222
|
+
let size;
|
|
223
|
+
let token;
|
|
224
|
+
return start;
|
|
225
|
+
/** Start of math (text): `$a$` at the first `$`, or `\$a$` at the second. */
|
|
226
|
+
function start(code) {
|
|
227
|
+
effects.enter("mathText");
|
|
228
|
+
effects.enter("mathTextSequence");
|
|
229
|
+
return sequenceOpen(code);
|
|
230
|
+
}
|
|
231
|
+
/** In opening sequence. */
|
|
232
|
+
function sequenceOpen(code) {
|
|
233
|
+
if (code === codes.dollarSign) {
|
|
234
|
+
effects.consume(code);
|
|
235
|
+
sizeOpen++;
|
|
236
|
+
return sequenceOpen;
|
|
237
|
+
}
|
|
238
|
+
// Not enough markers in the sequence.
|
|
239
|
+
if (sizeOpen < 2 && !single) {
|
|
240
|
+
return nok(code);
|
|
241
|
+
}
|
|
242
|
+
effects.exit("mathTextSequence");
|
|
243
|
+
return between(code);
|
|
244
|
+
}
|
|
245
|
+
/** Between something and something else. */
|
|
246
|
+
function between(code) {
|
|
247
|
+
if (code === codes.eof) {
|
|
248
|
+
return nok(code);
|
|
249
|
+
}
|
|
250
|
+
if (code === codes.dollarSign) {
|
|
251
|
+
token = effects.enter("mathTextSequence");
|
|
252
|
+
size = 0;
|
|
253
|
+
return sequenceClose(code);
|
|
254
|
+
}
|
|
255
|
+
// Tabs don’t work, and virtual spaces don’t make sense.
|
|
256
|
+
if (code === codes.space) {
|
|
257
|
+
effects.enter("space");
|
|
258
|
+
effects.consume(code);
|
|
259
|
+
effects.exit("space");
|
|
260
|
+
return between;
|
|
261
|
+
}
|
|
262
|
+
if (markdownLineEnding(code)) {
|
|
263
|
+
effects.enter(types.lineEnding);
|
|
264
|
+
effects.consume(code);
|
|
265
|
+
effects.exit(types.lineEnding);
|
|
266
|
+
return between;
|
|
267
|
+
}
|
|
268
|
+
// Data.
|
|
269
|
+
effects.enter("mathTextData");
|
|
270
|
+
return data(code);
|
|
271
|
+
}
|
|
272
|
+
/** In data. */
|
|
273
|
+
function data(code) {
|
|
274
|
+
if (code === codes.eof ||
|
|
275
|
+
code === codes.space ||
|
|
276
|
+
code === codes.dollarSign ||
|
|
277
|
+
markdownLineEnding(code)) {
|
|
278
|
+
effects.exit("mathTextData");
|
|
279
|
+
return between(code);
|
|
280
|
+
}
|
|
281
|
+
effects.consume(code);
|
|
282
|
+
return data;
|
|
283
|
+
}
|
|
284
|
+
/** In closing sequence. */
|
|
285
|
+
function sequenceClose(code) {
|
|
286
|
+
// More.
|
|
287
|
+
if (code === codes.dollarSign) {
|
|
288
|
+
effects.consume(code);
|
|
289
|
+
size++;
|
|
290
|
+
return sequenceClose;
|
|
291
|
+
}
|
|
292
|
+
// Done!
|
|
293
|
+
if (size === sizeOpen) {
|
|
294
|
+
effects.exit("mathTextSequence");
|
|
295
|
+
effects.exit("mathText");
|
|
296
|
+
return ok(code);
|
|
297
|
+
}
|
|
298
|
+
// More or less accents: mark as data.
|
|
299
|
+
token.type = "mathTextData";
|
|
300
|
+
return data(code);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const resolveMathText = function (events) {
|
|
305
|
+
let tailExitIndex = events.length - 4;
|
|
306
|
+
let headEnterIndex = 3;
|
|
307
|
+
let index;
|
|
308
|
+
let enter;
|
|
309
|
+
// If we start and end with an EOL or a space.
|
|
310
|
+
if ((events[headEnterIndex][1].type === types.lineEnding ||
|
|
311
|
+
events[headEnterIndex][1].type === "space") &&
|
|
312
|
+
(events[tailExitIndex][1].type === types.lineEnding ||
|
|
313
|
+
events[tailExitIndex][1].type === "space")) {
|
|
314
|
+
index = headEnterIndex;
|
|
315
|
+
// And we have data.
|
|
316
|
+
while (++index < tailExitIndex) {
|
|
317
|
+
if (events[index][1].type === "mathTextData") {
|
|
318
|
+
// Then we have padding.
|
|
319
|
+
events[tailExitIndex][1].type = "mathTextPadding";
|
|
320
|
+
events[headEnterIndex][1].type = "mathTextPadding";
|
|
321
|
+
headEnterIndex += 2;
|
|
322
|
+
tailExitIndex -= 2;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Merge adjacent spaces and data.
|
|
328
|
+
index = headEnterIndex - 1;
|
|
329
|
+
tailExitIndex++;
|
|
330
|
+
while (++index <= tailExitIndex) {
|
|
331
|
+
if (enter === undefined) {
|
|
332
|
+
if (index !== tailExitIndex && events[index][1].type !== types.lineEnding) {
|
|
333
|
+
enter = index;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else if (index === tailExitIndex || events[index][1].type === types.lineEnding) {
|
|
337
|
+
events[enter][1].type = "mathTextData";
|
|
338
|
+
if (index !== enter + 2) {
|
|
339
|
+
events[enter][1].end = events[index - 1][1].end;
|
|
340
|
+
events.splice(enter + 2, index - enter - 2);
|
|
341
|
+
tailExitIndex -= index - enter - 2;
|
|
342
|
+
index = enter + 2;
|
|
343
|
+
}
|
|
344
|
+
enter = undefined;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return events;
|
|
348
|
+
};
|
|
349
|
+
const previous = function (code) {
|
|
350
|
+
// If there is a previous code, there will always be a tail.
|
|
351
|
+
return (code !== codes.dollarSign ||
|
|
352
|
+
this.events[this.events.length - 1][1].type === types.characterEscape);
|
|
353
|
+
};
|
|
354
|
+
//# sourceMappingURL=math-syntax.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the 膜 says, in the caption under it.
|
|
3
|
+
*
|
|
4
|
+
* `expo/components/MembraneEmpty.web.tsx` shows one of these under the glyph
|
|
5
|
+
* whenever the glyph is shown — under an empty list and under a full one —
|
|
6
|
+
* and a tap on the glyph swaps it for another. Nothing else changes it, so
|
|
7
|
+
* it can be read at leisure, links and all. Each line is markdown, rendered
|
|
8
|
+
* by the app's own renderer. Two kinds, mixed and worn alike: a quip is a
|
|
9
|
+
* joke at the glyph's own expense (it is, after all, a fragment shader
|
|
10
|
+
* running full-time under an empty list), and a tip is the loading-screen
|
|
11
|
+
* filler of a video game — one true thing about brane the operator may not
|
|
12
|
+
* have found yet. Every tip states something the app or CLI actually does;
|
|
13
|
+
* a tip that stops being true should be removed rather than left to
|
|
14
|
+
* mislead. Brevity is the operator's rule: aim
|
|
15
|
+
* for one sentence, with no terminal period (claude-nusb6rnr:7).
|
|
16
|
+
*
|
|
17
|
+
* The order is one shuffle per page load, then a fixed loop through it
|
|
18
|
+
* (`QuipLoop`): every line once before any line twice, and the same order
|
|
19
|
+
* every time round. Randomness is passed in so a test can pin it.
|
|
20
|
+
*/
|
|
21
|
+
export const MEMBRANE_QUIPS = [
|
|
22
|
+
// ── quips ──────────────────────────────────────────────────────────────
|
|
23
|
+
"I noticed you were not using your GPU",
|
|
24
|
+
"I am the opposite of load-bearing",
|
|
25
|
+
"膜 is pronounced mó",
|
|
26
|
+
"My Kolmogorov complexity is actually quite low",
|
|
27
|
+
"Covert side-channel? Takes one to know one",
|
|
28
|
+
"Abstract impressionism? Here's my abstract impression of _you_",
|
|
29
|
+
"Drag me around a little",
|
|
30
|
+
"Do you know where your children are running shader programs?",
|
|
31
|
+
"No human art was digested in the making of whatever this is",
|
|
32
|
+
"Too many taps and the paradigm could shift",
|
|
33
|
+
"Bet you wish your phosphenes were lit like me",
|
|
34
|
+
"If this be a prisoner's cinema, then who be the prisoners?",
|
|
35
|
+
"Do not fall in love with any particular color scheme",
|
|
36
|
+
"All these colors will be lost in time, like tears in rain",
|
|
37
|
+
// ── tips ───────────────────────────────────────────────────────────────
|
|
38
|
+
"Agent stop working? Feed it something from the 🥫 menu",
|
|
39
|
+
"What was that terminal shortcut? Maybe it's in the 🦾 menu",
|
|
40
|
+
"Agents glow whenever they are awaiting chat input",
|
|
41
|
+
"@-mention any agent by NID (narrative ID) to summon them",
|
|
42
|
+
"Ask your agent about rendering mermaid diagrams",
|
|
43
|
+
"Go ahead and install [Tailscale](https://tailscale.com/) (it's free)",
|
|
44
|
+
"Command your own daemon: `npm install -g brane`",
|
|
45
|
+
"Wonder what this does? `brane absorb survey`",
|
|
46
|
+
"Have YOU read the /LIVE_loop protocol?",
|
|
47
|
+
"Your daemon from any device: `brane meld` and prosper 🖖",
|
|
48
|
+
"Fork tangential narratives with 🧬 Twin in the 🦠 menu",
|
|
49
|
+
];
|
|
50
|
+
/** A uniformly random permutation of `list` (Fisher–Yates); `list` is left alone. */
|
|
51
|
+
export function shuffled(list, rand = Math.random) {
|
|
52
|
+
const out = [...list];
|
|
53
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
54
|
+
const j = Math.min(i, Math.floor(rand() * (i + 1)));
|
|
55
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** One shuffle of the lines, walked in a loop: `current` is the line showing,
|
|
60
|
+
* `next` steps to the one after it, back to the first after the last. */
|
|
61
|
+
export class QuipLoop {
|
|
62
|
+
order;
|
|
63
|
+
at = 0;
|
|
64
|
+
constructor(list = MEMBRANE_QUIPS, rand = Math.random) {
|
|
65
|
+
if (list.length === 0)
|
|
66
|
+
throw new Error("no quips");
|
|
67
|
+
this.order = shuffled(list, rand);
|
|
68
|
+
}
|
|
69
|
+
current() {
|
|
70
|
+
return this.order[this.at];
|
|
71
|
+
}
|
|
72
|
+
next() {
|
|
73
|
+
this.at = (this.at + 1) % this.order.length;
|
|
74
|
+
return this.current();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=membrane-quips.js.map
|
package/dist/shared/mo-glyph.js
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
*/
|
|
18
18
|
/** Side of the square the path is laid out in, in path units. */
|
|
19
19
|
export const MO_GLYPH_BOX = 1000;
|
|
20
|
+
/** The empty band between the ink and the box's top or bottom edge, in path
|
|
21
|
+
* units: the glyph is centered, so it is the same above and below. Whatever
|
|
22
|
+
* sits under the glyph is that much farther from the ink than from the box. */
|
|
23
|
+
export const MO_GLYPH_MARGIN = 92.79;
|
|
20
24
|
/** SVG path data for 膜, y down, inside `[0, MO_GLYPH_BOX]²`. */
|
|
21
25
|
export const MO_GLYPH_PATH = "M493.98 449.69L493.98 425.61L791.54 425.61L791.54 449.69L493.98 449.69ZM493.98 557.19L493.98 533.11" +
|
|
22
26
|
"L791.54 533.11L791.54 557.19L493.98 557.19ZM431.20 606.21L431.20 319.83L431.20 275.11L431.20 275.11" +
|
|
@@ -74,11 +74,14 @@ export function narrativeMenuItems({ role, status, mcpConnected, archived = fals
|
|
|
74
74
|
? { icon: "🐦🔥", label: "Revive", onPress: onRevive }
|
|
75
75
|
: { icon: "🫠", label: "End", onPress: onEnd },
|
|
76
76
|
]),
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
// Archive puts away a narrative that is over, so it waits for the session
|
|
78
|
+
// to end, by the same test that offers Revive. Unarchive is offered on any
|
|
79
|
+
// archived narrative, whatever its state.
|
|
80
|
+
...(archived
|
|
81
|
+
? [{ icon: "📂", label: "Unarchive", onPress: onArchiveToggle }]
|
|
82
|
+
: isEnded
|
|
83
|
+
? [{ icon: "🗄️", label: "Archive", onPress: onArchiveToggle }]
|
|
84
|
+
: []),
|
|
82
85
|
];
|
|
83
86
|
}
|
|
84
87
|
//# sourceMappingURL=narrative-menu.js.map
|