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 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 has no provenance attestation are in
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`). This is the first release published
38
- from the public repository, so it carries npm provenance; 0.3.0 has
39
- none ([#597](https://github.com/benjamn/brane/pull/597)).
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
- A TypeScript CLI and userspace daemon for managing narrative agent sessions.
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
  [![Sponsor](https://img.shields.io/badge/Sponsor-%E2%9D%A4-ea4aaa?logo=githubsponsors)](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
@@ -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, unless its session is still running (archiving is not a
1387
- // way to cut off a live agent; revoking from the Devices list is).
1388
- // Unarchiving brings nothing back; a revive mints a fresh token.
1389
- if (archived && !liveTmuxSessions().has(narrativeId)) {
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
- // A connected external's bridge goes now, not at its next redial.
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 "micromark-extension-math";
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 "micromark-extension-math";
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
@@ -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
- icon: archived ? "📂" : "🗄️",
79
- label: archived ? "Unarchive" : "Archive",
80
- onPress: onArchiveToggle,
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