clankerbend 0.1.0 → 0.1.2
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/DEVELOPERS.md +6 -6
- package/README.md +31 -14
- package/apps/README.md +3 -3
- package/apps/sticky-notes/README.md +2 -2
- package/apps/sticky-notes/{onewhack.manifest.json → clankerbend.manifest.json} +1 -1
- package/apps/sticky-notes/package.json +3 -3
- package/apps/sticky-notes/public/app.js +42 -9
- package/apps/sticky-notes/src/sticky-notes-app.js +17 -21
- package/apps/vim-nav/README.md +22 -22
- package/apps/vim-nav/{onewhack.manifest.json → clankerbend.manifest.json} +4 -4
- package/apps/vim-nav/package.json +3 -3
- package/apps/vim-nav/public/app.js +44 -11
- package/apps/vim-nav/public/index.html +2 -2
- package/apps/vim-nav/server.mjs +2 -2
- package/apps/vim-nav/src/vim-nav-app.js +5 -5
- package/assets/clankerbend-banner.webp +0 -0
- package/assets/clankerbend-demo-thumbnail.webp +0 -0
- package/assets/clankerbend.svg +26 -0
- package/cli.mjs +22 -11
- package/docs/app-lifecycle.md +9 -9
- package/docs/app-manifest.md +4 -4
- package/docs/author-guide.md +5 -5
- package/docs/launcher-profiles.md +45 -10
- package/docs/protocol.md +248 -241
- package/host/README.md +4 -4
- package/host/src/app-registry.js +8 -6
- package/host/src/codex-desktop-cdp-adapter.js +85 -65
- package/host/src/codex-desktop-renderer-bridge.js +839 -270
- package/host/src/index.js +186 -34
- package/launch/accounts.mjs +360 -0
- package/launch/codex-mux-adapter.mjs +175 -0
- package/launch/profiles.mjs +40 -16
- package/launch/runtime-paths.mjs +9 -6
- package/launch/test-accounts.mjs +71 -0
- package/package.json +12 -10
- package/scripts/release-npm.mjs +1 -2
- package/server.mjs +21 -14
- package/assets/onewhack.jpg +0 -0
package/DEVELOPERS.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ClankerBend Developers
|
|
2
2
|
|
|
3
3
|
## Local Commands
|
|
4
4
|
|
|
5
5
|
For protocol-only local testing without Codex Desktop:
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npx clankerbend
|
|
8
|
+
npx clankerbend --mock
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
For local development:
|
|
@@ -30,18 +30,18 @@ context visibility.
|
|
|
30
30
|
|
|
31
31
|
## Useful Docs
|
|
32
32
|
|
|
33
|
-
- `docs/protocol.md`:
|
|
33
|
+
- `docs/protocol.md`: ClankerBend protocol
|
|
34
34
|
- `docs/app-manifest.md`: app manifest format
|
|
35
|
-
- `docs/author-guide.md`: writing
|
|
35
|
+
- `docs/author-guide.md`: writing ClankerBend apps
|
|
36
36
|
- `docs/launcher-profiles.md`: launcher profiles
|
|
37
37
|
- `docs/app-lifecycle.md`: app installation and lifecycle model
|
|
38
38
|
|
|
39
39
|
## Release
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
ClankerBend publishes the same package payload under two npm names:
|
|
42
42
|
|
|
43
43
|
- `@onewillai/clankerbend`: scoped canonical package
|
|
44
|
-
- `clankerbend`: short launcher name for `npx clankerbend
|
|
44
|
+
- `clankerbend`: short launcher name for `npx clankerbend`
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
47
|
npm test
|
package/README.md
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ClankerBend
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src="./assets/
|
|
4
|
+
<img src="./assets/clankerbend-banner.webp" alt="ClankerBend: mod your clankers with OneWill.ai" width="720">
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
ClankerBend lets you build Codex Desktop plugins without rebuilding or re-signing
|
|
8
8
|
Codex Desktop itself. It uses [CDP](https://x.com/OpenAIDevs/status/2065226355495895521)
|
|
9
9
|
to provide a more stable API for tasks such as transcript navigation, chat annotation,
|
|
10
10
|
attaching a file to the prompt window, opening the side panel, and so on.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.youtube.com/watch?v=FcQUxiL6enc">
|
|
14
|
+
<img src="./assets/clankerbend-demo-thumbnail.webp" alt="Watch the ClankerBend demo video" width="720">
|
|
15
|
+
</a>
|
|
16
|
+
</p>
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
ClankerBend is an independent [OneWill](https://onewill.ai) project compatible
|
|
15
19
|
with OpenAI Codex Desktop on macOS. It is not affiliated with or endorsed by
|
|
16
20
|
OpenAI (unless... 🥺).
|
|
17
21
|
|
|
@@ -21,11 +25,12 @@ You'll need macOS with [Codex Desktop](https://chatgpt.com/codex/)
|
|
|
21
25
|
and [nodejs](https://nodejs.org/en/download). Then just:
|
|
22
26
|
|
|
23
27
|
```sh
|
|
24
|
-
npx clankerbend
|
|
28
|
+
npx clankerbend
|
|
25
29
|
```
|
|
26
30
|
|
|
27
|
-
That starts Codex Desktop with the default
|
|
31
|
+
That starts Codex Desktop with the default ClankerBend profile and bundled apps:
|
|
28
32
|
|
|
33
|
+
- **ClankerID**: switch between Codex accounts.
|
|
29
34
|
- **VimNav**: Vim-style transcript navigation, number rails, role jumps, search,
|
|
30
35
|
and a side panel.
|
|
31
36
|
- **Sticky Notes**: select transcript text, click **Add note**, and the
|
|
@@ -35,15 +40,27 @@ That starts Codex Desktop with the default OneWhack profile and bundled apps:
|
|
|
35
40
|
|
|
36
41
|
## Runtime State
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
ClankerBend writes runtime state outside the package:
|
|
44
|
+
|
|
45
|
+
- macOS: `~/Library/Application Support/OneWill/ClankerBend`
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
Override with `ONEWILL_CLANKERBEND_STATE_DIR`.
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
Managed Codex account profiles live under the same state directory. The primary
|
|
50
|
+
profile remains your normal `CODEX_HOME` or `~/.codex`; managed profiles get
|
|
51
|
+
their own `codex-home` and Electron user-data directories. You can start a
|
|
52
|
+
saved profile directly with:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npx clankerbend --account work
|
|
56
|
+
```
|
|
43
57
|
|
|
44
58
|
## Security
|
|
45
59
|
|
|
46
|
-
|
|
47
|
-
prints the exact host URL, for example
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
ClankerBend binds to `127.0.0.1` on an OS-assigned ephemeral port. The launcher
|
|
61
|
+
prints the exact host URL and bearer token, for example
|
|
62
|
+
`Host: http://127.0.0.1:49152` and `Token: ...`.
|
|
63
|
+
`/clankerbend/*` endpoints require a bearer token by default. For local protocol
|
|
64
|
+
debugging, you can set `ONEWILL_CLANKERBEND_DISABLE_AUTH=1`. Hosted app pages
|
|
65
|
+
receive the same bearer token from their app URL fragment and from a
|
|
66
|
+
host-injected HTML bootstrap so URL rewriting does not break app auth.
|
package/apps/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ClankerBend Apps
|
|
2
2
|
|
|
3
|
-
This directory contains public
|
|
3
|
+
This directory contains public ClankerBend reference apps.
|
|
4
4
|
|
|
5
5
|
- `vim-nav/`: Vim-like transcript navigation for Codex Desktop transcript
|
|
6
6
|
anchors.
|
|
7
7
|
- `sticky-notes/`: selected transcript notes and composer context chips through
|
|
8
|
-
the shared
|
|
8
|
+
the shared ClankerBend host API.
|
|
9
9
|
|
|
10
10
|
The public `apps/` tree should contain standalone, auditable apps without
|
|
11
11
|
project-specific state or action vocabulary.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Sticky Notes
|
|
2
2
|
|
|
3
|
-
Sticky Notes is a public
|
|
3
|
+
Sticky Notes is a public ClankerBend reference app for selected transcript text,
|
|
4
4
|
anchored note overlays, composer context chips, and follow-up draft insertion.
|
|
5
5
|
|
|
6
6
|
The app does not inspect or mutate Codex Desktop DOM. It contributes selection
|
|
7
|
-
actions and handles note business logic through the
|
|
7
|
+
actions and handles note business logic through the ClankerBend host API.
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npm --prefix apps/sticky-notes test
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "clankerbend-sticky-notes",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "ClankerBend public reference app for transcript range notes and composer context.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "node test-
|
|
8
|
+
"test": "node test-clankerbend.mjs",
|
|
9
9
|
"test:desktop-real": "node test-desktop-real.mjs"
|
|
10
10
|
},
|
|
11
11
|
"engines": {
|
|
@@ -18,7 +18,7 @@ els.insertContext.addEventListener("click", () => runAction("sticky.compose.inse
|
|
|
18
18
|
bootstrap();
|
|
19
19
|
|
|
20
20
|
async function bootstrap() {
|
|
21
|
-
const state = await getJson("/
|
|
21
|
+
const state = await getJson("/clankerbend/state");
|
|
22
22
|
render(state);
|
|
23
23
|
connectEvents();
|
|
24
24
|
}
|
|
@@ -49,19 +49,52 @@ async function runAction(type, payload = {}) {
|
|
|
49
49
|
payload,
|
|
50
50
|
requestedAt: new Date().toISOString()
|
|
51
51
|
};
|
|
52
|
-
const result = await postJson(`/
|
|
53
|
-
const state = await getJson("/
|
|
52
|
+
const result = await postJson(`/clankerbend/apps/${encodeURIComponent(APP_ID)}/actions`, { action });
|
|
53
|
+
const state = await getJson("/clankerbend/state");
|
|
54
54
|
render(state);
|
|
55
55
|
return result;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function readToken() {
|
|
59
59
|
const params = new URLSearchParams(location.hash.startsWith("#") ? location.hash.slice(1) : location.hash);
|
|
60
|
-
const
|
|
61
|
-
|
|
60
|
+
const fragmentToken = params.get("clankerbend_token") || "";
|
|
61
|
+
const bootstrapToken = window.__CLANKERBEND_TOKEN || "";
|
|
62
|
+
const metaToken = document.querySelector("meta[name='clankerbend-token']")?.content || "";
|
|
63
|
+
const inlineToken = readInlineBootstrapToken();
|
|
64
|
+
const cachedToken = readCachedToken();
|
|
65
|
+
const value = fragmentToken || bootstrapToken || metaToken || inlineToken || cachedToken;
|
|
66
|
+
if (fragmentToken) history.replaceState(null, "", location.pathname + location.search);
|
|
67
|
+
if (value) writeCachedToken(value);
|
|
62
68
|
return value;
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
function readInlineBootstrapToken() {
|
|
72
|
+
for (const script of document.scripts) {
|
|
73
|
+
const match = script.textContent?.match(/window\.__CLANKERBEND_TOKEN=("[^"]*");?/);
|
|
74
|
+
if (!match) continue;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(match[1]);
|
|
77
|
+
} catch {
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readCachedToken() {
|
|
85
|
+
try {
|
|
86
|
+
return sessionStorage.getItem("clankerbend_token") || "";
|
|
87
|
+
} catch {
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function writeCachedToken(value) {
|
|
93
|
+
try {
|
|
94
|
+
sessionStorage.setItem("clankerbend_token", value);
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
|
|
65
98
|
function headers(extra = {}) {
|
|
66
99
|
return {
|
|
67
100
|
...extra,
|
|
@@ -71,12 +104,12 @@ function headers(extra = {}) {
|
|
|
71
104
|
|
|
72
105
|
function connectEvents() {
|
|
73
106
|
if (!token) {
|
|
74
|
-
const events = new EventSource("/
|
|
107
|
+
const events = new EventSource("/clankerbend/events");
|
|
75
108
|
events.addEventListener("state", (event) => render(JSON.parse(event.data)));
|
|
76
|
-
events.addEventListener("action", () => getJson("/
|
|
109
|
+
events.addEventListener("action", () => getJson("/clankerbend/state").then(render));
|
|
77
110
|
return;
|
|
78
111
|
}
|
|
79
|
-
fetch("/
|
|
112
|
+
fetch("/clankerbend/events", { headers: headers() }).then(async (res) => {
|
|
80
113
|
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
81
114
|
let buffer = "";
|
|
82
115
|
while (true) {
|
|
@@ -90,7 +123,7 @@ function connectEvents() {
|
|
|
90
123
|
const event = chunk.split(/\n/).find((line) => line.startsWith("event: "))?.slice(7);
|
|
91
124
|
const data = chunk.split(/\n/).filter((line) => line.startsWith("data: ")).map((line) => line.slice(6)).join("\n");
|
|
92
125
|
if (event === "state" && data) render(JSON.parse(data));
|
|
93
|
-
if (event === "action") getJson("/
|
|
126
|
+
if (event === "action") getJson("/clankerbend/state").then(render);
|
|
94
127
|
}
|
|
95
128
|
}
|
|
96
129
|
});
|
|
@@ -35,7 +35,7 @@ export function createStickyNotesApp(options = {}) {
|
|
|
35
35
|
|
|
36
36
|
getManifest(context) {
|
|
37
37
|
return {
|
|
38
|
-
|
|
38
|
+
clankerbendVersion: context.protocolVersion,
|
|
39
39
|
appId: STICKY_NOTES_APP_ID,
|
|
40
40
|
name: "Sticky Notes",
|
|
41
41
|
entry: context.entry,
|
|
@@ -170,11 +170,17 @@ function openNoteOverlay(action, context, selection) {
|
|
|
170
170
|
label: "Add note",
|
|
171
171
|
value: action.payload?.body || ""
|
|
172
172
|
}],
|
|
173
|
-
actions: [
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
173
|
+
actions: [
|
|
174
|
+
{
|
|
175
|
+
label: "Cancel",
|
|
176
|
+
type: "overlay.close"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
label: "Save",
|
|
180
|
+
type: "sticky.note.create",
|
|
181
|
+
payload: { selection }
|
|
182
|
+
}
|
|
183
|
+
]
|
|
178
184
|
}, { broadcast: false }).overlay;
|
|
179
185
|
return applied(action, { overlay });
|
|
180
186
|
}
|
|
@@ -232,27 +238,17 @@ function attachNoteFile(notes, noteId, context) {
|
|
|
232
238
|
|
|
233
239
|
function noteMarkdown(note) {
|
|
234
240
|
return [
|
|
235
|
-
"#
|
|
236
|
-
"",
|
|
237
|
-
`Note ID: ${note.noteId}`,
|
|
238
|
-
`Status: ${note.status}`,
|
|
239
|
-
`Anchor: ${note.anchorId}`,
|
|
240
|
-
`Created: ${note.createdAt}`,
|
|
241
|
-
`Updated: ${note.updatedAt}`,
|
|
241
|
+
"# Note",
|
|
242
242
|
"",
|
|
243
|
-
"
|
|
243
|
+
"The user made a note on the transcript above.",
|
|
244
244
|
"",
|
|
245
|
-
|
|
246
|
-
"",
|
|
247
|
-
"## Highlighted Text",
|
|
245
|
+
"## Highlighted text",
|
|
248
246
|
"",
|
|
249
247
|
blockquote(note.quote),
|
|
250
248
|
"",
|
|
251
|
-
"##
|
|
249
|
+
"## User notes",
|
|
252
250
|
"",
|
|
253
|
-
|
|
254
|
-
JSON.stringify(note.range || null, null, 2),
|
|
255
|
-
"```",
|
|
251
|
+
note.body,
|
|
256
252
|
""
|
|
257
253
|
].join("\n");
|
|
258
254
|
}
|
package/apps/vim-nav/README.md
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ClankerBend VimNav
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
ClankerBend is an independent OneWill project compatible with OpenAI Codex
|
|
4
4
|
Desktop. It is not affiliated with or endorsed by OpenAI.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
how an app registers with a central
|
|
6
|
+
VimNav is the public reference app for ClankerBend `0.1`. It demonstrates
|
|
7
|
+
how an app registers with a central ClankerBend host and uses transcript anchors,
|
|
8
8
|
annotations, selection, and app-scoped actions without owning CDP directly.
|
|
9
9
|
|
|
10
10
|
## Run
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
cd
|
|
13
|
+
cd clankerbend
|
|
14
14
|
npm start
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
The command starts a loopback
|
|
17
|
+
The command starts a loopback ClankerBend host on an ephemeral `127.0.0.1` port,
|
|
18
18
|
launches Codex Desktop with CDP, injects transcript markers, and serves the Vim
|
|
19
|
-
|
|
19
|
+
VimNav panel at `/apps/onewill.vim-nav/`.
|
|
20
20
|
|
|
21
21
|
For protocol-only mock mode:
|
|
22
22
|
|
|
@@ -41,7 +41,7 @@ errors, HTTP status/envelope alignment, and bearer-token behavior.
|
|
|
41
41
|
Codex Desktop routes normal typing into the prompt composer, so enter transcript
|
|
42
42
|
navigation mode first:
|
|
43
43
|
|
|
44
|
-
- `
|
|
44
|
+
- `Cmd+Option`: toggle Vim transcript mode
|
|
45
45
|
- `Escape` or `i`: exit Vim transcript mode
|
|
46
46
|
|
|
47
47
|
While Vim transcript mode is active, plain Vim keys are captured for transcript
|
|
@@ -58,12 +58,12 @@ active.
|
|
|
58
58
|
|
|
59
59
|
The focus-safe chords remain available as a fallback:
|
|
60
60
|
|
|
61
|
-
- `
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
65
|
-
- `
|
|
66
|
-
- `
|
|
61
|
+
- `Cmd+Option+j` or `Cmd+Option+Down`: next transcript anchor
|
|
62
|
+
- `Cmd+Option+k` or `Cmd+Option+Up`: previous transcript anchor
|
|
63
|
+
- `Cmd+Option+g` or `Cmd+Option+Home`: first transcript anchor
|
|
64
|
+
- `Cmd+Option+G` or `Cmd+Option+End`: last transcript anchor
|
|
65
|
+
- `Cmd+Option+{` or `Cmd+Option+PageUp`: previous user message
|
|
66
|
+
- `Cmd+Option+}` or `Cmd+Option+PageDown`: next user message
|
|
67
67
|
|
|
68
68
|
Panel commands support:
|
|
69
69
|
|
|
@@ -72,9 +72,9 @@ Panel commands support:
|
|
|
72
72
|
- `:42` or `42`: jump by visible ruler number
|
|
73
73
|
- `:latest assistant` / `:latest user`: jump by role
|
|
74
74
|
|
|
75
|
-
These commands are implemented as
|
|
75
|
+
These commands are implemented as ClankerBend app actions, not only as panel-local
|
|
76
76
|
keyboard parsing. External clients can call the same actions through
|
|
77
|
-
`POST /
|
|
77
|
+
`POST /clankerbend/apps/onewill.vim-nav/actions`.
|
|
78
78
|
|
|
79
79
|
## Writing Another App
|
|
80
80
|
|
|
@@ -90,9 +90,9 @@ Create an object with:
|
|
|
90
90
|
Then register it with one host:
|
|
91
91
|
|
|
92
92
|
```js
|
|
93
|
-
import {
|
|
93
|
+
import { ClankerBendHost, createMockTranscriptAdapter } from "../../host/src/index.js";
|
|
94
94
|
|
|
95
|
-
const host = new
|
|
95
|
+
const host = new ClankerBendHost({
|
|
96
96
|
transcriptAdapter: createMockTranscriptAdapter({ defaultAppId: "example.app" })
|
|
97
97
|
});
|
|
98
98
|
|
|
@@ -101,7 +101,7 @@ await host.start();
|
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
The host serves all mounted apps through one central server and routes commands
|
|
104
|
-
through `/
|
|
104
|
+
through `/clankerbend/apps/:appId/actions`.
|
|
105
105
|
|
|
106
106
|
## Real Desktop Validation
|
|
107
107
|
|
|
@@ -109,15 +109,15 @@ The default `npm test` path is mock/protocol-only and does not launch Codex
|
|
|
109
109
|
Desktop. To run the real Desktop regression harness:
|
|
110
110
|
|
|
111
111
|
```sh
|
|
112
|
-
cd
|
|
112
|
+
cd clankerbend
|
|
113
113
|
npm run test:vim-nav:desktop-real
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
The harness starts the
|
|
116
|
+
The harness starts the VimNav host, launches Codex Desktop with CDP,
|
|
117
117
|
creates a fresh chat, seeds deterministic transcript turns, injects the host
|
|
118
118
|
renderer bridge, and verifies:
|
|
119
119
|
|
|
120
|
-
- `
|
|
120
|
+
- `Cmd+Option` toggles Vim transcript mode without moving scroll
|
|
121
121
|
- `G` lands on the last known transcript item
|
|
122
122
|
- repeated `k` walks to transcript item `1` without skipping or renumbering
|
|
123
123
|
- `12G`, `gg`, and oversized `number+G` land on the expected anchors
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"clankerbendVersion": "0.1",
|
|
3
3
|
"appId": "onewill.vim-nav",
|
|
4
4
|
"version": "0.1.0",
|
|
5
|
-
"name": "
|
|
5
|
+
"name": "VimNav",
|
|
6
6
|
"description": "Public reference app for keyboard transcript navigation in Codex Desktop.",
|
|
7
7
|
"distribution": {
|
|
8
8
|
"kind": "local",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"entrypoint": {
|
|
16
16
|
"kind": "module",
|
|
17
17
|
"module": "./src/vim-nav-app.js",
|
|
18
|
-
"factory": "
|
|
18
|
+
"factory": "createVimNavApp",
|
|
19
19
|
"publicDir": "./public"
|
|
20
20
|
},
|
|
21
21
|
"platform": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"appServerRollback": false
|
|
46
46
|
},
|
|
47
47
|
"panel": {
|
|
48
|
-
"title": "
|
|
48
|
+
"title": "VimNav",
|
|
49
49
|
"reloadPolicy": "preserve",
|
|
50
50
|
"preferredWidth": 360
|
|
51
51
|
},
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "clankerbend-vim-nav",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Public
|
|
5
|
+
"description": "Public ClankerBend reference app for Vim-like Codex Desktop transcript navigation.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"start": "node server.mjs",
|
|
9
9
|
"start:mock": "node server.mjs --mock",
|
|
10
|
-
"test": "node test-
|
|
10
|
+
"test": "node test-clankerbend.mjs",
|
|
11
11
|
"test:desktop-real": "node test-desktop-real.mjs"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
@@ -20,7 +20,7 @@ const token = readToken();
|
|
|
20
20
|
|
|
21
21
|
els.openPanel.addEventListener("click", async () => {
|
|
22
22
|
try {
|
|
23
|
-
await postJson("/
|
|
23
|
+
await postJson("/clankerbend/panel/open", {});
|
|
24
24
|
} catch (err) {
|
|
25
25
|
els.commandStatus.textContent = err.message;
|
|
26
26
|
}
|
|
@@ -36,7 +36,7 @@ bootstrap();
|
|
|
36
36
|
|
|
37
37
|
async function bootstrap() {
|
|
38
38
|
try {
|
|
39
|
-
const state = await getJson("/
|
|
39
|
+
const state = await getJson("/clankerbend/state");
|
|
40
40
|
render(state, true);
|
|
41
41
|
connectEvents();
|
|
42
42
|
} catch (err) {
|
|
@@ -47,11 +47,44 @@ async function bootstrap() {
|
|
|
47
47
|
|
|
48
48
|
function readToken() {
|
|
49
49
|
const params = new URLSearchParams(location.hash.startsWith("#") ? location.hash.slice(1) : location.hash);
|
|
50
|
-
const
|
|
51
|
-
|
|
50
|
+
const fragmentToken = params.get("clankerbend_token") || "";
|
|
51
|
+
const bootstrapToken = window.__CLANKERBEND_TOKEN || "";
|
|
52
|
+
const metaToken = document.querySelector("meta[name='clankerbend-token']")?.content || "";
|
|
53
|
+
const inlineToken = readInlineBootstrapToken();
|
|
54
|
+
const cachedToken = readCachedToken();
|
|
55
|
+
const value = fragmentToken || bootstrapToken || metaToken || inlineToken || cachedToken;
|
|
56
|
+
if (fragmentToken) history.replaceState(null, "", location.pathname + location.search);
|
|
57
|
+
if (value) writeCachedToken(value);
|
|
52
58
|
return value;
|
|
53
59
|
}
|
|
54
60
|
|
|
61
|
+
function readInlineBootstrapToken() {
|
|
62
|
+
for (const script of document.scripts) {
|
|
63
|
+
const match = script.textContent?.match(/window\.__CLANKERBEND_TOKEN=("[^"]*");?/);
|
|
64
|
+
if (!match) continue;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(match[1]);
|
|
67
|
+
} catch {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readCachedToken() {
|
|
75
|
+
try {
|
|
76
|
+
return sessionStorage.getItem("clankerbend_token") || "";
|
|
77
|
+
} catch {
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeCachedToken(value) {
|
|
83
|
+
try {
|
|
84
|
+
sessionStorage.setItem("clankerbend_token", value);
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
|
|
55
88
|
function headers(extra = {}) {
|
|
56
89
|
return {
|
|
57
90
|
...extra,
|
|
@@ -61,17 +94,17 @@ function headers(extra = {}) {
|
|
|
61
94
|
|
|
62
95
|
async function connectEvents() {
|
|
63
96
|
if (!token) {
|
|
64
|
-
const events = new EventSource("/
|
|
97
|
+
const events = new EventSource("/clankerbend/events");
|
|
65
98
|
events.addEventListener("state", (event) => render(JSON.parse(event.data)));
|
|
66
|
-
events.addEventListener("app-state", () => getJson("/
|
|
67
|
-
events.addEventListener("action", () => getJson("/
|
|
99
|
+
events.addEventListener("app-state", () => getJson("/clankerbend/state").then((state) => render(state)));
|
|
100
|
+
events.addEventListener("action", () => getJson("/clankerbend/state").then((state) => render(state)));
|
|
68
101
|
events.addEventListener("error", () => {
|
|
69
102
|
els.subtitle.textContent = "event stream interrupted";
|
|
70
103
|
});
|
|
71
104
|
return;
|
|
72
105
|
}
|
|
73
106
|
|
|
74
|
-
const res = await fetch("/
|
|
107
|
+
const res = await fetch("/clankerbend/events", { headers: headers() });
|
|
75
108
|
if (!res.ok || !res.body) throw new Error(`events failed: ${res.status}`);
|
|
76
109
|
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
77
110
|
let buffer = "";
|
|
@@ -94,7 +127,7 @@ function handleSseChunk(chunk) {
|
|
|
94
127
|
const data = lines.filter((line) => line.startsWith("data: ")).map((line) => line.slice(6)).join("\n");
|
|
95
128
|
if (!data || event === "heartbeat") return;
|
|
96
129
|
if (event === "state") render(JSON.parse(data));
|
|
97
|
-
if (event === "app-state" || event === "action") getJson("/
|
|
130
|
+
if (event === "app-state" || event === "action") getJson("/clankerbend/state").then((state) => render(state));
|
|
98
131
|
}
|
|
99
132
|
|
|
100
133
|
async function runCommand(raw) {
|
|
@@ -193,8 +226,8 @@ async function runAction(type, payload = {}, anchorId) {
|
|
|
193
226
|
payload,
|
|
194
227
|
requestedAt: new Date().toISOString()
|
|
195
228
|
};
|
|
196
|
-
const result = await postJson(`/
|
|
197
|
-
if (result.ok) getJson("/
|
|
229
|
+
const result = await postJson(`/clankerbend/apps/${encodeURIComponent(APP_ID)}/actions`, { action });
|
|
230
|
+
if (result.ok) getJson("/clankerbend/state").then((state) => render(state));
|
|
198
231
|
return result.ok ? { ok: true, message: result.status || "ok" } : { ok: false, error: result.error || "action failed" };
|
|
199
232
|
}
|
|
200
233
|
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>
|
|
6
|
+
<title>VimNav</title>
|
|
7
7
|
<link rel="stylesheet" href="./styles.css">
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<main class="shell">
|
|
11
11
|
<header class="topbar">
|
|
12
12
|
<div>
|
|
13
|
-
<strong>
|
|
13
|
+
<strong>VimNav</strong>
|
|
14
14
|
<p id="subtitle">Transcript keyboard navigation</p>
|
|
15
15
|
</div>
|
|
16
16
|
<button id="open-panel" type="button">Panel</button>
|
package/apps/vim-nav/server.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import {
|
|
3
|
+
import { launchClankerBendCodex } from "../../server.mjs";
|
|
4
4
|
|
|
5
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
|
|
7
|
-
await
|
|
7
|
+
await launchClankerBendCodex({
|
|
8
8
|
mock: process.argv.includes("--mock"),
|
|
9
9
|
runDir: join(__dirname, "run")
|
|
10
10
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const VIM_NAV_APP_ID = "onewill.vim-nav";
|
|
2
2
|
|
|
3
|
-
export function
|
|
3
|
+
export function createVimNavApp(options = {}) {
|
|
4
4
|
return {
|
|
5
5
|
appId: VIM_NAV_APP_ID,
|
|
6
|
-
name: "
|
|
6
|
+
name: "VimNav",
|
|
7
7
|
publicDir: options.publicDir,
|
|
8
8
|
contributes: {
|
|
9
9
|
panel: true,
|
|
@@ -21,16 +21,16 @@ export function createVimNavigatorApp(options = {}) {
|
|
|
21
21
|
appServerRollback: false
|
|
22
22
|
},
|
|
23
23
|
panel: {
|
|
24
|
-
title: "
|
|
24
|
+
title: "VimNav",
|
|
25
25
|
reloadPolicy: "preserve",
|
|
26
26
|
preferredWidth: 360
|
|
27
27
|
},
|
|
28
28
|
|
|
29
29
|
getManifest(context) {
|
|
30
30
|
return {
|
|
31
|
-
|
|
31
|
+
clankerbendVersion: context.protocolVersion,
|
|
32
32
|
appId: VIM_NAV_APP_ID,
|
|
33
|
-
name: "
|
|
33
|
+
name: "VimNav",
|
|
34
34
|
entry: context.entry,
|
|
35
35
|
contributes: this.contributes,
|
|
36
36
|
permissions: this.permissions,
|
|
Binary file
|
|
Binary file
|