clankerbend 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DEVELOPERS.md ADDED
@@ -0,0 +1,63 @@
1
+ # OneWhack Developers
2
+
3
+ ## Local Commands
4
+
5
+ For protocol-only local testing without Codex Desktop:
6
+
7
+ ```sh
8
+ npx clankerbend codex --mock
9
+ ```
10
+
11
+ For local development:
12
+
13
+ ```sh
14
+ npm start
15
+ npm run start:mock
16
+ npm test
17
+ ```
18
+
19
+ `npm test` runs the fast behavior e2e suite through the host and app APIs. To
20
+ exercise the real Codex Desktop surface:
21
+
22
+ ```sh
23
+ npm run test:desktop-real:integration
24
+ ```
25
+
26
+ The real desktop tests create fresh chats, seed deterministic content, verify
27
+ VimNav against the real transcript DOM, and verify Sticky Notes through native
28
+ toolbar injection, pinned overlay save, runtime file attachment, and prompt
29
+ context visibility.
30
+
31
+ ## Useful Docs
32
+
33
+ - `docs/protocol.md`: OneWhack protocol
34
+ - `docs/app-manifest.md`: app manifest format
35
+ - `docs/author-guide.md`: writing OneWhack apps
36
+ - `docs/launcher-profiles.md`: launcher profiles
37
+ - `docs/app-lifecycle.md`: app installation and lifecycle model
38
+
39
+ ## Release
40
+
41
+ OneWhack publishes the same package payload under two npm names:
42
+
43
+ - `@onewillai/clankerbend`: scoped canonical package
44
+ - `clankerbend`: short launcher name for `npx clankerbend codex`
45
+
46
+ ```sh
47
+ npm test
48
+ npm run test:desktop-real:integration
49
+ npm run release:pack
50
+ npm run release:publish -- --yes
51
+ ```
52
+
53
+ `release:pack` creates:
54
+
55
+ - `dist/npm/clankerbend-<version>.tgz`
56
+ - `dist/npm/onewillai-clankerbend-<version>.tgz`
57
+
58
+ `release:publish` is guarded by `--yes`. It publishes the scoped package first
59
+ with `--access public`, then publishes the unscoped package. The publish steps
60
+ run interactively so npm can prompt for the configured 2FA method. Pass
61
+ `--otp=<code>` only for accounts that use authenticator-app TOTP codes.
62
+ Provenance is enabled automatically in supported CI providers, or explicitly
63
+ with `--provenance`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OneWill.ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # OneWhack
2
+
3
+ <p align="center">
4
+ <img src="./assets/onewhack.jpg" alt="OneWhack: mod your clanker with OneWill.ai" width="720">
5
+ </p>
6
+
7
+ OneWhack lets you build Codex Desktop plugins without rebuilding or re-signing
8
+ Codex Desktop itself. It uses [CDP](https://x.com/OpenAIDevs/status/2065226355495895521)
9
+ to provide a more stable API for tasks such as transcript navigation, chat annotation,
10
+ attaching a file to the prompt window, opening the side panel, and so on.
11
+
12
+ DEMO VIDEO TODO
13
+
14
+ OneWhack is an independent [OneWill](https://onewill.ai) project compatible
15
+ with OpenAI Codex Desktop on macOS. It is not affiliated with or endorsed by
16
+ OpenAI (unless... 🥺).
17
+
18
+ ## Quickstart
19
+
20
+ You'll need macOS with [Codex Desktop](https://chatgpt.com/codex/)
21
+ and [nodejs](https://nodejs.org/en/download). Then just:
22
+
23
+ ```sh
24
+ npx clankerbend codex
25
+ ```
26
+
27
+ That starts Codex Desktop with the default OneWhack profile and bundled apps:
28
+
29
+ - **VimNav**: Vim-style transcript navigation, number rails, role jumps, search,
30
+ and a side panel.
31
+ - **Sticky Notes**: select transcript text, click **Add note**, and the
32
+ generated note will (1) create a markdown file and (2) attach itself to the
33
+ current Codex prompt. Inspired by
34
+ [@zats](https://x.com/zats/status/2070492220084326907).
35
+
36
+ ## Runtime State
37
+
38
+ OneWhack writes runtime state outside the package:
39
+
40
+ - macOS: `~/Library/Application Support/OneWill/OneWhack`
41
+
42
+ Override with `ONEWILL_ONEWHACK_STATE_DIR`.
43
+
44
+ ## Security
45
+
46
+ OneWhack binds to `127.0.0.1` on an OS-assigned ephemeral port. The launcher
47
+ prints the exact host URL, for example `Host: http://127.0.0.1:49152`.
48
+ `/onewhack/*` endpoints require a bearer token by default. For local protocol
49
+ debugging, you can set `ONEWILL_ONEWHACK_DISABLE_AUTH=1`.
package/apps/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # OneWhack Apps
2
+
3
+ This directory contains public OneWhack reference apps.
4
+
5
+ - `vim-nav/`: Vim-like transcript navigation for Codex Desktop transcript
6
+ anchors.
7
+ - `sticky-notes/`: selected transcript notes and composer context chips through
8
+ the shared OneWhack host API.
9
+
10
+ The public `apps/` tree should contain standalone, auditable apps without
11
+ project-specific state or action vocabulary.
@@ -0,0 +1,11 @@
1
+ # Sticky Notes
2
+
3
+ Sticky Notes is a public OneWhack reference app for selected transcript text,
4
+ anchored note overlays, composer context chips, and follow-up draft insertion.
5
+
6
+ The app does not inspect or mutate Codex Desktop DOM. It contributes selection
7
+ actions and handles note business logic through the OneWhack host API.
8
+
9
+ ```sh
10
+ npm --prefix apps/sticky-notes test
11
+ ```
@@ -0,0 +1,74 @@
1
+ {
2
+ "oneWhackVersion": "0.1",
3
+ "appId": "onewill.sticky-notes",
4
+ "version": "0.1.0",
5
+ "name": "OneWill Sticky Notes",
6
+ "description": "Public reference app for selected transcript notes and composer context chips.",
7
+ "distribution": {
8
+ "kind": "local",
9
+ "source": ".",
10
+ "integrity": "dev-local",
11
+ "update": {
12
+ "channel": "local"
13
+ }
14
+ },
15
+ "entrypoint": {
16
+ "kind": "module",
17
+ "module": "./src/sticky-notes-app.js",
18
+ "factory": "createStickyNotesApp",
19
+ "publicDir": "./public"
20
+ },
21
+ "platform": {
22
+ "os": [
23
+ "darwin",
24
+ "linux",
25
+ "win32"
26
+ ],
27
+ "arch": [
28
+ "any"
29
+ ]
30
+ },
31
+ "capabilities": {
32
+ "panel": true,
33
+ "annotations": true,
34
+ "actions": true,
35
+ "appState": true,
36
+ "selectionActions": true,
37
+ "overlays": true,
38
+ "composerContext": true,
39
+ "composerDraft": true,
40
+ "rendererBridge": false
41
+ },
42
+ "permissions": {
43
+ "transcriptRead": true,
44
+ "transcriptAnnotate": true,
45
+ "transcriptNavigate": true,
46
+ "overlayWrite": true,
47
+ "composerWrite": true,
48
+ "appServerRead": false,
49
+ "appServerApprove": false,
50
+ "appServerRollback": false
51
+ },
52
+ "panel": {
53
+ "title": "Sticky Notes",
54
+ "reloadPolicy": "preserve",
55
+ "preferredWidth": 380
56
+ },
57
+ "lifecycle": {
58
+ "install": {
59
+ "kind": "noop"
60
+ },
61
+ "start": {
62
+ "kind": "in-process"
63
+ },
64
+ "stop": {
65
+ "kind": "in-process"
66
+ },
67
+ "update": {
68
+ "kind": "replace-manifest"
69
+ },
70
+ "remove": {
71
+ "kind": "forget"
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "onewhack-sticky-notes",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "OneWhack public reference app for transcript range notes and composer context.",
6
+ "license": "MIT",
7
+ "scripts": {
8
+ "test": "node test-onewhack.mjs",
9
+ "test:desktop-real": "node test-desktop-real.mjs"
10
+ },
11
+ "engines": {
12
+ "node": ">=22"
13
+ }
14
+ }
@@ -0,0 +1,117 @@
1
+ const APP_ID = "onewill.sticky-notes";
2
+
3
+ const els = {
4
+ subtitle: document.getElementById("subtitle"),
5
+ notes: document.getElementById("notes"),
6
+ userText: document.getElementById("user-text"),
7
+ insertContext: document.getElementById("insert-context")
8
+ };
9
+
10
+ const token = readToken();
11
+ let currentState = null;
12
+
13
+ els.insertContext.addEventListener("click", () => runAction("sticky.compose.insertContext", {
14
+ userText: els.userText.value,
15
+ mode: "replace"
16
+ }));
17
+
18
+ bootstrap();
19
+
20
+ async function bootstrap() {
21
+ const state = await getJson("/onewhack/state");
22
+ render(state);
23
+ connectEvents();
24
+ }
25
+
26
+ function render(state) {
27
+ currentState = state;
28
+ const app = (state.apps || []).find((candidate) => candidate.appId === APP_ID);
29
+ const entries = app?.entries || [];
30
+ els.subtitle.textContent = `${entries.length} note${entries.length === 1 ? "" : "s"} queued`;
31
+ els.notes.replaceChildren(...entries.map((entry) => {
32
+ const li = document.createElement("li");
33
+ const title = document.createElement("strong");
34
+ title.textContent = entry.title;
35
+ const summary = document.createElement("div");
36
+ summary.textContent = entry.summary;
37
+ const meta = document.createElement("small");
38
+ meta.textContent = entry.status;
39
+ li.append(title, summary, meta);
40
+ return li;
41
+ }));
42
+ }
43
+
44
+ async function runAction(type, payload = {}) {
45
+ const action = {
46
+ actionId: `${type}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
47
+ appId: APP_ID,
48
+ type,
49
+ payload,
50
+ requestedAt: new Date().toISOString()
51
+ };
52
+ const result = await postJson(`/onewhack/apps/${encodeURIComponent(APP_ID)}/actions`, { action });
53
+ const state = await getJson("/onewhack/state");
54
+ render(state);
55
+ return result;
56
+ }
57
+
58
+ function readToken() {
59
+ const params = new URLSearchParams(location.hash.startsWith("#") ? location.hash.slice(1) : location.hash);
60
+ const value = params.get("onewhack_token") || "";
61
+ if (value) history.replaceState(null, "", location.pathname + location.search);
62
+ return value;
63
+ }
64
+
65
+ function headers(extra = {}) {
66
+ return {
67
+ ...extra,
68
+ ...(token ? { authorization: `Bearer ${token}` } : {})
69
+ };
70
+ }
71
+
72
+ function connectEvents() {
73
+ if (!token) {
74
+ const events = new EventSource("/onewhack/events");
75
+ events.addEventListener("state", (event) => render(JSON.parse(event.data)));
76
+ events.addEventListener("action", () => getJson("/onewhack/state").then(render));
77
+ return;
78
+ }
79
+ fetch("/onewhack/events", { headers: headers() }).then(async (res) => {
80
+ const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
81
+ let buffer = "";
82
+ while (true) {
83
+ const { value, done } = await reader.read();
84
+ if (done) break;
85
+ buffer += value;
86
+ let boundary;
87
+ while ((boundary = buffer.indexOf("\n\n")) >= 0) {
88
+ const chunk = buffer.slice(0, boundary);
89
+ buffer = buffer.slice(boundary + 2);
90
+ const event = chunk.split(/\n/).find((line) => line.startsWith("event: "))?.slice(7);
91
+ const data = chunk.split(/\n/).filter((line) => line.startsWith("data: ")).map((line) => line.slice(6)).join("\n");
92
+ if (event === "state" && data) render(JSON.parse(data));
93
+ if (event === "action") getJson("/onewhack/state").then(render);
94
+ }
95
+ }
96
+ });
97
+ }
98
+
99
+ async function getJson(url) {
100
+ const res = await fetch(url, { headers: headers() });
101
+ return unwrap(res);
102
+ }
103
+
104
+ async function postJson(url, body) {
105
+ const res = await fetch(url, {
106
+ method: "POST",
107
+ headers: headers({ "content-type": "application/json" }),
108
+ body: JSON.stringify(body)
109
+ });
110
+ return unwrap(res);
111
+ }
112
+
113
+ async function unwrap(res) {
114
+ const json = await res.json();
115
+ if (!json.ok) throw new Error(json.error?.message || `request failed: ${res.status}`);
116
+ return json.data;
117
+ }
@@ -0,0 +1,23 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Sticky Notes</title>
7
+ <link rel="stylesheet" href="./styles.css">
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <header>
12
+ <h1>Sticky Notes</h1>
13
+ <p id="subtitle">Transcript notes</p>
14
+ </header>
15
+ <section class="composer">
16
+ <textarea id="user-text" placeholder="Follow-up request"></textarea>
17
+ <button id="insert-context" type="button">Insert context</button>
18
+ </section>
19
+ <ul id="notes"></ul>
20
+ </main>
21
+ <script type="module" src="./app.js"></script>
22
+ </body>
23
+ </html>
@@ -0,0 +1,84 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ body {
6
+ margin: 0;
7
+ background: #fbfaf7;
8
+ color: #24221d;
9
+ font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
10
+ }
11
+
12
+ main {
13
+ display: grid;
14
+ gap: 14px;
15
+ padding: 16px;
16
+ }
17
+
18
+ header {
19
+ border-bottom: 1px solid #e7dfc4;
20
+ padding-bottom: 10px;
21
+ }
22
+
23
+ h1 {
24
+ margin: 0;
25
+ font-size: 18px;
26
+ line-height: 1.2;
27
+ }
28
+
29
+ p {
30
+ margin: 4px 0 0;
31
+ color: #6c6658;
32
+ }
33
+
34
+ .composer {
35
+ display: grid;
36
+ gap: 8px;
37
+ }
38
+
39
+ textarea {
40
+ width: 100%;
41
+ min-height: 86px;
42
+ resize: vertical;
43
+ border: 1px solid #d9cfaa;
44
+ border-radius: 8px;
45
+ padding: 10px;
46
+ background: #fff8d8;
47
+ color: #24221d;
48
+ font: inherit;
49
+ }
50
+
51
+ button {
52
+ justify-self: start;
53
+ border: 1px solid #bfa643;
54
+ border-radius: 7px;
55
+ background: #f5d75d;
56
+ color: #2c2612;
57
+ padding: 7px 10px;
58
+ font: 650 13px/1 system-ui, sans-serif;
59
+ cursor: pointer;
60
+ }
61
+
62
+ ul {
63
+ display: grid;
64
+ gap: 8px;
65
+ margin: 0;
66
+ padding: 0;
67
+ list-style: none;
68
+ }
69
+
70
+ li {
71
+ border: 1px solid #ead87c;
72
+ border-radius: 8px;
73
+ background: #fff0a8;
74
+ padding: 10px;
75
+ }
76
+
77
+ strong {
78
+ display: block;
79
+ margin-bottom: 4px;
80
+ }
81
+
82
+ small {
83
+ color: #766b45;
84
+ }